Restructured data to use gino instead of sqlalchemy.

Signed-off-by: Cliff Hill <xlorep@darkhelm.org>
This commit is contained in:
2022-03-08 16:46:40 -05:00
parent 59c94c92de
commit 1aaedafbad
8 changed files with 2012 additions and 546 deletions

2
Jenkinsfile vendored
View File

@@ -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

File diff suppressed because it is too large Load Diff

View File

@@ -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"]

View File

@@ -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]
)

View File

@@ -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)),
)

View File

@@ -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
View File

@@ -0,0 +1,4 @@
"""Contains the gino DB connection for the application."""
import gino
db = gino.Gino()

View File

@@ -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,