models now are inherently SQL tables with SQLAlchemy's tricks.

Signed-off-by: Cliff Hill <xlorep@darkhelm.org>
This commit is contained in:
2021-09-29 16:20:10 -04:00
parent e49a58c39c
commit 45062c31a1
9 changed files with 459 additions and 169 deletions

214
poetry.lock generated
View File

@@ -211,30 +211,26 @@ python-versions = ">=2.7"
[[package]]
name = "filelock"
version = "3.0.12"
version = "3.1.0"
description = "A platform independent file lock."
category = "dev"
optional = false
python-versions = "*"
[[package]]
name = "gino"
version = "1.0.1"
description = "GINO Is Not ORM - a Python asyncio ORM on SQLAlchemy core."
category = "main"
optional = false
python-versions = ">=3.5,<4.0"
[package.dependencies]
asyncpg = ">=0.18,<1.0"
SQLAlchemy = ">=1.2.16,<1.4"
python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,>=2.7"
[package.extras]
starlette = ["gino-starlette (>=0.1.1,<0.2.0)"]
aiohttp = ["gino-aiohttp (>=0.1.0,<0.2.0)"]
tornado = ["gino-tornado (>=0.1.0,<0.2.0)"]
sanic = ["gino-sanic (>=0.1.0,<0.2.0)"]
quart = ["gino-quart (>=0.1.0,<0.2.0)"]
docs = ["furo (>=2021.8.17b43)", "sphinx (>=4.1)", "sphinx-autodoc-typehints (>=1.12)"]
testing = ["coverage (>=4)", "pytest (>=4)", "pytest-cov"]
[[package]]
name = "greenlet"
version = "1.1.2"
description = "Lightweight in-process concurrent programming"
category = "main"
optional = false
python-versions = ">=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*"
[package.extras]
docs = ["sphinx"]
[[package]]
name = "identify"
@@ -286,7 +282,7 @@ test = ["pytest (!=5.3.4)", "pytest-cov", "flaky", "nose", "ipyparallel"]
[[package]]
name = "ipython"
version = "7.27.0"
version = "7.28.0"
description = "IPython: Productive Interactive Computing"
category = "dev"
optional = false
@@ -408,7 +404,7 @@ qtconsole = "*"
[[package]]
name = "jupyter-client"
version = "7.0.3"
version = "7.0.5"
description = "Jupyter protocol implementation and client libraries"
category = "dev"
optional = false
@@ -546,7 +542,7 @@ test = ["codecov", "coverage", "ipython", "ipykernel", "ipywidgets", "pytest (>=
[[package]]
name = "nbconvert"
version = "6.1.0"
version = "6.2.0"
description = "Converting Jupyter Notebooks"
category = "dev"
optional = false
@@ -568,11 +564,11 @@ testpath = "*"
traitlets = ">=5.0"
[package.extras]
all = ["pytest", "pytest-cov", "pytest-dependency", "ipykernel", "ipywidgets (>=7)", "pyppeteer (==0.2.2)", "tornado (>=4.0)", "sphinx (>=1.5.1)", "sphinx-rtd-theme", "nbsphinx (>=0.2.12)", "ipython"]
all = ["pytest", "pytest-cov", "pytest-dependency", "ipykernel", "ipywidgets (>=7)", "pyppeteer (==0.2.6)", "tornado (>=4.0)", "sphinx (>=1.5.1)", "sphinx-rtd-theme", "nbsphinx (>=0.2.12)", "ipython"]
docs = ["sphinx (>=1.5.1)", "sphinx-rtd-theme", "nbsphinx (>=0.2.12)", "ipython"]
serve = ["tornado (>=4.0)"]
test = ["pytest", "pytest-cov", "pytest-dependency", "ipykernel", "ipywidgets (>=7)", "pyppeteer (==0.2.2)"]
webpdf = ["pyppeteer (==0.2.2)"]
test = ["pytest", "pytest-cov", "pytest-dependency", "ipykernel", "ipywidgets (>=7)", "pyppeteer (==0.2.6)"]
webpdf = ["pyppeteer (==0.2.6)"]
[[package]]
name = "nbformat"
@@ -697,7 +693,7 @@ python-versions = "*"
[[package]]
name = "platformdirs"
version = "2.3.0"
version = "2.4.0"
description = "A small Python module for determining appropriate platform-specific dirs, e.g. a \"user data dir\"."
category = "dev"
optional = false
@@ -1018,23 +1014,35 @@ python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*"
[[package]]
name = "sqlalchemy"
version = "1.3.24"
version = "1.4.25"
description = "Database Abstraction Library"
category = "main"
optional = false
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,>=2.7"
[package.dependencies]
greenlet = {version = "!=0.4.17", markers = "python_version >= \"3\" and (platform_machine == \"aarch64\" or platform_machine == \"ppc64le\" or platform_machine == \"x86_64\" or platform_machine == \"amd64\" or platform_machine == \"AMD64\" or platform_machine == \"win32\" or platform_machine == \"WIN32\")"}
[package.extras]
aiomysql = ["greenlet (!=0.4.17)", "aiomysql"]
aiosqlite = ["typing_extensions (!=3.10.0.1)", "greenlet (!=0.4.17)", "aiosqlite"]
asyncio = ["greenlet (!=0.4.17)"]
asyncmy = ["greenlet (!=0.4.17)", "asyncmy (>=0.2.0)"]
mariadb_connector = ["mariadb (>=1.0.1)"]
mssql = ["pyodbc"]
mssql_pymssql = ["pymssql"]
mssql_pyodbc = ["pyodbc"]
mysql = ["mysqlclient"]
oracle = ["cx-oracle"]
postgresql = ["psycopg2"]
postgresql_pg8000 = ["pg8000 (<1.16.6)"]
mypy = ["sqlalchemy2-stubs", "mypy (>=0.910)"]
mysql = ["mysqlclient (>=1.4.0,<2)", "mysqlclient (>=1.4.0)"]
mysql_connector = ["mysql-connector-python"]
oracle = ["cx_oracle (>=7,<8)", "cx_oracle (>=7)"]
postgresql = ["psycopg2 (>=2.7)"]
postgresql_asyncpg = ["greenlet (!=0.4.17)", "asyncpg"]
postgresql_pg8000 = ["pg8000 (>=1.16.6)"]
postgresql_psycopg2binary = ["psycopg2-binary"]
postgresql_psycopg2cffi = ["psycopg2cffi"]
pymysql = ["pymysql (<1)", "pymysql"]
sqlcipher = ["sqlcipher3-binary"]
[[package]]
name = "terminado"
@@ -1196,8 +1204,8 @@ notebook = ">=4.4.1"
[metadata]
lock-version = "1.1"
python-versions = ">=3.9,<3.11"
content-hash = "ddaa8d2b25ca8be048674b27b4e49951e5cf9271898f709761dff967f7cc48f3"
python-versions = ">=3.9,<3.10"
content-hash = "1f2c2176ef71a0bf6bcab39041763c91bc92735c595f0eb6d572f8a0f702b41b"
[metadata.files]
appdirs = [
@@ -1366,12 +1374,60 @@ entrypoints = [
{file = "entrypoints-0.3.tar.gz", hash = "sha256:c70dd71abe5a8c85e55e12c19bd91ccfeec11a6e99044204511f9ed547d48451"},
]
filelock = [
{file = "filelock-3.0.12-py3-none-any.whl", hash = "sha256:929b7d63ec5b7d6b71b0fa5ac14e030b3f70b75747cef1b10da9b879fef15836"},
{file = "filelock-3.0.12.tar.gz", hash = "sha256:18d82244ee114f543149c66a6e0c14e9c4f8a1044b5cdaadd0f82159d6a6ff59"},
{file = "filelock-3.1.0-py2.py3-none-any.whl", hash = "sha256:d9e9c7d8191e915339843c81c90d3e44f7c84e5fb03bdc6b1b4d019025cf953b"},
{file = "filelock-3.1.0.tar.gz", hash = "sha256:78925788ce8c8945fac28a68c1d05cf33a6a6c4fba14fe02835122c53268ceef"},
]
gino = [
{file = "gino-1.0.1-py3-none-any.whl", hash = "sha256:56df57cfdefbaf897a7c4897c265a0e91a8cca80716fb64f7d3cf6d501fdfb3d"},
{file = "gino-1.0.1.tar.gz", hash = "sha256:fe4189e82fe9d20c4a5f03fc775fb91c168061c5176b4c95623caeef22316150"},
greenlet = [
{file = "greenlet-1.1.2-cp27-cp27m-macosx_10_14_x86_64.whl", hash = "sha256:58df5c2a0e293bf665a51f8a100d3e9956febfbf1d9aaf8c0677cf70218910c6"},
{file = "greenlet-1.1.2-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:aec52725173bd3a7b56fe91bc56eccb26fbdff1386ef123abb63c84c5b43b63a"},
{file = "greenlet-1.1.2-cp27-cp27m-manylinux2010_x86_64.whl", hash = "sha256:833e1551925ed51e6b44c800e71e77dacd7e49181fdc9ac9a0bf3714d515785d"},
{file = "greenlet-1.1.2-cp27-cp27m-win32.whl", hash = "sha256:aa5b467f15e78b82257319aebc78dd2915e4c1436c3c0d1ad6f53e47ba6e2713"},
{file = "greenlet-1.1.2-cp27-cp27m-win_amd64.whl", hash = "sha256:40b951f601af999a8bf2ce8c71e8aaa4e8c6f78ff8afae7b808aae2dc50d4c40"},
{file = "greenlet-1.1.2-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:95e69877983ea39b7303570fa6760f81a3eec23d0e3ab2021b7144b94d06202d"},
{file = "greenlet-1.1.2-cp27-cp27mu-manylinux2010_x86_64.whl", hash = "sha256:356b3576ad078c89a6107caa9c50cc14e98e3a6c4874a37c3e0273e4baf33de8"},
{file = "greenlet-1.1.2-cp310-cp310-macosx_10_14_x86_64.whl", hash = "sha256:8639cadfda96737427330a094476d4c7a56ac03de7265622fcf4cfe57c8ae18d"},
{file = "greenlet-1.1.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:97e5306482182170ade15c4b0d8386ded995a07d7cc2ca8f27958d34d6736497"},
{file = "greenlet-1.1.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e6a36bb9474218c7a5b27ae476035497a6990e21d04c279884eb10d9b290f1b1"},
{file = "greenlet-1.1.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:abb7a75ed8b968f3061327c433a0fbd17b729947b400747c334a9c29a9af6c58"},
{file = "greenlet-1.1.2-cp310-cp310-win_amd64.whl", hash = "sha256:14d4f3cd4e8b524ae9b8aa567858beed70c392fdec26dbdb0a8a418392e71708"},
{file = "greenlet-1.1.2-cp35-cp35m-macosx_10_14_x86_64.whl", hash = "sha256:17ff94e7a83aa8671a25bf5b59326ec26da379ace2ebc4411d690d80a7fbcf23"},
{file = "greenlet-1.1.2-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:9f3cba480d3deb69f6ee2c1825060177a22c7826431458c697df88e6aeb3caee"},
{file = "greenlet-1.1.2-cp35-cp35m-manylinux2010_x86_64.whl", hash = "sha256:fa877ca7f6b48054f847b61d6fa7bed5cebb663ebc55e018fda12db09dcc664c"},
{file = "greenlet-1.1.2-cp35-cp35m-win32.whl", hash = "sha256:7cbd7574ce8e138bda9df4efc6bf2ab8572c9aff640d8ecfece1b006b68da963"},
{file = "greenlet-1.1.2-cp35-cp35m-win_amd64.whl", hash = "sha256:903bbd302a2378f984aef528f76d4c9b1748f318fe1294961c072bdc7f2ffa3e"},
{file = "greenlet-1.1.2-cp36-cp36m-macosx_10_14_x86_64.whl", hash = "sha256:049fe7579230e44daef03a259faa24511d10ebfa44f69411d99e6a184fe68073"},
{file = "greenlet-1.1.2-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:dd0b1e9e891f69e7675ba5c92e28b90eaa045f6ab134ffe70b52e948aa175b3c"},
{file = "greenlet-1.1.2-cp36-cp36m-manylinux2010_x86_64.whl", hash = "sha256:7418b6bfc7fe3331541b84bb2141c9baf1ec7132a7ecd9f375912eca810e714e"},
{file = "greenlet-1.1.2-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f9d29ca8a77117315101425ec7ec2a47a22ccf59f5593378fc4077ac5b754fce"},
{file = "greenlet-1.1.2-cp36-cp36m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:21915eb821a6b3d9d8eefdaf57d6c345b970ad722f856cd71739493ce003ad08"},
{file = "greenlet-1.1.2-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:eff9d20417ff9dcb0d25e2defc2574d10b491bf2e693b4e491914738b7908168"},
{file = "greenlet-1.1.2-cp36-cp36m-win32.whl", hash = "sha256:32ca72bbc673adbcfecb935bb3fb1b74e663d10a4b241aaa2f5a75fe1d1f90aa"},
{file = "greenlet-1.1.2-cp36-cp36m-win_amd64.whl", hash = "sha256:f0214eb2a23b85528310dad848ad2ac58e735612929c8072f6093f3585fd342d"},
{file = "greenlet-1.1.2-cp37-cp37m-macosx_10_14_x86_64.whl", hash = "sha256:b92e29e58bef6d9cfd340c72b04d74c4b4e9f70c9fa7c78b674d1fec18896dc4"},
{file = "greenlet-1.1.2-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:fdcec0b8399108577ec290f55551d926d9a1fa6cad45882093a7a07ac5ec147b"},
{file = "greenlet-1.1.2-cp37-cp37m-manylinux2010_x86_64.whl", hash = "sha256:93f81b134a165cc17123626ab8da2e30c0455441d4ab5576eed73a64c025b25c"},
{file = "greenlet-1.1.2-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1e12bdc622676ce47ae9abbf455c189e442afdde8818d9da983085df6312e7a1"},
{file = "greenlet-1.1.2-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8c790abda465726cfb8bb08bd4ca9a5d0a7bd77c7ac1ca1b839ad823b948ea28"},
{file = "greenlet-1.1.2-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f276df9830dba7a333544bd41070e8175762a7ac20350786b322b714b0e654f5"},
{file = "greenlet-1.1.2-cp37-cp37m-win32.whl", hash = "sha256:64e6175c2e53195278d7388c454e0b30997573f3f4bd63697f88d855f7a6a1fc"},
{file = "greenlet-1.1.2-cp37-cp37m-win_amd64.whl", hash = "sha256:b11548073a2213d950c3f671aa88e6f83cda6e2fb97a8b6317b1b5b33d850e06"},
{file = "greenlet-1.1.2-cp38-cp38-macosx_10_14_x86_64.whl", hash = "sha256:9633b3034d3d901f0a46b7939f8c4d64427dfba6bbc5a36b1a67364cf148a1b0"},
{file = "greenlet-1.1.2-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:eb6ea6da4c787111adf40f697b4e58732ee0942b5d3bd8f435277643329ba627"},
{file = "greenlet-1.1.2-cp38-cp38-manylinux2010_x86_64.whl", hash = "sha256:f3acda1924472472ddd60c29e5b9db0cec629fbe3c5c5accb74d6d6d14773478"},
{file = "greenlet-1.1.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e859fcb4cbe93504ea18008d1df98dee4f7766db66c435e4882ab35cf70cac43"},
{file = "greenlet-1.1.2-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:00e44c8afdbe5467e4f7b5851be223be68adb4272f44696ee71fe46b7036a711"},
{file = "greenlet-1.1.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ec8c433b3ab0419100bd45b47c9c8551248a5aee30ca5e9d399a0b57ac04651b"},
{file = "greenlet-1.1.2-cp38-cp38-win32.whl", hash = "sha256:288c6a76705dc54fba69fbcb59904ae4ad768b4c768839b8ca5fdadec6dd8cfd"},
{file = "greenlet-1.1.2-cp38-cp38-win_amd64.whl", hash = "sha256:8d2f1fb53a421b410751887eb4ff21386d119ef9cde3797bf5e7ed49fb51a3b3"},
{file = "greenlet-1.1.2-cp39-cp39-macosx_10_14_x86_64.whl", hash = "sha256:166eac03e48784a6a6e0e5f041cfebb1ab400b394db188c48b3a84737f505b67"},
{file = "greenlet-1.1.2-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:572e1787d1460da79590bf44304abbc0a2da944ea64ec549188fa84d89bba7ab"},
{file = "greenlet-1.1.2-cp39-cp39-manylinux2010_x86_64.whl", hash = "sha256:be5f425ff1f5f4b3c1e33ad64ab994eed12fc284a6ea71c5243fd564502ecbe5"},
{file = "greenlet-1.1.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b1692f7d6bc45e3200844be0dba153612103db241691088626a33ff1f24a0d88"},
{file = "greenlet-1.1.2-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7227b47e73dedaa513cdebb98469705ef0d66eb5a1250144468e9c3097d6b59b"},
{file = "greenlet-1.1.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7ff61ff178250f9bb3cd89752df0f1dd0e27316a8bd1465351652b1b4a4cdfd3"},
{file = "greenlet-1.1.2-cp39-cp39-win32.whl", hash = "sha256:f70a9e237bb792c7cc7e44c531fd48f5897961701cdaa06cf22fc14965c496cf"},
{file = "greenlet-1.1.2-cp39-cp39-win_amd64.whl", hash = "sha256:013d61294b6cd8fe3242932c1c5e36e5d1db2c8afb58606c5a67efce62c1f5fd"},
{file = "greenlet-1.1.2.tar.gz", hash = "sha256:e30f5ea4ae2346e62cedde8794a56858a67b878dd79f7df76a0767e356b1744a"},
]
identify = [
{file = "identify-2.2.15-py2.py3-none-any.whl", hash = "sha256:de83a84d774921669774a2000bf87ebba46b4d1c04775f4a5d37deff0cf39f73"},
@@ -1390,8 +1446,8 @@ ipykernel = [
{file = "ipykernel-6.4.1.tar.gz", hash = "sha256:df3355e5eec23126bc89767a676c5f0abfc7f4c3497d118c592b83b316e8c0cd"},
]
ipython = [
{file = "ipython-7.27.0-py3-none-any.whl", hash = "sha256:75b5e060a3417cf64f138e0bb78e58512742c57dc29db5a5058a2b1f0c10df02"},
{file = "ipython-7.27.0.tar.gz", hash = "sha256:58b55ebfdfa260dad10d509702dc2857cb25ad82609506b070cf2d7b7df5af13"},
{file = "ipython-7.28.0-py3-none-any.whl", hash = "sha256:f16148f9163e1e526f1008d7c8d966d9c15600ca20d1a754287cf96d00ba6f1d"},
{file = "ipython-7.28.0.tar.gz", hash = "sha256:2097be5c814d1b974aea57673176a924c4c8c9583890e7a5f082f547b9975b11"},
]
ipython-genutils = [
{file = "ipython_genutils-0.2.0-py2.py3-none-any.whl", hash = "sha256:72dd37233799e619666c9f639a9da83c34013a73e8bbc79a7a6348d93c61fab8"},
@@ -1419,8 +1475,8 @@ jupyter = [
{file = "jupyter-1.0.0.zip", hash = "sha256:3e1f86076bbb7c8c207829390305a2b1fe836d471ed54be66a3b8c41e7f46cc7"},
]
jupyter-client = [
{file = "jupyter_client-7.0.3-py3-none-any.whl", hash = "sha256:b07ceecb8f845f908bbd0f78bb17c0abac7b393de9d929bd92190e36c24c201e"},
{file = "jupyter_client-7.0.3.tar.gz", hash = "sha256:bb58e3218d74e072673948bd1e2a6bb3b65f32447b3e8c143eeca16b946ee230"},
{file = "jupyter_client-7.0.5-py3-none-any.whl", hash = "sha256:124a6e6979c38999d9153b1c4d1808c4c820a45066d5ed1857a5b59c04ffccb3"},
{file = "jupyter_client-7.0.5.tar.gz", hash = "sha256:382aca66dcaf96d7eaaa6c546d57cdf8b3b1cf5bc1f2704c58a1d8d244f1163d"},
]
jupyter-console = [
{file = "jupyter_console-6.4.0-py3-none-any.whl", hash = "sha256:7799c4ea951e0e96ba8260575423cb323ea5a03fcf5503560fa3e15748869e27"},
@@ -1515,8 +1571,8 @@ nbclient = [
{file = "nbclient-0.5.4.tar.gz", hash = "sha256:6c8ad36a28edad4562580847f9f1636fe5316a51a323ed85a24a4ad37d4aefce"},
]
nbconvert = [
{file = "nbconvert-6.1.0-py3-none-any.whl", hash = "sha256:37cd92ff2ae6a268e62075ff8b16129e0be4939c4dfcee53dc77cc8a7e06c684"},
{file = "nbconvert-6.1.0.tar.gz", hash = "sha256:d22a8ff202644d31db254d24d52c3a96c82156623fcd7c7f987bba2612303ec9"},
{file = "nbconvert-6.2.0-py3-none-any.whl", hash = "sha256:b1b9dc4f1ff6cafae0e6d91f42fb9046fdc32e6beb6d7e2fa2cd7191ad535240"},
{file = "nbconvert-6.2.0.tar.gz", hash = "sha256:16ceecd0afaa8fd26c245fa32e2c52066c02f13aa73387fffafd84750baea863"},
]
nbformat = [
{file = "nbformat-5.1.3-py3-none-any.whl", hash = "sha256:eb8447edd7127d043361bc17f2f5a807626bc8e878c7709a1c647abda28a9171"},
@@ -1587,8 +1643,8 @@ pickleshare = [
{file = "pickleshare-0.7.5.tar.gz", hash = "sha256:87683d47965c1da65cdacaf31c8441d12b8044cdec9aca500cd78fc2c683afca"},
]
platformdirs = [
{file = "platformdirs-2.3.0-py3-none-any.whl", hash = "sha256:8003ac87717ae2c7ee1ea5a84a1a61e87f3fbd16eb5aadba194ea30a9019f648"},
{file = "platformdirs-2.3.0.tar.gz", hash = "sha256:15b056538719b1c94bdaccb29e5f81879c7f7f0f4a153f46086d155dffcd4f0f"},
{file = "platformdirs-2.4.0-py3-none-any.whl", hash = "sha256:8868bbe3c3c80d42f20156f22e7131d2fb321f5bc86a2a345375c6481a67021d"},
{file = "platformdirs-2.4.0.tar.gz", hash = "sha256:367a5e80b3d04d2428ffa76d33f124cf11e8fff2acdaa9b43d545f5c7d661ef2"},
]
plexapi = [
{file = "PlexAPI-4.7.1-py3-none-any.whl", hash = "sha256:ecd1b15271d704b13a1df7ea2b2ffc9595d24da4733bf6224ec1131776776dcb"},
@@ -1810,40 +1866,36 @@ six = [
{file = "six-1.16.0.tar.gz", hash = "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926"},
]
sqlalchemy = [
{file = "SQLAlchemy-1.3.24-cp27-cp27m-macosx_10_14_x86_64.whl", hash = "sha256:87a2725ad7d41cd7376373c15fd8bf674e9c33ca56d0b8036add2d634dba372e"},
{file = "SQLAlchemy-1.3.24-cp27-cp27m-win32.whl", hash = "sha256:f597a243b8550a3a0b15122b14e49d8a7e622ba1c9d29776af741f1845478d79"},
{file = "SQLAlchemy-1.3.24-cp27-cp27m-win_amd64.whl", hash = "sha256:fc4cddb0b474b12ed7bdce6be1b9edc65352e8ce66bc10ff8cbbfb3d4047dbf4"},
{file = "SQLAlchemy-1.3.24-cp35-cp35m-macosx_10_14_x86_64.whl", hash = "sha256:f1149d6e5c49d069163e58a3196865e4321bad1803d7886e07d8710de392c548"},
{file = "SQLAlchemy-1.3.24-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:14f0eb5db872c231b20c18b1e5806352723a3a89fb4254af3b3e14f22eaaec75"},
{file = "SQLAlchemy-1.3.24-cp35-cp35m-manylinux2010_x86_64.whl", hash = "sha256:e98d09f487267f1e8d1179bf3b9d7709b30a916491997137dd24d6ae44d18d79"},
{file = "SQLAlchemy-1.3.24-cp35-cp35m-manylinux2014_aarch64.whl", hash = "sha256:fc1f2a5a5963e2e73bac4926bdaf7790c4d7d77e8fc0590817880e22dd9d0b8b"},
{file = "SQLAlchemy-1.3.24-cp35-cp35m-win32.whl", hash = "sha256:f3c5c52f7cb8b84bfaaf22d82cb9e6e9a8297f7c2ed14d806a0f5e4d22e83fb7"},
{file = "SQLAlchemy-1.3.24-cp35-cp35m-win_amd64.whl", hash = "sha256:0352db1befcbed2f9282e72843f1963860bf0e0472a4fa5cf8ee084318e0e6ab"},
{file = "SQLAlchemy-1.3.24-cp36-cp36m-macosx_10_14_x86_64.whl", hash = "sha256:2ed6343b625b16bcb63c5b10523fd15ed8934e1ed0f772c534985e9f5e73d894"},
{file = "SQLAlchemy-1.3.24-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:34fcec18f6e4b24b4a5f6185205a04f1eab1e56f8f1d028a2a03694ebcc2ddd4"},
{file = "SQLAlchemy-1.3.24-cp36-cp36m-manylinux2010_x86_64.whl", hash = "sha256:e47e257ba5934550d7235665eee6c911dc7178419b614ba9e1fbb1ce6325b14f"},
{file = "SQLAlchemy-1.3.24-cp36-cp36m-manylinux2014_aarch64.whl", hash = "sha256:816de75418ea0953b5eb7b8a74933ee5a46719491cd2b16f718afc4b291a9658"},
{file = "SQLAlchemy-1.3.24-cp36-cp36m-win32.whl", hash = "sha256:26155ea7a243cbf23287f390dba13d7927ffa1586d3208e0e8d615d0c506f996"},
{file = "SQLAlchemy-1.3.24-cp36-cp36m-win_amd64.whl", hash = "sha256:f03bd97650d2e42710fbe4cf8a59fae657f191df851fc9fc683ecef10746a375"},
{file = "SQLAlchemy-1.3.24-cp37-cp37m-macosx_10_14_x86_64.whl", hash = "sha256:a006d05d9aa052657ee3e4dc92544faae5fcbaafc6128217310945610d862d39"},
{file = "SQLAlchemy-1.3.24-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:1e2f89d2e5e3c7a88e25a3b0e43626dba8db2aa700253023b82e630d12b37109"},
{file = "SQLAlchemy-1.3.24-cp37-cp37m-manylinux2010_x86_64.whl", hash = "sha256:0d5d862b1cfbec5028ce1ecac06a3b42bc7703eb80e4b53fceb2738724311443"},
{file = "SQLAlchemy-1.3.24-cp37-cp37m-manylinux2014_aarch64.whl", hash = "sha256:0172423a27fbcae3751ef016663b72e1a516777de324a76e30efa170dbd3dd2d"},
{file = "SQLAlchemy-1.3.24-cp37-cp37m-win32.whl", hash = "sha256:d37843fb8df90376e9e91336724d78a32b988d3d20ab6656da4eb8ee3a45b63c"},
{file = "SQLAlchemy-1.3.24-cp37-cp37m-win_amd64.whl", hash = "sha256:c10ff6112d119f82b1618b6dc28126798481b9355d8748b64b9b55051eb4f01b"},
{file = "SQLAlchemy-1.3.24-cp38-cp38-macosx_10_14_x86_64.whl", hash = "sha256:861e459b0e97673af6cc5e7f597035c2e3acdfb2608132665406cded25ba64c7"},
{file = "SQLAlchemy-1.3.24-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:5de2464c254380d8a6c20a2746614d5a436260be1507491442cf1088e59430d2"},
{file = "SQLAlchemy-1.3.24-cp38-cp38-manylinux2010_x86_64.whl", hash = "sha256:d375d8ccd3cebae8d90270f7aa8532fe05908f79e78ae489068f3b4eee5994e8"},
{file = "SQLAlchemy-1.3.24-cp38-cp38-manylinux2014_aarch64.whl", hash = "sha256:014ea143572fee1c18322b7908140ad23b3994036ef4c0d630110faf942652f8"},
{file = "SQLAlchemy-1.3.24-cp38-cp38-win32.whl", hash = "sha256:6607ae6cd3a07f8a4c3198ffbf256c261661965742e2b5265a77cd5c679c9bba"},
{file = "SQLAlchemy-1.3.24-cp38-cp38-win_amd64.whl", hash = "sha256:fcb251305fa24a490b6a9ee2180e5f8252915fb778d3dafc70f9cc3f863827b9"},
{file = "SQLAlchemy-1.3.24-cp39-cp39-macosx_10_14_x86_64.whl", hash = "sha256:01aa5f803db724447c1d423ed583e42bf5264c597fd55e4add4301f163b0be48"},
{file = "SQLAlchemy-1.3.24-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:4d0e3515ef98aa4f0dc289ff2eebb0ece6260bbf37c2ea2022aad63797eacf60"},
{file = "SQLAlchemy-1.3.24-cp39-cp39-manylinux2010_x86_64.whl", hash = "sha256:bce28277f308db43a6b4965734366f533b3ff009571ec7ffa583cb77539b84d6"},
{file = "SQLAlchemy-1.3.24-cp39-cp39-manylinux2014_aarch64.whl", hash = "sha256:8110e6c414d3efc574543109ee618fe2c1f96fa31833a1ff36cc34e968c4f233"},
{file = "SQLAlchemy-1.3.24-cp39-cp39-win32.whl", hash = "sha256:ee5f5188edb20a29c1cc4a039b074fdc5575337c9a68f3063449ab47757bb064"},
{file = "SQLAlchemy-1.3.24-cp39-cp39-win_amd64.whl", hash = "sha256:09083c2487ca3c0865dc588e07aeaa25416da3d95f7482c07e92f47e080aa17b"},
{file = "SQLAlchemy-1.3.24.tar.gz", hash = "sha256:ebbb777cbf9312359b897bf81ba00dae0f5cb69fba2a18265dcc18a6f5ef7519"},
{file = "SQLAlchemy-1.4.25-cp27-cp27m-macosx_10_14_x86_64.whl", hash = "sha256:a36ea43919e51b0de0c0bc52bcfdad7683f6ea9fb81b340cdabb9df0e045e0f7"},
{file = "SQLAlchemy-1.4.25-cp27-cp27m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:75cd5d48389a7635393ff5a9214b90695c06b3d74912109c3b00ce7392b69c6c"},
{file = "SQLAlchemy-1.4.25-cp27-cp27m-win32.whl", hash = "sha256:16ef07e102d2d4f974ba9b0d4ac46345a411ad20ad988b3654d59ff08e553b1c"},
{file = "SQLAlchemy-1.4.25-cp27-cp27m-win_amd64.whl", hash = "sha256:a79abdb404d9256afb8aeaa0d3a4bc7d3b6d8b66103d8b0f2f91febd3909976e"},
{file = "SQLAlchemy-1.4.25-cp27-cp27mu-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:7ad59e2e16578b6c1a2873e4888134112365605b08a6067dd91e899e026efa1c"},
{file = "SQLAlchemy-1.4.25-cp36-cp36m-macosx_10_14_x86_64.whl", hash = "sha256:a505ecc0642f52e7c65afb02cc6181377d833b7df0994ecde15943b18d0fa89c"},
{file = "SQLAlchemy-1.4.25-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a28fe28c359835f3be20c89efd517b35e8f97dbb2ca09c6cf0d9ac07f62d7ef6"},
{file = "SQLAlchemy-1.4.25-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:41a916d815a3a23cb7fff8d11ad0c9b93369ac074e91e428075e088fe57d5358"},
{file = "SQLAlchemy-1.4.25-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:842c49dd584aedd75c2ee05f6c950730c3ffcddd21c5824ed0f820808387e1e3"},
{file = "SQLAlchemy-1.4.25-cp36-cp36m-win32.whl", hash = "sha256:6b602e3351f59f3999e9fb8b87e5b95cb2faab6a6ecdb482382ac6fdfbee5266"},
{file = "SQLAlchemy-1.4.25-cp36-cp36m-win_amd64.whl", hash = "sha256:6400b22e4e41cc27623a9a75630b7719579cd9a3a2027bcf16ad5aaa9a7806c0"},
{file = "SQLAlchemy-1.4.25-cp37-cp37m-macosx_10_14_x86_64.whl", hash = "sha256:dd4ed12a775f2cde4519f4267d3601990a97d8ecde5c944ab06bfd6e8e8ea177"},
{file = "SQLAlchemy-1.4.25-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7b7778a205f956755e05721eebf9f11a6ac18b2409bff5db53ce5fe7ede79831"},
{file = "SQLAlchemy-1.4.25-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:08d9396a2a38e672133266b31ed39b2b1f2b5ec712b5bff5e08033970563316a"},
{file = "SQLAlchemy-1.4.25-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e93978993a2ad0af43f132be3ea8805f56b2f2cd223403ec28d3e7d5c6d39ed1"},
{file = "SQLAlchemy-1.4.25-cp37-cp37m-win32.whl", hash = "sha256:0566a6e90951590c0307c75f9176597c88ef4be2724958ca1d28e8ae05ec8822"},
{file = "SQLAlchemy-1.4.25-cp37-cp37m-win_amd64.whl", hash = "sha256:0b08a53e40b34205acfeb5328b832f44437956d673a6c09fce55c66ab0e54916"},
{file = "SQLAlchemy-1.4.25-cp38-cp38-macosx_10_14_x86_64.whl", hash = "sha256:33a1e86abad782e90976de36150d910748b58e02cd7d35680d441f9a76806c18"},
{file = "SQLAlchemy-1.4.25-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2ed67aae8cde4d32aacbdba4f7f38183d14443b714498eada5e5a7a37769c0b7"},
{file = "SQLAlchemy-1.4.25-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:1ebd69365717becaa1b618220a3df97f7c08aa68e759491de516d1c3667bba54"},
{file = "SQLAlchemy-1.4.25-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:26b0cd2d5c7ea96d3230cb20acac3d89de3b593339c1447b4d64bfcf4eac1110"},
{file = "SQLAlchemy-1.4.25-cp38-cp38-win32.whl", hash = "sha256:c211e8ec81522ce87b0b39f0cf0712c998d4305a030459a0e115a2b3dc71598f"},
{file = "SQLAlchemy-1.4.25-cp38-cp38-win_amd64.whl", hash = "sha256:9a1df8c93a0dd9cef0839917f0c6c49f46c75810cf8852be49884da4a7de3c59"},
{file = "SQLAlchemy-1.4.25-cp39-cp39-macosx_10_14_x86_64.whl", hash = "sha256:1b38db2417b9f7005d6ceba7ce2a526bf10e3f6f635c0f163e6ed6a42b5b62b2"},
{file = "SQLAlchemy-1.4.25-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e37621b37c73b034997b5116678862f38ee70e5a054821c7b19d0e55df270dec"},
{file = "SQLAlchemy-1.4.25-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:91cd87d1de0111eaca11ccc3d31af441c753fa2bc22df72e5009cfb0a1af5b03"},
{file = "SQLAlchemy-1.4.25-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:90fe429285b171bcc252e21515703bdc2a4721008d1f13aa5b7150336f8a8493"},
{file = "SQLAlchemy-1.4.25-cp39-cp39-win32.whl", hash = "sha256:6003771ea597346ab1e97f2f58405c6cacbf6a308af3d28a9201a643c0ac7bb3"},
{file = "SQLAlchemy-1.4.25-cp39-cp39-win_amd64.whl", hash = "sha256:9ebe49c3960aa2219292ea2e5df6acdc425fc828f2f3d50b4cfae1692bcb5f02"},
{file = "SQLAlchemy-1.4.25.tar.gz", hash = "sha256:1adf3d25e2e33afbcd48cfad8076f9378793be43e7fec3e4334306cac6bec138"},
]
terminado = [
{file = "terminado-0.12.1-py3-none-any.whl", hash = "sha256:09fdde344324a1c9c6e610ee4ca165c4bb7f5bbf982fceeeb38998a988ef8452"},

View File

@@ -22,16 +22,16 @@ classifiers = [
Changelog = "https://gitlab.com/xlorepdarkhelm/plex-playlist/releases"
[tool.poetry.dependencies]
python = ">=3.9,<3.11"
python = ">=3.9,<3.10"
click = "^7.0"
PlexAPI = "^4.5.2"
appdirs = "^1.4.4"
PyYAML = "^5.4.1"
gino = "^1.0.1"
asyncpg = "^0.23.0"
uvloop = "^0.15.2"
desert = "^2020.11.18"
numpy = "^1.21.2"
SQLAlchemy = "^1.4.25"
[tool.poetry.dev-dependencies]
typeguard = "^2.12.0"

View File

@@ -52,6 +52,8 @@ class DataMeta(type):
class BaseData(metaclass=DataMeta):
"""Base class for data classes in the application."""
__sa_dataclass_metadata_key__ = "sa"
@classmethod
def load(cls: type[BaseData], data: DataDict) -> DataSubtype:
"""Load the given data dictionary into a class instance."""

View File

@@ -1,5 +1,8 @@
"""Application-level constants."""
import concurrent.futures
import dataclasses
import datetime
import enum
import pathlib
import appdirs # type: ignore
@@ -9,7 +12,19 @@ __all__ = ["APPNAME", "APPAUTHOR", "PATHS"]
APPNAME = "plex-playlist"
APPAUTHOR = "Cliff Hill"
DOWNLOAD_BATCH_SIZE = 1000
class Default(enum.Enum):
"""Contains default values used for initializing settings."""
BATCH_SIZE: int = 1000
PROCESS_DELAY: int = 0
DURATION: int = 0
PLAYTIME: datetime.timedelta = datetime.timedelta(days=1)
MAX_TRACKS: int = 0
PROCESS_POOL = concurrent.futures.ProcessPoolExecutor()
MAX_PROCESSES = PROCESS_POOL._max_workers # type: ignore [attr-defined]
@dataclasses.dataclass

View File

@@ -3,23 +3,62 @@ import dataclasses
import datetime
import typing
import sqlalchemy.orm # type: ignore [import]
from playlist.data import base
mapper_registry = sqlalchemy.orm.registry()
@mapper_registry.mapped
@dataclasses.dataclass
class Track(base.BaseData):
"""Model defining a Track object."""
id: int
track_num: int
title: str
artist: str
album_num: int
album: str
album_artist: str
duration: int
rating: typing.Optional[int]
comments: str
added: datetime.datetime
play_count: int
played: typing.Optional[datetime.datetime]
__tablename__ = "tracks"
id: int = dataclasses.field(
init=False,
metadata={"sa": sqlalchemy.Column(sqlalchemy.Integer, primary_key=True)},
)
plex_id: int = dataclasses.field(
metadata={"sa": sqlalchemy.Column(sqlalchemy.Integer, nullable=False)},
)
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)},
)
rating: typing.Optional[int] = dataclasses.field(
default=None,
metadata={"sa": sqlalchemy.Column(sqlalchemy.Integer)},
)
played: typing.Optional[datetime.datetime] = dataclasses.field(
default=None,
metadata={"sa": sqlalchemy.Column(sqlalchemy.DateTime)},
)

View File

@@ -2,42 +2,93 @@
from __future__ import annotations
import dataclasses
import datetime
import functools
from playlist import const
from playlist.data import base
from playlist.data import const
__all__ = ["get"]
__all__ = ["get", "write"]
SETTINGS_PATH = const.PATHS.CONFIG / "settings.yaml"
@dataclasses.dataclass
class Creds(base.BaseData):
class CredentialSettings(base.BaseData):
"""Credentials component for Settings object."""
baseurl: str
token: str
@classmethod
def create(cls: type[CredentialSettings]) -> CredentialSettings:
import getpass
def _make_creds() -> Creds:
import getpass
from playlist.plex import server
from playlist.plex import server
username = input("Plex Username: ")
password = getpass.getpass("Plex Password: ")
servername = input("Plex Server: ")
username = input("Plex Username: ")
password = getpass.getpass("Plex Password: ")
servername = input("Plex Server: ")
data = server.get_creds(username, password, servername)
return cls.load(data)
data = server.get_creds(username, password, servername)
return Creds.load(data)
@dataclasses.dataclass
class DownloadSettings(base.BaseData):
"""DownloadSettingser options for Settings object."""
batch_size: int
process_delay: float
@classmethod
def create(cls: type[DownloadSettings]) -> DownloadSettings:
return cls(
batch_size=const.Default.BATCH_SIZE.value,
process_delay=const.Default.PROCESS_DELAY.value,
)
@dataclasses.dataclass
class TrackSettings(base.BaseData):
"""Track options for Settings object."""
duration: float
@classmethod
def create(cls: type[TrackSettings]) -> TrackSettings:
return cls(duration=const.Default.DURATION.value)
@dataclasses.dataclass
class PlaylistSettings(base.BaseData):
"""Playlist options for Settings object."""
playtime: datetime.timedelta
max_tracks: int
@classmethod
def create(cls: type[PlaylistSettings]) -> PlaylistSettings:
return cls(
playtime=const.Default.PLAYTIME.value,
max_tracks=const.Default.MAX_TRACKS.value,
)
@dataclasses.dataclass
class Settings(base.YAMLData):
"""Settings object, loaded from settings.yaml file."""
creds: Creds = dataclasses.field(default_factory=_make_creds)
creds: CredentialSettings = dataclasses.field(
default_factory=CredentialSettings.create,
)
download: DownloadSettings = dataclasses.field(
default_factory=DownloadSettings.create,
)
track: TrackSettings = dataclasses.field(default_factory=TrackSettings.create)
playlist: PlaylistSettings = dataclasses.field(
default_factory=PlaylistSettings.create,
)
@functools.cache
@@ -51,3 +102,8 @@ def get() -> Settings:
instance = Settings.yaml_create(SETTINGS_PATH)
return instance
def write() -> None:
"""Store the current Settings object."""
get().yaml_write(SETTINGS_PATH)

View File

@@ -1,94 +1,72 @@
"""Contains the code to communicate to the Plex server."""
import asyncio
import collections.abc
import functools
import statistics
import time
import typing
import plexapi.audio # type: ignore [import]
import plexapi.myplex # type: ignore [import]
import plexapi.server # type: ignore [import]
from playlist import const
from playlist import utils
from playlist.data import const
from playlist.data import models
from playlist.data import settings
__all__ = ["gen_tracks", "total_track_count", "get_creds"]
server = plexapi.server.PlexServer(**settings.get().creds.dump())
def _downloader(
pos: int,
size: int,
loop: asyncio.AbstractEventLoop,
search_tracks: collections.abc.Callable, # type: ignore [type-arg]
) -> typing.Awaitable[list[plexapi.audio.Track]]:
return loop.run_in_executor(
None,
functools.partial(
search_tracks,
maxresults=size,
container_start=pos,
container_size=size,
),
)
def _track_dump(track: plexapi.audio.Track) -> models.Track.Dict: # type: ignore [name-defined]
return {
"id": track.ratingKey,
"track_num": track.index,
"title": track.title,
"artist": track.artist().title,
"album_num": track.parentIndex,
"album": track.parentTitle,
"album_artist": track.grandparentTitle,
"duration": track.duration,
"rating": track.userRating,
"comments": track.summary,
"added": str(track.addedAt),
"play_count": track.viewCount,
"played": str(track.lastViewedAt) if track.lastViewedAt is not None else None,
}
def get_creds(
username: str,
password: str,
server: str,
) -> settings.Creds.Dict: # type: ignore [name-defined]
"""Get the credentials to store in the Settings instance."""
account = plexapi.myplex.MyPlexAccount(username, password)
plex = account.reource(server).connect()
return {"baseurl": plex._baseurl, "token": plex._token}
throttle = asyncio.BoundedSemaphore(const.MAX_PROCESSES)
async def gen_tracks(
*,
batch_size: int = const.DOWNLOAD_BATCH_SIZE,
batch_size: int = settings.get().download.batch_size,
) -> typing.AsyncGenerator[models.Track, None]:
"""Generate all Tracks from the Server, asynchronously.
Keyword Args:
batch_size: determines how many Tracks are pulled from the Server at a time.
Yields:
models.Track: The Track that was pulled from Plex.
"""
quotient, remainder = divmod(await total_track_count(), batch_size)
loop = asyncio.get_running_loop()
search_tracks = server.library.section("Music").searchTracks
batches = [
_downloader(ndx, batch_size, loop, search_tracks) for ndx in range(quotient)
asyncio.create_task(_downloader(ndx, size, loop))
async for ndx, size in _gen_batch_params(batch_size)
]
batches.append(_downloader(quotient, remainder, loop, search_tracks))
for batch in asyncio.as_completed(batches):
batch_gen: collections.abc.Generator[models.Track, None, None] = (
models.Track.load(_track_dump(plex_track)) for plex_track in await batch
)
for track in batch_gen:
times = []
weights = []
durations = []
for batch_task in asyncio.as_completed(batches):
process_time, batch = await batch_task
weight = len(batch) / batch_size
times.append(process_time * weight)
weights.append(weight)
for track_data in batch:
track: models.Track = models.Track.load(track_data)
durations.append(track.duration)
yield track
avg_time_per_batch = utils.harmonic_mean(times, weights=weights)
avg_track_duration = statistics.geometric_mean(durations)
delay = avg_time_per_batch / const.MAX_PROCESSES
settings.get().download.process_delay = round(delay, ndigits=6)
settings.get().track.duration = round(avg_track_duration, ndigits=6)
playtime_seconds = settings.get().playlist.playtime.total_seconds()
settings.get().playlist.max_tracks = int(playtime_seconds / avg_track_duration)
settings.write()
async def total_track_count() -> int:
"""Get the total number of tracks in the server."""
"""Get the total number of tracks in the server.
Returns:
int: The total number of tracks in plex.
"""
server = plexapi.server.PlexServer(**settings.get().creds.dump())
loop = asyncio.get_running_loop()
num_tracks: int = await loop.run_in_executor(
None,
@@ -100,3 +78,126 @@ async def total_track_count() -> int:
),
)
return num_tracks
def get_creds(
username: str,
password: str,
server: str,
) -> settings.CredentialSettings.Dict: # type: ignore [name-defined]
"""Get the credentials to store in the Settings instance.
Args:
username: The username to log in as.
password: The password to log in with.
server: The server alias in Plex to use to connect to.
Returns:
settings.CredentialSettings.Dict: The dictionary containing the baseurl and token string
to connect with.
"""
account = plexapi.myplex.MyPlexAccount(username, password)
plex = account.reource(server).connect()
return {"baseurl": plex._baseurl, "token": plex._token}
async def _downloader(
pos: int,
size: int,
loop: asyncio.AbstractEventLoop,
) -> tuple[float, list[models.Track.Dict]]: # type: ignore [name-defined]
"""Download a batch of tracks from Plex, by running it inside a Process Pool.
Args:
pos: The index number of the batch to retrieve.
size: The size of the batch to retrieve.
loop: The asyncio event loop to use.
Returns:
tuple[float, list[models.Track.Dict]]: The time (in seconds) to process, and
the batch of Tracks that were retrieved, in dictionary form.
"""
async with throttle:
start = time.time()
batch = await loop.run_in_executor(
const.PROCESS_POOL,
_get_track_batch,
size,
pos,
)
end = time.time()
process_time = end - start
return process_time, batch
def _get_track_batch(
size: int,
pos: int,
) -> list[models.Track.Dict]: # type: ignore [name-defined]
"""Get a batch of Tracks from the Plex Server.
Args:
size: The size of the batch to retrieve.
pos: The index number of the batch to retrieve.
Returns:
list[models.Track.Dict]: The process time and list of Tracks retrieved,
in dictionary form.
"""
server = plexapi.server.PlexServer(**settings.get().creds.dump())
batch = server.library.section("Music").searchTracks(
maxresults=size,
container_start=pos,
container_size=size,
)
return [_track_dump(track) for track in batch]
def _track_dump(track: plexapi.audio.Track) -> models.Track.Dict: # type: ignore [name-defined]
"""Convert a PlexAPI Audio Track object into a Track in dictionary form.
Args:
track: The PlexAPI Audio Track to convert from.
Returns:
models.Track.Dict: The dictionary form for the Track object converted to.
"""
return {
"plex_id": track.ratingKey,
"track_num": track.index,
"title": track.title,
"artist": track.artist().title,
"album_num": track.parentIndex,
"album": track.parentTitle,
"album_artist": track.grandparentTitle,
"duration": round(track.duration / 1000, ndigits=6),
"rating": track.userRating,
"comments": track.summary,
"added": str(track.addedAt),
"play_count": track.viewCount,
"played": str(track.lastViewedAt) if track.lastViewedAt is not None else None,
}
async def _gen_batch_params(
batch_size: int,
) -> collections.abc.AsyncGenerator[tuple[int, int], None]:
"""Generate parameters for batches to be processed with.
This slows down processing of the first few batches to allow for different process
pool workers to be staggered, and make the processing more smooth.
Args:
batch_size: determines how many Tracks are pulled from the Plex Server at a time.
Yields:
tuple[int, int]: The batch index and batch size to process.
"""
quotient, remainder = divmod(await total_track_count(), batch_size)
for ndx in range(quotient):
if ndx and ndx < const.MAX_PROCESSES:
await asyncio.sleep(settings.get().download.process_delay)
yield ndx, batch_size
if quotient and quotient < const.MAX_PROCESSES:
await asyncio.sleep(settings.get().download.process_delay)
yield quotient, remainder

View File

@@ -1 +0,0 @@
"""Package conaining the full SQL api/implementation."""

View File

@@ -1,6 +1,7 @@
"""Utility functions & classes used in the application."""
from __future__ import annotations
import collections.abc
import contextlib
import datetime
import typing
@@ -15,3 +16,28 @@ def time_this() -> typing.Generator[None, None, None]:
finally:
end = datetime.datetime.now()
print(f"Elapsed time: {end - start}")
def harmonic_mean(
data: collections.abc.Sequence[float],
*,
weights: typing.Optional[collections.abc.Sequence[float]] = None,
) -> float:
"""Calculate the [weighted] harmonic mean of the given data.
Args:
data: The sequence of values to calculate.
Keyword Args:
weights: The sequence of weights to apply to the calculation. If not
given it assumes all weights are 1.
Returns:
float: The [weighted] harmonic mean of the data.
"""
if weights is None:
weights = [1] * len(data)
numerator = sum(weights)
denominator = sum(weight / value for weight, value in zip(weights, data))
return numerator / denominator