models now are inherently SQL tables with SQLAlchemy's tricks.
Signed-off-by: Cliff Hill <xlorep@darkhelm.org>
This commit is contained in:
214
poetry.lock
generated
214
poetry.lock
generated
@@ -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"},
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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."""
|
||||
|
||||
@@ -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
|
||||
@@ -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)},
|
||||
)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
"""Package conaining the full SQL api/implementation."""
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user