Restructured data to use gino instead of sqlalchemy.
Signed-off-by: Cliff Hill <xlorep@darkhelm.org>
This commit is contained in:
2
Jenkinsfile
vendored
2
Jenkinsfile
vendored
@@ -2,7 +2,7 @@ pipeline {
|
||||
agent {
|
||||
docker {
|
||||
image 'python:3.10-alpine3.14'
|
||||
args '--rm --name playlist-testing -v /etc/passwd:/etc/passwd:ro'
|
||||
args '--rm --name playlist-testing'
|
||||
}
|
||||
}
|
||||
stages {
|
||||
|
||||
2406
poetry.lock
generated
2406
poetry.lock
generated
File diff suppressed because it is too large
Load Diff
@@ -20,7 +20,7 @@ Changelog = "https://gitlab.com/xlorepdarkhelm/plex-playlist/releases"
|
||||
|
||||
[tool.poetry.dependencies]
|
||||
python = ">=3.10,<3.11"
|
||||
click = "^7.0"
|
||||
click = "^8.0.3"
|
||||
PlexAPI = "^4.5.2"
|
||||
appdirs = "^1.4.4"
|
||||
PyYAML = "^5.4.1"
|
||||
@@ -28,8 +28,10 @@ asyncpg = "^0.23.0"
|
||||
uvloop = "^0.15.2"
|
||||
desert = "^2020.11.18"
|
||||
numpy = "^1.21.2"
|
||||
SQLAlchemy = "^1.4.25"
|
||||
typeguard = "^2.12.1"
|
||||
gino = "^1.0.1"
|
||||
quart = "^0.16.3"
|
||||
marshmallow-sqlalchemy = "^0.27.0"
|
||||
|
||||
[tool.poetry.dev-dependencies]
|
||||
typeguard = "^2.12.0"
|
||||
@@ -39,6 +41,8 @@ pre-commit-hooks = "^4.0.1"
|
||||
pytest = "^6.2.5"
|
||||
pytest-asyncio = "^0.15.1"
|
||||
pytest-mock = "^3.6.1"
|
||||
black = "^22.1.0"
|
||||
types-all = "^1.0.0"
|
||||
|
||||
|
||||
[tool.poetry.scripts]
|
||||
@@ -46,7 +50,7 @@ plex-playlist = "playlist.__main__:main"
|
||||
|
||||
[tool.black]
|
||||
line-length = 88
|
||||
target-version = ["py39"]
|
||||
target-version = ["py310"]
|
||||
|
||||
[tool.coverage.paths]
|
||||
source = ["src", "*/site-packages"]
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
"""Utility functions & classes used in the application."""
|
||||
from __future__ import annotations
|
||||
|
||||
import dataclasses
|
||||
import functools
|
||||
import typing
|
||||
|
||||
@@ -12,34 +11,25 @@ import marshmallow
|
||||
DataSub = typing.TypeVar("DataSub", bound="BaseData")
|
||||
|
||||
|
||||
class DataMeta(type):
|
||||
"""Metaclass that is used for data classes in the application.
|
||||
|
||||
This defines convenience class properties and methods that are used across all dataclasses.
|
||||
"""
|
||||
|
||||
Schema: typing.ClassVar[marshmallow.schema.Schema]
|
||||
Dict: typing.ClassVar[type[dict[str, typing.Any]]]
|
||||
|
||||
@property # type: ignore [no-redef,misc]
|
||||
@functools.cache
|
||||
def Schema(cls: DataMeta) -> marshmallow.Schema: # noqa: N802
|
||||
"""The marshmallow Schema object for this data class."""
|
||||
return desert.schema(cls)
|
||||
|
||||
|
||||
@dataclasses.dataclass
|
||||
class BaseData(metaclass=DataMeta):
|
||||
class BaseData:
|
||||
"""Base class for data classes in the application."""
|
||||
|
||||
__sa_dataclass_metadata_key__ = "sa"
|
||||
@classmethod # type: ignore [misc]
|
||||
@property
|
||||
@functools.cache
|
||||
def Schema(cls: type[BaseData]) -> marshmallow.Schema: # noqa: N802
|
||||
"""The marshmallow Schema object for this data class."""
|
||||
return desert.schema(cls)
|
||||
|
||||
@classmethod
|
||||
def load(cls: type[BaseData], data: dict[str, object]) -> DataSub:
|
||||
"""Load the given data dictionary into a class instance."""
|
||||
return typing.cast(DataSub, cls.Schema.load(data))
|
||||
return typing.cast(DataSub, cls.Schema.load(data)) # type: ignore [attr-defined]
|
||||
|
||||
def dump(self: BaseData) -> dict[str, object]:
|
||||
"""Dump the class instance into a data dictionary."""
|
||||
cls = type(self)
|
||||
return typing.cast(dict[str, object], cls.Schema.dump(self))
|
||||
return typing.cast(
|
||||
dict[str, object],
|
||||
cls.Schema.dump(self), # type: ignore [attr-defined]
|
||||
)
|
||||
|
||||
@@ -2,65 +2,40 @@
|
||||
import dataclasses
|
||||
import datetime
|
||||
|
||||
import sqlalchemy.orm # type: ignore [import]
|
||||
import desert
|
||||
import marshmallow
|
||||
|
||||
from playlist.data import base
|
||||
|
||||
mapper_registry = sqlalchemy.orm.registry()
|
||||
from playlist.data.sql import db
|
||||
|
||||
|
||||
@mapper_registry.mapped
|
||||
@dataclasses.dataclass
|
||||
class Track(base.BaseData):
|
||||
class Track(db.Model, base.BaseData): # type: ignore [misc]
|
||||
"""Model defining a Track object."""
|
||||
|
||||
__tablename__ = "tracks"
|
||||
___tablename__ = "tracks"
|
||||
|
||||
id: int = dataclasses.field(
|
||||
init=False,
|
||||
repr=False,
|
||||
metadata={"sa": sqlalchemy.Column(sqlalchemy.Integer, primary_key=True)},
|
||||
)
|
||||
plex_id: int = dataclasses.field(
|
||||
metadata={
|
||||
"sa": sqlalchemy.Column(sqlalchemy.Integer, nullable=False, unique=True),
|
||||
},
|
||||
)
|
||||
track_num: int = dataclasses.field(
|
||||
metadata={"sa": sqlalchemy.Column(sqlalchemy.Integer, nullable=False)},
|
||||
)
|
||||
title: str = dataclasses.field(
|
||||
metadata={"sa": sqlalchemy.Column(sqlalchemy.String(100), nullable=False)},
|
||||
)
|
||||
artist: str = dataclasses.field(
|
||||
metadata={"sa": sqlalchemy.Column(sqlalchemy.String(100), nullable=False)},
|
||||
)
|
||||
album_num: int = dataclasses.field(
|
||||
metadata={"sa": sqlalchemy.Column(sqlalchemy.Integer, nullable=False)},
|
||||
)
|
||||
album: str = dataclasses.field(
|
||||
metadata={"sa": sqlalchemy.Column(sqlalchemy.String(100), nullable=False)},
|
||||
)
|
||||
album_artist: str = dataclasses.field(
|
||||
metadata={"sa": sqlalchemy.Column(sqlalchemy.String(100), nullable=False)},
|
||||
)
|
||||
duration: int = dataclasses.field(
|
||||
metadata={"sa": sqlalchemy.Column(sqlalchemy.Integer, nullable=False)},
|
||||
)
|
||||
comments: str = dataclasses.field(
|
||||
metadata={"sa": sqlalchemy.Column(sqlalchemy.String(2000), nullable=False)},
|
||||
)
|
||||
added: datetime.datetime = dataclasses.field(
|
||||
metadata={"sa": sqlalchemy.Column(sqlalchemy.DateTime, nullable=False)},
|
||||
)
|
||||
play_count: int = dataclasses.field(
|
||||
metadata={"sa": sqlalchemy.Column(sqlalchemy.Integer, nullable=False)},
|
||||
default=db.Column(db.Integer(), primary_key=True),
|
||||
)
|
||||
plex_id: int = db.Column(db.Integer)
|
||||
track_num: int = db.Column(db.Integer)
|
||||
title: str = db.Column(db.String)
|
||||
artist: str = db.Column(db.String)
|
||||
album_num: int = db.Column(db.Integer)
|
||||
album: str = db.Column(db.String)
|
||||
album_artist: str = db.Column(db.String)
|
||||
duration: int = db.Column(db.Integer)
|
||||
comments: str = db.Column(db.String)
|
||||
added: datetime.datetime = db.Column(db.DateTime)
|
||||
play_count: int = db.Column(db.Integer)
|
||||
rating: int | None = dataclasses.field(
|
||||
default=None,
|
||||
metadata={"sa": sqlalchemy.Column(sqlalchemy.Integer)},
|
||||
default=db.Column(db.Integer(), nullable=True, default=None),
|
||||
metadata=desert.metadata(marshmallow.fields.Int(allow_none=True)),
|
||||
)
|
||||
played: datetime.datetime | None = dataclasses.field(
|
||||
default=None,
|
||||
metadata={"sa": sqlalchemy.Column(sqlalchemy.DateTime)},
|
||||
default=db.Column(db.Integer, nullable=True, default=None),
|
||||
metadata=desert.metadata(marshmallow.fields.DateTime(allow_none=True)),
|
||||
)
|
||||
|
||||
@@ -6,9 +6,11 @@ import contextlib
|
||||
import dataclasses
|
||||
import datetime
|
||||
import functools
|
||||
import getpass
|
||||
import pathlib
|
||||
import typing
|
||||
|
||||
import plexapi.myplex
|
||||
import yaml
|
||||
|
||||
from playlist.data import base
|
||||
@@ -27,9 +29,19 @@ class CredentialSettings(base.BaseData):
|
||||
|
||||
@classmethod
|
||||
def create(cls: type[CredentialSettings]) -> CredentialSettings: # pragma: no cover
|
||||
from playlist.plex import server
|
||||
"""Get the credentials to store in the Settings instance.
|
||||
|
||||
return server.get_creds()
|
||||
Returns:
|
||||
CredentialSettings: The dictionary containing the baseurl and token string
|
||||
to connect with.
|
||||
"""
|
||||
username = input("Plex Username: ")
|
||||
password = getpass.getpass("Plex Password: ")
|
||||
server = input("Plex Server: ")
|
||||
|
||||
account = plexapi.myplex.MyPlexAccount(username, password)
|
||||
plex = account.resource(server).connect()
|
||||
return CredentialSettings(baseurl=plex._baseurl, token=plex._token)
|
||||
|
||||
|
||||
@dataclasses.dataclass
|
||||
|
||||
4
src/playlist/data/sql.py
Normal file
4
src/playlist/data/sql.py
Normal file
@@ -0,0 +1,4 @@
|
||||
"""Contains the gino DB connection for the application."""
|
||||
import gino
|
||||
|
||||
db = gino.Gino()
|
||||
@@ -1,7 +1,6 @@
|
||||
"""Contains the code to communicate to the Plex server."""
|
||||
import asyncio
|
||||
import collections.abc
|
||||
import getpass
|
||||
import statistics
|
||||
import time
|
||||
import typing
|
||||
@@ -15,7 +14,7 @@ from playlist.data import const
|
||||
from playlist.data import models
|
||||
from playlist.data import settings
|
||||
|
||||
__all__ = ["gen_tracks", "total_track_count", "get_creds"]
|
||||
__all__ = ["gen_tracks", "total_track_count"]
|
||||
|
||||
throttle = asyncio.BoundedSemaphore(const.MAX_PROCESSES)
|
||||
|
||||
@@ -106,22 +105,6 @@ async def total_track_count() -> int:
|
||||
return num_tracks
|
||||
|
||||
|
||||
def get_creds() -> settings.CredentialSettings:
|
||||
"""Get the credentials to store in the Settings instance.
|
||||
|
||||
Returns:
|
||||
settings.CredentialSettings: The dictionary containing the baseurl and token string
|
||||
to connect with.
|
||||
"""
|
||||
username = input("Plex Username: ")
|
||||
password = getpass.getpass("Plex Password: ")
|
||||
server = input("Plex Server: ")
|
||||
|
||||
account = plexapi.myplex.MyPlexAccount(username, password)
|
||||
plex = account.resource(server).connect()
|
||||
return settings.CredentialSettings(baseurl=plex._baseurl, token=plex._token)
|
||||
|
||||
|
||||
async def _downloader(
|
||||
pos: int,
|
||||
size: int,
|
||||
|
||||
Reference in New Issue
Block a user