From c9c95ba2faf758168980ecf3f2c7204e5136f7fe Mon Sep 17 00:00:00 2001 From: ducklet Date: Sat, 18 Mar 2023 22:09:03 +0100 Subject: [PATCH 01/31] improve typing for config --- unwind/config.py | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/unwind/config.py b/unwind/config.py index 6cc255a..9e3a908 100644 --- a/unwind/config.py +++ b/unwind/config.py @@ -2,20 +2,20 @@ import os import tomllib from pathlib import Path -datadir = Path(os.getenv("UNWIND_DATA") or "./data") -cachedir = ( - Path(cachedir) - if (cachedir := os.getenv("UNWIND_CACHEDIR", datadir / ".cache")) - else None +datadir: Path = Path(os.getenv("UNWIND_DATA") or "./data") +cachedir: Path = Path(p) if (p := os.getenv("UNWIND_CACHEDIR")) else datadir / ".cache" +debug: bool = os.getenv("DEBUG") == "1" +loglevel: str = os.getenv("UNWIND_LOGLEVEL") or ("DEBUG" if debug else "INFO") +storage_path: Path = ( + Path(p) if (p := os.getenv("UNWIND_STORAGE")) else datadir / "db.sqlite" +) +config_path: Path = ( + Path(p) if (p := os.getenv("UNWIND_CONFIG")) else datadir / "config.toml" ) -debug = os.getenv("DEBUG") == "1" -loglevel = os.getenv("UNWIND_LOGLEVEL") or ("DEBUG" if debug else "INFO") -storage_path = os.getenv("UNWIND_STORAGE", datadir / "db.sqlite") -config_path = os.getenv("UNWIND_CONFIG", datadir / "config.toml") with open(config_path, "rb") as fd: _config = tomllib.load(fd) -api_base = _config["api"].get("base", "/api/") -api_cors = _config["api"].get("cors", "*") -api_credentials = _config["api"].get("credentials", {}) +api_base: str = _config["api"].get("base", "/api/") +api_cors: str = _config["api"].get("cors", "*") +api_credentials: dict[str, str] = _config["api"].get("credentials", {}) From 00486778dbfb8a4b6f7f7cd3ebf1ac38757aeac0 Mon Sep 17 00:00:00 2001 From: ducklet Date: Sat, 18 Mar 2023 22:11:51 +0100 Subject: [PATCH 02/31] make SQLAlchemy 1.4 dependency explicit --- poetry.lock | 18 ++++++++++++++++-- pyproject.toml | 1 + scripts/tests | 2 +- 3 files changed, 18 insertions(+), 3 deletions(-) diff --git a/poetry.lock b/poetry.lock index 8ff7d02..7459b66 100644 --- a/poetry.lock +++ b/poetry.lock @@ -673,7 +673,9 @@ files = [ ] [package.dependencies] -greenlet = {version = "!=0.4.17", markers = "python_version >= \"3\" and platform_machine == \"aarch64\" or python_version >= \"3\" and platform_machine == \"ppc64le\" or python_version >= \"3\" and platform_machine == \"x86_64\" or python_version >= \"3\" and platform_machine == \"amd64\" or python_version >= \"3\" and platform_machine == \"AMD64\" or python_version >= \"3\" and platform_machine == \"win32\" or python_version >= \"3\" and platform_machine == \"WIN32\""} +aiosqlite = {version = "*", optional = true, markers = "python_version >= \"3\" and extra == \"aiosqlite\""} +greenlet = {version = "!=0.4.17", optional = true, markers = "python_version >= \"3\" and platform_machine == \"aarch64\" or python_version >= \"3\" and platform_machine == \"ppc64le\" or python_version >= \"3\" and platform_machine == \"x86_64\" or python_version >= \"3\" and platform_machine == \"amd64\" or python_version >= \"3\" and platform_machine == \"AMD64\" or python_version >= \"3\" and platform_machine == \"win32\" or python_version >= \"3\" and platform_machine == \"WIN32\" or python_version >= \"3\" and extra == \"aiosqlite\""} +typing-extensions = {version = "!=3.10.0.1", optional = true, markers = "extra == \"aiosqlite\""} [package.extras] aiomysql = ["aiomysql", "greenlet (!=0.4.17)"] @@ -714,6 +716,18 @@ anyio = ">=3.4.0,<5" [package.extras] full = ["httpx (>=0.22.0)", "itsdangerous", "jinja2", "python-multipart", "pyyaml"] +[[package]] +name = "typing-extensions" +version = "4.5.0" +description = "Backported and Experimental Type Hints for Python 3.7+" +category = "main" +optional = false +python-versions = ">=3.7" +files = [ + {file = "typing_extensions-4.5.0-py3-none-any.whl", hash = "sha256:fb33085c39dd998ac16d1431ebc293a8b3eedd00fd4a32de0ff79002c19511b4"}, + {file = "typing_extensions-4.5.0.tar.gz", hash = "sha256:5cb5f4a79139d699607b3ef622a1dedafa84e115ab0024e0d9c044a9479ca7cb"}, +] + [[package]] name = "ulid-py" version = "1.1.0" @@ -760,4 +774,4 @@ files = [ [metadata] lock-version = "2.0" python-versions = "^3.11" -content-hash = "a43dcab0548fc3be276e10ff19fe108211e5bdc42a8a161c744eeb4d20b14294" +content-hash = "e5420c03a1175d193f337682c451c648a1c41c6535a7ad7172a418f6c6b72e33" diff --git a/pyproject.toml b/pyproject.toml index 08c45e9..f044a77 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -14,6 +14,7 @@ ulid-py = "^1.1.0" databases = {extras = ["sqlite"], version = "^0.7.0"} uvicorn = "^0.21" httpx = "^0.23.3" +sqlalchemy = {version = "^1.4", extras = ["aiosqlite"]} [tool.poetry.group.dev] optional = true diff --git a/scripts/tests b/scripts/tests index df8b5a0..e16acd9 100755 --- a/scripts/tests +++ b/scripts/tests @@ -10,6 +10,6 @@ trap 'rm "$dbfile"' EXIT TERM INT QUIT [ -z "${DEBUG:-}" ] || set -x -SQLALCHEMY_WARN_20=1 \ +export SQLALCHEMY_WARN_20=1 # XXX remove when we switched to SQLAlchemy 2.0 UNWIND_STORAGE="$dbfile" \ python -m pytest "$@" From f97c5c847294e32573dac417e5d8f2ab95c9cf57 Mon Sep 17 00:00:00 2001 From: ducklet Date: Sat, 18 Mar 2023 23:30:40 +0100 Subject: [PATCH 03/31] add some route tests --- tests/test_web.py | 216 +++++++++++++++++++++++++++++++++++++++++++--- unwind/models.py | 15 +++- unwind/web.py | 10 +-- 3 files changed, 221 insertions(+), 20 deletions(-) diff --git a/tests/test_web.py b/tests/test_web.py index 358c2a2..364edd5 100644 --- a/tests/test_web.py +++ b/tests/test_web.py @@ -1,22 +1,140 @@ +from datetime import datetime import pytest from starlette.testclient import TestClient -from unwind import create_app, db, imdb, models +from unwind import config, create_app, db, imdb, models app = create_app() +@pytest.fixture(scope="module") +def unauthorized_client() -> TestClient: + # https://www.starlette.io/testclient/ + return TestClient(app) + + +@pytest.fixture(scope="module") +def authorized_client() -> TestClient: + client = TestClient(app) + client.auth = "user1", "secret1" + return client + + +@pytest.fixture(scope="module") +def admin_client() -> TestClient: + client = TestClient(app) + for token in config.api_credentials.values(): + break + else: + raise RuntimeError("No bearer tokens configured.") + client.headers = {"Authorization": f"Bearer {token}"} + return client + + @pytest.mark.asyncio -async def test_app(shared_conn: db.Database): +async def test_get_ratings_for_group( + shared_conn: db.Database, unauthorized_client: TestClient +): + user = models.User( + imdb_id="ur12345678", + name="user-1", + secret="secret-1", + groups=[], + ) + group = models.Group( + name="group-1", + users=[models.GroupUser(id=str(user.id), name=user.name)], + ) + user.groups = [models.UserGroup(id=str(group.id), access="r")] + path = app.url_path_for("get_ratings_for_group", group_id=str(group.id)) async with shared_conn.transaction(force_rollback=True): - # https://www.starlette.io/testclient/ - client = TestClient(app) - response = client.get("/api/v1/movies") + resp = unauthorized_client.get(path) + assert resp.status_code == 404, "Group does not exist (yet)" + + await db.add(user) + await db.add(group) + + resp = unauthorized_client.get(path) + assert resp.status_code == 200 + assert resp.json() == [] + + movie = models.Movie( + title="test movie", + release_year=2013, + media_type="Movie", + imdb_id="tt12345678", + genres={"genre-1"}, + ) + await db.add(movie) + + rating = models.Rating( + movie_id=movie.id, user_id=user.id, score=66, rating_date=datetime.now() + ) + await db.add(rating) + + rating_aggregate = { + "canonical_title": movie.title, + "imdb_score": movie.imdb_score, + "imdb_votes": movie.imdb_votes, + "link": imdb.movie_url(movie.imdb_id), + "media_type": movie.media_type, + "original_title": movie.original_title, + "user_scores": [rating.score], + "year": movie.release_year, + } + + resp = unauthorized_client.get(path) + assert resp.status_code == 200 + assert resp.json() == [rating_aggregate] + + filters = { + "imdb_id": movie.imdb_id, + "unwind_id": str(movie.id), + "title": movie.title, + "media_type": movie.media_type, + "year": movie.release_year, + } + for k, v in filters.items(): + resp = unauthorized_client.get(path, params={k: v}) + assert resp.status_code == 200 + assert resp.json() == [rating_aggregate] + + resp = unauthorized_client.get(path, params={"title": "no such thing"}) + assert resp.status_code == 200 + assert resp.json() == [] + + # Test "exact" query param. + resp = unauthorized_client.get( + path, params={"title": "test movie", "exact": "true"} + ) + assert resp.status_code == 200 + assert resp.json() == [rating_aggregate] + resp = unauthorized_client.get( + path, params={"title": "te mo", "exact": "false"} + ) + assert resp.status_code == 200 + assert resp.json() == [rating_aggregate] + resp = unauthorized_client.get(path, params={"title": "te mo", "exact": "true"}) + assert resp.status_code == 200 + assert resp.json() == [] + + # XXX Test "ignore_tv_episodes" query param. + # XXX Test "include_unrated" query param. + # XXX Test "per_page" query param. + + +@pytest.mark.asyncio +async def test_list_movies( + shared_conn: db.Database, + unauthorized_client: TestClient, + authorized_client: TestClient, +): + path = app.url_path_for("list_movies") + async with shared_conn.transaction(force_rollback=True): + response = unauthorized_client.get(path) assert response.status_code == 403 - client.auth = "user1", "secret1" - - response = client.get("/api/v1/movies") + response = authorized_client.get(path) assert response.status_code == 200 assert response.json() == [] @@ -29,7 +147,7 @@ async def test_app(shared_conn: db.Database): ) await db.add(m) - response = client.get("/api/v1/movies", params={"include_unrated": 1}) + response = authorized_client.get(path, params={"include_unrated": 1}) assert response.status_code == 200 assert response.json() == [{**models.asplain(m), "user_scores": []}] @@ -44,10 +162,86 @@ async def test_app(shared_conn: db.Database): "year": m.release_year, } - response = client.get("/api/v1/movies", params={"imdb_id": m.imdb_id}) + response = authorized_client.get(path, params={"imdb_id": m.imdb_id}) assert response.status_code == 200 assert response.json() == [m_plain] - response = client.get("/api/v1/movies", params={"unwind_id": str(m.id)}) + response = authorized_client.get(path, params={"unwind_id": str(m.id)}) + assert response.status_code == 200 + assert response.json() == [m_plain] + + +@pytest.mark.asyncio +async def test_list_users( + shared_conn: db.Database, + unauthorized_client: TestClient, + authorized_client: TestClient, + admin_client: TestClient, +): + path = app.url_path_for("list_users") + async with shared_conn.transaction(force_rollback=True): + response = unauthorized_client.get(path) + assert response.status_code == 403 + + response = authorized_client.get(path) + assert response.status_code == 403 + + response = admin_client.get(path) + assert response.status_code == 200 + assert response.json() == [] + + m = models.User( + imdb_id="ur12345678", + name="user-1", + secret="secret-1", + groups=[], + ) + await db.add(m) + + m_plain = { + "groups": m.groups, + "id": m.id, + "imdb_id": m.imdb_id, + "name": m.name, + "secret": m.secret, + } + + response = admin_client.get(path) + assert response.status_code == 200 + assert response.json() == [m_plain] + + +@pytest.mark.asyncio +async def test_list_groups( + shared_conn: db.Database, + unauthorized_client: TestClient, + authorized_client: TestClient, + admin_client: TestClient, +): + path = app.url_path_for("list_groups") + async with shared_conn.transaction(force_rollback=True): + response = unauthorized_client.get(path) + assert response.status_code == 403 + + response = authorized_client.get(path) + assert response.status_code == 403 + + response = admin_client.get(path) + assert response.status_code == 200 + assert response.json() == [] + + m = models.Group( + name="group-1", + users=[models.GroupUser(id="123", name="itsa-me")], + ) + await db.add(m) + + m_plain = { + "users": m.users, + "id": m.id, + "name": m.name, + } + + response = admin_client.get(path) assert response.status_code == 200 assert response.json() == [m_plain] diff --git a/unwind/models.py b/unwind/models.py index 4480307..0bf489d 100644 --- a/unwind/models.py +++ b/unwind/models.py @@ -13,6 +13,7 @@ from typing import ( Mapping, Type, TypeVar, + TypedDict, Union, get_args, get_origin, @@ -331,6 +332,11 @@ Access = Literal[ ] +class UserGroup(TypedDict): + id: str + access: Access + + @dataclass class User: _table: ClassVar[str] = "users" @@ -339,7 +345,7 @@ class User: imdb_id: str = None name: str = None # canonical user name secret: str = None - groups: list[dict[str, str]] = field(default_factory=list) + groups: list[UserGroup] = field(default_factory=list) def has_access(self, group_id: ULID | str, access: Access = "r"): group_id = group_id if isinstance(group_id, str) else str(group_id) @@ -355,10 +361,15 @@ class User: self.groups.append({"id": group_id, "access": access}) +class GroupUser(TypedDict): + id: str + name: str + + @dataclass class Group: _table: ClassVar[str] = "groups" id: ULID = field(default_factory=ULID) name: str = None - users: list[dict[str, str]] = field(default_factory=list) + users: list[GroupUser] = field(default_factory=list) diff --git a/unwind/web.py b/unwind/web.py index eb08e9c..3ebbcdc 100644 --- a/unwind/web.py +++ b/unwind/web.py @@ -179,7 +179,7 @@ async def auth_user(request) -> User | None: return user -_routes = [] +_routes: list[Route] = [] def route(path: str, *, methods: list[str] | None = None, **kwds): @@ -191,15 +191,11 @@ def route(path: str, *, methods: list[str] | None = None, **kwds): return decorator -route.registered = _routes - - @route("/groups/{group_id}/ratings") async def get_ratings_for_group(request): group_id = as_ulid(request.path_params["group_id"]) - group = await db.get(Group, id=str(group_id)) - if not group: + if (group := await db.get(Group, id=str(group_id))) is None: return not_found() user_ids = {u["id"] for u in group.users} @@ -632,7 +628,7 @@ def create_app(): return Starlette( lifespan=lifespan, routes=[ - Mount(f"{config.api_base}v1", routes=route.registered), + Mount(f"{config.api_base}v1", routes=_routes), ], middleware=[ Middleware(ResponseTimeMiddleware, header_name="Unwind-Elapsed"), From 50158150970644cf49826143692538f06f03d571 Mon Sep 17 00:00:00 2001 From: ducklet Date: Sat, 18 Mar 2023 23:51:40 +0100 Subject: [PATCH 04/31] add SQLAlchemy table definitions to models --- unwind/models.py | 67 ++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 67 insertions(+) diff --git a/unwind/models.py b/unwind/models.py index 0bf489d..964ac01 100644 --- a/unwind/models.py +++ b/unwind/models.py @@ -19,6 +19,9 @@ from typing import ( get_origin, ) +from sqlalchemy import Column, ForeignKey, Integer, String, Table +from sqlalchemy.orm import registry + from .types import ULID JSON = int | float | str | None | list["JSON"] | dict[str, "JSON"] @@ -26,6 +29,8 @@ JSONObject = dict[str, JSON] T = TypeVar("T") +mapper_registry = registry() + def annotations(tp: Type) -> tuple | None: return tp.__metadata__ if hasattr(tp, "__metadata__") else None @@ -198,8 +203,19 @@ def utcnow(): return datetime.utcnow().replace(tzinfo=timezone.utc) +@mapper_registry.mapped @dataclass class Progress: + __table__ = Table( + "progress", + mapper_registry.metadata, + Column("id", String, primary_key=True), # ULID + Column("type", String, nullable=False), + Column("state", String, nullable=False), # JSON {"percent": ..., "error": ...} + Column("started", String, nullable=False), # datetime + Column("stopped", String), + ) + _table: ClassVar[str] = "progress" id: ULID = field(default_factory=ULID) @@ -237,8 +253,26 @@ class Progress: self._state = state +@mapper_registry.mapped @dataclass class Movie: + __table__ = Table( + "movies", + mapper_registry.metadata, + Column("id", String, primary_key=True), # ULID + Column("title", String, nullable=False), + Column("original_title", String), + Column("release_year", Integer, nullable=False), + Column("media_type", String, nullable=False), + Column("imdb_id", String, nullable=False, unique=True), + Column("imdb_score", Integer), + Column("imdb_votes", Integer), + Column("runtime", Integer), + Column("genres", String, nullable=False), + Column("created", String, nullable=False), # datetime + Column("updated", String, nullable=False), # datetime + ) + _table: ClassVar[str] = "movies" id: ULID = field(default_factory=ULID) @@ -295,8 +329,21 @@ The contents of the Relation are ignored or discarded when using Relation = Annotated[T | None, _RelationSentinel] +@mapper_registry.mapped @dataclass class Rating: + __table__ = Table( + "ratings", + mapper_registry.metadata, + Column("id", String, primary_key=True), # ULID + Column("movie_id", ForeignKey("movies.id"), nullable=False), # ULID + Column("user_id", ForeignKey("users.id"), nullable=False), # ULID + Column("score", Integer, nullable=False), + Column("rating_date", String, nullable=False), # datetime + Column("favorite", Integer), # bool + Column("finished", Integer), # bool + ) + _table: ClassVar[str] = "ratings" id: ULID = field(default_factory=ULID) @@ -337,8 +384,19 @@ class UserGroup(TypedDict): access: Access +@mapper_registry.mapped @dataclass class User: + __table__ = Table( + "users", + mapper_registry.metadata, + Column("id", String, primary_key=True), # ULID + Column("imdb_id", String, nullable=False, unique=True), + Column("name", String, nullable=False), + Column("secret", String, nullable=False), + Column("groups", String, nullable=False), # JSON array + ) + _table: ClassVar[str] = "users" id: ULID = field(default_factory=ULID) @@ -366,8 +424,17 @@ class GroupUser(TypedDict): name: str +@mapper_registry.mapped @dataclass class Group: + __table__ = Table( + "groups", + mapper_registry.metadata, + Column("id", String, primary_key=True), # ULID + Column("name", String, nullable=False), + Column("users", String, nullable=False), # JSON array + ) + _table: ClassVar[str] = "groups" id: ULID = field(default_factory=ULID) From 1dd7bab4aad8c5a3485e478ec53529ce418559d8 Mon Sep 17 00:00:00 2001 From: ducklet Date: Sun, 19 Mar 2023 22:36:33 +0100 Subject: [PATCH 05/31] migrate `db.get_all` to pure SQLAlchemy --- tests/test_db.py | 36 ++++++++++++++++++++++++++++++++++++ unwind/db.py | 18 +++++++++--------- 2 files changed, 45 insertions(+), 9 deletions(-) diff --git a/tests/test_db.py b/tests/test_db.py index ac8e64b..4670ffe 100644 --- a/tests/test_db.py +++ b/tests/test_db.py @@ -5,6 +5,42 @@ import pytest from unwind import db, models, web_models +@pytest.mark.asyncio +async def test_get_all(shared_conn: db.Database): + async with shared_conn.transaction(force_rollback=True): + m1 = models.Movie( + title="test movie", + release_year=2013, + media_type="Movie", + imdb_id="tt0000000", + genres={"genre-1"}, + ) + await db.add(m1) + + m2 = models.Movie( + title="test movie", + release_year=2013, + media_type="Movie", + imdb_id="tt0000001", + genres={"genre-1"}, + ) + await db.add(m2) + + m3 = models.Movie( + title="test movie", + release_year=2014, + media_type="Movie", + imdb_id="tt0000002", + genres={"genre-1"}, + ) + await db.add(m3) + + assert [] == list(await db.get_all(models.Movie, id="blerp")) + assert [m1] == list(await db.get_all(models.Movie, id=str(m1.id))) + assert [m1, m2] == list(await db.get_all(models.Movie, release_year=2013)) + assert [m1, m2, m3] == list(await db.get_all(models.Movie)) + + @pytest.mark.asyncio async def test_add_and_get(shared_conn: db.Database): async with shared_conn.transaction(force_rollback=True): diff --git a/unwind/db.py b/unwind/db.py index c07b3a9..c3e4a0e 100644 --- a/unwind/db.py +++ b/unwind/db.py @@ -6,7 +6,7 @@ import threading from pathlib import Path from typing import Any, Iterable, Literal, Type, TypeVar -import sqlalchemy +import sqlalchemy as sa from databases import Database from . import config @@ -286,14 +286,14 @@ async def get_many(model: Type[ModelType], **kwds) -> Iterable[ModelType]: return (fromplain(model, row._mapping, serialized=True) for row in rows) -async def get_all(model: Type[ModelType], **kwds) -> Iterable[ModelType]: - values = {k: v for k, v in kwds.items() if v is not None} - - fields_ = ", ".join(f.name for f in fields(model)) - cond = " AND ".join(f"{k}=:{k}" for k in values) or "1=1" - query = f"SELECT {fields_} FROM {model._table} WHERE {cond}" +async def get_all(model: Type[ModelType], **field_values) -> Iterable[ModelType]: + """Return all items matching all given field value.""" + table: sa.Table = model.__table__ + query = sa.select(model).where( + *(table.c[k] == v for k, v in field_values.items() if v is not None) + ) async with locked_connection() as conn: - rows = await conn.fetch_all(query=query, values=values) + rows = await conn.fetch_all(query) return (fromplain(model, row._mapping, serialized=True) for row in rows) @@ -667,4 +667,4 @@ def bindparams(query: str, values: dict): return f":{pump_key}" pump_query = re.sub(r":(\w+)\b", pump, query) - return sqlalchemy.text(pump_query).bindparams(**pump_vals) + return sa.text(pump_query).bindparams(**pump_vals) From f1bafbb4a26b20cd8baf27f8ead30a4ad76e4d3c Mon Sep 17 00:00:00 2001 From: ducklet Date: Sun, 19 Mar 2023 00:06:14 +0100 Subject: [PATCH 06/31] report code coverage for tests --- poetry.lock | 85 +++++++++++++++++++++++++++++++++++++++++++++++++- pyproject.toml | 1 + scripts/tests | 2 +- 3 files changed, 86 insertions(+), 2 deletions(-) diff --git a/poetry.lock b/poetry.lock index 7459b66..c82718e 100644 --- a/poetry.lock +++ b/poetry.lock @@ -173,6 +173,70 @@ files = [ {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"}, ] +[[package]] +name = "coverage" +version = "7.2.2" +description = "Code coverage measurement for Python" +category = "dev" +optional = false +python-versions = ">=3.7" +files = [ + {file = "coverage-7.2.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:c90e73bdecb7b0d1cea65a08cb41e9d672ac6d7995603d6465ed4914b98b9ad7"}, + {file = "coverage-7.2.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:e2926b8abedf750c2ecf5035c07515770944acf02e1c46ab08f6348d24c5f94d"}, + {file = "coverage-7.2.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:57b77b9099f172804e695a40ebaa374f79e4fb8b92f3e167f66facbf92e8e7f5"}, + {file = "coverage-7.2.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:efe1c0adad110bf0ad7fb59f833880e489a61e39d699d37249bdf42f80590169"}, + {file = "coverage-7.2.2-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2199988e0bc8325d941b209f4fd1c6fa007024b1442c5576f1a32ca2e48941e6"}, + {file = "coverage-7.2.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:81f63e0fb74effd5be736cfe07d710307cc0a3ccb8f4741f7f053c057615a137"}, + {file = "coverage-7.2.2-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:186e0fc9cf497365036d51d4d2ab76113fb74f729bd25da0975daab2e107fd90"}, + {file = "coverage-7.2.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:420f94a35e3e00a2b43ad5740f935358e24478354ce41c99407cddd283be00d2"}, + {file = "coverage-7.2.2-cp310-cp310-win32.whl", hash = "sha256:38004671848b5745bb05d4d621526fca30cee164db42a1f185615f39dc997292"}, + {file = "coverage-7.2.2-cp310-cp310-win_amd64.whl", hash = "sha256:0ce383d5f56d0729d2dd40e53fe3afeb8f2237244b0975e1427bfb2cf0d32bab"}, + {file = "coverage-7.2.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:3eb55b7b26389dd4f8ae911ba9bc8c027411163839dea4c8b8be54c4ee9ae10b"}, + {file = "coverage-7.2.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:d2b96123a453a2d7f3995ddb9f28d01fd112319a7a4d5ca99796a7ff43f02af5"}, + {file = "coverage-7.2.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:299bc75cb2a41e6741b5e470b8c9fb78d931edbd0cd009c58e5c84de57c06731"}, + {file = "coverage-7.2.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5e1df45c23d4230e3d56d04414f9057eba501f78db60d4eeecfcb940501b08fd"}, + {file = "coverage-7.2.2-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:006ed5582e9cbc8115d2e22d6d2144a0725db542f654d9d4fda86793832f873d"}, + {file = "coverage-7.2.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:d683d230b5774816e7d784d7ed8444f2a40e7a450e5720d58af593cb0b94a212"}, + {file = "coverage-7.2.2-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:8efb48fa743d1c1a65ee8787b5b552681610f06c40a40b7ef94a5b517d885c54"}, + {file = "coverage-7.2.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:4c752d5264053a7cf2fe81c9e14f8a4fb261370a7bb344c2a011836a96fb3f57"}, + {file = "coverage-7.2.2-cp311-cp311-win32.whl", hash = "sha256:55272f33da9a5d7cccd3774aeca7a01e500a614eaea2a77091e9be000ecd401d"}, + {file = "coverage-7.2.2-cp311-cp311-win_amd64.whl", hash = "sha256:92ebc1619650409da324d001b3a36f14f63644c7f0a588e331f3b0f67491f512"}, + {file = "coverage-7.2.2-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:5afdad4cc4cc199fdf3e18088812edcf8f4c5a3c8e6cb69127513ad4cb7471a9"}, + {file = "coverage-7.2.2-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0484d9dd1e6f481b24070c87561c8d7151bdd8b044c93ac99faafd01f695c78e"}, + {file = "coverage-7.2.2-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d530191aa9c66ab4f190be8ac8cc7cfd8f4f3217da379606f3dd4e3d83feba69"}, + {file = "coverage-7.2.2-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4ac0f522c3b6109c4b764ffec71bf04ebc0523e926ca7cbe6c5ac88f84faced0"}, + {file = "coverage-7.2.2-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:ba279aae162b20444881fc3ed4e4f934c1cf8620f3dab3b531480cf602c76b7f"}, + {file = "coverage-7.2.2-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:53d0fd4c17175aded9c633e319360d41a1f3c6e352ba94edcb0fa5167e2bad67"}, + {file = "coverage-7.2.2-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:8c99cb7c26a3039a8a4ee3ca1efdde471e61b4837108847fb7d5be7789ed8fd9"}, + {file = "coverage-7.2.2-cp37-cp37m-win32.whl", hash = "sha256:5cc0783844c84af2522e3a99b9b761a979a3ef10fb87fc4048d1ee174e18a7d8"}, + {file = "coverage-7.2.2-cp37-cp37m-win_amd64.whl", hash = "sha256:817295f06eacdc8623dc4df7d8b49cea65925030d4e1e2a7c7218380c0072c25"}, + {file = "coverage-7.2.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:6146910231ece63facfc5984234ad1b06a36cecc9fd0c028e59ac7c9b18c38c6"}, + {file = "coverage-7.2.2-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:387fb46cb8e53ba7304d80aadca5dca84a2fbf6fe3faf6951d8cf2d46485d1e5"}, + {file = "coverage-7.2.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:046936ab032a2810dcaafd39cc4ef6dd295df1a7cbead08fe996d4765fca9fe4"}, + {file = "coverage-7.2.2-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e627dee428a176ffb13697a2c4318d3f60b2ccdde3acdc9b3f304206ec130ccd"}, + {file = "coverage-7.2.2-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4fa54fb483decc45f94011898727802309a109d89446a3c76387d016057d2c84"}, + {file = "coverage-7.2.2-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:3668291b50b69a0c1ef9f462c7df2c235da3c4073f49543b01e7eb1dee7dd540"}, + {file = "coverage-7.2.2-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:7c20b731211261dc9739bbe080c579a1835b0c2d9b274e5fcd903c3a7821cf88"}, + {file = "coverage-7.2.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:5764e1f7471cb8f64b8cda0554f3d4c4085ae4b417bfeab236799863703e5de2"}, + {file = "coverage-7.2.2-cp38-cp38-win32.whl", hash = "sha256:4f01911c010122f49a3e9bdc730eccc66f9b72bd410a3a9d3cb8448bb50d65d3"}, + {file = "coverage-7.2.2-cp38-cp38-win_amd64.whl", hash = "sha256:c448b5c9e3df5448a362208b8d4b9ed85305528313fca1b479f14f9fe0d873b8"}, + {file = "coverage-7.2.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:bfe7085783cda55e53510482fa7b5efc761fad1abe4d653b32710eb548ebdd2d"}, + {file = "coverage-7.2.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:9d22e94e6dc86de981b1b684b342bec5e331401599ce652900ec59db52940005"}, + {file = "coverage-7.2.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:507e4720791977934bba016101579b8c500fb21c5fa3cd4cf256477331ddd988"}, + {file = "coverage-7.2.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:bc4803779f0e4b06a2361f666e76f5c2e3715e8e379889d02251ec911befd149"}, + {file = "coverage-7.2.2-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:db8c2c5ace167fd25ab5dd732714c51d4633f58bac21fb0ff63b0349f62755a8"}, + {file = "coverage-7.2.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:4f68ee32d7c4164f1e2c8797535a6d0a3733355f5861e0f667e37df2d4b07140"}, + {file = "coverage-7.2.2-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:d52f0a114b6a58305b11a5cdecd42b2e7f1ec77eb20e2b33969d702feafdd016"}, + {file = "coverage-7.2.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:797aad79e7b6182cb49c08cc5d2f7aa7b2128133b0926060d0a8889ac43843be"}, + {file = "coverage-7.2.2-cp39-cp39-win32.whl", hash = "sha256:db45eec1dfccdadb179b0f9ca616872c6f700d23945ecc8f21bb105d74b1c5fc"}, + {file = "coverage-7.2.2-cp39-cp39-win_amd64.whl", hash = "sha256:8dbe2647bf58d2c5a6c5bcc685f23b5f371909a5624e9f5cd51436d6a9f6c6ef"}, + {file = "coverage-7.2.2-pp37.pp38.pp39-none-any.whl", hash = "sha256:872d6ce1f5be73f05bea4df498c140b9e7ee5418bfa2cc8204e7f9b817caa968"}, + {file = "coverage-7.2.2.tar.gz", hash = "sha256:36dd42da34fe94ed98c39887b86db9d06777b1c8f860520e21126a75507024f2"}, +] + +[package.extras] +toml = ["tomli"] + [[package]] name = "databases" version = "0.7.0" @@ -550,6 +614,25 @@ pytest = ">=6.1.0" docs = ["sphinx (>=5.3)", "sphinx-rtd-theme (>=1.0)"] testing = ["coverage (>=6.2)", "flaky (>=3.5.0)", "hypothesis (>=5.7.1)", "mypy (>=0.931)", "pytest-trio (>=0.7.0)"] +[[package]] +name = "pytest-cov" +version = "4.0.0" +description = "Pytest plugin for measuring coverage." +category = "dev" +optional = false +python-versions = ">=3.6" +files = [ + {file = "pytest-cov-4.0.0.tar.gz", hash = "sha256:996b79efde6433cdbd0088872dbc5fb3ed7fe1578b68cdbba634f14bb8dd0470"}, + {file = "pytest_cov-4.0.0-py3-none-any.whl", hash = "sha256:2feb1b751d66a8bd934e5edfa2e961d11309dc37b73b0eabe73b5945fee20f6b"}, +] + +[package.dependencies] +coverage = {version = ">=5.2.1", extras = ["toml"]} +pytest = ">=4.6" + +[package.extras] +testing = ["fields", "hunter", "process-tests", "pytest-xdist", "six", "virtualenv"] + [[package]] name = "rfc3986" version = "1.5.0" @@ -774,4 +857,4 @@ files = [ [metadata] lock-version = "2.0" python-versions = "^3.11" -content-hash = "e5420c03a1175d193f337682c451c648a1c41c6535a7ad7172a418f6c6b72e33" +content-hash = "c9f9a32904d3bcbbb8a43e37a811d17aee21ff8b0e22bc2ed01ff309470d1c82" diff --git a/pyproject.toml b/pyproject.toml index f044a77..167c314 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -26,6 +26,7 @@ pyright = "*" black = "*" isort = "*" pytest-asyncio = "*" +pytest-cov = "*" [build-system] requires = ["poetry-core>=1.0.0"] diff --git a/scripts/tests b/scripts/tests index e16acd9..8261f1e 100755 --- a/scripts/tests +++ b/scripts/tests @@ -12,4 +12,4 @@ trap 'rm "$dbfile"' EXIT TERM INT QUIT export SQLALCHEMY_WARN_20=1 # XXX remove when we switched to SQLAlchemy 2.0 UNWIND_STORAGE="$dbfile" \ - python -m pytest "$@" + python -m pytest --cov "$@" From a444909b1f71468c007e7c1857a41bb3398ac757 Mon Sep 17 00:00:00 2001 From: ducklet Date: Sun, 19 Mar 2023 22:59:08 +0100 Subject: [PATCH 07/31] minor refactoring --- tests/test_db.py | 71 +++++++++++++++++------------------------------- 1 file changed, 25 insertions(+), 46 deletions(-) diff --git a/tests/test_db.py b/tests/test_db.py index 4670ffe..822cdab 100644 --- a/tests/test_db.py +++ b/tests/test_db.py @@ -4,35 +4,32 @@ import pytest from unwind import db, models, web_models +_movie_imdb_id = 1234567 + + +def a_movie(**kwds) -> models.Movie: + global _movie_imdb_id + _movie_imdb_id += 1 + args = { + "title": "test movie", + "release_year": 2013, + "media_type": "Movie", + "imdb_id": f"tt{_movie_imdb_id}", + "genres": {"genre-1"}, + } | kwds + return models.Movie(**args) + @pytest.mark.asyncio async def test_get_all(shared_conn: db.Database): async with shared_conn.transaction(force_rollback=True): - m1 = models.Movie( - title="test movie", - release_year=2013, - media_type="Movie", - imdb_id="tt0000000", - genres={"genre-1"}, - ) + m1 = a_movie() await db.add(m1) - m2 = models.Movie( - title="test movie", - release_year=2013, - media_type="Movie", - imdb_id="tt0000001", - genres={"genre-1"}, - ) + m2 = a_movie(release_year=m1.release_year) await db.add(m2) - m3 = models.Movie( - title="test movie", - release_year=2014, - media_type="Movie", - imdb_id="tt0000002", - genres={"genre-1"}, - ) + m3 = a_movie(release_year=m1.release_year + 1) await db.add(m3) assert [] == list(await db.get_all(models.Movie, id="blerp")) @@ -44,22 +41,10 @@ async def test_get_all(shared_conn: db.Database): @pytest.mark.asyncio async def test_add_and_get(shared_conn: db.Database): async with shared_conn.transaction(force_rollback=True): - m1 = models.Movie( - title="test movie", - release_year=2013, - media_type="Movie", - imdb_id="tt0000000", - genres={"genre-1"}, - ) + m1 = a_movie() await db.add(m1) - m2 = models.Movie( - title="test movie", - release_year=2013, - media_type="Movie", - imdb_id="tt0000001", - genres={"genre-1"}, - ) + m2 = a_movie() await db.add(m2) assert m1 == await db.get(models.Movie, id=str(m1.id)) @@ -69,30 +54,24 @@ async def test_add_and_get(shared_conn: db.Database): @pytest.mark.asyncio async def test_find_ratings(shared_conn: db.Database): async with shared_conn.transaction(force_rollback=True): - m1 = models.Movie( + m1 = a_movie( title="test movie", release_year=2013, - media_type="Movie", - imdb_id="tt0000000", genres={"genre-1"}, ) await db.add(m1) - m2 = models.Movie( + m2 = a_movie( title="it's anöther Movie, Part 2", release_year=2015, - media_type="Movie", - imdb_id="tt0000001", genres={"genre-2"}, ) await db.add(m2) - m3 = models.Movie( + m3 = a_movie( title="movie it's, Part 3", - release_year=2015, - media_type="Movie", - imdb_id="tt0000002", - genres={"genre-2"}, + release_year=m2.release_year, + genres=m2.genres, ) await db.add(m3) From af9c166124e5db8b6aec4f631e670706fa34abe0 Mon Sep 17 00:00:00 2001 From: ducklet Date: Sun, 19 Mar 2023 23:14:59 +0100 Subject: [PATCH 08/31] migrate `db.get_many` to SQLAlchemy --- tests/test_db.py | 29 ++++++++++++++++++++++++++++- unwind/db.py | 31 +++++++++++++++++-------------- 2 files changed, 45 insertions(+), 15 deletions(-) diff --git a/tests/test_db.py b/tests/test_db.py index 822cdab..37e27c9 100644 --- a/tests/test_db.py +++ b/tests/test_db.py @@ -34,10 +34,37 @@ async def test_get_all(shared_conn: db.Database): assert [] == list(await db.get_all(models.Movie, id="blerp")) assert [m1] == list(await db.get_all(models.Movie, id=str(m1.id))) - assert [m1, m2] == list(await db.get_all(models.Movie, release_year=2013)) + assert [m1, m2] == list( + await db.get_all(models.Movie, release_year=m1.release_year) + ) assert [m1, m2, m3] == list(await db.get_all(models.Movie)) +@pytest.mark.asyncio +async def test_get_many(shared_conn: db.Database): + async with shared_conn.transaction(force_rollback=True): + m1 = a_movie() + await db.add(m1) + + m2 = a_movie(release_year=m1.release_year) + await db.add(m2) + + m3 = a_movie(release_year=m1.release_year + 1) + await db.add(m3) + + assert [] == list(await db.get_many(models.Movie)), "selected nothing" + assert [m1] == list(await db.get_many(models.Movie, id=[str(m1.id)])) + assert [m1] == list(await db.get_many(models.Movie, id={str(m1.id)})) + assert [m1, m2] == list( + await db.get_many(models.Movie, release_year=[m1.release_year]) + ) + assert [m1, m2, m3] == list( + await db.get_many( + models.Movie, release_year=[m1.release_year, m3.release_year] + ) + ) + + @pytest.mark.asyncio async def test_add_and_get(shared_conn: db.Database): async with shared_conn.transaction(force_rollback=True): diff --git a/unwind/db.py b/unwind/db.py index c3e4a0e..ea09873 100644 --- a/unwind/db.py +++ b/unwind/db.py @@ -266,28 +266,31 @@ async def get( return fromplain(model, row._mapping, serialized=True) if row else None -async def get_many(model: Type[ModelType], **kwds) -> Iterable[ModelType]: - keys = { - k: [f"{k}_{i}" for i, _ in enumerate(vs, start=1)] for k, vs in kwds.items() - } +async def get_many( + model: Type[ModelType], **field_sets: set | list +) -> Iterable[ModelType]: + """Return the items with any values matching all given field sets. - if not keys: + This is similar to `get_all`, but instead of a scalar value a list of values + must be given. If any of the given values is set for that field on an item, + the item is considered a match. + If no field values are given, no items will be returned. + """ + if not field_sets: return [] - values = {n: v for k, vs in kwds.items() for n, v in zip(keys[k], vs)} - - fields_ = ", ".join(f.name for f in fields(model)) - cond = " AND ".join( - f"{k} IN ({','.join(':'+n for n in ns)})" for k, ns in keys.items() - ) - query = f"SELECT {fields_} FROM {model._table} WHERE {cond}" + table: sa.Table = model.__table__ + query = sa.select(model).where(*(table.c[k].in_(v) for k, v in field_sets.items())) async with locked_connection() as conn: - rows = await conn.fetch_all(query=query, values=values) + rows = await conn.fetch_all(query) return (fromplain(model, row._mapping, serialized=True) for row in rows) async def get_all(model: Type[ModelType], **field_values) -> Iterable[ModelType]: - """Return all items matching all given field value.""" + """Filter all items by comparing all given field values. + + If no filters are given, all items will be returned. + """ table: sa.Table = model.__table__ query = sa.select(model).where( *(table.c[k] == v for k, v in field_values.items() if v is not None) From 6f6354cface73988eadb032e4446b58233efc300 Mon Sep 17 00:00:00 2001 From: ducklet Date: Mon, 20 Mar 2023 21:37:50 +0100 Subject: [PATCH 09/31] migrate `db.get` to SQLAlchemy --- tests/test_db.py | 27 +++++++++++++++++++++++++++ unwind/db.py | 28 ++++++++++++++++++---------- 2 files changed, 45 insertions(+), 10 deletions(-) diff --git a/tests/test_db.py b/tests/test_db.py index 37e27c9..04cf8b0 100644 --- a/tests/test_db.py +++ b/tests/test_db.py @@ -20,6 +20,33 @@ def a_movie(**kwds) -> models.Movie: return models.Movie(**args) +@pytest.mark.asyncio +async def test_get(shared_conn: db.Database): + async with shared_conn.transaction(force_rollback=True): + m1 = a_movie() + await db.add(m1) + + m2 = a_movie(release_year=m1.release_year + 1) + await db.add(m2) + + assert None == await db.get(models.Movie) + assert None == await db.get(models.Movie, id="blerp") + assert m1 == await db.get(models.Movie, id=str(m1.id)) + assert m2 == await db.get(models.Movie, release_year=m2.release_year) + assert None == await db.get( + models.Movie, id=str(m1.id), release_year=m2.release_year + ) + assert m2 == await db.get( + models.Movie, id=str(m2.id), release_year=m2.release_year + ) + assert m1 == await db.get( + models.Movie, media_type=m1.media_type, order_by=("release_year", "asc") + ) + assert m2 == await db.get( + models.Movie, media_type=m1.media_type, order_by=("release_year", "desc") + ) + + @pytest.mark.asyncio async def test_get_all(shared_conn: db.Database): async with shared_conn.transaction(force_rollback=True): diff --git a/unwind/db.py b/unwind/db.py index ea09873..39d383e 100644 --- a/unwind/db.py +++ b/unwind/db.py @@ -132,7 +132,7 @@ async def apply_db_patches(db: Database): async def get_import_progress() -> Progress | None: """Return the latest import progress.""" - return await get(Progress, type="import-imdb-movies", order_by="started DESC") + return await get(Progress, type="import-imdb-movies", order_by=("started", "desc")) async def stop_import_progress(*, error: BaseException | None = None): @@ -244,25 +244,33 @@ ModelType = TypeVar("ModelType") async def get( - model: Type[ModelType], *, order_by: str | None = None, **kwds + model: Type[ModelType], + *, + order_by: tuple[str, Literal["asc", "desc"]] | None = None, + **field_values, ) -> ModelType | None: """Load a model instance from the database. - Passing `kwds` allows to filter the instance to load. You have to encode the + Passing `field_values` allows to filter the item to load. You have to encode the values as the appropriate data type for the database prior to passing them to this function. """ - values = {k: v for k, v in kwds.items() if v is not None} - if not values: + if not field_values: return - fields_ = ", ".join(f.name for f in fields(model)) - cond = " AND ".join(f"{k}=:{k}" for k in values) - query = f"SELECT {fields_} FROM {model._table} WHERE {cond}" + table: sa.Table = model.__table__ + query = sa.select(model).where( + *(table.c[k] == v for k, v in field_values.items() if v is not None) + ) if order_by: - query += f" ORDER BY {order_by}" + order_col, order_dir = order_by + query = query.order_by( + table.c[order_col].asc() + if order_dir == "asc" + else table.c[order_col].desc() + ) async with locked_connection() as conn: - row = await conn.fetch_one(query=query, values=values) + row = await conn.fetch_one(query) return fromplain(model, row._mapping, serialized=True) if row else None From b91fcd3f55942e41c46986086df10fdd377dc8a0 Mon Sep 17 00:00:00 2001 From: ducklet Date: Thu, 23 Mar 2023 22:49:17 +0100 Subject: [PATCH 10/31] migrate `db.add`, `db.update`, `db.remove` to SQLA --- tests/test_db.py | 25 +++++++++++++++++++++++++ unwind/db.py | 18 +++++++++--------- 2 files changed, 34 insertions(+), 9 deletions(-) diff --git a/tests/test_db.py b/tests/test_db.py index 04cf8b0..33fff26 100644 --- a/tests/test_db.py +++ b/tests/test_db.py @@ -105,6 +105,31 @@ async def test_add_and_get(shared_conn: db.Database): assert m2 == await db.get(models.Movie, id=str(m2.id)) +@pytest.mark.asyncio +async def test_update(shared_conn: db.Database): + async with shared_conn.transaction(force_rollback=True): + m = a_movie() + await db.add(m) + + assert m == await db.get(models.Movie, id=str(m.id)) + m.title += "something else" + assert m != await db.get(models.Movie, id=str(m.id)) + + await db.update(m) + assert m == await db.get(models.Movie, id=str(m.id)) + + +@pytest.mark.asyncio +async def test_remove(shared_conn: db.Database): + async with shared_conn.transaction(force_rollback=True): + m1 = a_movie() + await db.add(m1) + assert m1 == await db.get(models.Movie, id=str(m1.id)) + + await db.remove(m1) + assert None == await db.get(models.Movie, id=str(m1.id)) + + @pytest.mark.asyncio async def test_find_ratings(shared_conn: db.Database): async with shared_conn.transaction(force_rollback=True): diff --git a/unwind/db.py b/unwind/db.py index 39d383e..1f15bc9 100644 --- a/unwind/db.py +++ b/unwind/db.py @@ -232,12 +232,11 @@ async def add(item): if getattr(item, "_is_lazy", False): item._lazy_init() + table: sa.Table = item.__table__ values = asplain(item, serialize=True) - keys = ", ".join(f"{k}" for k in values) - placeholders = ", ".join(f":{k}" for k in values) - query = f"INSERT INTO {item._table} ({keys}) VALUES ({placeholders})" + stmt = table.insert().values(values) async with locked_connection() as conn: - await conn.execute(query=query, values=values) + await conn.execute(stmt) ModelType = TypeVar("ModelType") @@ -313,18 +312,19 @@ async def update(item): if getattr(item, "_is_lazy", False): item._lazy_init() + table: sa.Table = item.__table__ values = asplain(item, serialize=True) - keys = ", ".join(f"{k}=:{k}" for k in values if k != "id") - query = f"UPDATE {item._table} SET {keys} WHERE id=:id" + stmt = table.update().where(table.c.id == values["id"]).values(values) async with locked_connection() as conn: - await conn.execute(query=query, values=values) + await conn.execute(stmt) async def remove(item): + table: sa.Table = item.__table__ values = asplain(item, filter_fields={"id"}, serialize=True) - query = f"DELETE FROM {item._table} WHERE id=:id" + stmt = table.delete().where(table.c.id == values["id"]) async with locked_connection() as conn: - await conn.execute(query=query, values=values) + await conn.execute(stmt) async def add_or_update_user(user: User): From d4933bf1a6a0af65c9e4cbdb3b372e14cf3acc83 Mon Sep 17 00:00:00 2001 From: ducklet Date: Thu, 23 Mar 2023 23:33:59 +0100 Subject: [PATCH 11/31] migrate `db.ratings_for_movie_ids` to SQLAlchemy --- unwind/db.py | 47 ++++++++++++++++++++++------------------------- 1 file changed, 22 insertions(+), 25 deletions(-) diff --git a/unwind/db.py b/unwind/db.py index 1f15bc9..b91d3c1 100644 --- a/unwind/db.py +++ b/unwind/db.py @@ -505,40 +505,37 @@ async def find_ratings( async def ratings_for_movie_ids( ids: Iterable[ULID | str] = [], imdb_ids: Iterable[str] = [] ) -> Iterable[dict[str, Any]]: - conds: list[str] = [] - vals: dict[str, str] = {} + conds = [] + + ratings = Rating.__table__ + movies = Movie.__table__ if ids: - sqlin, sqlin_vals = sql_in(f"{Movie._table}.id", (str(x) for x in ids)) - conds.append(sqlin) - vals.update(sqlin_vals) + conds.append(movies.c.id.in_([str(x) for x in ids])) if imdb_ids: - sqlin, sqlin_vals = sql_in(f"{Movie._table}.imdb_id", imdb_ids) - conds.append(sqlin) - vals.update(sqlin_vals) + conds.append(movies.c.imdb_id.in_(imdb_ids)) if not conds: return [] - query = f""" - SELECT - {Rating._table}.score AS user_score, - {Rating._table}.user_id AS user_id, - {Movie._table}.imdb_score, - {Movie._table}.imdb_votes, - {Movie._table}.imdb_id AS movie_imdb_id, - {Movie._table}.media_type AS media_type, - {Movie._table}.title AS canonical_title, - {Movie._table}.original_title AS original_title, - {Movie._table}.release_year AS release_year - FROM {Movie._table} - LEFT JOIN {Rating._table} ON {Movie._table}.id={Rating._table}.movie_id - WHERE {(' OR '.join(conds))} - """ - + query = ( + sa.select( + ratings.c.score.label("user_score"), + ratings.c.user_id.label("user_id"), + movies.c.imdb_score, + movies.c.imdb_votes, + movies.c.imdb_id.label("movie_imdb_id"), + movies.c.media_type.label("media_type"), + movies.c.title.label("canonical_title"), + movies.c.original_title.label("original_title"), + movies.c.release_year.label("release_year"), + ) + .outerjoin_from(movies, ratings, movies.c.id == ratings.c.movie_id) + .where(sa.or_(*conds)) + ) async with locked_connection() as conn: - rows = await conn.fetch_all(bindparams(query, vals)) + rows = await conn.fetch_all(query) return tuple(dict(r._mapping) for r in rows) From 1a3528e096b4ca623cb9cb9bc14c99c63693e659 Mon Sep 17 00:00:00 2001 From: ducklet Date: Tue, 28 Mar 2023 21:50:14 +0200 Subject: [PATCH 12/31] migrate `db.find_ratings` to SQLAlchemy --- tests/test_web.py | 1 + unwind/db.py | 102 ++++++++++++++++++++++------------------------ unwind/models.py | 6 ++- 3 files changed, 54 insertions(+), 55 deletions(-) diff --git a/tests/test_web.py b/tests/test_web.py index 364edd5..5a4c3c5 100644 --- a/tests/test_web.py +++ b/tests/test_web.py @@ -1,4 +1,5 @@ from datetime import datetime + import pytest from starlette.testclient import TestClient diff --git a/unwind/db.py b/unwind/db.py index b91d3c1..34abd81 100644 --- a/unwind/db.py +++ b/unwind/db.py @@ -18,7 +18,9 @@ from .models import ( asplain, fields, fromplain, + movies, optional_fields, + ratings, utcnow, ) from .types import ULID @@ -427,77 +429,72 @@ async def find_ratings( limit_rows: int = 10, user_ids: Iterable[str] = [], ): - values: dict[str, int | str] = { - "limit_rows": limit_rows, - } - conditions = [] + if title: - values["escape"] = "#" - escaped_title = sql_escape(title, char=values["escape"]) - values["pattern"] = ( + escape_char = "#" + escaped_title = sql_escape(title, char=escape_char) + pattern = ( "_".join(escaped_title.split()) if exact else "%" + "%".join(escaped_title.split()) + "%" ) conditions.append( - f""" - ( - {Movie._table}.title LIKE :pattern ESCAPE :escape - OR {Movie._table}.original_title LIKE :pattern ESCAPE :escape + sa.or_( + movies.c.title.like(pattern, escape=escape_char), + movies.c.original_title.like(pattern, escape=escape_char), ) - """ ) - if yearcomp: - op, year = yearcomp - assert op in "<=>" - values["year"] = year - conditions.append(f"{Movie._table}.release_year{op}:year") + match yearcomp: + case ("<", year): + conditions.append(movies.c.release_year < year) + case ("=", year): + conditions.append(movies.c.release_year == year) + case (">", year): + conditions.append(movies.c.release_year > year) - if media_type: - values["media_type"] = media_type - conditions.append(f"{Movie._table}.media_type=:media_type") + if media_type is not None: + conditions.append(movies.c.media_type == media_type) if ignore_tv_episodes: - conditions.append(f"{Movie._table}.media_type!='TV Episode'") + conditions.append(movies.c.media_type != "TV Episode") - user_condition = "1=1" + user_condition = [] if user_ids: - uvs = {f"user_id_{i}": v for i, v in enumerate(user_ids, start=1)} - values.update(uvs) - user_condition = f"{Rating._table}.user_id IN ({','.join(':'+n for n in uvs)})" + user_condition.append(ratings.c.user_id.in_(user_ids)) - query = f""" - SELECT DISTINCT {Rating._table}.movie_id - FROM {Rating._table} - LEFT JOIN {Movie._table} ON {Movie._table}.id={Rating._table}.movie_id - WHERE {user_condition}{(' AND ' + ' AND '.join(conditions)) if conditions else ''} - ORDER BY length({Movie._table}.title) ASC, {Rating._table}.rating_date DESC, {Movie._table}.imdb_score DESC - LIMIT :limit_rows - """ + query = ( + sa.select(ratings.c.movie_id) + .distinct() + .outerjoin_from(ratings, movies, movies.c.id == ratings.c.movie_id) + .where(*conditions, *user_condition) + .order_by( + sa.func.length(movies.c.title).asc(), + ratings.c.rating_date.desc(), + movies.c.imdb_score.desc(), + ) + .limit(limit_rows) + ) async with locked_connection() as conn: - rows = await conn.fetch_all(bindparams(query, values)) - movie_ids = tuple(r._mapping["movie_id"] for r in rows) + rows = conn.iterate(query) + movie_ids = [r.movie_id async for r in rows] if include_unrated and len(movie_ids) < limit_rows: - sqlin, sqlin_vals = sql_in("id", movie_ids, not_=True) - query = f""" - SELECT DISTINCT id AS movie_id - FROM {Movie._table} - WHERE {sqlin} - {('AND ' + ' AND '.join(conditions)) if conditions else ''} - ORDER BY length(title) ASC, imdb_score DESC, release_year DESC - LIMIT :limit_rows - """ - async with locked_connection() as conn: - rows = await conn.fetch_all( - bindparams( - query, - {**values, **sqlin_vals, "limit_rows": limit_rows - len(movie_ids)}, - ) + query = ( + sa.select(movies.c.id.label("movie_id")) + .distinct() + .where(movies.c.id.not_in(movie_ids), *conditions) + .order_by( + sa.func.length(movies.c.title).asc(), + movies.c.imdb_score.desc(), + movies.c.release_year.desc(), ) - movie_ids += tuple(r._mapping["movie_id"] for r in rows) + .limit(limit_rows - len(movie_ids)) + ) + async with locked_connection() as conn: + rows = conn.iterate(query) + movie_ids += [r.movie_id async for r in rows] return await ratings_for_movie_ids(ids=movie_ids) @@ -507,9 +504,6 @@ async def ratings_for_movie_ids( ) -> Iterable[dict[str, Any]]: conds = [] - ratings = Rating.__table__ - movies = Movie.__table__ - if ids: conds.append(movies.c.id.in_([str(x) for x in ids])) diff --git a/unwind/models.py b/unwind/models.py index 964ac01..6d40b35 100644 --- a/unwind/models.py +++ b/unwind/models.py @@ -12,8 +12,8 @@ from typing import ( Literal, Mapping, Type, - TypeVar, TypedDict, + TypeVar, Union, get_args, get_origin, @@ -318,6 +318,8 @@ class Movie: self._is_lazy = False +movies = Movie.__table__ + _RelationSentinel = object() """Mark a model field as containing external data. @@ -372,6 +374,8 @@ class Rating: ) +ratings = Rating.__table__ + Access = Literal[ "r", # read "i", # index From 1fd7e730b349f48f07c1961644e83363f9ee0dfe Mon Sep 17 00:00:00 2001 From: ducklet Date: Tue, 28 Mar 2023 00:23:37 +0200 Subject: [PATCH 13/31] migrate `db.ratings_for_movies` to SQLAlchemy --- tests/test_db.py | 66 ++++++++++++++++++++++++++++++++++++++++++++++++ unwind/db.py | 28 +++----------------- 2 files changed, 70 insertions(+), 24 deletions(-) diff --git a/tests/test_db.py b/tests/test_db.py index 33fff26..92d75ae 100644 --- a/tests/test_db.py +++ b/tests/test_db.py @@ -250,3 +250,69 @@ async def test_find_ratings(shared_conn: db.Database): rows = await db.find_ratings(title="test", include_unrated=True) ratings = tuple(web_models.Rating(**r) for r in rows) assert (web_models.Rating.from_movie(m1),) == ratings + + +@pytest.mark.asyncio +async def test_ratings_for_movies(shared_conn: db.Database): + async with shared_conn.transaction(force_rollback=True): + m1 = a_movie() + await db.add(m1) + + m2 = a_movie() + await db.add(m2) + + u1 = models.User( + imdb_id="u00001", + name="User1", + secret="secret1", + ) + await db.add(u1) + + u2 = models.User( + imdb_id="u00002", + name="User2", + secret="secret2", + ) + await db.add(u2) + + r1 = models.Rating( + movie_id=m2.id, + movie=m2, + user_id=u1.id, + user=u1, + score=66, + rating_date=datetime.now(), + ) + await db.add(r1) + + # --- + + movie_ids = [m1.id] + user_ids = [] + assert tuple() == tuple( + await db.ratings_for_movies(movie_ids=movie_ids, user_ids=user_ids) + ) + + movie_ids = [m2.id] + user_ids = [] + assert (r1,) == tuple( + await db.ratings_for_movies(movie_ids=movie_ids, user_ids=user_ids) + ) + + movie_ids = [m2.id] + user_ids = [u2.id] + assert tuple() == tuple( + await db.ratings_for_movies(movie_ids=movie_ids, user_ids=user_ids) + ) + + movie_ids = [m2.id] + user_ids = [u1.id] + assert (r1,) == tuple( + await db.ratings_for_movies(movie_ids=movie_ids, user_ids=user_ids) + ) + + movie_ids = [m1.id, m2.id] + user_ids = [u1.id, u2.id] + assert (r1,) == tuple( + await db.ratings_for_movies(movie_ids=movie_ids, user_ids=user_ids) + ) diff --git a/unwind/db.py b/unwind/db.py index 34abd81..9e9f959 100644 --- a/unwind/db.py +++ b/unwind/db.py @@ -537,38 +537,18 @@ def sql_fields(tp: Type): return (f"{tp._table}.{f.name}" for f in fields(tp)) -def sql_in(column: str, values: Iterable[T], not_=False) -> tuple[str, dict[str, T]]: - c = column.replace(".", "___") - value_map = {f"{c}_{i}": v for i, v in enumerate(values, start=1)} - placeholders = ",".join(":" + k for k in value_map) - if not_: - return f"{column} NOT IN ({placeholders})", value_map - return f"{column} IN ({placeholders})", value_map - - async def ratings_for_movies( movie_ids: Iterable[ULID], user_ids: Iterable[ULID] = [] ) -> Iterable[Rating]: - values: dict[str, str] = {} - conditions: list[str] = [] - - q, vm = sql_in("movie_id", [str(m) for m in movie_ids]) - conditions.append(q) - values.update(vm) + conditions = [ratings.c.movie_id.in_(str(x) for x in movie_ids)] if user_ids: - q, vm = sql_in("user_id", [str(m) for m in user_ids]) - conditions.append(q) - values.update(vm) + conditions.append(ratings.c.user_id.in_(str(x) for x in user_ids)) - query = f""" - SELECT {','.join(sql_fields(Rating))} - FROM {Rating._table} - WHERE {' AND '.join(f'({c})' for c in conditions) if conditions else '1=1'} - """ + query = sa.select(ratings).where(*conditions) async with locked_connection() as conn: - rows = await conn.fetch_all(query, values) + rows = await conn.fetch_all(query) return (fromplain(Rating, row._mapping, serialized=True) for row in rows) From 84bbe331ee60c6e8463162a2c9c6c04067e3006d Mon Sep 17 00:00:00 2001 From: ducklet Date: Tue, 28 Mar 2023 21:49:02 +0200 Subject: [PATCH 14/31] migrate `db.find_movies` to SQLAlchemy --- tests/test_db.py | 83 ++++++++++++++++++++++++++++++++++++++++++++++++ unwind/db.py | 73 ++++++++++++++++++++---------------------- 2 files changed, 118 insertions(+), 38 deletions(-) diff --git a/tests/test_db.py b/tests/test_db.py index 92d75ae..d476408 100644 --- a/tests/test_db.py +++ b/tests/test_db.py @@ -316,3 +316,86 @@ async def test_ratings_for_movies(shared_conn: db.Database): assert (r1,) == tuple( await db.ratings_for_movies(movie_ids=movie_ids, user_ids=user_ids) ) + + +@pytest.mark.asyncio +async def test_find_movies(shared_conn: db.Database): + async with shared_conn.transaction(force_rollback=True): + m1 = a_movie(title="movie one") + await db.add(m1) + + m2 = a_movie(title="movie two", imdb_score=33, release_year=m1.release_year + 1) + await db.add(m2) + + u1 = models.User( + imdb_id="u00001", + name="User1", + secret="secret1", + ) + await db.add(u1) + + u2 = models.User( + imdb_id="u00002", + name="User2", + secret="secret2", + ) + await db.add(u2) + + r1 = models.Rating( + movie_id=m2.id, + movie=m2, + user_id=u1.id, + user=u1, + score=66, + rating_date=datetime.now(), + ) + await db.add(r1) + + # --- + + assert () == tuple(await db.find_movies(title=m1.title, include_unrated=False)) + assert ((m1, []),) == tuple( + await db.find_movies(title=m1.title, include_unrated=True) + ) + + assert ((m1, []),) == tuple( + await db.find_movies(title="mo on", exact=False, include_unrated=True) + ) + assert ((m1, []),) == tuple( + await db.find_movies(title="movie one", exact=True, include_unrated=True) + ) + assert () == tuple( + await db.find_movies(title="mo on", exact=True, include_unrated=True) + ) + + assert ((m2, []),) == tuple( + await db.find_movies(title="movie", exact=False, include_unrated=False) + ) + assert ((m2, []), (m1, [])) == tuple( + await db.find_movies(title="movie", exact=False, include_unrated=True) + ) + + assert ((m1, []),) == tuple( + await db.find_movies(include_unrated=True, yearcomp=("=", m1.release_year)) + ) + assert ((m2, []),) == tuple( + await db.find_movies(include_unrated=True, yearcomp=("=", m2.release_year)) + ) + assert ((m1, []),) == tuple( + await db.find_movies(include_unrated=True, yearcomp=("<", m2.release_year)) + ) + assert ((m2, []),) == tuple( + await db.find_movies(include_unrated=True, yearcomp=(">", m1.release_year)) + ) + + assert ((m2, []), (m1, [])) == tuple(await db.find_movies(include_unrated=True)) + assert ((m2, []),) == tuple( + await db.find_movies(include_unrated=True, limit_rows=1) + ) + assert ((m1, []),) == tuple( + await db.find_movies(include_unrated=True, skip_rows=1) + ) + + assert ((m2, [r1]), (m1, [])) == tuple( + await db.find_movies(include_unrated=True, user_ids=[u1.id, u2.id]) + ) diff --git a/unwind/db.py b/unwind/db.py index 9e9f959..a610b78 100644 --- a/unwind/db.py +++ b/unwind/db.py @@ -565,66 +565,63 @@ async def find_movies( include_unrated: bool = False, user_ids: list[ULID] = [], ) -> Iterable[tuple[Movie, list[Rating]]]: - values: dict[str, int | str] = { - "limit_rows": limit_rows, - "skip_rows": skip_rows, - } - conditions = [] + if title: - values["escape"] = "#" - escaped_title = sql_escape(title, char=values["escape"]) - values["pattern"] = ( + escape_char = "#" + escaped_title = sql_escape(title, char=escape_char) + pattern = ( "_".join(escaped_title.split()) if exact else "%" + "%".join(escaped_title.split()) + "%" ) conditions.append( - f""" - ( - {Movie._table}.title LIKE :pattern ESCAPE :escape - OR {Movie._table}.original_title LIKE :pattern ESCAPE :escape + sa.or_( + movies.c.title.like(pattern, escape=escape_char), + movies.c.original_title.like(pattern, escape=escape_char), ) - """ ) - if yearcomp: - op, year = yearcomp - assert op in "<=>" - values["year"] = year - conditions.append(f"{Movie._table}.release_year{op}:year") + match yearcomp: + case ("<", year): + conditions.append(movies.c.release_year < year) + case ("=", year): + conditions.append(movies.c.release_year == year) + case (">", year): + conditions.append(movies.c.release_year > year) - if media_type: - values["media_type"] = media_type - conditions.append(f"{Movie._table}.media_type=:media_type") + if media_type is not None: + conditions.append(movies.c.media_type == media_type) if ignore_tv_episodes: - conditions.append(f"{Movie._table}.media_type!='TV Episode'") + conditions.append(movies.c.media_type != "TV Episode") if not include_unrated: - conditions.append(f"{Movie._table}.imdb_score NOTNULL") + conditions.append(movies.c.imdb_score != None) + + query = ( + sa.select(movies) + .where(*conditions) + .order_by( + sa.func.length(movies.c.title).asc(), + movies.c.imdb_score.desc(), + movies.c.release_year.desc(), + ) + .limit(limit_rows) + .offset(skip_rows) + ) - query = f""" - SELECT {','.join(sql_fields(Movie))} - FROM {Movie._table} - WHERE {(' AND '.join(conditions)) if conditions else '1=1'} - ORDER BY - length({Movie._table}.title) ASC, - {Movie._table}.imdb_score DESC, - {Movie._table}.release_year DESC - LIMIT :skip_rows, :limit_rows - """ async with locked_connection() as conn: - rows = await conn.fetch_all(bindparams(query, values)) + rows = await conn.fetch_all(query) - movies = [fromplain(Movie, row._mapping, serialized=True) for row in rows] + movies_ = [fromplain(Movie, row._mapping, serialized=True) for row in rows] if not user_ids: - return ((m, []) for m in movies) + return ((m, []) for m in movies_) - ratings = await ratings_for_movies((m.id for m in movies), user_ids) + ratings = await ratings_for_movies((m.id for m in movies_), user_ids) - aggreg: dict[ULID, tuple[Movie, list[Rating]]] = {m.id: (m, []) for m in movies} + aggreg: dict[ULID, tuple[Movie, list[Rating]]] = {m.id: (m, []) for m in movies_} for rating in ratings: aggreg[rating.movie_id][1].append(rating) From e27b57050a3eaa04f638866842dcee02d84235e4 Mon Sep 17 00:00:00 2001 From: ducklet Date: Tue, 28 Mar 2023 22:04:14 +0200 Subject: [PATCH 15/31] remove unused functions --- unwind/db.py | 29 +---------------------------- 1 file changed, 1 insertion(+), 28 deletions(-) diff --git a/unwind/db.py b/unwind/db.py index a610b78..6c21a9d 100644 --- a/unwind/db.py +++ b/unwind/db.py @@ -1,7 +1,6 @@ import asyncio import contextlib import logging -import re import threading from pathlib import Path from typing import Any, Iterable, Literal, Type, TypeVar @@ -16,7 +15,6 @@ from .models import ( Rating, User, asplain, - fields, fromplain, movies, optional_fields, @@ -414,7 +412,7 @@ async def add_or_update_rating(rating: Rating) -> bool: return False -def sql_escape(s: str, char="#"): +def sql_escape(s: str, char: str = "#") -> str: return s.replace(char, 2 * char).replace("%", f"{char}%").replace("_", f"{char}_") @@ -533,10 +531,6 @@ async def ratings_for_movie_ids( return tuple(dict(r._mapping) for r in rows) -def sql_fields(tp: Type): - return (f"{tp._table}.{f.name}" for f in fields(tp)) - - async def ratings_for_movies( movie_ids: Iterable[ULID], user_ids: Iterable[ULID] = [] ) -> Iterable[Rating]: @@ -626,24 +620,3 @@ async def find_movies( aggreg[rating.movie_id][1].append(rating) return aggreg.values() - - -def bindparams(query: str, values: dict): - """Bind values to a query. - - This is similar to what SQLAlchemy and Databases do, but it allows to - easily use the same placeholder in multiple places. - """ - pump_vals = {} - pump_keys = {} - - def pump(match): - key = match[1] - val = values[key] - pump_keys[key] = 1 + pump_keys.setdefault(key, 0) - pump_key = f"{key}_{pump_keys[key]}" - pump_vals[pump_key] = val - return f":{pump_key}" - - pump_query = re.sub(r":(\w+)\b", pump, query) - return sa.text(pump_query).bindparams(**pump_vals) From 37e8d53b78d72f69b302de17dc3ad9b2e05d3dd1 Mon Sep 17 00:00:00 2001 From: ducklet Date: Tue, 28 Mar 2023 23:03:35 +0200 Subject: [PATCH 16/31] migrate `db.current_patch_level` to SQLAlchemy --- tests/test_db.py | 9 +++++++++ unwind/db.py | 39 +++++++++++++++------------------------ unwind/models.py | 28 +++++++++++++++++++++++----- 3 files changed, 47 insertions(+), 29 deletions(-) diff --git a/tests/test_db.py b/tests/test_db.py index d476408..7c4c96e 100644 --- a/tests/test_db.py +++ b/tests/test_db.py @@ -20,6 +20,15 @@ def a_movie(**kwds) -> models.Movie: return models.Movie(**args) +@pytest.mark.asyncio +async def test_current_patch_level(shared_conn: db.Database): + async with shared_conn.transaction(force_rollback=True): + patch_level = "some-patch-level" + assert patch_level != await db.current_patch_level(shared_conn) + await db.set_current_patch_level(shared_conn, patch_level) + assert patch_level == await db.current_patch_level(shared_conn) + + @pytest.mark.asyncio async def test_get(shared_conn: db.Database): async with shared_conn.transaction(force_rollback=True): diff --git a/unwind/db.py b/unwind/db.py index 6c21a9d..4b90085 100644 --- a/unwind/db.py +++ b/unwind/db.py @@ -7,6 +7,7 @@ from typing import Any, Iterable, Literal, Type, TypeVar import sqlalchemy as sa from databases import Database +from sqlalchemy.dialects.sqlite import insert from . import config from .models import ( @@ -15,7 +16,9 @@ from .models import ( Rating, User, asplain, + db_patches, fromplain, + metadata, movies, optional_fields, ratings, @@ -50,38 +53,22 @@ async def close_connection_pool() -> None: # Run automatic ANALYZE prior to closing the db, # see https://sqlite.com/lang_analyze.html. - await db.execute("PRAGMA analysis_limit=400") - await db.execute("PRAGMA optimize") + await db.execute(sa.text("PRAGMA analysis_limit=400")) + await db.execute(sa.text("PRAGMA optimize")) await db.disconnect() -async def _create_patch_db(db): - query = """ - CREATE TABLE IF NOT EXISTS db_patches ( - id INTEGER PRIMARY KEY, - current TEXT - ) - """ - await db.execute(query) - - -async def current_patch_level(db) -> str: - await _create_patch_db(db) - - query = "SELECT current FROM db_patches" +async def current_patch_level(db: Database) -> str: + query = sa.select(db_patches.c.current) current = await db.fetch_val(query) return current or "" -async def set_current_patch_level(db, current: str): - await _create_patch_db(db) - - query = """ - INSERT INTO db_patches VALUES (1, :current) - ON CONFLICT DO UPDATE SET current=excluded.current - """ - await db.execute(query, values={"current": current}) +async def set_current_patch_level(db: Database, current: str) -> None: + stmt = insert(db_patches).values(id=1, current=current) + stmt = stmt.on_conflict_do_update(set_={"current": stmt.excluded.current}) + await db.execute(stmt) db_patches_dir = Path(__file__).parent / "sql" @@ -222,8 +209,12 @@ def shared_connection() -> Database: if _shared_connection is None: uri = f"sqlite:///{config.storage_path}" + # uri = f"sqlite+aiosqlite:///{config.storage_path}" _shared_connection = Database(uri) + engine = sa.create_engine(uri, future=True) + metadata.create_all(engine, tables=[db_patches]) + return _shared_connection diff --git a/unwind/models.py b/unwind/models.py index 6d40b35..5628bb0 100644 --- a/unwind/models.py +++ b/unwind/models.py @@ -30,6 +30,7 @@ JSONObject = dict[str, JSON] T = TypeVar("T") mapper_registry = registry() +metadata = mapper_registry.metadata def annotations(tp: Type) -> tuple | None: @@ -203,12 +204,29 @@ def utcnow(): return datetime.utcnow().replace(tzinfo=timezone.utc) +@mapper_registry.mapped +@dataclass +class DbPatch: + __table__ = Table( + "db_patches", + metadata, + Column("id", Integer, primary_key=True), + Column("current", String), + ) + + id: int + current: str + + +db_patches = DbPatch.__table__ + + @mapper_registry.mapped @dataclass class Progress: __table__ = Table( "progress", - mapper_registry.metadata, + metadata, Column("id", String, primary_key=True), # ULID Column("type", String, nullable=False), Column("state", String, nullable=False), # JSON {"percent": ..., "error": ...} @@ -258,7 +276,7 @@ class Progress: class Movie: __table__ = Table( "movies", - mapper_registry.metadata, + metadata, Column("id", String, primary_key=True), # ULID Column("title", String, nullable=False), Column("original_title", String), @@ -336,7 +354,7 @@ Relation = Annotated[T | None, _RelationSentinel] class Rating: __table__ = Table( "ratings", - mapper_registry.metadata, + metadata, Column("id", String, primary_key=True), # ULID Column("movie_id", ForeignKey("movies.id"), nullable=False), # ULID Column("user_id", ForeignKey("users.id"), nullable=False), # ULID @@ -393,7 +411,7 @@ class UserGroup(TypedDict): class User: __table__ = Table( "users", - mapper_registry.metadata, + metadata, Column("id", String, primary_key=True), # ULID Column("imdb_id", String, nullable=False, unique=True), Column("name", String, nullable=False), @@ -433,7 +451,7 @@ class GroupUser(TypedDict): class Group: __table__ = Table( "groups", - mapper_registry.metadata, + metadata, Column("id", String, primary_key=True), # ULID Column("name", String, nullable=False), Column("users", String, nullable=False), # JSON array From 8b5cbdf9037fcd20ccda55e2950afa42150d686b Mon Sep 17 00:00:00 2001 From: ducklet Date: Tue, 28 Mar 2023 23:06:14 +0200 Subject: [PATCH 17/31] make sure WAL mode is active --- unwind/db.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/unwind/db.py b/unwind/db.py index 4b90085..fb0cabe 100644 --- a/unwind/db.py +++ b/unwind/db.py @@ -40,6 +40,8 @@ async def open_connection_pool() -> None: db = shared_connection() await db.connect() + await db.execute(sa.text("PRAGMA journal_mode=WAL")) + await apply_db_patches(db) From 2963a1d3f6ef916d8926e389a8adf4424bda6fb4 Mon Sep 17 00:00:00 2001 From: ducklet Date: Tue, 28 Mar 2023 23:32:24 +0200 Subject: [PATCH 18/31] improve strict typing --- unwind/db.py | 23 +++++++++++++---------- unwind/models.py | 18 ++++++++++++------ 2 files changed, 25 insertions(+), 16 deletions(-) diff --git a/unwind/db.py b/unwind/db.py index fb0cabe..63c042a 100644 --- a/unwind/db.py +++ b/unwind/db.py @@ -11,6 +11,7 @@ from sqlalchemy.dialects.sqlite import insert from . import config from .models import ( + Model, Movie, Progress, Rating, @@ -76,7 +77,7 @@ async def set_current_patch_level(db: Database, current: str) -> None: db_patches_dir = Path(__file__).parent / "sql" -async def apply_db_patches(db: Database): +async def apply_db_patches(db: Database) -> None: """Apply all remaining patches to the database. Beware that patches will be applied in lexicographical order, @@ -124,7 +125,7 @@ async def get_import_progress() -> Progress | None: return await get(Progress, type="import-imdb-movies", order_by=("started", "desc")) -async def stop_import_progress(*, error: BaseException | None = None): +async def stop_import_progress(*, error: BaseException | None = None) -> None: """Stop the current import. If an error is given, it will be logged to the progress state. @@ -220,9 +221,10 @@ def shared_connection() -> Database: return _shared_connection -async def add(item): +async def add(item: Model) -> None: # Support late initializing - used for optimization. if getattr(item, "_is_lazy", False): + assert hasattr(item, "_lazy_init") item._lazy_init() table: sa.Table = item.__table__ @@ -232,7 +234,7 @@ async def add(item): await conn.execute(stmt) -ModelType = TypeVar("ModelType") +ModelType = TypeVar("ModelType", bound=Model) async def get( @@ -300,9 +302,10 @@ async def get_all(model: Type[ModelType], **field_values) -> Iterable[ModelType] return (fromplain(model, row._mapping, serialized=True) for row in rows) -async def update(item): +async def update(item: Model) -> None: # Support late initializing - used for optimization. if getattr(item, "_is_lazy", False): + assert hasattr(item, "_lazy_init") item._lazy_init() table: sa.Table = item.__table__ @@ -312,7 +315,7 @@ async def update(item): await conn.execute(stmt) -async def remove(item): +async def remove(item: Model) -> None: table: sa.Table = item.__table__ values = asplain(item, filter_fields={"id"}, serialize=True) stmt = table.delete().where(table.c.id == values["id"]) @@ -320,7 +323,7 @@ async def remove(item): await conn.execute(stmt) -async def add_or_update_user(user: User): +async def add_or_update_user(user: User) -> None: db_user = await get(User, imdb_id=user.imdb_id) if not db_user: await add(user) @@ -331,7 +334,7 @@ async def add_or_update_user(user: User): await update(user) -async def add_or_update_many_movies(movies: list[Movie]): +async def add_or_update_many_movies(movies: list[Movie]) -> None: """Add or update Movies in the database. This is an optimized version of `add_or_update_movie` for the purpose @@ -361,7 +364,7 @@ async def add_or_update_many_movies(movies: list[Movie]): await update(movie) -async def add_or_update_movie(movie: Movie): +async def add_or_update_movie(movie: Movie) -> None: """Add or update a Movie in the database. This is an upsert operation, but it will also update the Movie you pass @@ -419,7 +422,7 @@ async def find_ratings( yearcomp: tuple[Literal["<", "=", ">"], int] | None = None, limit_rows: int = 10, user_ids: Iterable[str] = [], -): +) -> Iterable[dict[str, Any]]: conditions = [] if title: diff --git a/unwind/models.py b/unwind/models.py index 5628bb0..93b51f9 100644 --- a/unwind/models.py +++ b/unwind/models.py @@ -11,6 +11,7 @@ from typing import ( Container, Literal, Mapping, + Protocol, Type, TypedDict, TypeVar, @@ -29,6 +30,11 @@ JSONObject = dict[str, JSON] T = TypeVar("T") + +class Model(Protocol): + __table__: ClassVar[Table] + + mapper_registry = registry() metadata = mapper_registry.metadata @@ -207,7 +213,7 @@ def utcnow(): @mapper_registry.mapped @dataclass class DbPatch: - __table__ = Table( + __table__: ClassVar[Table] = Table( "db_patches", metadata, Column("id", Integer, primary_key=True), @@ -224,7 +230,7 @@ db_patches = DbPatch.__table__ @mapper_registry.mapped @dataclass class Progress: - __table__ = Table( + __table__: ClassVar[Table] = Table( "progress", metadata, Column("id", String, primary_key=True), # ULID @@ -274,7 +280,7 @@ class Progress: @mapper_registry.mapped @dataclass class Movie: - __table__ = Table( + __table__: ClassVar[Table] = Table( "movies", metadata, Column("id", String, primary_key=True), # ULID @@ -352,7 +358,7 @@ Relation = Annotated[T | None, _RelationSentinel] @mapper_registry.mapped @dataclass class Rating: - __table__ = Table( + __table__: ClassVar[Table] = Table( "ratings", metadata, Column("id", String, primary_key=True), # ULID @@ -409,7 +415,7 @@ class UserGroup(TypedDict): @mapper_registry.mapped @dataclass class User: - __table__ = Table( + __table__: ClassVar[Table] = Table( "users", metadata, Column("id", String, primary_key=True), # ULID @@ -449,7 +455,7 @@ class GroupUser(TypedDict): @mapper_registry.mapped @dataclass class Group: - __table__ = Table( + __table__: ClassVar[Table] = Table( "groups", metadata, Column("id", String, primary_key=True), # ULID From 91d06f607c8f717127261e8f5b5129f5cf2bc96d Mon Sep 17 00:00:00 2001 From: ducklet Date: Sat, 22 Jul 2023 18:37:54 +0200 Subject: [PATCH 19/31] update Python dependencies --- poetry.lock | 498 +++++++++++++++++++++---------------------------- pyproject.toml | 6 +- 2 files changed, 217 insertions(+), 287 deletions(-) diff --git a/poetry.lock b/poetry.lock index c82718e..acde07a 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1,27 +1,29 @@ -# This file is automatically @generated by Poetry 1.4.0 and should not be changed by hand. +# This file is automatically @generated by Poetry 1.5.1 and should not be changed by hand. [[package]] name = "aiosqlite" -version = "0.18.0" +version = "0.19.0" description = "asyncio bridge to the standard sqlite3 module" -category = "main" optional = false python-versions = ">=3.7" files = [ - {file = "aiosqlite-0.18.0-py3-none-any.whl", hash = "sha256:c3511b841e3a2c5614900ba1d179f366826857586f78abd75e7cbeb88e75a557"}, - {file = "aiosqlite-0.18.0.tar.gz", hash = "sha256:faa843ef5fb08bafe9a9b3859012d3d9d6f77ce3637899de20606b7fc39aa213"}, + {file = "aiosqlite-0.19.0-py3-none-any.whl", hash = "sha256:edba222e03453e094a3ce605db1b970c4b3376264e56f32e2a4959f948d66a96"}, + {file = "aiosqlite-0.19.0.tar.gz", hash = "sha256:95ee77b91c8d2808bd08a59fbebf66270e9090c3d92ffbf260dc0db0b979577d"}, ] +[package.extras] +dev = ["aiounittest (==1.4.1)", "attribution (==1.6.2)", "black (==23.3.0)", "coverage[toml] (==7.2.3)", "flake8 (==5.0.4)", "flake8-bugbear (==23.3.12)", "flit (==3.7.1)", "mypy (==1.2.0)", "ufmt (==2.1.0)", "usort (==1.0.6)"] +docs = ["sphinx (==6.1.3)", "sphinx-mdinclude (==0.5.3)"] + [[package]] name = "anyio" -version = "3.6.2" +version = "3.7.1" description = "High level compatibility layer for multiple asynchronous event loop implementations" -category = "main" optional = false -python-versions = ">=3.6.2" +python-versions = ">=3.7" files = [ - {file = "anyio-3.6.2-py3-none-any.whl", hash = "sha256:fbbe32bd270d2a2ef3ed1c5d45041250284e31fc0a4df4a5a6071842051a51e3"}, - {file = "anyio-3.6.2.tar.gz", hash = "sha256:25ea0d673ae30af41a0c442f81cf3b38c7e79fdc7b60335a4c14e05eb0947421"}, + {file = "anyio-3.7.1-py3-none-any.whl", hash = "sha256:91dee416e570e92c64041bd18b900d1d6fa78dff7048769ce5ac5ddad004fbb5"}, + {file = "anyio-3.7.1.tar.gz", hash = "sha256:44a3c9aba0f5defa43261a8b3efb97891f2bd7d804e0e1f56419befa1adfc780"}, ] [package.dependencies] @@ -29,39 +31,19 @@ idna = ">=2.8" sniffio = ">=1.1" [package.extras] -doc = ["packaging", "sphinx-autodoc-typehints (>=1.2.0)", "sphinx-rtd-theme"] -test = ["contextlib2", "coverage[toml] (>=4.5)", "hypothesis (>=4.0)", "mock (>=4)", "pytest (>=7.0)", "pytest-mock (>=3.6.1)", "trustme", "uvloop (<0.15)", "uvloop (>=0.15)"] -trio = ["trio (>=0.16,<0.22)"] - -[[package]] -name = "attrs" -version = "22.2.0" -description = "Classes Without Boilerplate" -category = "dev" -optional = false -python-versions = ">=3.6" -files = [ - {file = "attrs-22.2.0-py3-none-any.whl", hash = "sha256:29e95c7f6778868dbd49170f98f8818f78f3dc5e0e37c0b1f474e3561b240836"}, - {file = "attrs-22.2.0.tar.gz", hash = "sha256:c9227bfc2f01993c03f68db37d1d15c9690188323c067c641f1a35ca58185f99"}, -] - -[package.extras] -cov = ["attrs[tests]", "coverage-enable-subprocess", "coverage[toml] (>=5.3)"] -dev = ["attrs[docs,tests]"] -docs = ["furo", "myst-parser", "sphinx", "sphinx-notfound-page", "sphinxcontrib-towncrier", "towncrier", "zope.interface"] -tests = ["attrs[tests-no-zope]", "zope.interface"] -tests-no-zope = ["cloudpickle", "cloudpickle", "hypothesis", "hypothesis", "mypy (>=0.971,<0.990)", "mypy (>=0.971,<0.990)", "pympler", "pympler", "pytest (>=4.3.0)", "pytest (>=4.3.0)", "pytest-mypy-plugins", "pytest-mypy-plugins", "pytest-xdist[psutil]", "pytest-xdist[psutil]"] +doc = ["Sphinx", "packaging", "sphinx-autodoc-typehints (>=1.2.0)", "sphinx-rtd-theme (>=1.2.2)", "sphinxcontrib-jquery"] +test = ["anyio[trio]", "coverage[toml] (>=4.5)", "hypothesis (>=4.0)", "mock (>=4)", "psutil (>=5.9)", "pytest (>=7.0)", "pytest-mock (>=3.6.1)", "trustme", "uvloop (>=0.17)"] +trio = ["trio (<0.22)"] [[package]] name = "autoflake" -version = "2.0.2" +version = "2.2.0" description = "Removes unused imports and unused variables" -category = "dev" optional = false -python-versions = ">=3.7" +python-versions = ">=3.8" files = [ - {file = "autoflake-2.0.2-py3-none-any.whl", hash = "sha256:a82d8efdcbbb7129a8a23238c529fb9d9919c562e26bb7963ea6890fbfff7d02"}, - {file = "autoflake-2.0.2.tar.gz", hash = "sha256:e0164421ff13f805f08a023e249d84200bd00463d213b490906bfefa67e83830"}, + {file = "autoflake-2.2.0-py3-none-any.whl", hash = "sha256:de409b009a34c1c2a7cc2aae84c4c05047f9773594317c6a6968bd497600d4a0"}, + {file = "autoflake-2.2.0.tar.gz", hash = "sha256:62e1f74a0fdad898a96fee6f99fe8241af90ad99c7110c884b35855778412251"}, ] [package.dependencies] @@ -69,14 +51,13 @@ pyflakes = ">=3.0.0" [[package]] name = "beautifulsoup4" -version = "4.11.2" +version = "4.12.2" description = "Screen-scraping library" -category = "main" optional = false python-versions = ">=3.6.0" files = [ - {file = "beautifulsoup4-4.11.2-py3-none-any.whl", hash = "sha256:0e79446b10b3ecb499c1556f7e228a53e64a2bfcebd455f370d8927cb5b59e39"}, - {file = "beautifulsoup4-4.11.2.tar.gz", hash = "sha256:bc4bdda6717de5a2987436fb8d72f45dc90dd856bdfd512a1314ce90349a0106"}, + {file = "beautifulsoup4-4.12.2-py3-none-any.whl", hash = "sha256:bd2520ca0d9d7d12694a53d44ac482d181b4ec1888909b035a3dbf40d0f57d4a"}, + {file = "beautifulsoup4-4.12.2.tar.gz", hash = "sha256:492bbc69dca35d12daac71c4db1bfff0c876c00ef4a2ffacce226d4638eb72da"}, ] [package.dependencies] @@ -88,37 +69,33 @@ lxml = ["lxml"] [[package]] name = "black" -version = "23.1.0" +version = "23.7.0" description = "The uncompromising code formatter." -category = "dev" optional = false -python-versions = ">=3.7" +python-versions = ">=3.8" files = [ - {file = "black-23.1.0-cp310-cp310-macosx_10_16_arm64.whl", hash = "sha256:b6a92a41ee34b883b359998f0c8e6eb8e99803aa8bf3123bf2b2e6fec505a221"}, - {file = "black-23.1.0-cp310-cp310-macosx_10_16_universal2.whl", hash = "sha256:57c18c5165c1dbe291d5306e53fb3988122890e57bd9b3dcb75f967f13411a26"}, - {file = "black-23.1.0-cp310-cp310-macosx_10_16_x86_64.whl", hash = "sha256:9880d7d419bb7e709b37e28deb5e68a49227713b623c72b2b931028ea65f619b"}, - {file = "black-23.1.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e6663f91b6feca5d06f2ccd49a10f254f9298cc1f7f49c46e498a0771b507104"}, - {file = "black-23.1.0-cp310-cp310-win_amd64.whl", hash = "sha256:9afd3f493666a0cd8f8df9a0200c6359ac53940cbde049dcb1a7eb6ee2dd7074"}, - {file = "black-23.1.0-cp311-cp311-macosx_10_16_arm64.whl", hash = "sha256:bfffba28dc52a58f04492181392ee380e95262af14ee01d4bc7bb1b1c6ca8d27"}, - {file = "black-23.1.0-cp311-cp311-macosx_10_16_universal2.whl", hash = "sha256:c1c476bc7b7d021321e7d93dc2cbd78ce103b84d5a4cf97ed535fbc0d6660648"}, - {file = "black-23.1.0-cp311-cp311-macosx_10_16_x86_64.whl", hash = "sha256:382998821f58e5c8238d3166c492139573325287820963d2f7de4d518bd76958"}, - {file = "black-23.1.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2bf649fda611c8550ca9d7592b69f0637218c2369b7744694c5e4902873b2f3a"}, - {file = "black-23.1.0-cp311-cp311-win_amd64.whl", hash = "sha256:121ca7f10b4a01fd99951234abdbd97728e1240be89fde18480ffac16503d481"}, - {file = "black-23.1.0-cp37-cp37m-macosx_10_16_x86_64.whl", hash = "sha256:a8471939da5e824b891b25751955be52ee7f8a30a916d570a5ba8e0f2eb2ecad"}, - {file = "black-23.1.0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8178318cb74f98bc571eef19068f6ab5613b3e59d4f47771582f04e175570ed8"}, - {file = "black-23.1.0-cp37-cp37m-win_amd64.whl", hash = "sha256:a436e7881d33acaf2536c46a454bb964a50eff59b21b51c6ccf5a40601fbef24"}, - {file = "black-23.1.0-cp38-cp38-macosx_10_16_arm64.whl", hash = "sha256:a59db0a2094d2259c554676403fa2fac3473ccf1354c1c63eccf7ae65aac8ab6"}, - {file = "black-23.1.0-cp38-cp38-macosx_10_16_universal2.whl", hash = "sha256:0052dba51dec07ed029ed61b18183942043e00008ec65d5028814afaab9a22fd"}, - {file = "black-23.1.0-cp38-cp38-macosx_10_16_x86_64.whl", hash = "sha256:49f7b39e30f326a34b5c9a4213213a6b221d7ae9d58ec70df1c4a307cf2a1580"}, - {file = "black-23.1.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:162e37d49e93bd6eb6f1afc3e17a3d23a823042530c37c3c42eeeaf026f38468"}, - {file = "black-23.1.0-cp38-cp38-win_amd64.whl", hash = "sha256:8b70eb40a78dfac24842458476135f9b99ab952dd3f2dab738c1881a9b38b753"}, - {file = "black-23.1.0-cp39-cp39-macosx_10_16_arm64.whl", hash = "sha256:a29650759a6a0944e7cca036674655c2f0f63806ddecc45ed40b7b8aa314b651"}, - {file = "black-23.1.0-cp39-cp39-macosx_10_16_universal2.whl", hash = "sha256:bb460c8561c8c1bec7824ecbc3ce085eb50005883a6203dcfb0122e95797ee06"}, - {file = "black-23.1.0-cp39-cp39-macosx_10_16_x86_64.whl", hash = "sha256:c91dfc2c2a4e50df0026f88d2215e166616e0c80e86004d0003ece0488db2739"}, - {file = "black-23.1.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2a951cc83ab535d248c89f300eccbd625e80ab880fbcfb5ac8afb5f01a258ac9"}, - {file = "black-23.1.0-cp39-cp39-win_amd64.whl", hash = "sha256:0680d4380db3719ebcfb2613f34e86c8e6d15ffeabcf8ec59355c5e7b85bb555"}, - {file = "black-23.1.0-py3-none-any.whl", hash = "sha256:7a0f701d314cfa0896b9001df70a530eb2472babb76086344e688829efd97d32"}, - {file = "black-23.1.0.tar.gz", hash = "sha256:b0bd97bea8903f5a2ba7219257a44e3f1f9d00073d6cc1add68f0beec69692ac"}, + {file = "black-23.7.0-cp310-cp310-macosx_10_16_arm64.whl", hash = "sha256:5c4bc552ab52f6c1c506ccae05681fab58c3f72d59ae6e6639e8885e94fe2587"}, + {file = "black-23.7.0-cp310-cp310-macosx_10_16_universal2.whl", hash = "sha256:552513d5cd5694590d7ef6f46e1767a4df9af168d449ff767b13b084c020e63f"}, + {file = "black-23.7.0-cp310-cp310-macosx_10_16_x86_64.whl", hash = "sha256:86cee259349b4448adb4ef9b204bb4467aae74a386bce85d56ba4f5dc0da27be"}, + {file = "black-23.7.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:501387a9edcb75d7ae8a4412bb8749900386eaef258f1aefab18adddea1936bc"}, + {file = "black-23.7.0-cp310-cp310-win_amd64.whl", hash = "sha256:fb074d8b213749fa1d077d630db0d5f8cc3b2ae63587ad4116e8a436e9bbe995"}, + {file = "black-23.7.0-cp311-cp311-macosx_10_16_arm64.whl", hash = "sha256:b5b0ee6d96b345a8b420100b7d71ebfdd19fab5e8301aff48ec270042cd40ac2"}, + {file = "black-23.7.0-cp311-cp311-macosx_10_16_universal2.whl", hash = "sha256:893695a76b140881531062d48476ebe4a48f5d1e9388177e175d76234ca247cd"}, + {file = "black-23.7.0-cp311-cp311-macosx_10_16_x86_64.whl", hash = "sha256:c333286dc3ddca6fdff74670b911cccedacb4ef0a60b34e491b8a67c833b343a"}, + {file = "black-23.7.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:831d8f54c3a8c8cf55f64d0422ee875eecac26f5f649fb6c1df65316b67c8926"}, + {file = "black-23.7.0-cp311-cp311-win_amd64.whl", hash = "sha256:7f3bf2dec7d541b4619b8ce526bda74a6b0bffc480a163fed32eb8b3c9aed8ad"}, + {file = "black-23.7.0-cp38-cp38-macosx_10_16_arm64.whl", hash = "sha256:f9062af71c59c004cd519e2fb8f5d25d39e46d3af011b41ab43b9c74e27e236f"}, + {file = "black-23.7.0-cp38-cp38-macosx_10_16_universal2.whl", hash = "sha256:01ede61aac8c154b55f35301fac3e730baf0c9cf8120f65a9cd61a81cfb4a0c3"}, + {file = "black-23.7.0-cp38-cp38-macosx_10_16_x86_64.whl", hash = "sha256:327a8c2550ddc573b51e2c352adb88143464bb9d92c10416feb86b0f5aee5ff6"}, + {file = "black-23.7.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6d1c6022b86f83b632d06f2b02774134def5d4d4f1dac8bef16d90cda18ba28a"}, + {file = "black-23.7.0-cp38-cp38-win_amd64.whl", hash = "sha256:27eb7a0c71604d5de083757fbdb245b1a4fae60e9596514c6ec497eb63f95320"}, + {file = "black-23.7.0-cp39-cp39-macosx_10_16_arm64.whl", hash = "sha256:8417dbd2f57b5701492cd46edcecc4f9208dc75529bcf76c514864e48da867d9"}, + {file = "black-23.7.0-cp39-cp39-macosx_10_16_universal2.whl", hash = "sha256:47e56d83aad53ca140da0af87678fb38e44fd6bc0af71eebab2d1f59b1acf1d3"}, + {file = "black-23.7.0-cp39-cp39-macosx_10_16_x86_64.whl", hash = "sha256:25cc308838fe71f7065df53aedd20327969d05671bac95b38fdf37ebe70ac087"}, + {file = "black-23.7.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:642496b675095d423f9b8448243336f8ec71c9d4d57ec17bf795b67f08132a91"}, + {file = "black-23.7.0-cp39-cp39-win_amd64.whl", hash = "sha256:ad0014efc7acf0bd745792bd0d8857413652979200ab924fbf239062adc12491"}, + {file = "black-23.7.0-py3-none-any.whl", hash = "sha256:9fd59d418c60c0348505f2ddf9609c1e1de8e7493eab96198fc89d9f865e7a96"}, + {file = "black-23.7.0.tar.gz", hash = "sha256:022a582720b0d9480ed82576c920a8c1dde97cc38ff11d8d8859b3bd6ca9eedb"}, ] [package.dependencies] @@ -136,26 +113,24 @@ uvloop = ["uvloop (>=0.15.2)"] [[package]] name = "certifi" -version = "2022.12.7" +version = "2023.7.22" description = "Python package for providing Mozilla's CA Bundle." -category = "main" optional = false python-versions = ">=3.6" files = [ - {file = "certifi-2022.12.7-py3-none-any.whl", hash = "sha256:4ad3232f5e926d6718ec31cfc1fcadfde020920e278684144551c91769c7bc18"}, - {file = "certifi-2022.12.7.tar.gz", hash = "sha256:35824b4c3a97115964b408844d64aa14db1cc518f6562e8d7261699d1350a9e3"}, + {file = "certifi-2023.7.22-py3-none-any.whl", hash = "sha256:92d6037539857d8206b8f6ae472e8b77db8058fec5937a1ef3f54304089edbb9"}, + {file = "certifi-2023.7.22.tar.gz", hash = "sha256:539cc1d13202e33ca466e88b2807e29f4c13049d6d87031a3c110744495cb082"}, ] [[package]] name = "click" -version = "8.1.3" +version = "8.1.6" description = "Composable command line interface toolkit" -category = "main" optional = false python-versions = ">=3.7" files = [ - {file = "click-8.1.3-py3-none-any.whl", hash = "sha256:bb4d8133cb15a609f44e8213d9b391b0809795062913b383c62be0ee95b1db48"}, - {file = "click-8.1.3.tar.gz", hash = "sha256:7682dc8afb30297001674575ea00d1814d808d6a36af415a82bd481d37ba7b8e"}, + {file = "click-8.1.6-py3-none-any.whl", hash = "sha256:fa244bb30b3b5ee2cae3da8f55c9e5e0c0e86093306301fb418eb9dc40fbded5"}, + {file = "click-8.1.6.tar.gz", hash = "sha256:48ee849951919527a045bfe3bf7baa8a959c423134e1a5b98c05c20ba75a1cbd"}, ] [package.dependencies] @@ -165,7 +140,6 @@ colorama = {version = "*", markers = "platform_system == \"Windows\""} name = "colorama" version = "0.4.6" description = "Cross-platform colored terminal text." -category = "main" optional = false python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" files = [ @@ -175,63 +149,71 @@ files = [ [[package]] name = "coverage" -version = "7.2.2" +version = "7.2.7" description = "Code coverage measurement for Python" -category = "dev" optional = false python-versions = ">=3.7" files = [ - {file = "coverage-7.2.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:c90e73bdecb7b0d1cea65a08cb41e9d672ac6d7995603d6465ed4914b98b9ad7"}, - {file = "coverage-7.2.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:e2926b8abedf750c2ecf5035c07515770944acf02e1c46ab08f6348d24c5f94d"}, - {file = "coverage-7.2.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:57b77b9099f172804e695a40ebaa374f79e4fb8b92f3e167f66facbf92e8e7f5"}, - {file = "coverage-7.2.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:efe1c0adad110bf0ad7fb59f833880e489a61e39d699d37249bdf42f80590169"}, - {file = "coverage-7.2.2-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2199988e0bc8325d941b209f4fd1c6fa007024b1442c5576f1a32ca2e48941e6"}, - {file = "coverage-7.2.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:81f63e0fb74effd5be736cfe07d710307cc0a3ccb8f4741f7f053c057615a137"}, - {file = "coverage-7.2.2-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:186e0fc9cf497365036d51d4d2ab76113fb74f729bd25da0975daab2e107fd90"}, - {file = "coverage-7.2.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:420f94a35e3e00a2b43ad5740f935358e24478354ce41c99407cddd283be00d2"}, - {file = "coverage-7.2.2-cp310-cp310-win32.whl", hash = "sha256:38004671848b5745bb05d4d621526fca30cee164db42a1f185615f39dc997292"}, - {file = "coverage-7.2.2-cp310-cp310-win_amd64.whl", hash = "sha256:0ce383d5f56d0729d2dd40e53fe3afeb8f2237244b0975e1427bfb2cf0d32bab"}, - {file = "coverage-7.2.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:3eb55b7b26389dd4f8ae911ba9bc8c027411163839dea4c8b8be54c4ee9ae10b"}, - {file = "coverage-7.2.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:d2b96123a453a2d7f3995ddb9f28d01fd112319a7a4d5ca99796a7ff43f02af5"}, - {file = "coverage-7.2.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:299bc75cb2a41e6741b5e470b8c9fb78d931edbd0cd009c58e5c84de57c06731"}, - {file = "coverage-7.2.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5e1df45c23d4230e3d56d04414f9057eba501f78db60d4eeecfcb940501b08fd"}, - {file = "coverage-7.2.2-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:006ed5582e9cbc8115d2e22d6d2144a0725db542f654d9d4fda86793832f873d"}, - {file = "coverage-7.2.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:d683d230b5774816e7d784d7ed8444f2a40e7a450e5720d58af593cb0b94a212"}, - {file = "coverage-7.2.2-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:8efb48fa743d1c1a65ee8787b5b552681610f06c40a40b7ef94a5b517d885c54"}, - {file = "coverage-7.2.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:4c752d5264053a7cf2fe81c9e14f8a4fb261370a7bb344c2a011836a96fb3f57"}, - {file = "coverage-7.2.2-cp311-cp311-win32.whl", hash = "sha256:55272f33da9a5d7cccd3774aeca7a01e500a614eaea2a77091e9be000ecd401d"}, - {file = "coverage-7.2.2-cp311-cp311-win_amd64.whl", hash = "sha256:92ebc1619650409da324d001b3a36f14f63644c7f0a588e331f3b0f67491f512"}, - {file = "coverage-7.2.2-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:5afdad4cc4cc199fdf3e18088812edcf8f4c5a3c8e6cb69127513ad4cb7471a9"}, - {file = "coverage-7.2.2-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0484d9dd1e6f481b24070c87561c8d7151bdd8b044c93ac99faafd01f695c78e"}, - {file = "coverage-7.2.2-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d530191aa9c66ab4f190be8ac8cc7cfd8f4f3217da379606f3dd4e3d83feba69"}, - {file = "coverage-7.2.2-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4ac0f522c3b6109c4b764ffec71bf04ebc0523e926ca7cbe6c5ac88f84faced0"}, - {file = "coverage-7.2.2-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:ba279aae162b20444881fc3ed4e4f934c1cf8620f3dab3b531480cf602c76b7f"}, - {file = "coverage-7.2.2-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:53d0fd4c17175aded9c633e319360d41a1f3c6e352ba94edcb0fa5167e2bad67"}, - {file = "coverage-7.2.2-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:8c99cb7c26a3039a8a4ee3ca1efdde471e61b4837108847fb7d5be7789ed8fd9"}, - {file = "coverage-7.2.2-cp37-cp37m-win32.whl", hash = "sha256:5cc0783844c84af2522e3a99b9b761a979a3ef10fb87fc4048d1ee174e18a7d8"}, - {file = "coverage-7.2.2-cp37-cp37m-win_amd64.whl", hash = "sha256:817295f06eacdc8623dc4df7d8b49cea65925030d4e1e2a7c7218380c0072c25"}, - {file = "coverage-7.2.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:6146910231ece63facfc5984234ad1b06a36cecc9fd0c028e59ac7c9b18c38c6"}, - {file = "coverage-7.2.2-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:387fb46cb8e53ba7304d80aadca5dca84a2fbf6fe3faf6951d8cf2d46485d1e5"}, - {file = "coverage-7.2.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:046936ab032a2810dcaafd39cc4ef6dd295df1a7cbead08fe996d4765fca9fe4"}, - {file = "coverage-7.2.2-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e627dee428a176ffb13697a2c4318d3f60b2ccdde3acdc9b3f304206ec130ccd"}, - {file = "coverage-7.2.2-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4fa54fb483decc45f94011898727802309a109d89446a3c76387d016057d2c84"}, - {file = "coverage-7.2.2-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:3668291b50b69a0c1ef9f462c7df2c235da3c4073f49543b01e7eb1dee7dd540"}, - {file = "coverage-7.2.2-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:7c20b731211261dc9739bbe080c579a1835b0c2d9b274e5fcd903c3a7821cf88"}, - {file = "coverage-7.2.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:5764e1f7471cb8f64b8cda0554f3d4c4085ae4b417bfeab236799863703e5de2"}, - {file = "coverage-7.2.2-cp38-cp38-win32.whl", hash = "sha256:4f01911c010122f49a3e9bdc730eccc66f9b72bd410a3a9d3cb8448bb50d65d3"}, - {file = "coverage-7.2.2-cp38-cp38-win_amd64.whl", hash = "sha256:c448b5c9e3df5448a362208b8d4b9ed85305528313fca1b479f14f9fe0d873b8"}, - {file = "coverage-7.2.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:bfe7085783cda55e53510482fa7b5efc761fad1abe4d653b32710eb548ebdd2d"}, - {file = "coverage-7.2.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:9d22e94e6dc86de981b1b684b342bec5e331401599ce652900ec59db52940005"}, - {file = "coverage-7.2.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:507e4720791977934bba016101579b8c500fb21c5fa3cd4cf256477331ddd988"}, - {file = "coverage-7.2.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:bc4803779f0e4b06a2361f666e76f5c2e3715e8e379889d02251ec911befd149"}, - {file = "coverage-7.2.2-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:db8c2c5ace167fd25ab5dd732714c51d4633f58bac21fb0ff63b0349f62755a8"}, - {file = "coverage-7.2.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:4f68ee32d7c4164f1e2c8797535a6d0a3733355f5861e0f667e37df2d4b07140"}, - {file = "coverage-7.2.2-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:d52f0a114b6a58305b11a5cdecd42b2e7f1ec77eb20e2b33969d702feafdd016"}, - {file = "coverage-7.2.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:797aad79e7b6182cb49c08cc5d2f7aa7b2128133b0926060d0a8889ac43843be"}, - {file = "coverage-7.2.2-cp39-cp39-win32.whl", hash = "sha256:db45eec1dfccdadb179b0f9ca616872c6f700d23945ecc8f21bb105d74b1c5fc"}, - {file = "coverage-7.2.2-cp39-cp39-win_amd64.whl", hash = "sha256:8dbe2647bf58d2c5a6c5bcc685f23b5f371909a5624e9f5cd51436d6a9f6c6ef"}, - {file = "coverage-7.2.2-pp37.pp38.pp39-none-any.whl", hash = "sha256:872d6ce1f5be73f05bea4df498c140b9e7ee5418bfa2cc8204e7f9b817caa968"}, - {file = "coverage-7.2.2.tar.gz", hash = "sha256:36dd42da34fe94ed98c39887b86db9d06777b1c8f860520e21126a75507024f2"}, + {file = "coverage-7.2.7-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d39b5b4f2a66ccae8b7263ac3c8170994b65266797fb96cbbfd3fb5b23921db8"}, + {file = "coverage-7.2.7-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:6d040ef7c9859bb11dfeb056ff5b3872436e3b5e401817d87a31e1750b9ae2fb"}, + {file = "coverage-7.2.7-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ba90a9563ba44a72fda2e85302c3abc71c5589cea608ca16c22b9804262aaeb6"}, + {file = "coverage-7.2.7-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e7d9405291c6928619403db1d10bd07888888ec1abcbd9748fdaa971d7d661b2"}, + {file = "coverage-7.2.7-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:31563e97dae5598556600466ad9beea39fb04e0229e61c12eaa206e0aa202063"}, + {file = "coverage-7.2.7-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:ebba1cd308ef115925421d3e6a586e655ca5a77b5bf41e02eb0e4562a111f2d1"}, + {file = "coverage-7.2.7-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:cb017fd1b2603ef59e374ba2063f593abe0fc45f2ad9abdde5b4d83bd922a353"}, + {file = "coverage-7.2.7-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:d62a5c7dad11015c66fbb9d881bc4caa5b12f16292f857842d9d1871595f4495"}, + {file = "coverage-7.2.7-cp310-cp310-win32.whl", hash = "sha256:ee57190f24fba796e36bb6d3aa8a8783c643d8fa9760c89f7a98ab5455fbf818"}, + {file = "coverage-7.2.7-cp310-cp310-win_amd64.whl", hash = "sha256:f75f7168ab25dd93110c8a8117a22450c19976afbc44234cbf71481094c1b850"}, + {file = "coverage-7.2.7-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:06a9a2be0b5b576c3f18f1a241f0473575c4a26021b52b2a85263a00f034d51f"}, + {file = "coverage-7.2.7-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:5baa06420f837184130752b7c5ea0808762083bf3487b5038d68b012e5937dbe"}, + {file = "coverage-7.2.7-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fdec9e8cbf13a5bf63290fc6013d216a4c7232efb51548594ca3631a7f13c3a3"}, + {file = "coverage-7.2.7-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:52edc1a60c0d34afa421c9c37078817b2e67a392cab17d97283b64c5833f427f"}, + {file = "coverage-7.2.7-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:63426706118b7f5cf6bb6c895dc215d8a418d5952544042c8a2d9fe87fcf09cb"}, + {file = "coverage-7.2.7-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:afb17f84d56068a7c29f5fa37bfd38d5aba69e3304af08ee94da8ed5b0865833"}, + {file = "coverage-7.2.7-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:48c19d2159d433ccc99e729ceae7d5293fbffa0bdb94952d3579983d1c8c9d97"}, + {file = "coverage-7.2.7-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:0e1f928eaf5469c11e886fe0885ad2bf1ec606434e79842a879277895a50942a"}, + {file = "coverage-7.2.7-cp311-cp311-win32.whl", hash = "sha256:33d6d3ea29d5b3a1a632b3c4e4f4ecae24ef170b0b9ee493883f2df10039959a"}, + {file = "coverage-7.2.7-cp311-cp311-win_amd64.whl", hash = "sha256:5b7540161790b2f28143191f5f8ec02fb132660ff175b7747b95dcb77ac26562"}, + {file = "coverage-7.2.7-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:f2f67fe12b22cd130d34d0ef79206061bfb5eda52feb6ce0dba0644e20a03cf4"}, + {file = "coverage-7.2.7-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a342242fe22407f3c17f4b499276a02b01e80f861f1682ad1d95b04018e0c0d4"}, + {file = "coverage-7.2.7-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:171717c7cb6b453aebac9a2ef603699da237f341b38eebfee9be75d27dc38e01"}, + {file = "coverage-7.2.7-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:49969a9f7ffa086d973d91cec8d2e31080436ef0fb4a359cae927e742abfaaa6"}, + {file = "coverage-7.2.7-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:b46517c02ccd08092f4fa99f24c3b83d8f92f739b4657b0f146246a0ca6a831d"}, + {file = "coverage-7.2.7-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:a3d33a6b3eae87ceaefa91ffdc130b5e8536182cd6dfdbfc1aa56b46ff8c86de"}, + {file = "coverage-7.2.7-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:976b9c42fb2a43ebf304fa7d4a310e5f16cc99992f33eced91ef6f908bd8f33d"}, + {file = "coverage-7.2.7-cp312-cp312-win32.whl", hash = "sha256:8de8bb0e5ad103888d65abef8bca41ab93721647590a3f740100cd65c3b00511"}, + {file = "coverage-7.2.7-cp312-cp312-win_amd64.whl", hash = "sha256:9e31cb64d7de6b6f09702bb27c02d1904b3aebfca610c12772452c4e6c21a0d3"}, + {file = "coverage-7.2.7-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:58c2ccc2f00ecb51253cbe5d8d7122a34590fac9646a960d1430d5b15321d95f"}, + {file = "coverage-7.2.7-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d22656368f0e6189e24722214ed8d66b8022db19d182927b9a248a2a8a2f67eb"}, + {file = "coverage-7.2.7-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a895fcc7b15c3fc72beb43cdcbdf0ddb7d2ebc959edac9cef390b0d14f39f8a9"}, + {file = "coverage-7.2.7-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e84606b74eb7de6ff581a7915e2dab7a28a0517fbe1c9239eb227e1354064dcd"}, + {file = "coverage-7.2.7-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:0a5f9e1dbd7fbe30196578ca36f3fba75376fb99888c395c5880b355e2875f8a"}, + {file = "coverage-7.2.7-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:419bfd2caae268623dd469eff96d510a920c90928b60f2073d79f8fe2bbc5959"}, + {file = "coverage-7.2.7-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:2aee274c46590717f38ae5e4650988d1af340fe06167546cc32fe2f58ed05b02"}, + {file = "coverage-7.2.7-cp37-cp37m-win32.whl", hash = "sha256:61b9a528fb348373c433e8966535074b802c7a5d7f23c4f421e6c6e2f1697a6f"}, + {file = "coverage-7.2.7-cp37-cp37m-win_amd64.whl", hash = "sha256:b1c546aca0ca4d028901d825015dc8e4d56aac4b541877690eb76490f1dc8ed0"}, + {file = "coverage-7.2.7-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:54b896376ab563bd38453cecb813c295cf347cf5906e8b41d340b0321a5433e5"}, + {file = "coverage-7.2.7-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:3d376df58cc111dc8e21e3b6e24606b5bb5dee6024f46a5abca99124b2229ef5"}, + {file = "coverage-7.2.7-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5e330fc79bd7207e46c7d7fd2bb4af2963f5f635703925543a70b99574b0fea9"}, + {file = "coverage-7.2.7-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1e9d683426464e4a252bf70c3498756055016f99ddaec3774bf368e76bbe02b6"}, + {file = "coverage-7.2.7-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8d13c64ee2d33eccf7437961b6ea7ad8673e2be040b4f7fd4fd4d4d28d9ccb1e"}, + {file = "coverage-7.2.7-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:b7aa5f8a41217360e600da646004f878250a0d6738bcdc11a0a39928d7dc2050"}, + {file = "coverage-7.2.7-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:8fa03bce9bfbeeef9f3b160a8bed39a221d82308b4152b27d82d8daa7041fee5"}, + {file = "coverage-7.2.7-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:245167dd26180ab4c91d5e1496a30be4cd721a5cf2abf52974f965f10f11419f"}, + {file = "coverage-7.2.7-cp38-cp38-win32.whl", hash = "sha256:d2c2db7fd82e9b72937969bceac4d6ca89660db0a0967614ce2481e81a0b771e"}, + {file = "coverage-7.2.7-cp38-cp38-win_amd64.whl", hash = "sha256:2e07b54284e381531c87f785f613b833569c14ecacdcb85d56b25c4622c16c3c"}, + {file = "coverage-7.2.7-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:537891ae8ce59ef63d0123f7ac9e2ae0fc8b72c7ccbe5296fec45fd68967b6c9"}, + {file = "coverage-7.2.7-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:06fb182e69f33f6cd1d39a6c597294cff3143554b64b9825d1dc69d18cc2fff2"}, + {file = "coverage-7.2.7-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:201e7389591af40950a6480bd9edfa8ed04346ff80002cec1a66cac4549c1ad7"}, + {file = "coverage-7.2.7-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f6951407391b639504e3b3be51b7ba5f3528adbf1a8ac3302b687ecababf929e"}, + {file = "coverage-7.2.7-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6f48351d66575f535669306aa7d6d6f71bc43372473b54a832222803eb956fd1"}, + {file = "coverage-7.2.7-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:b29019c76039dc3c0fd815c41392a044ce555d9bcdd38b0fb60fb4cd8e475ba9"}, + {file = "coverage-7.2.7-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:81c13a1fc7468c40f13420732805a4c38a105d89848b7c10af65a90beff25250"}, + {file = "coverage-7.2.7-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:975d70ab7e3c80a3fe86001d8751f6778905ec723f5b110aed1e450da9d4b7f2"}, + {file = "coverage-7.2.7-cp39-cp39-win32.whl", hash = "sha256:7ee7d9d4822c8acc74a5e26c50604dff824710bc8de424904c0982e25c39c6cb"}, + {file = "coverage-7.2.7-cp39-cp39-win_amd64.whl", hash = "sha256:eb393e5ebc85245347950143969b241d08b52b88a3dc39479822e073a1a8eb27"}, + {file = "coverage-7.2.7-pp37.pp38.pp39-none-any.whl", hash = "sha256:b7b4c971f05e6ae490fef852c218b0e79d4e52f79ef0c8475566584a8fb3e01d"}, + {file = "coverage-7.2.7.tar.gz", hash = "sha256:924d94291ca674905fe9481f12294eb11f2d3d3fd1adb20314ba89e94f44ed59"}, ] [package.extras] @@ -241,7 +223,6 @@ toml = ["tomli"] name = "databases" version = "0.7.0" description = "Async database support for Python." -category = "main" optional = false python-versions = ">=3.7" files = [ @@ -267,7 +248,6 @@ sqlite = ["aiosqlite"] name = "greenlet" version = "2.0.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.*" files = [ @@ -341,7 +321,6 @@ test = ["objgraph", "psutil"] name = "h11" version = "0.14.0" description = "A pure-Python, bring-your-own-I/O implementation of HTTP/1.1" -category = "main" optional = false python-versions = ">=3.7" files = [ @@ -353,7 +332,6 @@ files = [ name = "html5lib" version = "1.1" description = "HTML parser based on the WHATWG HTML specification" -category = "main" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" files = [ @@ -373,55 +351,52 @@ lxml = ["lxml"] [[package]] name = "httpcore" -version = "0.16.3" +version = "0.17.3" description = "A minimal low-level HTTP client." -category = "main" optional = false python-versions = ">=3.7" files = [ - {file = "httpcore-0.16.3-py3-none-any.whl", hash = "sha256:da1fb708784a938aa084bde4feb8317056c55037247c787bd7e19eb2c2949dc0"}, - {file = "httpcore-0.16.3.tar.gz", hash = "sha256:c5d6f04e2fc530f39e0c077e6a30caa53f1451096120f1f38b954afd0b17c0cb"}, + {file = "httpcore-0.17.3-py3-none-any.whl", hash = "sha256:c2789b767ddddfa2a5782e3199b2b7f6894540b17b16ec26b2c4d8e103510b87"}, + {file = "httpcore-0.17.3.tar.gz", hash = "sha256:a6f30213335e34c1ade7be6ec7c47f19f50c56db36abef1a9dfa3815b1cb3888"}, ] [package.dependencies] anyio = ">=3.0,<5.0" certifi = "*" h11 = ">=0.13,<0.15" -sniffio = ">=1.0.0,<2.0.0" +sniffio = "==1.*" [package.extras] http2 = ["h2 (>=3,<5)"] -socks = ["socksio (>=1.0.0,<2.0.0)"] +socks = ["socksio (==1.*)"] [[package]] name = "httpx" -version = "0.23.3" +version = "0.24.1" description = "The next generation HTTP client." -category = "main" optional = false python-versions = ">=3.7" files = [ - {file = "httpx-0.23.3-py3-none-any.whl", hash = "sha256:a211fcce9b1254ea24f0cd6af9869b3d29aba40154e947d2a07bb499b3e310d6"}, - {file = "httpx-0.23.3.tar.gz", hash = "sha256:9818458eb565bb54898ccb9b8b251a28785dd4a55afbc23d0eb410754fe7d0f9"}, + {file = "httpx-0.24.1-py3-none-any.whl", hash = "sha256:06781eb9ac53cde990577af654bd990a4949de37a28bdb4a230d434f3a30b9bd"}, + {file = "httpx-0.24.1.tar.gz", hash = "sha256:5853a43053df830c20f8110c5e69fe44d035d850b2dfe795e196f00fdb774bdd"}, ] [package.dependencies] certifi = "*" -httpcore = ">=0.15.0,<0.17.0" -rfc3986 = {version = ">=1.3,<2", extras = ["idna2008"]} +httpcore = ">=0.15.0,<0.18.0" +idna = "*" sniffio = "*" [package.extras] brotli = ["brotli", "brotlicffi"] -cli = ["click (>=8.0.0,<9.0.0)", "pygments (>=2.0.0,<3.0.0)", "rich (>=10,<13)"] +cli = ["click (==8.*)", "pygments (==2.*)", "rich (>=10,<14)"] http2 = ["h2 (>=3,<5)"] -socks = ["socksio (>=1.0.0,<2.0.0)"] +socks = ["socksio (==1.*)"] [[package]] name = "idna" version = "3.4" description = "Internationalized Domain Names in Applications (IDNA)" -category = "main" optional = false python-versions = ">=3.5" files = [ @@ -433,7 +408,6 @@ files = [ name = "iniconfig" version = "2.0.0" description = "brain-dead simple config-ini parsing" -category = "dev" optional = false python-versions = ">=3.7" files = [ @@ -445,7 +419,6 @@ files = [ name = "isort" version = "5.12.0" description = "A Python utility / library to sort Python imports." -category = "dev" optional = false python-versions = ">=3.8.0" files = [ @@ -463,7 +436,6 @@ requirements-deprecated-finder = ["pip-api", "pipreqs"] name = "mypy-extensions" version = "1.0.0" description = "Type system extensions for programs checked with the mypy type checker." -category = "dev" optional = false python-versions = ">=3.5" files = [ @@ -473,14 +445,13 @@ files = [ [[package]] name = "nodeenv" -version = "1.7.0" +version = "1.8.0" description = "Node.js virtual environment builder" -category = "dev" optional = false python-versions = ">=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*" files = [ - {file = "nodeenv-1.7.0-py2.py3-none-any.whl", hash = "sha256:27083a7b96a25f2f5e1d8cb4b6317ee8aeda3bdd121394e5ac54e498028a042e"}, - {file = "nodeenv-1.7.0.tar.gz", hash = "sha256:e0e7f7dfb85fc5394c6fe1e8fa98131a2473e04311a45afb6508f7cf1836fa2b"}, + {file = "nodeenv-1.8.0-py2.py3-none-any.whl", hash = "sha256:df865724bb3c3adc86b3876fa209771517b0cfe596beff01a92700e0e8be4cec"}, + {file = "nodeenv-1.8.0.tar.gz", hash = "sha256:d51e0c37e64fbf47d017feac3145cdbb58836d7eee8c6f6d3b6880c5456227d2"}, ] [package.dependencies] @@ -488,21 +459,19 @@ setuptools = "*" [[package]] name = "packaging" -version = "23.0" +version = "23.1" description = "Core utilities for Python packages" -category = "dev" optional = false python-versions = ">=3.7" files = [ - {file = "packaging-23.0-py3-none-any.whl", hash = "sha256:714ac14496c3e68c99c29b00845f7a2b85f3bb6f1078fd9f72fd20f0570002b2"}, - {file = "packaging-23.0.tar.gz", hash = "sha256:b6ad297f8907de0fa2fe1ccbd26fdaf387f5f47c7275fedf8cce89f99446cf97"}, + {file = "packaging-23.1-py3-none-any.whl", hash = "sha256:994793af429502c4ea2ebf6bf664629d07c1a9fe974af92966e4b8d2df7edc61"}, + {file = "packaging-23.1.tar.gz", hash = "sha256:a392980d2b6cffa644431898be54b0045151319d1e7ec34f0cfed48767dd334f"}, ] [[package]] name = "pathspec" version = "0.11.1" description = "Utility library for gitignore style pattern matching of file paths." -category = "dev" optional = false python-versions = ">=3.7" files = [ @@ -512,30 +481,28 @@ files = [ [[package]] name = "platformdirs" -version = "3.1.1" +version = "3.9.1" description = "A small Python package for determining appropriate platform-specific dirs, e.g. a \"user data dir\"." -category = "dev" optional = false python-versions = ">=3.7" files = [ - {file = "platformdirs-3.1.1-py3-none-any.whl", hash = "sha256:e5986afb596e4bb5bde29a79ac9061aa955b94fca2399b7aaac4090860920dd8"}, - {file = "platformdirs-3.1.1.tar.gz", hash = "sha256:024996549ee88ec1a9aa99ff7f8fc819bb59e2c3477b410d90a16d32d6e707aa"}, + {file = "platformdirs-3.9.1-py3-none-any.whl", hash = "sha256:ad8291ae0ae5072f66c16945166cb11c63394c7a3ad1b1bc9828ca3162da8c2f"}, + {file = "platformdirs-3.9.1.tar.gz", hash = "sha256:1b42b450ad933e981d56e59f1b97495428c9bd60698baab9f3eb3d00d5822421"}, ] [package.extras] -docs = ["furo (>=2022.12.7)", "proselint (>=0.13)", "sphinx (>=6.1.3)", "sphinx-autodoc-typehints (>=1.22,!=1.23.4)"] -test = ["appdirs (==1.4.4)", "covdefaults (>=2.2.2)", "pytest (>=7.2.1)", "pytest-cov (>=4)", "pytest-mock (>=3.10)"] +docs = ["furo (>=2023.5.20)", "proselint (>=0.13)", "sphinx (>=7.0.1)", "sphinx-autodoc-typehints (>=1.23,!=1.23.4)"] +test = ["appdirs (==1.4.4)", "covdefaults (>=2.3)", "pytest (>=7.3.1)", "pytest-cov (>=4.1)", "pytest-mock (>=3.10)"] [[package]] name = "pluggy" -version = "1.0.0" +version = "1.2.0" description = "plugin and hook calling mechanisms for python" -category = "dev" optional = false -python-versions = ">=3.6" +python-versions = ">=3.7" files = [ - {file = "pluggy-1.0.0-py2.py3-none-any.whl", hash = "sha256:74134bbf457f031a36d68416e1509f34bd5ccc019f0bcc952c7b909d06b37bd3"}, - {file = "pluggy-1.0.0.tar.gz", hash = "sha256:4224373bacce55f955a878bf9cfa763c1e360858e330072059e10bad68531159"}, + {file = "pluggy-1.2.0-py3-none-any.whl", hash = "sha256:c2fd55a7d7a3863cba1a013e4e2414658b1d07b6bc57b3919e0c63c9abb99849"}, + {file = "pluggy-1.2.0.tar.gz", hash = "sha256:d12f0c4b579b15f5e054301bb226ee85eeeba08ffec228092f8defbaa3a4c4b3"}, ] [package.extras] @@ -546,7 +513,6 @@ testing = ["pytest", "pytest-benchmark"] name = "pyflakes" version = "3.0.1" description = "passive checker of Python programs" -category = "dev" optional = false python-versions = ">=3.6" files = [ @@ -556,14 +522,13 @@ files = [ [[package]] name = "pyright" -version = "1.1.299" +version = "1.1.318" description = "Command line wrapper for pyright" -category = "dev" optional = false python-versions = ">=3.7" files = [ - {file = "pyright-1.1.299-py3-none-any.whl", hash = "sha256:f34dfd0c2fcade34f9878b1fc69cb9456476dc78227e0a2fa046107ec55c0235"}, - {file = "pyright-1.1.299.tar.gz", hash = "sha256:b3a9a6affa1252c52793e8663ade59ff966f8495ecfad6328deffe59cfc5a9a9"}, + {file = "pyright-1.1.318-py3-none-any.whl", hash = "sha256:056c1b2e711c3526e32919de1684ae599d34b7ec27e94398858a43f56ac9ba9b"}, + {file = "pyright-1.1.318.tar.gz", hash = "sha256:69dcf9c32d5be27d531750de627e76a7cadc741d333b547c09044278b508db7b"}, ] [package.dependencies] @@ -575,40 +540,37 @@ dev = ["twine (>=3.4.1)"] [[package]] name = "pytest" -version = "7.2.2" +version = "7.4.0" description = "pytest: simple powerful testing with Python" -category = "dev" optional = false python-versions = ">=3.7" files = [ - {file = "pytest-7.2.2-py3-none-any.whl", hash = "sha256:130328f552dcfac0b1cec75c12e3f005619dc5f874f0a06e8ff7263f0ee6225e"}, - {file = "pytest-7.2.2.tar.gz", hash = "sha256:c99ab0c73aceb050f68929bc93af19ab6db0558791c6a0715723abe9d0ade9d4"}, + {file = "pytest-7.4.0-py3-none-any.whl", hash = "sha256:78bf16451a2eb8c7a2ea98e32dc119fd2aa758f1d5d66dbf0a59d69a3969df32"}, + {file = "pytest-7.4.0.tar.gz", hash = "sha256:b4bf8c45bd59934ed84001ad51e11b4ee40d40a1229d2c79f9c592b0a3f6bd8a"}, ] [package.dependencies] -attrs = ">=19.2.0" colorama = {version = "*", markers = "sys_platform == \"win32\""} iniconfig = "*" packaging = "*" pluggy = ">=0.12,<2.0" [package.extras] -testing = ["argcomplete", "hypothesis (>=3.56)", "mock", "nose", "pygments (>=2.7.2)", "requests", "xmlschema"] +testing = ["argcomplete", "attrs (>=19.2.0)", "hypothesis (>=3.56)", "mock", "nose", "pygments (>=2.7.2)", "requests", "setuptools", "xmlschema"] [[package]] name = "pytest-asyncio" -version = "0.20.3" +version = "0.21.1" description = "Pytest support for asyncio" -category = "dev" optional = false python-versions = ">=3.7" files = [ - {file = "pytest-asyncio-0.20.3.tar.gz", hash = "sha256:83cbf01169ce3e8eb71c6c278ccb0574d1a7a3bb8eaaf5e50e0ad342afb33b36"}, - {file = "pytest_asyncio-0.20.3-py3-none-any.whl", hash = "sha256:f129998b209d04fcc65c96fc85c11e5316738358909a8399e93be553d7656442"}, + {file = "pytest-asyncio-0.21.1.tar.gz", hash = "sha256:40a7eae6dded22c7b604986855ea48400ab15b069ae38116e8c01238e9eeb64d"}, + {file = "pytest_asyncio-0.21.1-py3-none-any.whl", hash = "sha256:8666c1c8ac02631d7c51ba282e0c69a8a452b211ffedf2599099845da5c5c37b"}, ] [package.dependencies] -pytest = ">=6.1.0" +pytest = ">=7.0.0" [package.extras] docs = ["sphinx (>=5.3)", "sphinx-rtd-theme (>=1.0)"] @@ -616,14 +578,13 @@ testing = ["coverage (>=6.2)", "flaky (>=3.5.0)", "hypothesis (>=5.7.1)", "mypy [[package]] name = "pytest-cov" -version = "4.0.0" +version = "4.1.0" description = "Pytest plugin for measuring coverage." -category = "dev" optional = false -python-versions = ">=3.6" +python-versions = ">=3.7" files = [ - {file = "pytest-cov-4.0.0.tar.gz", hash = "sha256:996b79efde6433cdbd0088872dbc5fb3ed7fe1578b68cdbba634f14bb8dd0470"}, - {file = "pytest_cov-4.0.0-py3-none-any.whl", hash = "sha256:2feb1b751d66a8bd934e5edfa2e961d11309dc37b73b0eabe73b5945fee20f6b"}, + {file = "pytest-cov-4.1.0.tar.gz", hash = "sha256:3904b13dfbfec47f003b8e77fd5b589cd11904a21ddf1ab38a64f204d6a10ef6"}, + {file = "pytest_cov-4.1.0-py3-none-any.whl", hash = "sha256:6ba70b9e97e69fcc3fb45bfeab2d0a138fb65c4d0d6a41ef33983ad114be8c3a"}, ] [package.dependencies] @@ -633,46 +594,26 @@ pytest = ">=4.6" [package.extras] testing = ["fields", "hunter", "process-tests", "pytest-xdist", "six", "virtualenv"] -[[package]] -name = "rfc3986" -version = "1.5.0" -description = "Validating URI References per RFC 3986" -category = "main" -optional = false -python-versions = "*" -files = [ - {file = "rfc3986-1.5.0-py2.py3-none-any.whl", hash = "sha256:a86d6e1f5b1dc238b218b012df0aa79409667bb209e58da56d0b94704e712a97"}, - {file = "rfc3986-1.5.0.tar.gz", hash = "sha256:270aaf10d87d0d4e095063c65bf3ddbc6ee3d0b226328ce21e036f946e421835"}, -] - -[package.dependencies] -idna = {version = "*", optional = true, markers = "extra == \"idna2008\""} - -[package.extras] -idna2008 = ["idna"] - [[package]] name = "setuptools" -version = "67.6.0" +version = "68.0.0" description = "Easily download, build, install, upgrade, and uninstall Python packages" -category = "dev" optional = false python-versions = ">=3.7" files = [ - {file = "setuptools-67.6.0-py3-none-any.whl", hash = "sha256:b78aaa36f6b90a074c1fa651168723acbf45d14cb1196b6f02c0fd07f17623b2"}, - {file = "setuptools-67.6.0.tar.gz", hash = "sha256:2ee892cd5f29f3373097f5a814697e397cf3ce313616df0af11231e2ad118077"}, + {file = "setuptools-68.0.0-py3-none-any.whl", hash = "sha256:11e52c67415a381d10d6b462ced9cfb97066179f0e871399e006c4ab101fc85f"}, + {file = "setuptools-68.0.0.tar.gz", hash = "sha256:baf1fdb41c6da4cd2eae722e135500da913332ab3f2f5c7d33af9b492acb5235"}, ] [package.extras] docs = ["furo", "jaraco.packaging (>=9)", "jaraco.tidelift (>=1.4)", "pygments-github-lexers (==0.0.5)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-favicon", "sphinx-hoverxref (<2)", "sphinx-inline-tabs", "sphinx-lint", "sphinx-notfound-page (==0.8.3)", "sphinx-reredirects", "sphinxcontrib-towncrier"] -testing = ["build[virtualenv]", "filelock (>=3.4.0)", "flake8 (<5)", "flake8-2020", "ini2toml[lite] (>=0.9)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "pip (>=19.1)", "pip-run (>=8.8)", "pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=1.3)", "pytest-flake8", "pytest-mypy (>=0.9.1)", "pytest-perf", "pytest-timeout", "pytest-xdist", "tomli-w (>=1.0.0)", "virtualenv (>=13.0.0)", "wheel"] +testing = ["build[virtualenv]", "filelock (>=3.4.0)", "flake8-2020", "ini2toml[lite] (>=0.9)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "pip (>=19.1)", "pip-run (>=8.8)", "pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=1.3)", "pytest-mypy (>=0.9.1)", "pytest-perf", "pytest-ruff", "pytest-timeout", "pytest-xdist", "tomli-w (>=1.0.0)", "virtualenv (>=13.0.0)", "wheel"] testing-integration = ["build[virtualenv]", "filelock (>=3.4.0)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "pytest", "pytest-enabler", "pytest-xdist", "tomli", "virtualenv (>=13.0.0)", "wheel"] [[package]] name = "six" version = "1.16.0" description = "Python 2 and 3 compatibility utilities" -category = "main" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*" files = [ @@ -684,7 +625,6 @@ files = [ name = "sniffio" version = "1.3.0" description = "Sniff out which async library your code is running under" -category = "main" optional = false python-versions = ">=3.7" files = [ @@ -694,70 +634,65 @@ files = [ [[package]] name = "soupsieve" -version = "2.4" +version = "2.4.1" description = "A modern CSS selector implementation for Beautiful Soup." -category = "main" optional = false python-versions = ">=3.7" files = [ - {file = "soupsieve-2.4-py3-none-any.whl", hash = "sha256:49e5368c2cda80ee7e84da9dbe3e110b70a4575f196efb74e51b94549d921955"}, - {file = "soupsieve-2.4.tar.gz", hash = "sha256:e28dba9ca6c7c00173e34e4ba57448f0688bb681b7c5e8bf4971daafc093d69a"}, + {file = "soupsieve-2.4.1-py3-none-any.whl", hash = "sha256:1c1bfee6819544a3447586c889157365a27e10d88cde3ad3da0cf0ddf646feb8"}, + {file = "soupsieve-2.4.1.tar.gz", hash = "sha256:89d12b2d5dfcd2c9e8c22326da9d9aa9cb3dfab0a83a024f05704076ee8d35ea"}, ] [[package]] name = "sqlalchemy" -version = "1.4.46" +version = "1.4.49" description = "Database Abstraction Library" -category = "main" optional = false python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,>=2.7" files = [ - {file = "SQLAlchemy-1.4.46-cp27-cp27m-macosx_10_14_x86_64.whl", hash = "sha256:7001f16a9a8e06488c3c7154827c48455d1c1507d7228d43e781afbc8ceccf6d"}, - {file = "SQLAlchemy-1.4.46-cp27-cp27m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:c7a46639ba058d320c9f53a81db38119a74b8a7a1884df44d09fbe807d028aaf"}, - {file = "SQLAlchemy-1.4.46-cp27-cp27m-win32.whl", hash = "sha256:c04144a24103135ea0315d459431ac196fe96f55d3213bfd6d39d0247775c854"}, - {file = "SQLAlchemy-1.4.46-cp27-cp27m-win_amd64.whl", hash = "sha256:7b81b1030c42b003fc10ddd17825571603117f848814a344d305262d370e7c34"}, - {file = "SQLAlchemy-1.4.46-cp27-cp27mu-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:939f9a018d2ad04036746e15d119c0428b1e557470361aa798e6e7d7f5875be0"}, - {file = "SQLAlchemy-1.4.46-cp310-cp310-macosx_11_0_x86_64.whl", hash = "sha256:b7f4b6aa6e87991ec7ce0e769689a977776db6704947e562102431474799a857"}, - {file = "SQLAlchemy-1.4.46-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5dbf17ac9a61e7a3f1c7ca47237aac93cabd7f08ad92ac5b96d6f8dea4287fc1"}, - {file = "SQLAlchemy-1.4.46-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:7f8267682eb41a0584cf66d8a697fef64b53281d01c93a503e1344197f2e01fe"}, - {file = "SQLAlchemy-1.4.46-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:64cb0ad8a190bc22d2112001cfecdec45baffdf41871de777239da6a28ed74b6"}, - {file = "SQLAlchemy-1.4.46-cp310-cp310-win32.whl", hash = "sha256:5f752676fc126edc1c4af0ec2e4d2adca48ddfae5de46bb40adbd3f903eb2120"}, - {file = "SQLAlchemy-1.4.46-cp310-cp310-win_amd64.whl", hash = "sha256:31de1e2c45e67a5ec1ecca6ec26aefc299dd5151e355eb5199cd9516b57340be"}, - {file = "SQLAlchemy-1.4.46-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:d68e1762997bfebf9e5cf2a9fd0bcf9ca2fdd8136ce7b24bbd3bbfa4328f3e4a"}, - {file = "SQLAlchemy-1.4.46-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4d112b0f3c1bc5ff70554a97344625ef621c1bfe02a73c5d97cac91f8cd7a41e"}, - {file = "SQLAlchemy-1.4.46-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:69fac0a7054d86b997af12dc23f581cf0b25fb1c7d1fed43257dee3af32d3d6d"}, - {file = "SQLAlchemy-1.4.46-cp311-cp311-win32.whl", hash = "sha256:887865924c3d6e9a473dc82b70977395301533b3030d0f020c38fd9eba5419f2"}, - {file = "SQLAlchemy-1.4.46-cp311-cp311-win_amd64.whl", hash = "sha256:984ee13543a346324319a1fb72b698e521506f6f22dc37d7752a329e9cd00a32"}, - {file = "SQLAlchemy-1.4.46-cp36-cp36m-macosx_10_14_x86_64.whl", hash = "sha256:9167d4227b56591a4cc5524f1b79ccd7ea994f36e4c648ab42ca995d28ebbb96"}, - {file = "SQLAlchemy-1.4.46-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d61e9ecc849d8d44d7f80894ecff4abe347136e9d926560b818f6243409f3c86"}, - {file = "SQLAlchemy-1.4.46-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:3ec187acf85984263299a3f15c34a6c0671f83565d86d10f43ace49881a82718"}, - {file = "SQLAlchemy-1.4.46-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9883f5fae4fd8e3f875adc2add69f8b945625811689a6c65866a35ee9c0aea23"}, - {file = "SQLAlchemy-1.4.46-cp36-cp36m-win32.whl", hash = "sha256:535377e9b10aff5a045e3d9ada8a62d02058b422c0504ebdcf07930599890eb0"}, - {file = "SQLAlchemy-1.4.46-cp36-cp36m-win_amd64.whl", hash = "sha256:18cafdb27834fa03569d29f571df7115812a0e59fd6a3a03ccb0d33678ec8420"}, - {file = "SQLAlchemy-1.4.46-cp37-cp37m-macosx_10_15_x86_64.whl", hash = "sha256:a1ad90c97029cc3ab4ffd57443a20fac21d2ec3c89532b084b073b3feb5abff3"}, - {file = "SQLAlchemy-1.4.46-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4847f4b1d822754e35707db913396a29d874ee77b9c3c3ef3f04d5a9a6209618"}, - {file = "SQLAlchemy-1.4.46-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:c5a99282848b6cae0056b85da17392a26b2d39178394fc25700bcf967e06e97a"}, - {file = "SQLAlchemy-1.4.46-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d4b1cc7835b39835c75cf7c20c926b42e97d074147c902a9ebb7cf2c840dc4e2"}, - {file = "SQLAlchemy-1.4.46-cp37-cp37m-win32.whl", hash = "sha256:c522e496f9b9b70296a7675272ec21937ccfc15da664b74b9f58d98a641ce1b6"}, - {file = "SQLAlchemy-1.4.46-cp37-cp37m-win_amd64.whl", hash = "sha256:ae067ab639fa499f67ded52f5bc8e084f045d10b5ac7bb928ae4ca2b6c0429a5"}, - {file = "SQLAlchemy-1.4.46-cp38-cp38-macosx_10_15_x86_64.whl", hash = "sha256:e3c1808008124850115a3f7e793a975cfa5c8a26ceeeb9ff9cbb4485cac556df"}, - {file = "SQLAlchemy-1.4.46-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d4d164df3d83d204c69f840da30b292ac7dc54285096c6171245b8d7807185aa"}, - {file = "SQLAlchemy-1.4.46-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:b33ffbdbbf5446cf36cd4cc530c9d9905d3c2fe56ed09e25c22c850cdb9fac92"}, - {file = "SQLAlchemy-1.4.46-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3d94682732d1a0def5672471ba42a29ff5e21bb0aae0afa00bb10796fc1e28dd"}, - {file = "SQLAlchemy-1.4.46-cp38-cp38-win32.whl", hash = "sha256:f8cb80fe8d14307e4124f6fad64dfd87ab749c9d275f82b8b4ec84c84ecebdbe"}, - {file = "SQLAlchemy-1.4.46-cp38-cp38-win_amd64.whl", hash = "sha256:07e48cbcdda6b8bc7a59d6728bd3f5f574ffe03f2c9fb384239f3789c2d95c2e"}, - {file = "SQLAlchemy-1.4.46-cp39-cp39-macosx_11_0_x86_64.whl", hash = "sha256:1b1e5e96e2789d89f023d080bee432e2fef64d95857969e70d3cadec80bd26f0"}, - {file = "SQLAlchemy-1.4.46-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a3714e5b33226131ac0da60d18995a102a17dddd42368b7bdd206737297823ad"}, - {file = "SQLAlchemy-1.4.46-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:955162ad1a931fe416eded6bb144ba891ccbf9b2e49dc7ded39274dd9c5affc5"}, - {file = "SQLAlchemy-1.4.46-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b6e4cb5c63f705c9d546a054c60d326cbde7421421e2d2565ce3e2eee4e1a01f"}, - {file = "SQLAlchemy-1.4.46-cp39-cp39-win32.whl", hash = "sha256:51e1ba2884c6a2b8e19109dc08c71c49530006c1084156ecadfaadf5f9b8b053"}, - {file = "SQLAlchemy-1.4.46-cp39-cp39-win_amd64.whl", hash = "sha256:315676344e3558f1f80d02535f410e80ea4e8fddba31ec78fe390eff5fb8f466"}, - {file = "SQLAlchemy-1.4.46.tar.gz", hash = "sha256:6913b8247d8a292ef8315162a51931e2b40ce91681f1b6f18f697045200c4a30"}, + {file = "SQLAlchemy-1.4.49-cp27-cp27m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:2e126cf98b7fd38f1e33c64484406b78e937b1a280e078ef558b95bf5b6895f6"}, + {file = "SQLAlchemy-1.4.49-cp27-cp27mu-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:03db81b89fe7ef3857b4a00b63dedd632d6183d4ea5a31c5d8a92e000a41fc71"}, + {file = "SQLAlchemy-1.4.49-cp310-cp310-macosx_11_0_x86_64.whl", hash = "sha256:95b9df9afd680b7a3b13b38adf6e3a38995da5e162cc7524ef08e3be4e5ed3e1"}, + {file = "SQLAlchemy-1.4.49-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a63e43bf3f668c11bb0444ce6e809c1227b8f067ca1068898f3008a273f52b09"}, + {file = "SQLAlchemy-1.4.49-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:f835c050ebaa4e48b18403bed2c0fda986525896efd76c245bdd4db995e51a4c"}, + {file = "SQLAlchemy-1.4.49-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9c21b172dfb22e0db303ff6419451f0cac891d2e911bb9fbf8003d717f1bcf91"}, + {file = "SQLAlchemy-1.4.49-cp310-cp310-win32.whl", hash = "sha256:5fb1ebdfc8373b5a291485757bd6431de8d7ed42c27439f543c81f6c8febd729"}, + {file = "SQLAlchemy-1.4.49-cp310-cp310-win_amd64.whl", hash = "sha256:f8a65990c9c490f4651b5c02abccc9f113a7f56fa482031ac8cb88b70bc8ccaa"}, + {file = "SQLAlchemy-1.4.49-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:8923dfdf24d5aa8a3adb59723f54118dd4fe62cf59ed0d0d65d940579c1170a4"}, + {file = "SQLAlchemy-1.4.49-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a9ab2c507a7a439f13ca4499db6d3f50423d1d65dc9b5ed897e70941d9e135b0"}, + {file = "SQLAlchemy-1.4.49-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5debe7d49b8acf1f3035317e63d9ec8d5e4d904c6e75a2a9246a119f5f2fdf3d"}, + {file = "SQLAlchemy-1.4.49-cp311-cp311-win32.whl", hash = "sha256:82b08e82da3756765c2e75f327b9bf6b0f043c9c3925fb95fb51e1567fa4ee87"}, + {file = "SQLAlchemy-1.4.49-cp311-cp311-win_amd64.whl", hash = "sha256:171e04eeb5d1c0d96a544caf982621a1711d078dbc5c96f11d6469169bd003f1"}, + {file = "SQLAlchemy-1.4.49-cp36-cp36m-macosx_10_14_x86_64.whl", hash = "sha256:36e58f8c4fe43984384e3fbe6341ac99b6b4e083de2fe838f0fdb91cebe9e9cb"}, + {file = "SQLAlchemy-1.4.49-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b31e67ff419013f99ad6f8fc73ee19ea31585e1e9fe773744c0f3ce58c039c30"}, + {file = "SQLAlchemy-1.4.49-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:c14b29d9e1529f99efd550cd04dbb6db6ba5d690abb96d52de2bff4ed518bc95"}, + {file = "SQLAlchemy-1.4.49-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c40f3470e084d31247aea228aa1c39bbc0904c2b9ccbf5d3cfa2ea2dac06f26d"}, + {file = "SQLAlchemy-1.4.49-cp36-cp36m-win32.whl", hash = "sha256:706bfa02157b97c136547c406f263e4c6274a7b061b3eb9742915dd774bbc264"}, + {file = "SQLAlchemy-1.4.49-cp36-cp36m-win_amd64.whl", hash = "sha256:a7f7b5c07ae5c0cfd24c2db86071fb2a3d947da7bd487e359cc91e67ac1c6d2e"}, + {file = "SQLAlchemy-1.4.49-cp37-cp37m-macosx_11_0_x86_64.whl", hash = "sha256:4afbbf5ef41ac18e02c8dc1f86c04b22b7a2125f2a030e25bbb4aff31abb224b"}, + {file = "SQLAlchemy-1.4.49-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:24e300c0c2147484a002b175f4e1361f102e82c345bf263242f0449672a4bccf"}, + {file = "SQLAlchemy-1.4.49-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:201de072b818f8ad55c80d18d1a788729cccf9be6d9dc3b9d8613b053cd4836d"}, + {file = "SQLAlchemy-1.4.49-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7653ed6817c710d0c95558232aba799307d14ae084cc9b1f4c389157ec50df5c"}, + {file = "SQLAlchemy-1.4.49-cp37-cp37m-win32.whl", hash = "sha256:647e0b309cb4512b1f1b78471fdaf72921b6fa6e750b9f891e09c6e2f0e5326f"}, + {file = "SQLAlchemy-1.4.49-cp37-cp37m-win_amd64.whl", hash = "sha256:ab73ed1a05ff539afc4a7f8cf371764cdf79768ecb7d2ec691e3ff89abbc541e"}, + {file = "SQLAlchemy-1.4.49-cp38-cp38-macosx_11_0_x86_64.whl", hash = "sha256:37ce517c011560d68f1ffb28af65d7e06f873f191eb3a73af5671e9c3fada08a"}, + {file = "SQLAlchemy-1.4.49-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a1878ce508edea4a879015ab5215546c444233881301e97ca16fe251e89f1c55"}, + {file = "SQLAlchemy-1.4.49-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:0e8e608983e6f85d0852ca61f97e521b62e67969e6e640fe6c6b575d4db68557"}, + {file = "SQLAlchemy-1.4.49-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ccf956da45290df6e809ea12c54c02ace7f8ff4d765d6d3dfb3655ee876ce58d"}, + {file = "SQLAlchemy-1.4.49-cp38-cp38-win32.whl", hash = "sha256:f167c8175ab908ce48bd6550679cc6ea20ae169379e73c7720a28f89e53aa532"}, + {file = "SQLAlchemy-1.4.49-cp38-cp38-win_amd64.whl", hash = "sha256:45806315aae81a0c202752558f0df52b42d11dd7ba0097bf71e253b4215f34f4"}, + {file = "SQLAlchemy-1.4.49-cp39-cp39-macosx_11_0_x86_64.whl", hash = "sha256:b6d0c4b15d65087738a6e22e0ff461b407533ff65a73b818089efc8eb2b3e1de"}, + {file = "SQLAlchemy-1.4.49-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a843e34abfd4c797018fd8d00ffffa99fd5184c421f190b6ca99def4087689bd"}, + {file = "SQLAlchemy-1.4.49-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:1c890421651b45a681181301b3497e4d57c0d01dc001e10438a40e9a9c25ee77"}, + {file = "SQLAlchemy-1.4.49-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d26f280b8f0a8f497bc10573849ad6dc62e671d2468826e5c748d04ed9e670d5"}, + {file = "SQLAlchemy-1.4.49-cp39-cp39-win32.whl", hash = "sha256:ec2268de67f73b43320383947e74700e95c6770d0c68c4e615e9897e46296294"}, + {file = "SQLAlchemy-1.4.49-cp39-cp39-win_amd64.whl", hash = "sha256:bbdf16372859b8ed3f4d05f925a984771cd2abd18bd187042f24be4886c2a15f"}, + {file = "SQLAlchemy-1.4.49.tar.gz", hash = "sha256:06ff25cbae30c396c4b7737464f2a7fc37a67b7da409993b182b024cec80aed9"}, ] [package.dependencies] aiosqlite = {version = "*", optional = true, markers = "python_version >= \"3\" and extra == \"aiosqlite\""} -greenlet = {version = "!=0.4.17", optional = true, markers = "python_version >= \"3\" and platform_machine == \"aarch64\" or python_version >= \"3\" and platform_machine == \"ppc64le\" or python_version >= \"3\" and platform_machine == \"x86_64\" or python_version >= \"3\" and platform_machine == \"amd64\" or python_version >= \"3\" and platform_machine == \"AMD64\" or python_version >= \"3\" and platform_machine == \"win32\" or python_version >= \"3\" and platform_machine == \"WIN32\" or python_version >= \"3\" and extra == \"aiosqlite\""} +greenlet = {version = "!=0.4.17", optional = true, markers = "python_version >= \"3\" and (platform_machine == \"win32\" or platform_machine == \"WIN32\" or platform_machine == \"AMD64\" or platform_machine == \"amd64\" or platform_machine == \"x86_64\" or platform_machine == \"ppc64le\" or platform_machine == \"aarch64\" or extra == \"aiosqlite\")"} typing-extensions = {version = "!=3.10.0.1", optional = true, markers = "extra == \"aiosqlite\""} [package.extras] @@ -783,14 +718,13 @@ sqlcipher = ["sqlcipher3-binary"] [[package]] name = "starlette" -version = "0.26.1" +version = "0.30.0" description = "The little ASGI library that shines." -category = "main" optional = false -python-versions = ">=3.7" +python-versions = ">=3.8" files = [ - {file = "starlette-0.26.1-py3-none-any.whl", hash = "sha256:e87fce5d7cbdde34b76f0ac69013fd9d190d581d80681493016666e6f96c6d5e"}, - {file = "starlette-0.26.1.tar.gz", hash = "sha256:41da799057ea8620e4667a3e69a5b1923ebd32b1819c8fa75634bbe8d8bea9bd"}, + {file = "starlette-0.30.0-py3-none-any.whl", hash = "sha256:cb15a5dfbd8de70c999bd1ae4b7e1ba625d74520bc57b28cc4086c7969431f2d"}, + {file = "starlette-0.30.0.tar.gz", hash = "sha256:9cf6bd5f2fbc091c2f22701f9b7f7dfcbd304a567845cffbf89d706543fd2a03"}, ] [package.dependencies] @@ -801,21 +735,19 @@ full = ["httpx (>=0.22.0)", "itsdangerous", "jinja2", "python-multipart", "pyyam [[package]] name = "typing-extensions" -version = "4.5.0" +version = "4.7.1" description = "Backported and Experimental Type Hints for Python 3.7+" -category = "main" optional = false python-versions = ">=3.7" files = [ - {file = "typing_extensions-4.5.0-py3-none-any.whl", hash = "sha256:fb33085c39dd998ac16d1431ebc293a8b3eedd00fd4a32de0ff79002c19511b4"}, - {file = "typing_extensions-4.5.0.tar.gz", hash = "sha256:5cb5f4a79139d699607b3ef622a1dedafa84e115ab0024e0d9c044a9479ca7cb"}, + {file = "typing_extensions-4.7.1-py3-none-any.whl", hash = "sha256:440d5dd3af93b060174bf433bccd69b0babc3b15b1a8dca43789fd7f61514b36"}, + {file = "typing_extensions-4.7.1.tar.gz", hash = "sha256:b75ddc264f0ba5615db7ba217daeb99701ad295353c45f9e95963337ceeeffb2"}, ] [[package]] name = "ulid-py" version = "1.1.0" description = "Universally Unique Lexicographically Sortable Identifier" -category = "main" optional = false python-versions = "*" files = [ @@ -825,14 +757,13 @@ files = [ [[package]] name = "uvicorn" -version = "0.21.1" +version = "0.23.1" description = "The lightning-fast ASGI server." -category = "main" optional = false -python-versions = ">=3.7" +python-versions = ">=3.8" files = [ - {file = "uvicorn-0.21.1-py3-none-any.whl", hash = "sha256:e47cac98a6da10cd41e6fd036d472c6f58ede6c5dbee3dbee3ef7a100ed97742"}, - {file = "uvicorn-0.21.1.tar.gz", hash = "sha256:0fac9cb342ba099e0d582966005f3fdba5b0290579fed4a6266dc702ca7bb032"}, + {file = "uvicorn-0.23.1-py3-none-any.whl", hash = "sha256:1d55d46b83ee4ce82b4e82f621f2050adb3eb7b5481c13f9af1744951cae2f1f"}, + {file = "uvicorn-0.23.1.tar.gz", hash = "sha256:da9b0c8443b2d7ee9db00a345f1eee6db7317432c9d4400f5049cc8d358383be"}, ] [package.dependencies] @@ -846,7 +777,6 @@ standard = ["colorama (>=0.4)", "httptools (>=0.5.0)", "python-dotenv (>=0.13)", name = "webencodings" version = "0.5.1" description = "Character encoding aliases for legacy web content" -category = "main" optional = false python-versions = "*" files = [ @@ -857,4 +787,4 @@ files = [ [metadata] lock-version = "2.0" python-versions = "^3.11" -content-hash = "c9f9a32904d3bcbbb8a43e37a811d17aee21ff8b0e22bc2ed01ff309470d1c82" +content-hash = "dddba1976dd913e947b3a9374b167d4d2dd145298cf63f5a42d4139ae4b7e3be" diff --git a/pyproject.toml b/pyproject.toml index 167c314..ce43209 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -9,11 +9,11 @@ license = "LOL" python = "^3.11" beautifulsoup4 = "^4.9.3" html5lib = "^1.1" -starlette = "^0.26" +starlette = "^0.30" ulid-py = "^1.1.0" databases = {extras = ["sqlite"], version = "^0.7.0"} -uvicorn = "^0.21" -httpx = "^0.23.3" +uvicorn = "^0.23" +httpx = "^0.24" sqlalchemy = {version = "^1.4", extras = ["aiosqlite"]} [tool.poetry.group.dev] From 86c3030e31bdb37737fe73bcecb33d437b83f59d Mon Sep 17 00:00:00 2001 From: ducklet Date: Sat, 22 Jul 2023 19:36:19 +0200 Subject: [PATCH 20/31] remove unused classvars --- unwind/models.py | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/unwind/models.py b/unwind/models.py index 93b51f9..77e5b29 100644 --- a/unwind/models.py +++ b/unwind/models.py @@ -240,8 +240,6 @@ class Progress: Column("stopped", String), ) - _table: ClassVar[str] = "progress" - id: ULID = field(default_factory=ULID) type: str = None state: str = None @@ -297,8 +295,6 @@ class Movie: Column("updated", String, nullable=False), # datetime ) - _table: ClassVar[str] = "movies" - id: ULID = field(default_factory=ULID) title: str = None # canonical title (usually English) original_title: str | None = ( @@ -370,8 +366,6 @@ class Rating: Column("finished", Integer), # bool ) - _table: ClassVar[str] = "ratings" - id: ULID = field(default_factory=ULID) movie_id: ULID = None @@ -425,8 +419,6 @@ class User: Column("groups", String, nullable=False), # JSON array ) - _table: ClassVar[str] = "users" - id: ULID = field(default_factory=ULID) imdb_id: str = None name: str = None # canonical user name @@ -463,8 +455,6 @@ class Group: Column("users", String, nullable=False), # JSON array ) - _table: ClassVar[str] = "groups" - id: ULID = field(default_factory=ULID) name: str = None users: list[GroupUser] = field(default_factory=list) From 25f31db756bcd499833e3eed1d6e75ea98624f80 Mon Sep 17 00:00:00 2001 From: ducklet Date: Sat, 22 Jul 2023 19:37:01 +0200 Subject: [PATCH 21/31] improve strict typing --- tests/test_db.py | 16 ++++++++++------ unwind/db.py | 21 +++++++++++---------- unwind/models.py | 3 +++ unwind/request.py | 18 ++++++++++-------- 4 files changed, 34 insertions(+), 24 deletions(-) diff --git a/tests/test_db.py b/tests/test_db.py index 7c4c96e..cd5f295 100644 --- a/tests/test_db.py +++ b/tests/test_db.py @@ -38,21 +38,25 @@ async def test_get(shared_conn: db.Database): m2 = a_movie(release_year=m1.release_year + 1) await db.add(m2) - assert None == await db.get(models.Movie) - assert None == await db.get(models.Movie, id="blerp") + assert None is await db.get(models.Movie) + assert None is await db.get(models.Movie, id="blerp") assert m1 == await db.get(models.Movie, id=str(m1.id)) assert m2 == await db.get(models.Movie, release_year=m2.release_year) - assert None == await db.get( + assert None is await db.get( models.Movie, id=str(m1.id), release_year=m2.release_year ) assert m2 == await db.get( models.Movie, id=str(m2.id), release_year=m2.release_year ) assert m1 == await db.get( - models.Movie, media_type=m1.media_type, order_by=("release_year", "asc") + models.Movie, + media_type=m1.media_type, + order_by=(models.movies.c.release_year, "asc"), ) assert m2 == await db.get( - models.Movie, media_type=m1.media_type, order_by=("release_year", "desc") + models.Movie, + media_type=m1.media_type, + order_by=(models.movies.c.release_year, "desc"), ) @@ -136,7 +140,7 @@ async def test_remove(shared_conn: db.Database): assert m1 == await db.get(models.Movie, id=str(m1.id)) await db.remove(m1) - assert None == await db.get(models.Movie, id=str(m1.id)) + assert None is await db.get(models.Movie, id=str(m1.id)) @pytest.mark.asyncio diff --git a/unwind/db.py b/unwind/db.py index 63c042a..7a0169d 100644 --- a/unwind/db.py +++ b/unwind/db.py @@ -22,6 +22,7 @@ from .models import ( metadata, movies, optional_fields, + progress, ratings, utcnow, ) @@ -110,19 +111,21 @@ async def apply_db_patches(db: Database) -> None: async with db.transaction(): for query in queries: - await db.execute(query) + await db.execute(sa.text(query)) await set_current_patch_level(db, patch_lvl) did_patch = True if did_patch: - await db.execute("vacuum") + await db.execute(sa.text("vacuum")) async def get_import_progress() -> Progress | None: """Return the latest import progress.""" - return await get(Progress, type="import-imdb-movies", order_by=("started", "desc")) + return await get( + Progress, type="import-imdb-movies", order_by=(progress.c.started, "desc") + ) async def stop_import_progress(*, error: BaseException | None = None) -> None: @@ -225,7 +228,7 @@ async def add(item: Model) -> None: # Support late initializing - used for optimization. if getattr(item, "_is_lazy", False): assert hasattr(item, "_lazy_init") - item._lazy_init() + item._lazy_init() # pyright: ignore [reportGeneralTypeIssues] table: sa.Table = item.__table__ values = asplain(item, serialize=True) @@ -240,7 +243,7 @@ ModelType = TypeVar("ModelType", bound=Model) async def get( model: Type[ModelType], *, - order_by: tuple[str, Literal["asc", "desc"]] | None = None, + order_by: tuple[sa.Column, Literal["asc", "desc"]] | None = None, **field_values, ) -> ModelType | None: """Load a model instance from the database. @@ -259,9 +262,7 @@ async def get( if order_by: order_col, order_dir = order_by query = query.order_by( - table.c[order_col].asc() - if order_dir == "asc" - else table.c[order_col].desc() + order_col.asc() if order_dir == "asc" else order_col.desc() ) async with locked_connection() as conn: row = await conn.fetch_one(query) @@ -306,7 +307,7 @@ async def update(item: Model) -> None: # Support late initializing - used for optimization. if getattr(item, "_is_lazy", False): assert hasattr(item, "_lazy_init") - item._lazy_init() + item._lazy_init() # pyright: ignore [reportGeneralTypeIssues] table: sa.Table = item.__table__ values = asplain(item, serialize=True) @@ -587,7 +588,7 @@ async def find_movies( conditions.append(movies.c.media_type != "TV Episode") if not include_unrated: - conditions.append(movies.c.imdb_score != None) + conditions.append(movies.c.imdb_score.is_not(None)) query = ( sa.select(movies) diff --git a/unwind/models.py b/unwind/models.py index 77e5b29..13af462 100644 --- a/unwind/models.py +++ b/unwind/models.py @@ -275,6 +275,9 @@ class Progress: self._state = state +progress = Progress.__table__ + + @mapper_registry.mapped @dataclass class Movie: diff --git a/unwind/request.py b/unwind/request.py index 4e57564..afd2b86 100644 --- a/unwind/request.py +++ b/unwind/request.py @@ -11,7 +11,7 @@ from hashlib import md5 from pathlib import Path from random import random from time import sleep, time -from typing import Callable, ParamSpec, TypeVar, cast +from typing import Any, Callable, ParamSpec, TypeVar, cast import bs4 import httpx @@ -190,9 +190,11 @@ async def asoup_from_url(url): def _last_modified_from_response(resp: _Response_T) -> float | None: if last_mod := resp.headers.get("last-modified"): try: - return email.utils.parsedate_to_datetime(last_mod).timestamp() - except: + dt = email.utils.parsedate_to_datetime(last_mod) + except ValueError: log.exception("🐛 Received invalid value for Last-Modified: %s", last_mod) + else: + return dt.timestamp() def _last_modified_from_file(path: Path) -> float: @@ -206,8 +208,8 @@ async def adownload( replace_existing: bool | None = None, only_if_newer: bool = False, timeout: float | None = None, - chunk_callback=None, - response_callback=None, + chunk_callback: Callable[[bytes], Any] | None = None, + response_callback: Callable[[_Response_T], Any] | None = None, ) -> bytes | None: """Download a file. @@ -246,7 +248,7 @@ async def adownload( if response_callback is not None: try: response_callback(resp) - except: + except BaseException: log.exception("🐛 Error in response callback.") log.debug( @@ -275,7 +277,7 @@ async def adownload( # Check Last-Modified in case the server ignored If-Modified-Since. # XXX also check Content-Length? if file_exists and only_if_newer and resp_lastmod is not None: - assert file_lastmod + assert file_lastmod # pyright: ignore [reportUnboundVariable] if resp_lastmod <= file_lastmod: log.debug("✋ Local file is newer, skipping download: %a", req.url) @@ -299,7 +301,7 @@ async def adownload( if chunk_callback: try: chunk_callback(chunk) - except: + except BaseException: log.exception("🐛 Error in chunk callback.") finally: os.close(tempfd) From b5a93cb92f626e90e885580bdaab68a4dce982d4 Mon Sep 17 00:00:00 2001 From: ducklet Date: Sat, 22 Jul 2023 20:19:23 +0200 Subject: [PATCH 22/31] update Npm dependencies --- unwind-ui/package-lock.json | 1332 ++++++++++------------------------- 1 file changed, 389 insertions(+), 943 deletions(-) diff --git a/unwind-ui/package-lock.json b/unwind-ui/package-lock.json index 1b7995d..1232bbd 100644 --- a/unwind-ui/package-lock.json +++ b/unwind-ui/package-lock.json @@ -1,7 +1,7 @@ { "name": "unwind-ui", "version": "0.0.0", - "lockfileVersion": 2, + "lockfileVersion": 3, "requires": true, "packages": { "": { @@ -19,9 +19,9 @@ } }, "node_modules/@babel/parser": { - "version": "7.16.8", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.16.8.tgz", - "integrity": "sha512-i7jDUfrVBWc+7OKcBzEe5n7fbv3i2fWtxKzzCvOjnzSxMfWMigAhtfJ7qzZNGFNMsCCd67+uz553dYKWXPvCKw==", + "version": "7.22.7", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.22.7.tgz", + "integrity": "sha512-7NF8pOkHP5o2vpmGgNGcfAeCvOYhGLyA3Z4eBQkT1RJlWu47n63bCs93QfJ2hIAFCil7L5P2IWhs1oToVgrL0Q==", "bin": { "parser": "bin/babel-parser.js" }, @@ -29,6 +29,27 @@ "node": ">=6.0.0" } }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.14.54", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.14.54.tgz", + "integrity": "sha512-bZBrLAIX1kpWelV0XemxBZllyRmM6vgFQQG2GdNb+r3Fkp0FOh1NJSvekXDs7jq70k4euu1cryLMfU+mTXlEpw==", + "cpu": [ + "loong64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.4.15", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.15.tgz", + "integrity": "sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg==" + }, "node_modules/@vitejs/plugin-vue": { "version": "1.10.2", "resolved": "https://registry.npmjs.org/@vitejs/plugin-vue/-/plugin-vue-1.10.2.tgz", @@ -42,106 +63,106 @@ } }, "node_modules/@vue/compiler-core": { - "version": "3.2.26", - "resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.2.26.tgz", - "integrity": "sha512-N5XNBobZbaASdzY9Lga2D9Lul5vdCIOXvUMd6ThcN8zgqQhPKfCV+wfAJNNJKQkSHudnYRO2gEB+lp0iN3g2Tw==", + "version": "3.3.4", + "resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.3.4.tgz", + "integrity": "sha512-cquyDNvZ6jTbf/+x+AgM2Arrp6G4Dzbb0R64jiG804HRMfRiFXWI6kqUVqZ6ZR0bQhIoQjB4+2bhNtVwndW15g==", "dependencies": { - "@babel/parser": "^7.16.4", - "@vue/shared": "3.2.26", + "@babel/parser": "^7.21.3", + "@vue/shared": "3.3.4", "estree-walker": "^2.0.2", - "source-map": "^0.6.1" + "source-map-js": "^1.0.2" } }, "node_modules/@vue/compiler-dom": { - "version": "3.2.26", - "resolved": "https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.2.26.tgz", - "integrity": "sha512-smBfaOW6mQDxcT3p9TKT6mE22vjxjJL50GFVJiI0chXYGU/xzC05QRGrW3HHVuJrmLTLx5zBhsZ2dIATERbarg==", + "version": "3.3.4", + "resolved": "https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.3.4.tgz", + "integrity": "sha512-wyM+OjOVpuUukIq6p5+nwHYtj9cFroz9cwkfmP9O1nzH68BenTTv0u7/ndggT8cIQlnBeOo6sUT/gvHcIkLA5w==", "dependencies": { - "@vue/compiler-core": "3.2.26", - "@vue/shared": "3.2.26" + "@vue/compiler-core": "3.3.4", + "@vue/shared": "3.3.4" } }, "node_modules/@vue/compiler-sfc": { - "version": "3.2.26", - "resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-3.2.26.tgz", - "integrity": "sha512-ePpnfktV90UcLdsDQUh2JdiTuhV0Skv2iYXxfNMOK/F3Q+2BO0AulcVcfoksOpTJGmhhfosWfMyEaEf0UaWpIw==", + "version": "3.3.4", + "resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-3.3.4.tgz", + "integrity": "sha512-6y/d8uw+5TkCuzBkgLS0v3lSM3hJDntFEiUORM11pQ/hKvkhSKZrXW6i69UyXlJQisJxuUEJKAWEqWbWsLeNKQ==", "dependencies": { - "@babel/parser": "^7.16.4", - "@vue/compiler-core": "3.2.26", - "@vue/compiler-dom": "3.2.26", - "@vue/compiler-ssr": "3.2.26", - "@vue/reactivity-transform": "3.2.26", - "@vue/shared": "3.2.26", + "@babel/parser": "^7.20.15", + "@vue/compiler-core": "3.3.4", + "@vue/compiler-dom": "3.3.4", + "@vue/compiler-ssr": "3.3.4", + "@vue/reactivity-transform": "3.3.4", + "@vue/shared": "3.3.4", "estree-walker": "^2.0.2", - "magic-string": "^0.25.7", + "magic-string": "^0.30.0", "postcss": "^8.1.10", - "source-map": "^0.6.1" + "source-map-js": "^1.0.2" } }, "node_modules/@vue/compiler-ssr": { - "version": "3.2.26", - "resolved": "https://registry.npmjs.org/@vue/compiler-ssr/-/compiler-ssr-3.2.26.tgz", - "integrity": "sha512-2mywLX0ODc4Zn8qBoA2PDCsLEZfpUGZcyoFRLSOjyGGK6wDy2/5kyDOWtf0S0UvtoyVq95OTSGIALjZ4k2q/ag==", + "version": "3.3.4", + "resolved": "https://registry.npmjs.org/@vue/compiler-ssr/-/compiler-ssr-3.3.4.tgz", + "integrity": "sha512-m0v6oKpup2nMSehwA6Uuu+j+wEwcy7QmwMkVNVfrV9P2qE5KshC6RwOCq8fjGS/Eak/uNb8AaWekfiXxbBB6gQ==", "dependencies": { - "@vue/compiler-dom": "3.2.26", - "@vue/shared": "3.2.26" + "@vue/compiler-dom": "3.3.4", + "@vue/shared": "3.3.4" } }, "node_modules/@vue/reactivity": { - "version": "3.2.26", - "resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.2.26.tgz", - "integrity": "sha512-h38bxCZLW6oFJVDlCcAiUKFnXI8xP8d+eO0pcDxx+7dQfSPje2AO6M9S9QO6MrxQB7fGP0DH0dYQ8ksf6hrXKQ==", + "version": "3.3.4", + "resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.3.4.tgz", + "integrity": "sha512-kLTDLwd0B1jG08NBF3R5rqULtv/f8x3rOFByTDz4J53ttIQEDmALqKqXY0J+XQeN0aV2FBxY8nJDf88yvOPAqQ==", "dependencies": { - "@vue/shared": "3.2.26" + "@vue/shared": "3.3.4" } }, "node_modules/@vue/reactivity-transform": { - "version": "3.2.26", - "resolved": "https://registry.npmjs.org/@vue/reactivity-transform/-/reactivity-transform-3.2.26.tgz", - "integrity": "sha512-XKMyuCmzNA7nvFlYhdKwD78rcnmPb7q46uoR00zkX6yZrUmcCQ5OikiwUEVbvNhL5hBJuvbSO95jB5zkUon+eQ==", + "version": "3.3.4", + "resolved": "https://registry.npmjs.org/@vue/reactivity-transform/-/reactivity-transform-3.3.4.tgz", + "integrity": "sha512-MXgwjako4nu5WFLAjpBnCj/ieqcjE2aJBINUNQzkZQfzIZA4xn+0fV1tIYBJvvva3N3OvKGofRLvQIwEQPpaXw==", "dependencies": { - "@babel/parser": "^7.16.4", - "@vue/compiler-core": "3.2.26", - "@vue/shared": "3.2.26", + "@babel/parser": "^7.20.15", + "@vue/compiler-core": "3.3.4", + "@vue/shared": "3.3.4", "estree-walker": "^2.0.2", - "magic-string": "^0.25.7" + "magic-string": "^0.30.0" } }, "node_modules/@vue/runtime-core": { - "version": "3.2.26", - "resolved": "https://registry.npmjs.org/@vue/runtime-core/-/runtime-core-3.2.26.tgz", - "integrity": "sha512-BcYi7qZ9Nn+CJDJrHQ6Zsmxei2hDW0L6AB4vPvUQGBm2fZyC0GXd/4nVbyA2ubmuhctD5RbYY8L+5GUJszv9mQ==", + "version": "3.3.4", + "resolved": "https://registry.npmjs.org/@vue/runtime-core/-/runtime-core-3.3.4.tgz", + "integrity": "sha512-R+bqxMN6pWO7zGI4OMlmvePOdP2c93GsHFM/siJI7O2nxFRzj55pLwkpCedEY+bTMgp5miZ8CxfIZo3S+gFqvA==", "dependencies": { - "@vue/reactivity": "3.2.26", - "@vue/shared": "3.2.26" + "@vue/reactivity": "3.3.4", + "@vue/shared": "3.3.4" } }, "node_modules/@vue/runtime-dom": { - "version": "3.2.26", - "resolved": "https://registry.npmjs.org/@vue/runtime-dom/-/runtime-dom-3.2.26.tgz", - "integrity": "sha512-dY56UIiZI+gjc4e8JQBwAifljyexfVCkIAu/WX8snh8vSOt/gMSEGwPRcl2UpYpBYeyExV8WCbgvwWRNt9cHhQ==", + "version": "3.3.4", + "resolved": "https://registry.npmjs.org/@vue/runtime-dom/-/runtime-dom-3.3.4.tgz", + "integrity": "sha512-Aj5bTJ3u5sFsUckRghsNjVTtxZQ1OyMWCr5dZRAPijF/0Vy4xEoRCwLyHXcj4D0UFbJ4lbx3gPTgg06K/GnPnQ==", "dependencies": { - "@vue/runtime-core": "3.2.26", - "@vue/shared": "3.2.26", - "csstype": "^2.6.8" + "@vue/runtime-core": "3.3.4", + "@vue/shared": "3.3.4", + "csstype": "^3.1.1" } }, "node_modules/@vue/server-renderer": { - "version": "3.2.26", - "resolved": "https://registry.npmjs.org/@vue/server-renderer/-/server-renderer-3.2.26.tgz", - "integrity": "sha512-Jp5SggDUvvUYSBIvYEhy76t4nr1vapY/FIFloWmQzn7UxqaHrrBpbxrqPcTrSgGrcaglj0VBp22BKJNre4aA1w==", + "version": "3.3.4", + "resolved": "https://registry.npmjs.org/@vue/server-renderer/-/server-renderer-3.3.4.tgz", + "integrity": "sha512-Q6jDDzR23ViIb67v+vM1Dqntu+HUexQcsWKhhQa4ARVzxOY2HbC7QRW/ggkDBd5BU+uM1sV6XOAP0b216o34JQ==", "dependencies": { - "@vue/compiler-ssr": "3.2.26", - "@vue/shared": "3.2.26" + "@vue/compiler-ssr": "3.3.4", + "@vue/shared": "3.3.4" }, "peerDependencies": { - "vue": "3.2.26" + "vue": "3.3.4" } }, "node_modules/@vue/shared": { - "version": "3.2.26", - "resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.2.26.tgz", - "integrity": "sha512-vPV6Cq+NIWbH5pZu+V+2QHE9y1qfuTq49uNWw4f7FDEeZaDU2H2cx5jcUZOAKW7qTrUS4k6qZPbMy1x4N96nbA==" + "version": "3.3.4", + "resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.3.4.tgz", + "integrity": "sha512-7OjdcV8vQ74eiz1TZLzZP4JwqM5fA94K6yntPS5Z25r9HDuGNzaGdgvwKYq6S+MxwF0TFRwe50fIR/MYnakdkQ==" }, "node_modules/balanced-match": { "version": "1.0.2", @@ -161,7 +182,7 @@ "node_modules/binary": { "version": "0.3.0", "resolved": "https://registry.npmjs.org/binary/-/binary-0.3.0.tgz", - "integrity": "sha1-n2BVO8XOjDOG87VTz/R0Yq3sqnk=", + "integrity": "sha512-D4H1y5KYwpJgK8wk1Cue5LLPgmwHKYSChkbspQg5JtVuR5ulGckxfR62H3AE9UDkdMC8yyXlqYihuz3Aqg2XZg==", "dev": true, "dependencies": { "buffers": "~0.1.1", @@ -174,7 +195,7 @@ "node_modules/bluebird": { "version": "3.4.7", "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.4.7.tgz", - "integrity": "sha1-9y12C+Cbf3bQjtj66Ysomo0F+rM=", + "integrity": "sha512-iD3898SR7sWVRHbiQv+sHUtHnMvC1o3nW5rAcqnq3uOn07DSAppZYUkIGslDz6gXC7HfunPe7YVBgoEJASPcHA==", "dev": true }, "node_modules/brace-expansion": { @@ -199,21 +220,21 @@ "node_modules/buffers": { "version": "0.1.1", "resolved": "https://registry.npmjs.org/buffers/-/buffers-0.1.1.tgz", - "integrity": "sha1-skV5w77U1tOWru5tmorn9Ugqt7s=", + "integrity": "sha512-9q/rDEGSb/Qsvv2qvzIzdluL5k7AaJOTrw23z9reQthrbF7is4CtlT0DXyO1oei2DCp4uojjzQ7igaSHp1kAEQ==", "dev": true, "engines": { "node": ">=0.2.0" } }, "node_modules/bulma": { - "version": "0.9.3", - "resolved": "https://registry.npmjs.org/bulma/-/bulma-0.9.3.tgz", - "integrity": "sha512-0d7GNW1PY4ud8TWxdNcP6Cc8Bu7MxcntD/RRLGWuiw/s0a9P+XlH/6QoOIrmbj6o8WWJzJYhytiu9nFjTszk1g==" + "version": "0.9.4", + "resolved": "https://registry.npmjs.org/bulma/-/bulma-0.9.4.tgz", + "integrity": "sha512-86FlT5+1GrsgKbPLRRY7cGDg8fsJiP/jzTqXXVqiUZZ2aZT8uemEOHlU1CDU+TxklPEZ11HZNNWclRBBecP4CQ==" }, "node_modules/chainsaw": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/chainsaw/-/chainsaw-0.1.0.tgz", - "integrity": "sha1-XqtQsor+WAdNDVgpE4iCi15fvJg=", + "integrity": "sha512-75kWfWt6MEKNC8xYXIdRpDehRYY/tNSgwKaJq+dbbDcxORuVrrQ+SEHoWsniVn9XPYfP4gmdWIeDk/4YNp1rNQ==", "dev": true, "dependencies": { "traverse": ">=0.3.0 <0.4" @@ -225,7 +246,7 @@ "node_modules/concat-map": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", - "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", "dev": true }, "node_modules/core-util-is": { @@ -235,52 +256,75 @@ "dev": true }, "node_modules/csstype": { - "version": "2.6.19", - "resolved": "https://registry.npmjs.org/csstype/-/csstype-2.6.19.tgz", - "integrity": "sha512-ZVxXaNy28/k3kJg0Fou5MiYpp88j7H9hLZp8PDC3jV0WFjfH5E9xHb56L0W59cPbKbcHXeP4qyT8PrHp8t6LcQ==" + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.2.tgz", + "integrity": "sha512-I7K1Uu0MBPzaFKg4nI5Q7Vs2t+3gWWW648spaF+Rg7pI9ds18Ugn+lvg4SHczUdKlHI5LWBXyqfS8+DufyBsgQ==" }, "node_modules/duplexer2": { "version": "0.1.4", "resolved": "https://registry.npmjs.org/duplexer2/-/duplexer2-0.1.4.tgz", - "integrity": "sha1-ixLauHjA1p4+eJEFFmKjL8a93ME=", + "integrity": "sha512-asLFVfWWtJ90ZyOUHMqk7/S2w2guQKxUI2itj3d92ADHhxUSbCMGi1f1cBcJ7xM1To+pE/Khbwo1yuNbMEPKeA==", "dev": true, "dependencies": { "readable-stream": "^2.0.2" } }, "node_modules/esbuild": { - "version": "0.13.15", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.13.15.tgz", - "integrity": "sha512-raCxt02HBKv8RJxE8vkTSCXGIyKHdEdGfUmiYb8wnabnaEmHzyW7DCHb5tEN0xU8ryqg5xw54mcwnYkC4x3AIw==", + "version": "0.14.54", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.14.54.tgz", + "integrity": "sha512-Cy9llcy8DvET5uznocPyqL3BFRrFXSVqbgpMJ9Wz8oVjZlh/zUSNbPRbov0VX7VxN2JH1Oa0uNxZ7eLRb62pJA==", "dev": true, "hasInstallScript": true, "bin": { "esbuild": "bin/esbuild" }, + "engines": { + "node": ">=12" + }, "optionalDependencies": { - "esbuild-android-arm64": "0.13.15", - "esbuild-darwin-64": "0.13.15", - "esbuild-darwin-arm64": "0.13.15", - "esbuild-freebsd-64": "0.13.15", - "esbuild-freebsd-arm64": "0.13.15", - "esbuild-linux-32": "0.13.15", - "esbuild-linux-64": "0.13.15", - "esbuild-linux-arm": "0.13.15", - "esbuild-linux-arm64": "0.13.15", - "esbuild-linux-mips64le": "0.13.15", - "esbuild-linux-ppc64le": "0.13.15", - "esbuild-netbsd-64": "0.13.15", - "esbuild-openbsd-64": "0.13.15", - "esbuild-sunos-64": "0.13.15", - "esbuild-windows-32": "0.13.15", - "esbuild-windows-64": "0.13.15", - "esbuild-windows-arm64": "0.13.15" + "@esbuild/linux-loong64": "0.14.54", + "esbuild-android-64": "0.14.54", + "esbuild-android-arm64": "0.14.54", + "esbuild-darwin-64": "0.14.54", + "esbuild-darwin-arm64": "0.14.54", + "esbuild-freebsd-64": "0.14.54", + "esbuild-freebsd-arm64": "0.14.54", + "esbuild-linux-32": "0.14.54", + "esbuild-linux-64": "0.14.54", + "esbuild-linux-arm": "0.14.54", + "esbuild-linux-arm64": "0.14.54", + "esbuild-linux-mips64le": "0.14.54", + "esbuild-linux-ppc64le": "0.14.54", + "esbuild-linux-riscv64": "0.14.54", + "esbuild-linux-s390x": "0.14.54", + "esbuild-netbsd-64": "0.14.54", + "esbuild-openbsd-64": "0.14.54", + "esbuild-sunos-64": "0.14.54", + "esbuild-windows-32": "0.14.54", + "esbuild-windows-64": "0.14.54", + "esbuild-windows-arm64": "0.14.54" + } + }, + "node_modules/esbuild-android-64": { + "version": "0.14.54", + "resolved": "https://registry.npmjs.org/esbuild-android-64/-/esbuild-android-64-0.14.54.tgz", + "integrity": "sha512-Tz2++Aqqz0rJ7kYBfz+iqyE3QMycD4vk7LBRyWaAVFgFtQ/O8EJOnVmTOiDWYZ/uYzB4kvP+bqejYdVKzE5lAQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" } }, "node_modules/esbuild-android-arm64": { - "version": "0.13.15", - "resolved": "https://registry.npmjs.org/esbuild-android-arm64/-/esbuild-android-arm64-0.13.15.tgz", - "integrity": "sha512-m602nft/XXeO8YQPUDVoHfjyRVPdPgjyyXOxZ44MK/agewFFkPa8tUo6lAzSWh5Ui5PB4KR9UIFTSBKh/RrCmg==", + "version": "0.14.54", + "resolved": "https://registry.npmjs.org/esbuild-android-arm64/-/esbuild-android-arm64-0.14.54.tgz", + "integrity": "sha512-F9E+/QDi9sSkLaClO8SOV6etqPd+5DgJje1F9lOWoNncDdOBL2YF59IhsWATSt0TLZbYCf3pNlTHvVV5VfHdvg==", "cpu": [ "arm64" ], @@ -288,12 +332,15 @@ "optional": true, "os": [ "android" - ] + ], + "engines": { + "node": ">=12" + } }, "node_modules/esbuild-darwin-64": { - "version": "0.13.15", - "resolved": "https://registry.npmjs.org/esbuild-darwin-64/-/esbuild-darwin-64-0.13.15.tgz", - "integrity": "sha512-ihOQRGs2yyp7t5bArCwnvn2Atr6X4axqPpEdCFPVp7iUj4cVSdisgvEKdNR7yH3JDjW6aQDw40iQFoTqejqxvQ==", + "version": "0.14.54", + "resolved": "https://registry.npmjs.org/esbuild-darwin-64/-/esbuild-darwin-64-0.14.54.tgz", + "integrity": "sha512-jtdKWV3nBviOd5v4hOpkVmpxsBy90CGzebpbO9beiqUYVMBtSc0AL9zGftFuBon7PNDcdvNCEuQqw2x0wP9yug==", "cpu": [ "x64" ], @@ -301,12 +348,15 @@ "optional": true, "os": [ "darwin" - ] + ], + "engines": { + "node": ">=12" + } }, "node_modules/esbuild-darwin-arm64": { - "version": "0.13.15", - "resolved": "https://registry.npmjs.org/esbuild-darwin-arm64/-/esbuild-darwin-arm64-0.13.15.tgz", - "integrity": "sha512-i1FZssTVxUqNlJ6cBTj5YQj4imWy3m49RZRnHhLpefFIh0To05ow9DTrXROTE1urGTQCloFUXTX8QfGJy1P8dQ==", + "version": "0.14.54", + "resolved": "https://registry.npmjs.org/esbuild-darwin-arm64/-/esbuild-darwin-arm64-0.14.54.tgz", + "integrity": "sha512-OPafJHD2oUPyvJMrsCvDGkRrVCar5aVyHfWGQzY1dWnzErjrDuSETxwA2HSsyg2jORLY8yBfzc1MIpUkXlctmw==", "cpu": [ "arm64" ], @@ -314,12 +364,15 @@ "optional": true, "os": [ "darwin" - ] + ], + "engines": { + "node": ">=12" + } }, "node_modules/esbuild-freebsd-64": { - "version": "0.13.15", - "resolved": "https://registry.npmjs.org/esbuild-freebsd-64/-/esbuild-freebsd-64-0.13.15.tgz", - "integrity": "sha512-G3dLBXUI6lC6Z09/x+WtXBXbOYQZ0E8TDBqvn7aMaOCzryJs8LyVXKY4CPnHFXZAbSwkCbqiPuSQ1+HhrNk7EA==", + "version": "0.14.54", + "resolved": "https://registry.npmjs.org/esbuild-freebsd-64/-/esbuild-freebsd-64-0.14.54.tgz", + "integrity": "sha512-OKwd4gmwHqOTp4mOGZKe/XUlbDJ4Q9TjX0hMPIDBUWWu/kwhBAudJdBoxnjNf9ocIB6GN6CPowYpR/hRCbSYAg==", "cpu": [ "x64" ], @@ -327,12 +380,15 @@ "optional": true, "os": [ "freebsd" - ] + ], + "engines": { + "node": ">=12" + } }, "node_modules/esbuild-freebsd-arm64": { - "version": "0.13.15", - "resolved": "https://registry.npmjs.org/esbuild-freebsd-arm64/-/esbuild-freebsd-arm64-0.13.15.tgz", - "integrity": "sha512-KJx0fzEDf1uhNOZQStV4ujg30WlnwqUASaGSFPhznLM/bbheu9HhqZ6mJJZM32lkyfGJikw0jg7v3S0oAvtvQQ==", + "version": "0.14.54", + "resolved": "https://registry.npmjs.org/esbuild-freebsd-arm64/-/esbuild-freebsd-arm64-0.14.54.tgz", + "integrity": "sha512-sFwueGr7OvIFiQT6WeG0jRLjkjdqWWSrfbVwZp8iMP+8UHEHRBvlaxL6IuKNDwAozNUmbb8nIMXa7oAOARGs1Q==", "cpu": [ "arm64" ], @@ -340,12 +396,15 @@ "optional": true, "os": [ "freebsd" - ] + ], + "engines": { + "node": ">=12" + } }, "node_modules/esbuild-linux-32": { - "version": "0.13.15", - "resolved": "https://registry.npmjs.org/esbuild-linux-32/-/esbuild-linux-32-0.13.15.tgz", - "integrity": "sha512-ZvTBPk0YWCLMCXiFmD5EUtB30zIPvC5Itxz0mdTu/xZBbbHJftQgLWY49wEPSn2T/TxahYCRDWun5smRa0Tu+g==", + "version": "0.14.54", + "resolved": "https://registry.npmjs.org/esbuild-linux-32/-/esbuild-linux-32-0.14.54.tgz", + "integrity": "sha512-1ZuY+JDI//WmklKlBgJnglpUL1owm2OX+8E1syCD6UAxcMM/XoWd76OHSjl/0MR0LisSAXDqgjT3uJqT67O3qw==", "cpu": [ "ia32" ], @@ -353,12 +412,15 @@ "optional": true, "os": [ "linux" - ] + ], + "engines": { + "node": ">=12" + } }, "node_modules/esbuild-linux-64": { - "version": "0.13.15", - "resolved": "https://registry.npmjs.org/esbuild-linux-64/-/esbuild-linux-64-0.13.15.tgz", - "integrity": "sha512-eCKzkNSLywNeQTRBxJRQ0jxRCl2YWdMB3+PkWFo2BBQYC5mISLIVIjThNtn6HUNqua1pnvgP5xX0nHbZbPj5oA==", + "version": "0.14.54", + "resolved": "https://registry.npmjs.org/esbuild-linux-64/-/esbuild-linux-64-0.14.54.tgz", + "integrity": "sha512-EgjAgH5HwTbtNsTqQOXWApBaPVdDn7XcK+/PtJwZLT1UmpLoznPd8c5CxqsH2dQK3j05YsB3L17T8vE7cp4cCg==", "cpu": [ "x64" ], @@ -366,12 +428,15 @@ "optional": true, "os": [ "linux" - ] + ], + "engines": { + "node": ">=12" + } }, "node_modules/esbuild-linux-arm": { - "version": "0.13.15", - "resolved": "https://registry.npmjs.org/esbuild-linux-arm/-/esbuild-linux-arm-0.13.15.tgz", - "integrity": "sha512-wUHttDi/ol0tD8ZgUMDH8Ef7IbDX+/UsWJOXaAyTdkT7Yy9ZBqPg8bgB/Dn3CZ9SBpNieozrPRHm0BGww7W/jA==", + "version": "0.14.54", + "resolved": "https://registry.npmjs.org/esbuild-linux-arm/-/esbuild-linux-arm-0.14.54.tgz", + "integrity": "sha512-qqz/SjemQhVMTnvcLGoLOdFpCYbz4v4fUo+TfsWG+1aOu70/80RV6bgNpR2JCrppV2moUQkww+6bWxXRL9YMGw==", "cpu": [ "arm" ], @@ -379,12 +444,15 @@ "optional": true, "os": [ "linux" - ] + ], + "engines": { + "node": ">=12" + } }, "node_modules/esbuild-linux-arm64": { - "version": "0.13.15", - "resolved": "https://registry.npmjs.org/esbuild-linux-arm64/-/esbuild-linux-arm64-0.13.15.tgz", - "integrity": "sha512-bYpuUlN6qYU9slzr/ltyLTR9YTBS7qUDymO8SV7kjeNext61OdmqFAzuVZom+OLW1HPHseBfJ/JfdSlx8oTUoA==", + "version": "0.14.54", + "resolved": "https://registry.npmjs.org/esbuild-linux-arm64/-/esbuild-linux-arm64-0.14.54.tgz", + "integrity": "sha512-WL71L+0Rwv+Gv/HTmxTEmpv0UgmxYa5ftZILVi2QmZBgX3q7+tDeOQNqGtdXSdsL8TQi1vIaVFHUPDe0O0kdig==", "cpu": [ "arm64" ], @@ -392,12 +460,15 @@ "optional": true, "os": [ "linux" - ] + ], + "engines": { + "node": ">=12" + } }, "node_modules/esbuild-linux-mips64le": { - "version": "0.13.15", - "resolved": "https://registry.npmjs.org/esbuild-linux-mips64le/-/esbuild-linux-mips64le-0.13.15.tgz", - "integrity": "sha512-KlVjIG828uFPyJkO/8gKwy9RbXhCEUeFsCGOJBepUlpa7G8/SeZgncUEz/tOOUJTcWMTmFMtdd3GElGyAtbSWg==", + "version": "0.14.54", + "resolved": "https://registry.npmjs.org/esbuild-linux-mips64le/-/esbuild-linux-mips64le-0.14.54.tgz", + "integrity": "sha512-qTHGQB8D1etd0u1+sB6p0ikLKRVuCWhYQhAHRPkO+OF3I/iSlTKNNS0Lh2Oc0g0UFGguaFZZiPJdJey3AGpAlw==", "cpu": [ "mips64el" ], @@ -405,12 +476,15 @@ "optional": true, "os": [ "linux" - ] + ], + "engines": { + "node": ">=12" + } }, "node_modules/esbuild-linux-ppc64le": { - "version": "0.13.15", - "resolved": "https://registry.npmjs.org/esbuild-linux-ppc64le/-/esbuild-linux-ppc64le-0.13.15.tgz", - "integrity": "sha512-h6gYF+OsaqEuBjeesTBtUPw0bmiDu7eAeuc2OEH9S6mV9/jPhPdhOWzdeshb0BskRZxPhxPOjqZ+/OqLcxQwEQ==", + "version": "0.14.54", + "resolved": "https://registry.npmjs.org/esbuild-linux-ppc64le/-/esbuild-linux-ppc64le-0.14.54.tgz", + "integrity": "sha512-j3OMlzHiqwZBDPRCDFKcx595XVfOfOnv68Ax3U4UKZ3MTYQB5Yz3X1mn5GnodEVYzhtZgxEBidLWeIs8FDSfrQ==", "cpu": [ "ppc64" ], @@ -418,12 +492,47 @@ "optional": true, "os": [ "linux" - ] + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-linux-riscv64": { + "version": "0.14.54", + "resolved": "https://registry.npmjs.org/esbuild-linux-riscv64/-/esbuild-linux-riscv64-0.14.54.tgz", + "integrity": "sha512-y7Vt7Wl9dkOGZjxQZnDAqqn+XOqFD7IMWiewY5SPlNlzMX39ocPQlOaoxvT4FllA5viyV26/QzHtvTjVNOxHZg==", + "cpu": [ + "riscv64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-linux-s390x": { + "version": "0.14.54", + "resolved": "https://registry.npmjs.org/esbuild-linux-s390x/-/esbuild-linux-s390x-0.14.54.tgz", + "integrity": "sha512-zaHpW9dziAsi7lRcyV4r8dhfG1qBidQWUXweUjnw+lliChJqQr+6XD71K41oEIC3Mx1KStovEmlzm+MkGZHnHA==", + "cpu": [ + "s390x" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } }, "node_modules/esbuild-netbsd-64": { - "version": "0.13.15", - "resolved": "https://registry.npmjs.org/esbuild-netbsd-64/-/esbuild-netbsd-64-0.13.15.tgz", - "integrity": "sha512-3+yE9emwoevLMyvu+iR3rsa+Xwhie7ZEHMGDQ6dkqP/ndFzRHkobHUKTe+NCApSqG5ce2z4rFu+NX/UHnxlh3w==", + "version": "0.14.54", + "resolved": "https://registry.npmjs.org/esbuild-netbsd-64/-/esbuild-netbsd-64-0.14.54.tgz", + "integrity": "sha512-PR01lmIMnfJTgeU9VJTDY9ZerDWVFIUzAtJuDHwwceppW7cQWjBBqP48NdeRtoP04/AtO9a7w3viI+PIDr6d+w==", "cpu": [ "x64" ], @@ -431,12 +540,15 @@ "optional": true, "os": [ "netbsd" - ] + ], + "engines": { + "node": ">=12" + } }, "node_modules/esbuild-openbsd-64": { - "version": "0.13.15", - "resolved": "https://registry.npmjs.org/esbuild-openbsd-64/-/esbuild-openbsd-64-0.13.15.tgz", - "integrity": "sha512-wTfvtwYJYAFL1fSs8yHIdf5GEE4NkbtbXtjLWjM3Cw8mmQKqsg8kTiqJ9NJQe5NX/5Qlo7Xd9r1yKMMkHllp5g==", + "version": "0.14.54", + "resolved": "https://registry.npmjs.org/esbuild-openbsd-64/-/esbuild-openbsd-64-0.14.54.tgz", + "integrity": "sha512-Qyk7ikT2o7Wu76UsvvDS5q0amJvmRzDyVlL0qf5VLsLchjCa1+IAvd8kTBgUxD7VBUUVgItLkk609ZHUc1oCaw==", "cpu": [ "x64" ], @@ -444,12 +556,15 @@ "optional": true, "os": [ "openbsd" - ] + ], + "engines": { + "node": ">=12" + } }, "node_modules/esbuild-sunos-64": { - "version": "0.13.15", - "resolved": "https://registry.npmjs.org/esbuild-sunos-64/-/esbuild-sunos-64-0.13.15.tgz", - "integrity": "sha512-lbivT9Bx3t1iWWrSnGyBP9ODriEvWDRiweAs69vI+miJoeKwHWOComSRukttbuzjZ8r1q0mQJ8Z7yUsDJ3hKdw==", + "version": "0.14.54", + "resolved": "https://registry.npmjs.org/esbuild-sunos-64/-/esbuild-sunos-64-0.14.54.tgz", + "integrity": "sha512-28GZ24KmMSeKi5ueWzMcco6EBHStL3B6ubM7M51RmPwXQGLe0teBGJocmWhgwccA1GeFXqxzILIxXpHbl9Q/Kw==", "cpu": [ "x64" ], @@ -457,12 +572,15 @@ "optional": true, "os": [ "sunos" - ] + ], + "engines": { + "node": ">=12" + } }, "node_modules/esbuild-windows-32": { - "version": "0.13.15", - "resolved": "https://registry.npmjs.org/esbuild-windows-32/-/esbuild-windows-32-0.13.15.tgz", - "integrity": "sha512-fDMEf2g3SsJ599MBr50cY5ve5lP1wyVwTe6aLJsM01KtxyKkB4UT+fc5MXQFn3RLrAIAZOG+tHC+yXObpSn7Nw==", + "version": "0.14.54", + "resolved": "https://registry.npmjs.org/esbuild-windows-32/-/esbuild-windows-32-0.14.54.tgz", + "integrity": "sha512-T+rdZW19ql9MjS7pixmZYVObd9G7kcaZo+sETqNH4RCkuuYSuv9AGHUVnPoP9hhuE1WM1ZimHz1CIBHBboLU7w==", "cpu": [ "ia32" ], @@ -470,12 +588,15 @@ "optional": true, "os": [ "win32" - ] + ], + "engines": { + "node": ">=12" + } }, "node_modules/esbuild-windows-64": { - "version": "0.13.15", - "resolved": "https://registry.npmjs.org/esbuild-windows-64/-/esbuild-windows-64-0.13.15.tgz", - "integrity": "sha512-9aMsPRGDWCd3bGjUIKG/ZOJPKsiztlxl/Q3C1XDswO6eNX/Jtwu4M+jb6YDH9hRSUflQWX0XKAfWzgy5Wk54JQ==", + "version": "0.14.54", + "resolved": "https://registry.npmjs.org/esbuild-windows-64/-/esbuild-windows-64-0.14.54.tgz", + "integrity": "sha512-AoHTRBUuYwXtZhjXZbA1pGfTo8cJo3vZIcWGLiUcTNgHpJJMC1rVA44ZereBHMJtotyN71S8Qw0npiCIkW96cQ==", "cpu": [ "x64" ], @@ -483,12 +604,15 @@ "optional": true, "os": [ "win32" - ] + ], + "engines": { + "node": ">=12" + } }, "node_modules/esbuild-windows-arm64": { - "version": "0.13.15", - "resolved": "https://registry.npmjs.org/esbuild-windows-arm64/-/esbuild-windows-arm64-0.13.15.tgz", - "integrity": "sha512-zzvyCVVpbwQQATaf3IG8mu1IwGEiDxKkYUdA4FpoCHi1KtPa13jeScYDjlW0Qh+ebWzpKfR2ZwvqAQkSWNcKjA==", + "version": "0.14.54", + "resolved": "https://registry.npmjs.org/esbuild-windows-arm64/-/esbuild-windows-arm64-0.14.54.tgz", + "integrity": "sha512-M0kuUvXhot1zOISQGXwWn6YtS+Y/1RT9WrVIOywZnJHo3jCDyewAc79aKNQWFCQm+xNHVTq9h8dZKvygoXQQRg==", "cpu": [ "arm64" ], @@ -496,7 +620,10 @@ "optional": true, "os": [ "win32" - ] + ], + "engines": { + "node": ">=12" + } }, "node_modules/estree-walker": { "version": "2.0.2", @@ -506,7 +633,7 @@ "node_modules/fs.realpath": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", - "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=", + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", "dev": true }, "node_modules/fsevents": { @@ -545,15 +672,15 @@ "dev": true }, "node_modules/glob": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.0.tgz", - "integrity": "sha512-lmLf6gtyrPq8tTjSmrO94wBeQbFR3HbLHbuyD69wuyQkImp2hWqMGB47OX65FBkPffO641IP9jWa1z4ivqG26Q==", + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", "dev": true, "dependencies": { "fs.realpath": "^1.0.0", "inflight": "^1.0.4", "inherits": "2", - "minimatch": "^3.0.4", + "minimatch": "^3.1.1", "once": "^1.3.0", "path-is-absolute": "^1.0.0" }, @@ -565,9 +692,9 @@ } }, "node_modules/graceful-fs": { - "version": "4.2.9", - "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.9.tgz", - "integrity": "sha512-NtNxqUcXgpW2iMrfqSfR73Glt39K+BLwWsPs94yR63v45T0Wbej7eRmL5cWfwEgqXnmjQp3zaJTshdRW/qC2ZQ==", + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", "dev": true }, "node_modules/has": { @@ -585,7 +712,7 @@ "node_modules/inflight": { "version": "1.0.6", "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", - "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=", + "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", "dev": true, "dependencies": { "once": "^1.3.0", @@ -599,9 +726,9 @@ "dev": true }, "node_modules/is-core-module": { - "version": "2.8.1", - "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.8.1.tgz", - "integrity": "sha512-SdNCUs284hr40hFTFP6l0IfZ/RSrMXF3qgoRHd3/79unUTvrFO/JoXwkGm+5J/Oe3E/b5GsnG330uUNgRpu1PA==", + "version": "2.12.1", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.12.1.tgz", + "integrity": "sha512-Q4ZuBAe2FUsKtyQJoQHlvP8OvBERxO3jEmy1I7hcRXcJBGGHFh/aJBswbXuS9sgrDH2QUO8ilkwNPHvHMd8clg==", "dev": true, "dependencies": { "has": "^1.0.3" @@ -613,27 +740,30 @@ "node_modules/isarray": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", - "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=", + "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==", "dev": true }, "node_modules/listenercount": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/listenercount/-/listenercount-1.0.1.tgz", - "integrity": "sha1-hMinKrWcRyUyFIDJdeZQg0LnCTc=", + "integrity": "sha512-3mk/Zag0+IJxeDrxSgaDPy4zZ3w05PRZeJNnlWhzFz5OkX49J4krc+A8X2d2M69vGMBEX0uyl8M+W+8gH+kBqQ==", "dev": true }, "node_modules/magic-string": { - "version": "0.25.7", - "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.25.7.tgz", - "integrity": "sha512-4CrMT5DOHTDk4HYDlzmwu4FVCcIYI8gauveasrdCu2IKIFOJ3f0v/8MDGJCDL9oD2ppz/Av1b0Nj345H9M+XIA==", + "version": "0.30.1", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.1.tgz", + "integrity": "sha512-mbVKXPmS0z0G4XqFDCTllmDQ6coZzn94aMlb0o/A4HEHJCKcanlDZwYJgwnkmgD3jyWhUgj9VsPrfd972yPffA==", "dependencies": { - "sourcemap-codec": "^1.4.4" + "@jridgewell/sourcemap-codec": "^1.4.15" + }, + "engines": { + "node": ">=12" } }, "node_modules/minimatch": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", - "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", "dev": true, "dependencies": { "brace-expansion": "^1.1.7" @@ -643,27 +773,36 @@ } }, "node_modules/minimist": { - "version": "1.2.5", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.5.tgz", - "integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==", - "dev": true + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "dev": true, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } }, "node_modules/mkdirp": { - "version": "0.5.5", - "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.5.tgz", - "integrity": "sha512-NKmAlESf6jMGym1++R0Ra7wvhV+wFW63FaSOFPwRahvea0gMUcGUhVeAg/0BC0wiv9ih5NYPB1Wn1UEI1/L+xQ==", + "version": "0.5.6", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz", + "integrity": "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==", "dev": true, "dependencies": { - "minimist": "^1.2.5" + "minimist": "^1.2.6" }, "bin": { "mkdirp": "bin/cmd.js" } }, "node_modules/nanoid": { - "version": "3.1.32", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.1.32.tgz", - "integrity": "sha512-F8mf7R3iT9bvThBoW4tGXhXFHCctyCiUUPrWF8WaTqa3h96d9QybkSeba43XVOOE3oiLfkVDe4bT8MeGmkrTxw==", + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.6.tgz", + "integrity": "sha512-BGcqMMJuToF7i1rt+2PWSNVnWIkGCU78jBG3RxO/bZlnZPK2Cmi2QaffxGO/2RvWi9sL+FAiRiXMgsyxQ1DIDA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], "bin": { "nanoid": "bin/nanoid.cjs" }, @@ -674,7 +813,7 @@ "node_modules/once": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", - "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", "dev": true, "dependencies": { "wrappy": "1" @@ -683,7 +822,7 @@ "node_modules/path-is-absolute": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", - "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=", + "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", "dev": true, "engines": { "node": ">=0.10.0" @@ -701,20 +840,30 @@ "integrity": "sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==" }, "node_modules/postcss": { - "version": "8.4.5", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.5.tgz", - "integrity": "sha512-jBDboWM8qpaqwkMwItqTQTiFikhs/67OYVvblFFTM7MrZjt6yMKd6r2kgXizEbTTljacm4NldIlZnhbjr84QYg==", + "version": "8.4.27", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.27.tgz", + "integrity": "sha512-gY/ACJtJPSmUFPDCHtX78+01fHa64FaU4zaaWfuh1MhGJISufJAH4cun6k/8fwsHYeK4UQmENQK+tRLCFJE8JQ==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], "dependencies": { - "nanoid": "^3.1.30", + "nanoid": "^3.3.6", "picocolors": "^1.0.0", - "source-map-js": "^1.0.1" + "source-map-js": "^1.0.2" }, "engines": { "node": "^10 || ^12 || >=14" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/postcss/" } }, "node_modules/process-nextick-args": { @@ -724,9 +873,9 @@ "dev": true }, "node_modules/readable-stream": { - "version": "2.3.7", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz", - "integrity": "sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==", + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", "dev": true, "dependencies": { "core-util-is": "~1.0.0", @@ -739,12 +888,12 @@ } }, "node_modules/resolve": { - "version": "1.21.0", - "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.21.0.tgz", - "integrity": "sha512-3wCbTpk5WJlyE4mSOtDLhqQmGFi0/TD9VPwmiolnk8U0wRgMEktqCXd3vy5buTO3tljvalNvKrjHEfrd2WpEKA==", + "version": "1.22.2", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.2.tgz", + "integrity": "sha512-Sb+mjNHOULsBv818T40qSPeRiuWLyaGMa5ewydRLFimneixmVy2zdivRl+AF6jaYPC8ERxGDmFSiqui6SfPd+g==", "dev": true, "dependencies": { - "is-core-module": "^2.8.0", + "is-core-module": "^2.11.0", "path-parse": "^1.0.7", "supports-preserve-symlinks-flag": "^1.0.0" }, @@ -768,9 +917,9 @@ } }, "node_modules/rollup": { - "version": "2.63.0", - "resolved": "https://registry.npmjs.org/rollup/-/rollup-2.63.0.tgz", - "integrity": "sha512-nps0idjmD+NXl6OREfyYXMn/dar3WGcyKn+KBzPdaLecub3x/LrId0wUcthcr8oZUAcZAR8NKcfGGFlNgGL1kQ==", + "version": "2.77.3", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-2.77.3.tgz", + "integrity": "sha512-/qxNTG7FbmefJWoeeYJFbHehJ2HNWnjkAFRKzWN/45eNBBF/r8lo992CwcJXEzyVxs5FmfId+vTSTQDb+bxA+g==", "dev": true, "bin": { "rollup": "dist/bin/rollup" @@ -791,30 +940,17 @@ "node_modules/setimmediate": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/setimmediate/-/setimmediate-1.0.5.tgz", - "integrity": "sha1-KQy7Iy4waULX1+qbg3Mqt4VvgoU=", + "integrity": "sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA==", "dev": true }, - "node_modules/source-map": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/source-map-js": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.0.1.tgz", - "integrity": "sha512-4+TN2b3tqOCd/kaGRJ/sTYA0tR0mdXx26ipdolxcwtJVqEnqNYvlCAt1q3ypy4QMlYus+Zh34RNtYLoq2oQ4IA==", + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.0.2.tgz", + "integrity": "sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw==", "engines": { "node": ">=0.10.0" } }, - "node_modules/sourcemap-codec": { - "version": "1.4.8", - "resolved": "https://registry.npmjs.org/sourcemap-codec/-/sourcemap-codec-1.4.8.tgz", - "integrity": "sha512-9NykojV5Uih4lgo5So5dtw+f0JgJX30KCNI8gwhz2J9A15wD0Ml6tjHKwf6fTSa6fAdVBdZeNOs9eJ71qCk8vA==" - }, "node_modules/string_decoder": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", @@ -839,16 +975,16 @@ "node_modules/traverse": { "version": "0.3.9", "resolved": "https://registry.npmjs.org/traverse/-/traverse-0.3.9.tgz", - "integrity": "sha1-cXuPIgzAu3tE5AUUwisui7xw2Lk=", + "integrity": "sha512-iawgk0hLP3SxGKDfnDJf8wTz4p2qImnyihM5Hh/sGvQ3K37dPi/w8sRhdNIxYA1TwFwc5mDhIJq+O0RsvXBKdQ==", "dev": true, "engines": { "node": "*" } }, "node_modules/typescript": { - "version": "4.5.4", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.5.4.tgz", - "integrity": "sha512-VgYs2A2QIRuGphtzFV7aQJduJ2gyfTljngLzjpfW9FoYZF6xuw1W0vW9ghCKLfcWrCFxK81CSGRAvS1pn4fIUg==", + "version": "4.9.5", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.9.5.tgz", + "integrity": "sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g==", "dev": true, "bin": { "tsc": "bin/tsc", @@ -879,19 +1015,19 @@ "node_modules/util-deprecate": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", - "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", "dev": true }, "node_modules/vite": { - "version": "2.7.12", - "resolved": "https://registry.npmjs.org/vite/-/vite-2.7.12.tgz", - "integrity": "sha512-KvPYToRQWhRfBeVkyhkZ5hASuHQkqZUUdUcE3xyYtq5oYEPIJ0h9LWiWTO6v990glmSac2cEPeYeXzpX5Z6qKQ==", + "version": "2.9.16", + "resolved": "https://registry.npmjs.org/vite/-/vite-2.9.16.tgz", + "integrity": "sha512-X+6q8KPyeuBvTQV8AVSnKDvXoBMnTx8zxh54sOwmmuOdxkjMmEJXH2UEchA+vTMps1xw9vL64uwJOWryULg7nA==", "dev": true, "dependencies": { - "esbuild": "^0.13.12", - "postcss": "^8.4.5", - "resolve": "^1.20.0", - "rollup": "^2.59.0" + "esbuild": "^0.14.27", + "postcss": "^8.4.13", + "resolve": "^1.22.0", + "rollup": ">=2.59.0 <2.78.0" }, "bin": { "vite": "bin/vite.js" @@ -920,15 +1056,15 @@ } }, "node_modules/vue": { - "version": "3.2.26", - "resolved": "https://registry.npmjs.org/vue/-/vue-3.2.26.tgz", - "integrity": "sha512-KD4lULmskL5cCsEkfhERVRIOEDrfEL9CwAsLYpzptOGjaGFNWo3BQ9g8MAb7RaIO71rmVOziZ/uEN/rHwcUIhg==", + "version": "3.3.4", + "resolved": "https://registry.npmjs.org/vue/-/vue-3.3.4.tgz", + "integrity": "sha512-VTyEYn3yvIeY1Py0WaYGZsXnz3y5UnGi62GjVEqvEGPl6nxbOrCXbVOTQWBEJUqAyTUk2uJ5JLVnYJ6ZzGbrSw==", "dependencies": { - "@vue/compiler-dom": "3.2.26", - "@vue/compiler-sfc": "3.2.26", - "@vue/runtime-dom": "3.2.26", - "@vue/server-renderer": "3.2.26", - "@vue/shared": "3.2.26" + "@vue/compiler-dom": "3.3.4", + "@vue/compiler-sfc": "3.3.4", + "@vue/runtime-dom": "3.3.4", + "@vue/server-renderer": "3.3.4", + "@vue/shared": "3.3.4" } }, "node_modules/vue-tsc": { @@ -946,697 +1082,7 @@ "node_modules/wrappy": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", - "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=", - "dev": true - } - }, - "dependencies": { - "@babel/parser": { - "version": "7.16.8", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.16.8.tgz", - "integrity": "sha512-i7jDUfrVBWc+7OKcBzEe5n7fbv3i2fWtxKzzCvOjnzSxMfWMigAhtfJ7qzZNGFNMsCCd67+uz553dYKWXPvCKw==" - }, - "@vitejs/plugin-vue": { - "version": "1.10.2", - "resolved": "https://registry.npmjs.org/@vitejs/plugin-vue/-/plugin-vue-1.10.2.tgz", - "integrity": "sha512-/QJ0Z9qfhAFtKRY+r57ziY4BSbGUTGsPRMpB/Ron3QPwBZM4OZAZHdTa4a8PafCwU5DTatXG8TMDoP8z+oDqJw==", - "dev": true, - "requires": {} - }, - "@vue/compiler-core": { - "version": "3.2.26", - "resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.2.26.tgz", - "integrity": "sha512-N5XNBobZbaASdzY9Lga2D9Lul5vdCIOXvUMd6ThcN8zgqQhPKfCV+wfAJNNJKQkSHudnYRO2gEB+lp0iN3g2Tw==", - "requires": { - "@babel/parser": "^7.16.4", - "@vue/shared": "3.2.26", - "estree-walker": "^2.0.2", - "source-map": "^0.6.1" - } - }, - "@vue/compiler-dom": { - "version": "3.2.26", - "resolved": "https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.2.26.tgz", - "integrity": "sha512-smBfaOW6mQDxcT3p9TKT6mE22vjxjJL50GFVJiI0chXYGU/xzC05QRGrW3HHVuJrmLTLx5zBhsZ2dIATERbarg==", - "requires": { - "@vue/compiler-core": "3.2.26", - "@vue/shared": "3.2.26" - } - }, - "@vue/compiler-sfc": { - "version": "3.2.26", - "resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-3.2.26.tgz", - "integrity": "sha512-ePpnfktV90UcLdsDQUh2JdiTuhV0Skv2iYXxfNMOK/F3Q+2BO0AulcVcfoksOpTJGmhhfosWfMyEaEf0UaWpIw==", - "requires": { - "@babel/parser": "^7.16.4", - "@vue/compiler-core": "3.2.26", - "@vue/compiler-dom": "3.2.26", - "@vue/compiler-ssr": "3.2.26", - "@vue/reactivity-transform": "3.2.26", - "@vue/shared": "3.2.26", - "estree-walker": "^2.0.2", - "magic-string": "^0.25.7", - "postcss": "^8.1.10", - "source-map": "^0.6.1" - } - }, - "@vue/compiler-ssr": { - "version": "3.2.26", - "resolved": "https://registry.npmjs.org/@vue/compiler-ssr/-/compiler-ssr-3.2.26.tgz", - "integrity": "sha512-2mywLX0ODc4Zn8qBoA2PDCsLEZfpUGZcyoFRLSOjyGGK6wDy2/5kyDOWtf0S0UvtoyVq95OTSGIALjZ4k2q/ag==", - "requires": { - "@vue/compiler-dom": "3.2.26", - "@vue/shared": "3.2.26" - } - }, - "@vue/reactivity": { - "version": "3.2.26", - "resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.2.26.tgz", - "integrity": "sha512-h38bxCZLW6oFJVDlCcAiUKFnXI8xP8d+eO0pcDxx+7dQfSPje2AO6M9S9QO6MrxQB7fGP0DH0dYQ8ksf6hrXKQ==", - "requires": { - "@vue/shared": "3.2.26" - } - }, - "@vue/reactivity-transform": { - "version": "3.2.26", - "resolved": "https://registry.npmjs.org/@vue/reactivity-transform/-/reactivity-transform-3.2.26.tgz", - "integrity": "sha512-XKMyuCmzNA7nvFlYhdKwD78rcnmPb7q46uoR00zkX6yZrUmcCQ5OikiwUEVbvNhL5hBJuvbSO95jB5zkUon+eQ==", - "requires": { - "@babel/parser": "^7.16.4", - "@vue/compiler-core": "3.2.26", - "@vue/shared": "3.2.26", - "estree-walker": "^2.0.2", - "magic-string": "^0.25.7" - } - }, - "@vue/runtime-core": { - "version": "3.2.26", - "resolved": "https://registry.npmjs.org/@vue/runtime-core/-/runtime-core-3.2.26.tgz", - "integrity": "sha512-BcYi7qZ9Nn+CJDJrHQ6Zsmxei2hDW0L6AB4vPvUQGBm2fZyC0GXd/4nVbyA2ubmuhctD5RbYY8L+5GUJszv9mQ==", - "requires": { - "@vue/reactivity": "3.2.26", - "@vue/shared": "3.2.26" - } - }, - "@vue/runtime-dom": { - "version": "3.2.26", - "resolved": "https://registry.npmjs.org/@vue/runtime-dom/-/runtime-dom-3.2.26.tgz", - "integrity": "sha512-dY56UIiZI+gjc4e8JQBwAifljyexfVCkIAu/WX8snh8vSOt/gMSEGwPRcl2UpYpBYeyExV8WCbgvwWRNt9cHhQ==", - "requires": { - "@vue/runtime-core": "3.2.26", - "@vue/shared": "3.2.26", - "csstype": "^2.6.8" - } - }, - "@vue/server-renderer": { - "version": "3.2.26", - "resolved": "https://registry.npmjs.org/@vue/server-renderer/-/server-renderer-3.2.26.tgz", - "integrity": "sha512-Jp5SggDUvvUYSBIvYEhy76t4nr1vapY/FIFloWmQzn7UxqaHrrBpbxrqPcTrSgGrcaglj0VBp22BKJNre4aA1w==", - "requires": { - "@vue/compiler-ssr": "3.2.26", - "@vue/shared": "3.2.26" - } - }, - "@vue/shared": { - "version": "3.2.26", - "resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.2.26.tgz", - "integrity": "sha512-vPV6Cq+NIWbH5pZu+V+2QHE9y1qfuTq49uNWw4f7FDEeZaDU2H2cx5jcUZOAKW7qTrUS4k6qZPbMy1x4N96nbA==" - }, - "balanced-match": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", - "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", - "dev": true - }, - "big-integer": { - "version": "1.6.51", - "resolved": "https://registry.npmjs.org/big-integer/-/big-integer-1.6.51.tgz", - "integrity": "sha512-GPEid2Y9QU1Exl1rpO9B2IPJGHPSupF5GnVIP0blYvNOMer2bTvSWs1jGOUg04hTmu67nmLsQ9TBo1puaotBHg==", - "dev": true - }, - "binary": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/binary/-/binary-0.3.0.tgz", - "integrity": "sha1-n2BVO8XOjDOG87VTz/R0Yq3sqnk=", - "dev": true, - "requires": { - "buffers": "~0.1.1", - "chainsaw": "~0.1.0" - } - }, - "bluebird": { - "version": "3.4.7", - "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.4.7.tgz", - "integrity": "sha1-9y12C+Cbf3bQjtj66Ysomo0F+rM=", - "dev": true - }, - "brace-expansion": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", - "dev": true, - "requires": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, - "buffer-indexof-polyfill": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/buffer-indexof-polyfill/-/buffer-indexof-polyfill-1.0.2.tgz", - "integrity": "sha512-I7wzHwA3t1/lwXQh+A5PbNvJxgfo5r3xulgpYDB5zckTu/Z9oUK9biouBKQUjEqzaz3HnAT6TYoovmE+GqSf7A==", - "dev": true - }, - "buffers": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/buffers/-/buffers-0.1.1.tgz", - "integrity": "sha1-skV5w77U1tOWru5tmorn9Ugqt7s=", - "dev": true - }, - "bulma": { - "version": "0.9.3", - "resolved": "https://registry.npmjs.org/bulma/-/bulma-0.9.3.tgz", - "integrity": "sha512-0d7GNW1PY4ud8TWxdNcP6Cc8Bu7MxcntD/RRLGWuiw/s0a9P+XlH/6QoOIrmbj6o8WWJzJYhytiu9nFjTszk1g==" - }, - "chainsaw": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/chainsaw/-/chainsaw-0.1.0.tgz", - "integrity": "sha1-XqtQsor+WAdNDVgpE4iCi15fvJg=", - "dev": true, - "requires": { - "traverse": ">=0.3.0 <0.4" - } - }, - "concat-map": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", - "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=", - "dev": true - }, - "core-util-is": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz", - "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==", - "dev": true - }, - "csstype": { - "version": "2.6.19", - "resolved": "https://registry.npmjs.org/csstype/-/csstype-2.6.19.tgz", - "integrity": "sha512-ZVxXaNy28/k3kJg0Fou5MiYpp88j7H9hLZp8PDC3jV0WFjfH5E9xHb56L0W59cPbKbcHXeP4qyT8PrHp8t6LcQ==" - }, - "duplexer2": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/duplexer2/-/duplexer2-0.1.4.tgz", - "integrity": "sha1-ixLauHjA1p4+eJEFFmKjL8a93ME=", - "dev": true, - "requires": { - "readable-stream": "^2.0.2" - } - }, - "esbuild": { - "version": "0.13.15", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.13.15.tgz", - "integrity": "sha512-raCxt02HBKv8RJxE8vkTSCXGIyKHdEdGfUmiYb8wnabnaEmHzyW7DCHb5tEN0xU8ryqg5xw54mcwnYkC4x3AIw==", - "dev": true, - "requires": { - "esbuild-android-arm64": "0.13.15", - "esbuild-darwin-64": "0.13.15", - "esbuild-darwin-arm64": "0.13.15", - "esbuild-freebsd-64": "0.13.15", - "esbuild-freebsd-arm64": "0.13.15", - "esbuild-linux-32": "0.13.15", - "esbuild-linux-64": "0.13.15", - "esbuild-linux-arm": "0.13.15", - "esbuild-linux-arm64": "0.13.15", - "esbuild-linux-mips64le": "0.13.15", - "esbuild-linux-ppc64le": "0.13.15", - "esbuild-netbsd-64": "0.13.15", - "esbuild-openbsd-64": "0.13.15", - "esbuild-sunos-64": "0.13.15", - "esbuild-windows-32": "0.13.15", - "esbuild-windows-64": "0.13.15", - "esbuild-windows-arm64": "0.13.15" - } - }, - "esbuild-android-arm64": { - "version": "0.13.15", - "resolved": "https://registry.npmjs.org/esbuild-android-arm64/-/esbuild-android-arm64-0.13.15.tgz", - "integrity": "sha512-m602nft/XXeO8YQPUDVoHfjyRVPdPgjyyXOxZ44MK/agewFFkPa8tUo6lAzSWh5Ui5PB4KR9UIFTSBKh/RrCmg==", - "dev": true, - "optional": true - }, - "esbuild-darwin-64": { - "version": "0.13.15", - "resolved": "https://registry.npmjs.org/esbuild-darwin-64/-/esbuild-darwin-64-0.13.15.tgz", - "integrity": "sha512-ihOQRGs2yyp7t5bArCwnvn2Atr6X4axqPpEdCFPVp7iUj4cVSdisgvEKdNR7yH3JDjW6aQDw40iQFoTqejqxvQ==", - "dev": true, - "optional": true - }, - "esbuild-darwin-arm64": { - "version": "0.13.15", - "resolved": "https://registry.npmjs.org/esbuild-darwin-arm64/-/esbuild-darwin-arm64-0.13.15.tgz", - "integrity": "sha512-i1FZssTVxUqNlJ6cBTj5YQj4imWy3m49RZRnHhLpefFIh0To05ow9DTrXROTE1urGTQCloFUXTX8QfGJy1P8dQ==", - "dev": true, - "optional": true - }, - "esbuild-freebsd-64": { - "version": "0.13.15", - "resolved": "https://registry.npmjs.org/esbuild-freebsd-64/-/esbuild-freebsd-64-0.13.15.tgz", - "integrity": "sha512-G3dLBXUI6lC6Z09/x+WtXBXbOYQZ0E8TDBqvn7aMaOCzryJs8LyVXKY4CPnHFXZAbSwkCbqiPuSQ1+HhrNk7EA==", - "dev": true, - "optional": true - }, - "esbuild-freebsd-arm64": { - "version": "0.13.15", - "resolved": "https://registry.npmjs.org/esbuild-freebsd-arm64/-/esbuild-freebsd-arm64-0.13.15.tgz", - "integrity": "sha512-KJx0fzEDf1uhNOZQStV4ujg30WlnwqUASaGSFPhznLM/bbheu9HhqZ6mJJZM32lkyfGJikw0jg7v3S0oAvtvQQ==", - "dev": true, - "optional": true - }, - "esbuild-linux-32": { - "version": "0.13.15", - "resolved": "https://registry.npmjs.org/esbuild-linux-32/-/esbuild-linux-32-0.13.15.tgz", - "integrity": "sha512-ZvTBPk0YWCLMCXiFmD5EUtB30zIPvC5Itxz0mdTu/xZBbbHJftQgLWY49wEPSn2T/TxahYCRDWun5smRa0Tu+g==", - "dev": true, - "optional": true - }, - "esbuild-linux-64": { - "version": "0.13.15", - "resolved": "https://registry.npmjs.org/esbuild-linux-64/-/esbuild-linux-64-0.13.15.tgz", - "integrity": "sha512-eCKzkNSLywNeQTRBxJRQ0jxRCl2YWdMB3+PkWFo2BBQYC5mISLIVIjThNtn6HUNqua1pnvgP5xX0nHbZbPj5oA==", - "dev": true, - "optional": true - }, - "esbuild-linux-arm": { - "version": "0.13.15", - "resolved": "https://registry.npmjs.org/esbuild-linux-arm/-/esbuild-linux-arm-0.13.15.tgz", - "integrity": "sha512-wUHttDi/ol0tD8ZgUMDH8Ef7IbDX+/UsWJOXaAyTdkT7Yy9ZBqPg8bgB/Dn3CZ9SBpNieozrPRHm0BGww7W/jA==", - "dev": true, - "optional": true - }, - "esbuild-linux-arm64": { - "version": "0.13.15", - "resolved": "https://registry.npmjs.org/esbuild-linux-arm64/-/esbuild-linux-arm64-0.13.15.tgz", - "integrity": "sha512-bYpuUlN6qYU9slzr/ltyLTR9YTBS7qUDymO8SV7kjeNext61OdmqFAzuVZom+OLW1HPHseBfJ/JfdSlx8oTUoA==", - "dev": true, - "optional": true - }, - "esbuild-linux-mips64le": { - "version": "0.13.15", - "resolved": "https://registry.npmjs.org/esbuild-linux-mips64le/-/esbuild-linux-mips64le-0.13.15.tgz", - "integrity": "sha512-KlVjIG828uFPyJkO/8gKwy9RbXhCEUeFsCGOJBepUlpa7G8/SeZgncUEz/tOOUJTcWMTmFMtdd3GElGyAtbSWg==", - "dev": true, - "optional": true - }, - "esbuild-linux-ppc64le": { - "version": "0.13.15", - "resolved": "https://registry.npmjs.org/esbuild-linux-ppc64le/-/esbuild-linux-ppc64le-0.13.15.tgz", - "integrity": "sha512-h6gYF+OsaqEuBjeesTBtUPw0bmiDu7eAeuc2OEH9S6mV9/jPhPdhOWzdeshb0BskRZxPhxPOjqZ+/OqLcxQwEQ==", - "dev": true, - "optional": true - }, - "esbuild-netbsd-64": { - "version": "0.13.15", - "resolved": "https://registry.npmjs.org/esbuild-netbsd-64/-/esbuild-netbsd-64-0.13.15.tgz", - "integrity": "sha512-3+yE9emwoevLMyvu+iR3rsa+Xwhie7ZEHMGDQ6dkqP/ndFzRHkobHUKTe+NCApSqG5ce2z4rFu+NX/UHnxlh3w==", - "dev": true, - "optional": true - }, - "esbuild-openbsd-64": { - "version": "0.13.15", - "resolved": "https://registry.npmjs.org/esbuild-openbsd-64/-/esbuild-openbsd-64-0.13.15.tgz", - "integrity": "sha512-wTfvtwYJYAFL1fSs8yHIdf5GEE4NkbtbXtjLWjM3Cw8mmQKqsg8kTiqJ9NJQe5NX/5Qlo7Xd9r1yKMMkHllp5g==", - "dev": true, - "optional": true - }, - "esbuild-sunos-64": { - "version": "0.13.15", - "resolved": "https://registry.npmjs.org/esbuild-sunos-64/-/esbuild-sunos-64-0.13.15.tgz", - "integrity": "sha512-lbivT9Bx3t1iWWrSnGyBP9ODriEvWDRiweAs69vI+miJoeKwHWOComSRukttbuzjZ8r1q0mQJ8Z7yUsDJ3hKdw==", - "dev": true, - "optional": true - }, - "esbuild-windows-32": { - "version": "0.13.15", - "resolved": "https://registry.npmjs.org/esbuild-windows-32/-/esbuild-windows-32-0.13.15.tgz", - "integrity": "sha512-fDMEf2g3SsJ599MBr50cY5ve5lP1wyVwTe6aLJsM01KtxyKkB4UT+fc5MXQFn3RLrAIAZOG+tHC+yXObpSn7Nw==", - "dev": true, - "optional": true - }, - "esbuild-windows-64": { - "version": "0.13.15", - "resolved": "https://registry.npmjs.org/esbuild-windows-64/-/esbuild-windows-64-0.13.15.tgz", - "integrity": "sha512-9aMsPRGDWCd3bGjUIKG/ZOJPKsiztlxl/Q3C1XDswO6eNX/Jtwu4M+jb6YDH9hRSUflQWX0XKAfWzgy5Wk54JQ==", - "dev": true, - "optional": true - }, - "esbuild-windows-arm64": { - "version": "0.13.15", - "resolved": "https://registry.npmjs.org/esbuild-windows-arm64/-/esbuild-windows-arm64-0.13.15.tgz", - "integrity": "sha512-zzvyCVVpbwQQATaf3IG8mu1IwGEiDxKkYUdA4FpoCHi1KtPa13jeScYDjlW0Qh+ebWzpKfR2ZwvqAQkSWNcKjA==", - "dev": true, - "optional": true - }, - "estree-walker": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz", - "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==" - }, - "fs.realpath": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", - "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=", - "dev": true - }, - "fsevents": { - "version": "2.3.2", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", - "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", - "dev": true, - "optional": true - }, - "fstream": { - "version": "1.0.12", - "resolved": "https://registry.npmjs.org/fstream/-/fstream-1.0.12.tgz", - "integrity": "sha512-WvJ193OHa0GHPEL+AycEJgxvBEwyfRkN1vhjca23OaPVMCaLCXTd5qAu82AjTcgP1UJmytkOKb63Ypde7raDIg==", - "dev": true, - "requires": { - "graceful-fs": "^4.1.2", - "inherits": "~2.0.0", - "mkdirp": ">=0.5 0", - "rimraf": "2" - } - }, - "function-bind": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", - "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==", - "dev": true - }, - "glob": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.0.tgz", - "integrity": "sha512-lmLf6gtyrPq8tTjSmrO94wBeQbFR3HbLHbuyD69wuyQkImp2hWqMGB47OX65FBkPffO641IP9jWa1z4ivqG26Q==", - "dev": true, - "requires": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^3.0.4", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" - } - }, - "graceful-fs": { - "version": "4.2.9", - "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.9.tgz", - "integrity": "sha512-NtNxqUcXgpW2iMrfqSfR73Glt39K+BLwWsPs94yR63v45T0Wbej7eRmL5cWfwEgqXnmjQp3zaJTshdRW/qC2ZQ==", - "dev": true - }, - "has": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", - "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==", - "dev": true, - "requires": { - "function-bind": "^1.1.1" - } - }, - "inflight": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", - "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=", - "dev": true, - "requires": { - "once": "^1.3.0", - "wrappy": "1" - } - }, - "inherits": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", - "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", - "dev": true - }, - "is-core-module": { - "version": "2.8.1", - "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.8.1.tgz", - "integrity": "sha512-SdNCUs284hr40hFTFP6l0IfZ/RSrMXF3qgoRHd3/79unUTvrFO/JoXwkGm+5J/Oe3E/b5GsnG330uUNgRpu1PA==", - "dev": true, - "requires": { - "has": "^1.0.3" - } - }, - "isarray": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", - "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=", - "dev": true - }, - "listenercount": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/listenercount/-/listenercount-1.0.1.tgz", - "integrity": "sha1-hMinKrWcRyUyFIDJdeZQg0LnCTc=", - "dev": true - }, - "magic-string": { - "version": "0.25.7", - "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.25.7.tgz", - "integrity": "sha512-4CrMT5DOHTDk4HYDlzmwu4FVCcIYI8gauveasrdCu2IKIFOJ3f0v/8MDGJCDL9oD2ppz/Av1b0Nj345H9M+XIA==", - "requires": { - "sourcemap-codec": "^1.4.4" - } - }, - "minimatch": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", - "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", - "dev": true, - "requires": { - "brace-expansion": "^1.1.7" - } - }, - "minimist": { - "version": "1.2.5", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.5.tgz", - "integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==", - "dev": true - }, - "mkdirp": { - "version": "0.5.5", - "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.5.tgz", - "integrity": "sha512-NKmAlESf6jMGym1++R0Ra7wvhV+wFW63FaSOFPwRahvea0gMUcGUhVeAg/0BC0wiv9ih5NYPB1Wn1UEI1/L+xQ==", - "dev": true, - "requires": { - "minimist": "^1.2.5" - } - }, - "nanoid": { - "version": "3.1.32", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.1.32.tgz", - "integrity": "sha512-F8mf7R3iT9bvThBoW4tGXhXFHCctyCiUUPrWF8WaTqa3h96d9QybkSeba43XVOOE3oiLfkVDe4bT8MeGmkrTxw==" - }, - "once": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", - "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", - "dev": true, - "requires": { - "wrappy": "1" - } - }, - "path-is-absolute": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", - "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=", - "dev": true - }, - "path-parse": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", - "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", - "dev": true - }, - "picocolors": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz", - "integrity": "sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==" - }, - "postcss": { - "version": "8.4.5", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.5.tgz", - "integrity": "sha512-jBDboWM8qpaqwkMwItqTQTiFikhs/67OYVvblFFTM7MrZjt6yMKd6r2kgXizEbTTljacm4NldIlZnhbjr84QYg==", - "requires": { - "nanoid": "^3.1.30", - "picocolors": "^1.0.0", - "source-map-js": "^1.0.1" - } - }, - "process-nextick-args": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", - "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==", - "dev": true - }, - "readable-stream": { - "version": "2.3.7", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz", - "integrity": "sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==", - "dev": true, - "requires": { - "core-util-is": "~1.0.0", - "inherits": "~2.0.3", - "isarray": "~1.0.0", - "process-nextick-args": "~2.0.0", - "safe-buffer": "~5.1.1", - "string_decoder": "~1.1.1", - "util-deprecate": "~1.0.1" - } - }, - "resolve": { - "version": "1.21.0", - "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.21.0.tgz", - "integrity": "sha512-3wCbTpk5WJlyE4mSOtDLhqQmGFi0/TD9VPwmiolnk8U0wRgMEktqCXd3vy5buTO3tljvalNvKrjHEfrd2WpEKA==", - "dev": true, - "requires": { - "is-core-module": "^2.8.0", - "path-parse": "^1.0.7", - "supports-preserve-symlinks-flag": "^1.0.0" - } - }, - "rimraf": { - "version": "2.7.1", - "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.7.1.tgz", - "integrity": "sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==", - "dev": true, - "requires": { - "glob": "^7.1.3" - } - }, - "rollup": { - "version": "2.63.0", - "resolved": "https://registry.npmjs.org/rollup/-/rollup-2.63.0.tgz", - "integrity": "sha512-nps0idjmD+NXl6OREfyYXMn/dar3WGcyKn+KBzPdaLecub3x/LrId0wUcthcr8oZUAcZAR8NKcfGGFlNgGL1kQ==", - "dev": true, - "requires": { - "fsevents": "~2.3.2" - } - }, - "safe-buffer": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", - "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", - "dev": true - }, - "setimmediate": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/setimmediate/-/setimmediate-1.0.5.tgz", - "integrity": "sha1-KQy7Iy4waULX1+qbg3Mqt4VvgoU=", - "dev": true - }, - "source-map": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==" - }, - "source-map-js": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.0.1.tgz", - "integrity": "sha512-4+TN2b3tqOCd/kaGRJ/sTYA0tR0mdXx26ipdolxcwtJVqEnqNYvlCAt1q3ypy4QMlYus+Zh34RNtYLoq2oQ4IA==" - }, - "sourcemap-codec": { - "version": "1.4.8", - "resolved": "https://registry.npmjs.org/sourcemap-codec/-/sourcemap-codec-1.4.8.tgz", - "integrity": "sha512-9NykojV5Uih4lgo5So5dtw+f0JgJX30KCNI8gwhz2J9A15wD0Ml6tjHKwf6fTSa6fAdVBdZeNOs9eJ71qCk8vA==" - }, - "string_decoder": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", - "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", - "dev": true, - "requires": { - "safe-buffer": "~5.1.0" - } - }, - "supports-preserve-symlinks-flag": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", - "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", - "dev": true - }, - "traverse": { - "version": "0.3.9", - "resolved": "https://registry.npmjs.org/traverse/-/traverse-0.3.9.tgz", - "integrity": "sha1-cXuPIgzAu3tE5AUUwisui7xw2Lk=", - "dev": true - }, - "typescript": { - "version": "4.5.4", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.5.4.tgz", - "integrity": "sha512-VgYs2A2QIRuGphtzFV7aQJduJ2gyfTljngLzjpfW9FoYZF6xuw1W0vW9ghCKLfcWrCFxK81CSGRAvS1pn4fIUg==", - "dev": true - }, - "unzipper": { - "version": "0.10.11", - "resolved": "https://registry.npmjs.org/unzipper/-/unzipper-0.10.11.tgz", - "integrity": "sha512-+BrAq2oFqWod5IESRjL3S8baohbevGcVA+teAIOYWM3pDVdseogqbzhhvvmiyQrUNKFUnDMtELW3X8ykbyDCJw==", - "dev": true, - "requires": { - "big-integer": "^1.6.17", - "binary": "~0.3.0", - "bluebird": "~3.4.1", - "buffer-indexof-polyfill": "~1.0.0", - "duplexer2": "~0.1.4", - "fstream": "^1.0.12", - "graceful-fs": "^4.2.2", - "listenercount": "~1.0.1", - "readable-stream": "~2.3.6", - "setimmediate": "~1.0.4" - } - }, - "util-deprecate": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", - "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=", - "dev": true - }, - "vite": { - "version": "2.7.12", - "resolved": "https://registry.npmjs.org/vite/-/vite-2.7.12.tgz", - "integrity": "sha512-KvPYToRQWhRfBeVkyhkZ5hASuHQkqZUUdUcE3xyYtq5oYEPIJ0h9LWiWTO6v990glmSac2cEPeYeXzpX5Z6qKQ==", - "dev": true, - "requires": { - "esbuild": "^0.13.12", - "fsevents": "~2.3.2", - "postcss": "^8.4.5", - "resolve": "^1.20.0", - "rollup": "^2.59.0" - } - }, - "vue": { - "version": "3.2.26", - "resolved": "https://registry.npmjs.org/vue/-/vue-3.2.26.tgz", - "integrity": "sha512-KD4lULmskL5cCsEkfhERVRIOEDrfEL9CwAsLYpzptOGjaGFNWo3BQ9g8MAb7RaIO71rmVOziZ/uEN/rHwcUIhg==", - "requires": { - "@vue/compiler-dom": "3.2.26", - "@vue/compiler-sfc": "3.2.26", - "@vue/runtime-dom": "3.2.26", - "@vue/server-renderer": "3.2.26", - "@vue/shared": "3.2.26" - } - }, - "vue-tsc": { - "version": "0.0.24", - "resolved": "https://registry.npmjs.org/vue-tsc/-/vue-tsc-0.0.24.tgz", - "integrity": "sha512-Qx0V7jkWMtvddtaWa1SA8YKkBCRmjq9zZUB2UIMZiso6JSH538oHD2VumSzkoDnAfFbY3t0/j1mB2abpA0bGWA==", - "dev": true, - "requires": { - "unzipper": "0.10.11" - } - }, - "wrappy": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", - "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", "dev": true } } From 7665dfe7697f9960c1d91188f659a03f8421a1ca Mon Sep 17 00:00:00 2001 From: ducklet Date: Sun, 26 Nov 2023 15:10:16 +0100 Subject: [PATCH 23/31] switch from black/isort/autoflake to ruff --- poetry.lock | 155 +++++++++------------------------------------- pyproject.toml | 15 ++--- scripts/lint-py | 6 +- unwind/request.py | 4 +- 4 files changed, 39 insertions(+), 141 deletions(-) diff --git a/poetry.lock b/poetry.lock index acde07a..0ed6343 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1,4 +1,4 @@ -# This file is automatically @generated by Poetry 1.5.1 and should not be changed by hand. +# This file is automatically @generated by Poetry 1.7.1 and should not be changed by hand. [[package]] name = "aiosqlite" @@ -35,20 +35,6 @@ doc = ["Sphinx", "packaging", "sphinx-autodoc-typehints (>=1.2.0)", "sphinx-rtd- test = ["anyio[trio]", "coverage[toml] (>=4.5)", "hypothesis (>=4.0)", "mock (>=4)", "psutil (>=5.9)", "pytest (>=7.0)", "pytest-mock (>=3.6.1)", "trustme", "uvloop (>=0.17)"] trio = ["trio (<0.22)"] -[[package]] -name = "autoflake" -version = "2.2.0" -description = "Removes unused imports and unused variables" -optional = false -python-versions = ">=3.8" -files = [ - {file = "autoflake-2.2.0-py3-none-any.whl", hash = "sha256:de409b009a34c1c2a7cc2aae84c4c05047f9773594317c6a6968bd497600d4a0"}, - {file = "autoflake-2.2.0.tar.gz", hash = "sha256:62e1f74a0fdad898a96fee6f99fe8241af90ad99c7110c884b35855778412251"}, -] - -[package.dependencies] -pyflakes = ">=3.0.0" - [[package]] name = "beautifulsoup4" version = "4.12.2" @@ -67,50 +53,6 @@ soupsieve = ">1.2" html5lib = ["html5lib"] lxml = ["lxml"] -[[package]] -name = "black" -version = "23.7.0" -description = "The uncompromising code formatter." -optional = false -python-versions = ">=3.8" -files = [ - {file = "black-23.7.0-cp310-cp310-macosx_10_16_arm64.whl", hash = "sha256:5c4bc552ab52f6c1c506ccae05681fab58c3f72d59ae6e6639e8885e94fe2587"}, - {file = "black-23.7.0-cp310-cp310-macosx_10_16_universal2.whl", hash = "sha256:552513d5cd5694590d7ef6f46e1767a4df9af168d449ff767b13b084c020e63f"}, - {file = "black-23.7.0-cp310-cp310-macosx_10_16_x86_64.whl", hash = "sha256:86cee259349b4448adb4ef9b204bb4467aae74a386bce85d56ba4f5dc0da27be"}, - {file = "black-23.7.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:501387a9edcb75d7ae8a4412bb8749900386eaef258f1aefab18adddea1936bc"}, - {file = "black-23.7.0-cp310-cp310-win_amd64.whl", hash = "sha256:fb074d8b213749fa1d077d630db0d5f8cc3b2ae63587ad4116e8a436e9bbe995"}, - {file = "black-23.7.0-cp311-cp311-macosx_10_16_arm64.whl", hash = "sha256:b5b0ee6d96b345a8b420100b7d71ebfdd19fab5e8301aff48ec270042cd40ac2"}, - {file = "black-23.7.0-cp311-cp311-macosx_10_16_universal2.whl", hash = "sha256:893695a76b140881531062d48476ebe4a48f5d1e9388177e175d76234ca247cd"}, - {file = "black-23.7.0-cp311-cp311-macosx_10_16_x86_64.whl", hash = "sha256:c333286dc3ddca6fdff74670b911cccedacb4ef0a60b34e491b8a67c833b343a"}, - {file = "black-23.7.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:831d8f54c3a8c8cf55f64d0422ee875eecac26f5f649fb6c1df65316b67c8926"}, - {file = "black-23.7.0-cp311-cp311-win_amd64.whl", hash = "sha256:7f3bf2dec7d541b4619b8ce526bda74a6b0bffc480a163fed32eb8b3c9aed8ad"}, - {file = "black-23.7.0-cp38-cp38-macosx_10_16_arm64.whl", hash = "sha256:f9062af71c59c004cd519e2fb8f5d25d39e46d3af011b41ab43b9c74e27e236f"}, - {file = "black-23.7.0-cp38-cp38-macosx_10_16_universal2.whl", hash = "sha256:01ede61aac8c154b55f35301fac3e730baf0c9cf8120f65a9cd61a81cfb4a0c3"}, - {file = "black-23.7.0-cp38-cp38-macosx_10_16_x86_64.whl", hash = "sha256:327a8c2550ddc573b51e2c352adb88143464bb9d92c10416feb86b0f5aee5ff6"}, - {file = "black-23.7.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6d1c6022b86f83b632d06f2b02774134def5d4d4f1dac8bef16d90cda18ba28a"}, - {file = "black-23.7.0-cp38-cp38-win_amd64.whl", hash = "sha256:27eb7a0c71604d5de083757fbdb245b1a4fae60e9596514c6ec497eb63f95320"}, - {file = "black-23.7.0-cp39-cp39-macosx_10_16_arm64.whl", hash = "sha256:8417dbd2f57b5701492cd46edcecc4f9208dc75529bcf76c514864e48da867d9"}, - {file = "black-23.7.0-cp39-cp39-macosx_10_16_universal2.whl", hash = "sha256:47e56d83aad53ca140da0af87678fb38e44fd6bc0af71eebab2d1f59b1acf1d3"}, - {file = "black-23.7.0-cp39-cp39-macosx_10_16_x86_64.whl", hash = "sha256:25cc308838fe71f7065df53aedd20327969d05671bac95b38fdf37ebe70ac087"}, - {file = "black-23.7.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:642496b675095d423f9b8448243336f8ec71c9d4d57ec17bf795b67f08132a91"}, - {file = "black-23.7.0-cp39-cp39-win_amd64.whl", hash = "sha256:ad0014efc7acf0bd745792bd0d8857413652979200ab924fbf239062adc12491"}, - {file = "black-23.7.0-py3-none-any.whl", hash = "sha256:9fd59d418c60c0348505f2ddf9609c1e1de8e7493eab96198fc89d9f865e7a96"}, - {file = "black-23.7.0.tar.gz", hash = "sha256:022a582720b0d9480ed82576c920a8c1dde97cc38ff11d8d8859b3bd6ca9eedb"}, -] - -[package.dependencies] -click = ">=8.0.0" -mypy-extensions = ">=0.4.3" -packaging = ">=22.0" -pathspec = ">=0.9.0" -platformdirs = ">=2" - -[package.extras] -colorama = ["colorama (>=0.4.3)"] -d = ["aiohttp (>=3.7.4)"] -jupyter = ["ipython (>=7.8.0)", "tokenize-rt (>=3.2.0)"] -uvloop = ["uvloop (>=0.15.2)"] - [[package]] name = "certifi" version = "2023.7.22" @@ -415,34 +357,6 @@ files = [ {file = "iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3"}, ] -[[package]] -name = "isort" -version = "5.12.0" -description = "A Python utility / library to sort Python imports." -optional = false -python-versions = ">=3.8.0" -files = [ - {file = "isort-5.12.0-py3-none-any.whl", hash = "sha256:f84c2818376e66cf843d497486ea8fed8700b340f308f076c6fb1229dff318b6"}, - {file = "isort-5.12.0.tar.gz", hash = "sha256:8bef7dde241278824a6d83f44a544709b065191b95b6e50894bdc722fcba0504"}, -] - -[package.extras] -colors = ["colorama (>=0.4.3)"] -pipfile-deprecated-finder = ["pip-shims (>=0.5.2)", "pipreqs", "requirementslib"] -plugins = ["setuptools"] -requirements-deprecated-finder = ["pip-api", "pipreqs"] - -[[package]] -name = "mypy-extensions" -version = "1.0.0" -description = "Type system extensions for programs checked with the mypy type checker." -optional = false -python-versions = ">=3.5" -files = [ - {file = "mypy_extensions-1.0.0-py3-none-any.whl", hash = "sha256:4392f6c0eb8a5668a69e23d168ffa70f0be9ccfd32b5cc2d26a34ae5b844552d"}, - {file = "mypy_extensions-1.0.0.tar.gz", hash = "sha256:75dbf8955dc00442a438fc4d0666508a9a97b6bd41aa2f0ffe9d2f2725af0782"}, -] - [[package]] name = "nodeenv" version = "1.8.0" @@ -468,32 +382,6 @@ files = [ {file = "packaging-23.1.tar.gz", hash = "sha256:a392980d2b6cffa644431898be54b0045151319d1e7ec34f0cfed48767dd334f"}, ] -[[package]] -name = "pathspec" -version = "0.11.1" -description = "Utility library for gitignore style pattern matching of file paths." -optional = false -python-versions = ">=3.7" -files = [ - {file = "pathspec-0.11.1-py3-none-any.whl", hash = "sha256:d8af70af76652554bd134c22b3e8a1cc46ed7d91edcdd721ef1a0c51a84a5293"}, - {file = "pathspec-0.11.1.tar.gz", hash = "sha256:2798de800fa92780e33acca925945e9a19a133b715067cf165b8866c15a31687"}, -] - -[[package]] -name = "platformdirs" -version = "3.9.1" -description = "A small Python package for determining appropriate platform-specific dirs, e.g. a \"user data dir\"." -optional = false -python-versions = ">=3.7" -files = [ - {file = "platformdirs-3.9.1-py3-none-any.whl", hash = "sha256:ad8291ae0ae5072f66c16945166cb11c63394c7a3ad1b1bc9828ca3162da8c2f"}, - {file = "platformdirs-3.9.1.tar.gz", hash = "sha256:1b42b450ad933e981d56e59f1b97495428c9bd60698baab9f3eb3d00d5822421"}, -] - -[package.extras] -docs = ["furo (>=2023.5.20)", "proselint (>=0.13)", "sphinx (>=7.0.1)", "sphinx-autodoc-typehints (>=1.23,!=1.23.4)"] -test = ["appdirs (==1.4.4)", "covdefaults (>=2.3)", "pytest (>=7.3.1)", "pytest-cov (>=4.1)", "pytest-mock (>=3.10)"] - [[package]] name = "pluggy" version = "1.2.0" @@ -509,17 +397,6 @@ files = [ dev = ["pre-commit", "tox"] testing = ["pytest", "pytest-benchmark"] -[[package]] -name = "pyflakes" -version = "3.0.1" -description = "passive checker of Python programs" -optional = false -python-versions = ">=3.6" -files = [ - {file = "pyflakes-3.0.1-py2.py3-none-any.whl", hash = "sha256:ec55bf7fe21fff7f1ad2f7da62363d749e2a470500eab1b555334b67aa1ef8cf"}, - {file = "pyflakes-3.0.1.tar.gz", hash = "sha256:ec8b276a6b60bd80defed25add7e439881c19e64850afd9b346283d4165fd0fd"}, -] - [[package]] name = "pyright" version = "1.1.318" @@ -594,6 +471,32 @@ pytest = ">=4.6" [package.extras] testing = ["fields", "hunter", "process-tests", "pytest-xdist", "six", "virtualenv"] +[[package]] +name = "ruff" +version = "0.1.6" +description = "An extremely fast Python linter and code formatter, written in Rust." +optional = false +python-versions = ">=3.7" +files = [ + {file = "ruff-0.1.6-py3-none-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:88b8cdf6abf98130991cbc9f6438f35f6e8d41a02622cc5ee130a02a0ed28703"}, + {file = "ruff-0.1.6-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:5c549ed437680b6105a1299d2cd30e4964211606eeb48a0ff7a93ef70b902248"}, + {file = "ruff-0.1.6-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1cf5f701062e294f2167e66d11b092bba7af6a057668ed618a9253e1e90cfd76"}, + {file = "ruff-0.1.6-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:05991ee20d4ac4bb78385360c684e4b417edd971030ab12a4fbd075ff535050e"}, + {file = "ruff-0.1.6-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:87455a0c1f739b3c069e2f4c43b66479a54dea0276dd5d4d67b091265f6fd1dc"}, + {file = "ruff-0.1.6-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:683aa5bdda5a48cb8266fcde8eea2a6af4e5700a392c56ea5fb5f0d4bfdc0240"}, + {file = "ruff-0.1.6-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:137852105586dcbf80c1717facb6781555c4e99f520c9c827bd414fac67ddfb6"}, + {file = "ruff-0.1.6-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:bd98138a98d48a1c36c394fd6b84cd943ac92a08278aa8ac8c0fdefcf7138f35"}, + {file = "ruff-0.1.6-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3a0cd909d25f227ac5c36d4e7e681577275fb74ba3b11d288aff7ec47e3ae745"}, + {file = "ruff-0.1.6-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:e8fd1c62a47aa88a02707b5dd20c5ff20d035d634aa74826b42a1da77861b5ff"}, + {file = "ruff-0.1.6-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:fd89b45d374935829134a082617954120d7a1470a9f0ec0e7f3ead983edc48cc"}, + {file = "ruff-0.1.6-py3-none-musllinux_1_2_i686.whl", hash = "sha256:491262006e92f825b145cd1e52948073c56560243b55fb3b4ecb142f6f0e9543"}, + {file = "ruff-0.1.6-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:ea284789861b8b5ca9d5443591a92a397ac183d4351882ab52f6296b4fdd5462"}, + {file = "ruff-0.1.6-py3-none-win32.whl", hash = "sha256:1610e14750826dfc207ccbcdd7331b6bd285607d4181df9c1c6ae26646d6848a"}, + {file = "ruff-0.1.6-py3-none-win_amd64.whl", hash = "sha256:4558b3e178145491e9bc3b2ee3c4b42f19d19384eaa5c59d10acf6e8f8b57e33"}, + {file = "ruff-0.1.6-py3-none-win_arm64.whl", hash = "sha256:03910e81df0d8db0e30050725a5802441c2022ea3ae4fe0609b76081731accbc"}, + {file = "ruff-0.1.6.tar.gz", hash = "sha256:1b09f29b16c6ead5ea6b097ef2764b42372aebe363722f1605ecbcd2b9207184"}, +] + [[package]] name = "setuptools" version = "68.0.0" @@ -692,7 +595,7 @@ files = [ [package.dependencies] aiosqlite = {version = "*", optional = true, markers = "python_version >= \"3\" and extra == \"aiosqlite\""} -greenlet = {version = "!=0.4.17", optional = true, markers = "python_version >= \"3\" and (platform_machine == \"win32\" or platform_machine == \"WIN32\" or platform_machine == \"AMD64\" or platform_machine == \"amd64\" or platform_machine == \"x86_64\" or platform_machine == \"ppc64le\" or platform_machine == \"aarch64\" or extra == \"aiosqlite\")"} +greenlet = {version = "!=0.4.17", optional = true, 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\" or extra == \"aiosqlite\")"} typing-extensions = {version = "!=3.10.0.1", optional = true, markers = "extra == \"aiosqlite\""} [package.extras] @@ -787,4 +690,4 @@ files = [ [metadata] lock-version = "2.0" python-versions = "^3.11" -content-hash = "dddba1976dd913e947b3a9374b167d4d2dd145298cf63f5a42d4139ae4b7e3be" +content-hash = "a3ac6ffc5086acd93ceea6edb9d72444407d7918f2674883087170762135be80" diff --git a/pyproject.toml b/pyproject.toml index ce43209..742a19e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -20,13 +20,11 @@ sqlalchemy = {version = "^1.4", extras = ["aiosqlite"]} optional = true [tool.poetry.group.dev.dependencies] -autoflake = "*" pytest = "*" pyright = "*" -black = "*" -isort = "*" pytest-asyncio = "*" pytest-cov = "*" +ruff = "*" [build-system] requires = ["poetry-core>=1.0.0"] @@ -35,12 +33,7 @@ build-backend = "poetry.core.masonry.api" [tool.pyright] pythonVersion = "3.11" -[tool.isort] -profile = "black" - -[tool.autoflake] -remove-duplicate-keys = true -remove-unused-variables = true -remove-all-unused-imports = true +[tool.ruff] +target-version = "py311" ignore-init-module-imports = true -ignore-pass-after-docstring = true +select = ["I", "F401", "F601", "F602", "F841"] diff --git a/scripts/lint-py b/scripts/lint-py index 54cb0f2..33387ae 100755 --- a/scripts/lint-py +++ b/scripts/lint-py @@ -4,7 +4,7 @@ cd "$RUN_DIR" [ -z "${DEBUG:-}" ] || set -x -autoflake --quiet --check --recursive unwind tests -isort unwind tests -black unwind tests +ruff check --fix . ||: +ruff format . + pyright diff --git a/unwind/request.py b/unwind/request.py index afd2b86..b4a41d4 100644 --- a/unwind/request.py +++ b/unwind/request.py @@ -269,7 +269,9 @@ async def adownload( resp.raise_for_status() if to_path is None: - await resp.aread() # Download the response stream to allow `resp.content` access. + await ( + resp.aread() + ) # Download the response stream to allow `resp.content` access. return resp.content resp_lastmod = _last_modified_from_response(resp) From 2c65436df5a1f569826bef565613fe16b3637a30 Mon Sep 17 00:00:00 2001 From: ducklet Date: Sun, 26 Nov 2023 15:11:24 +0100 Subject: [PATCH 24/31] add .python-version file --- .python-version | 1 + 1 file changed, 1 insertion(+) create mode 100644 .python-version diff --git a/.python-version b/.python-version new file mode 100644 index 0000000..2c07333 --- /dev/null +++ b/.python-version @@ -0,0 +1 @@ +3.11 From 79068f2f103514fccf20de37aaed2ccc080aab1b Mon Sep 17 00:00:00 2001 From: ducklet Date: Sun, 26 Nov 2023 16:46:38 +0100 Subject: [PATCH 25/31] add honcho as explicit dev dependency --- poetry.lock | 19 ++++++++++++++++++- pyproject.toml | 1 + 2 files changed, 19 insertions(+), 1 deletion(-) diff --git a/poetry.lock b/poetry.lock index 0ed6343..038655e 100644 --- a/poetry.lock +++ b/poetry.lock @@ -270,6 +270,23 @@ files = [ {file = "h11-0.14.0.tar.gz", hash = "sha256:8f19fbbe99e72420ff35c00b27a34cb9937e902a8b810e2c88300c6f0a3b699d"}, ] +[[package]] +name = "honcho" +version = "1.1.0" +description = "Honcho: a Python clone of Foreman. For managing Procfile-based applications." +optional = false +python-versions = "*" +files = [ + {file = "honcho-1.1.0-py2.py3-none-any.whl", hash = "sha256:a4d6e3a88a7b51b66351ecfc6e9d79d8f4b87351db9ad7e923f5632cc498122f"}, + {file = "honcho-1.1.0.tar.gz", hash = "sha256:c5eca0bded4bef6697a23aec0422fd4f6508ea3581979a3485fc4b89357eb2a9"}, +] + +[package.dependencies] +colorama = {version = "*", markers = "sys_platform == \"win32\""} + +[package.extras] +export = ["jinja2 (>=2.7,<3)"] + [[package]] name = "html5lib" version = "1.1" @@ -690,4 +707,4 @@ files = [ [metadata] lock-version = "2.0" python-versions = "^3.11" -content-hash = "a3ac6ffc5086acd93ceea6edb9d72444407d7918f2674883087170762135be80" +content-hash = "17bf1e1330a25f59ffec4ff3aa70d8e9d9e93553a81f72c0823d76125f6ccd08" diff --git a/pyproject.toml b/pyproject.toml index 742a19e..73eab62 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -25,6 +25,7 @@ pyright = "*" pytest-asyncio = "*" pytest-cov = "*" ruff = "*" +honcho = "*" [build-system] requires = ["poetry-core>=1.0.0"] From 6d0c61fceb0425d1a896cfd53e213b5ca4f20718 Mon Sep 17 00:00:00 2001 From: ducklet Date: Sun, 26 Nov 2023 17:00:31 +0100 Subject: [PATCH 26/31] upgrade to Python 3.12 This includes only the most basic steps of the upgrade, to make the existing code run with Python 3.12. No refactoring to make use of new features is included. --- .python-version | 2 +- Dockerfile | 2 +- poetry.lock | 406 ++++++++++++++++++++++------------------------- pyproject.toml | 6 +- unwind/models.py | 2 +- 5 files changed, 197 insertions(+), 221 deletions(-) diff --git a/.python-version b/.python-version index 2c07333..e4fba21 100644 --- a/.python-version +++ b/.python-version @@ -1 +1 @@ -3.11 +3.12 diff --git a/Dockerfile b/Dockerfile index 7be9b6c..00b6a2c 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,4 +1,4 @@ -FROM docker.io/library/python:3.11-alpine +FROM docker.io/library/python:3.12-alpine RUN apk update --no-cache \ && apk upgrade --no-cache \ diff --git a/poetry.lock b/poetry.lock index 038655e..9dd4032 100644 --- a/poetry.lock +++ b/poetry.lock @@ -17,13 +17,13 @@ docs = ["sphinx (==6.1.3)", "sphinx-mdinclude (==0.5.3)"] [[package]] name = "anyio" -version = "3.7.1" +version = "4.1.0" description = "High level compatibility layer for multiple asynchronous event loop implementations" optional = false -python-versions = ">=3.7" +python-versions = ">=3.8" files = [ - {file = "anyio-3.7.1-py3-none-any.whl", hash = "sha256:91dee416e570e92c64041bd18b900d1d6fa78dff7048769ce5ac5ddad004fbb5"}, - {file = "anyio-3.7.1.tar.gz", hash = "sha256:44a3c9aba0f5defa43261a8b3efb97891f2bd7d804e0e1f56419befa1adfc780"}, + {file = "anyio-4.1.0-py3-none-any.whl", hash = "sha256:56a415fbc462291813a94528a779597226619c8e78af7de0507333f700011e5f"}, + {file = "anyio-4.1.0.tar.gz", hash = "sha256:5a0bec7085176715be77df87fc66d6c9d70626bd752fcc85f57cdbee5b3760da"}, ] [package.dependencies] @@ -31,9 +31,9 @@ idna = ">=2.8" sniffio = ">=1.1" [package.extras] -doc = ["Sphinx", "packaging", "sphinx-autodoc-typehints (>=1.2.0)", "sphinx-rtd-theme (>=1.2.2)", "sphinxcontrib-jquery"] -test = ["anyio[trio]", "coverage[toml] (>=4.5)", "hypothesis (>=4.0)", "mock (>=4)", "psutil (>=5.9)", "pytest (>=7.0)", "pytest-mock (>=3.6.1)", "trustme", "uvloop (>=0.17)"] -trio = ["trio (<0.22)"] +doc = ["Sphinx (>=7)", "packaging", "sphinx-autodoc-typehints (>=1.2.0)", "sphinx-rtd-theme"] +test = ["anyio[trio]", "coverage[toml] (>=7)", "exceptiongroup (>=1.2.0)", "hypothesis (>=4.0)", "psutil (>=5.9)", "pytest (>=7.0)", "pytest-mock (>=3.6.1)", "trustme", "uvloop (>=0.17)"] +trio = ["trio (>=0.23)"] [[package]] name = "beautifulsoup4" @@ -55,24 +55,24 @@ lxml = ["lxml"] [[package]] name = "certifi" -version = "2023.7.22" +version = "2023.11.17" description = "Python package for providing Mozilla's CA Bundle." optional = false python-versions = ">=3.6" files = [ - {file = "certifi-2023.7.22-py3-none-any.whl", hash = "sha256:92d6037539857d8206b8f6ae472e8b77db8058fec5937a1ef3f54304089edbb9"}, - {file = "certifi-2023.7.22.tar.gz", hash = "sha256:539cc1d13202e33ca466e88b2807e29f4c13049d6d87031a3c110744495cb082"}, + {file = "certifi-2023.11.17-py3-none-any.whl", hash = "sha256:e036ab49d5b79556f99cfc2d9320b34cfbe5be05c5871b51de9329f0603b0474"}, + {file = "certifi-2023.11.17.tar.gz", hash = "sha256:9b469f3a900bf28dc19b8cfbf8019bf47f7fdd1a65a1d4ffb98fc14166beb4d1"}, ] [[package]] name = "click" -version = "8.1.6" +version = "8.1.7" description = "Composable command line interface toolkit" optional = false python-versions = ">=3.7" files = [ - {file = "click-8.1.6-py3-none-any.whl", hash = "sha256:fa244bb30b3b5ee2cae3da8f55c9e5e0c0e86093306301fb418eb9dc40fbded5"}, - {file = "click-8.1.6.tar.gz", hash = "sha256:48ee849951919527a045bfe3bf7baa8a959c423134e1a5b98c05c20ba75a1cbd"}, + {file = "click-8.1.7-py3-none-any.whl", hash = "sha256:ae74fb96c20a0277a1d615f1e4d73c8414f5a98db8b799a7931d1582f3390c28"}, + {file = "click-8.1.7.tar.gz", hash = "sha256:ca9853ad459e787e2192211578cc907e7594e294c7ccc834310722b41b9ca6de"}, ] [package.dependencies] @@ -91,71 +91,63 @@ files = [ [[package]] name = "coverage" -version = "7.2.7" +version = "7.3.2" description = "Code coverage measurement for Python" optional = false -python-versions = ">=3.7" +python-versions = ">=3.8" files = [ - {file = "coverage-7.2.7-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d39b5b4f2a66ccae8b7263ac3c8170994b65266797fb96cbbfd3fb5b23921db8"}, - {file = "coverage-7.2.7-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:6d040ef7c9859bb11dfeb056ff5b3872436e3b5e401817d87a31e1750b9ae2fb"}, - {file = "coverage-7.2.7-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ba90a9563ba44a72fda2e85302c3abc71c5589cea608ca16c22b9804262aaeb6"}, - {file = "coverage-7.2.7-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e7d9405291c6928619403db1d10bd07888888ec1abcbd9748fdaa971d7d661b2"}, - {file = "coverage-7.2.7-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:31563e97dae5598556600466ad9beea39fb04e0229e61c12eaa206e0aa202063"}, - {file = "coverage-7.2.7-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:ebba1cd308ef115925421d3e6a586e655ca5a77b5bf41e02eb0e4562a111f2d1"}, - {file = "coverage-7.2.7-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:cb017fd1b2603ef59e374ba2063f593abe0fc45f2ad9abdde5b4d83bd922a353"}, - {file = "coverage-7.2.7-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:d62a5c7dad11015c66fbb9d881bc4caa5b12f16292f857842d9d1871595f4495"}, - {file = "coverage-7.2.7-cp310-cp310-win32.whl", hash = "sha256:ee57190f24fba796e36bb6d3aa8a8783c643d8fa9760c89f7a98ab5455fbf818"}, - {file = "coverage-7.2.7-cp310-cp310-win_amd64.whl", hash = "sha256:f75f7168ab25dd93110c8a8117a22450c19976afbc44234cbf71481094c1b850"}, - {file = "coverage-7.2.7-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:06a9a2be0b5b576c3f18f1a241f0473575c4a26021b52b2a85263a00f034d51f"}, - {file = "coverage-7.2.7-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:5baa06420f837184130752b7c5ea0808762083bf3487b5038d68b012e5937dbe"}, - {file = "coverage-7.2.7-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fdec9e8cbf13a5bf63290fc6013d216a4c7232efb51548594ca3631a7f13c3a3"}, - {file = "coverage-7.2.7-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:52edc1a60c0d34afa421c9c37078817b2e67a392cab17d97283b64c5833f427f"}, - {file = "coverage-7.2.7-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:63426706118b7f5cf6bb6c895dc215d8a418d5952544042c8a2d9fe87fcf09cb"}, - {file = "coverage-7.2.7-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:afb17f84d56068a7c29f5fa37bfd38d5aba69e3304af08ee94da8ed5b0865833"}, - {file = "coverage-7.2.7-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:48c19d2159d433ccc99e729ceae7d5293fbffa0bdb94952d3579983d1c8c9d97"}, - {file = "coverage-7.2.7-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:0e1f928eaf5469c11e886fe0885ad2bf1ec606434e79842a879277895a50942a"}, - {file = "coverage-7.2.7-cp311-cp311-win32.whl", hash = "sha256:33d6d3ea29d5b3a1a632b3c4e4f4ecae24ef170b0b9ee493883f2df10039959a"}, - {file = "coverage-7.2.7-cp311-cp311-win_amd64.whl", hash = "sha256:5b7540161790b2f28143191f5f8ec02fb132660ff175b7747b95dcb77ac26562"}, - {file = "coverage-7.2.7-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:f2f67fe12b22cd130d34d0ef79206061bfb5eda52feb6ce0dba0644e20a03cf4"}, - {file = "coverage-7.2.7-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a342242fe22407f3c17f4b499276a02b01e80f861f1682ad1d95b04018e0c0d4"}, - {file = "coverage-7.2.7-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:171717c7cb6b453aebac9a2ef603699da237f341b38eebfee9be75d27dc38e01"}, - {file = "coverage-7.2.7-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:49969a9f7ffa086d973d91cec8d2e31080436ef0fb4a359cae927e742abfaaa6"}, - {file = "coverage-7.2.7-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:b46517c02ccd08092f4fa99f24c3b83d8f92f739b4657b0f146246a0ca6a831d"}, - {file = "coverage-7.2.7-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:a3d33a6b3eae87ceaefa91ffdc130b5e8536182cd6dfdbfc1aa56b46ff8c86de"}, - {file = "coverage-7.2.7-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:976b9c42fb2a43ebf304fa7d4a310e5f16cc99992f33eced91ef6f908bd8f33d"}, - {file = "coverage-7.2.7-cp312-cp312-win32.whl", hash = "sha256:8de8bb0e5ad103888d65abef8bca41ab93721647590a3f740100cd65c3b00511"}, - {file = "coverage-7.2.7-cp312-cp312-win_amd64.whl", hash = "sha256:9e31cb64d7de6b6f09702bb27c02d1904b3aebfca610c12772452c4e6c21a0d3"}, - {file = "coverage-7.2.7-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:58c2ccc2f00ecb51253cbe5d8d7122a34590fac9646a960d1430d5b15321d95f"}, - {file = "coverage-7.2.7-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d22656368f0e6189e24722214ed8d66b8022db19d182927b9a248a2a8a2f67eb"}, - {file = "coverage-7.2.7-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a895fcc7b15c3fc72beb43cdcbdf0ddb7d2ebc959edac9cef390b0d14f39f8a9"}, - {file = "coverage-7.2.7-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e84606b74eb7de6ff581a7915e2dab7a28a0517fbe1c9239eb227e1354064dcd"}, - {file = "coverage-7.2.7-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:0a5f9e1dbd7fbe30196578ca36f3fba75376fb99888c395c5880b355e2875f8a"}, - {file = "coverage-7.2.7-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:419bfd2caae268623dd469eff96d510a920c90928b60f2073d79f8fe2bbc5959"}, - {file = "coverage-7.2.7-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:2aee274c46590717f38ae5e4650988d1af340fe06167546cc32fe2f58ed05b02"}, - {file = "coverage-7.2.7-cp37-cp37m-win32.whl", hash = "sha256:61b9a528fb348373c433e8966535074b802c7a5d7f23c4f421e6c6e2f1697a6f"}, - {file = "coverage-7.2.7-cp37-cp37m-win_amd64.whl", hash = "sha256:b1c546aca0ca4d028901d825015dc8e4d56aac4b541877690eb76490f1dc8ed0"}, - {file = "coverage-7.2.7-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:54b896376ab563bd38453cecb813c295cf347cf5906e8b41d340b0321a5433e5"}, - {file = "coverage-7.2.7-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:3d376df58cc111dc8e21e3b6e24606b5bb5dee6024f46a5abca99124b2229ef5"}, - {file = "coverage-7.2.7-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5e330fc79bd7207e46c7d7fd2bb4af2963f5f635703925543a70b99574b0fea9"}, - {file = "coverage-7.2.7-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1e9d683426464e4a252bf70c3498756055016f99ddaec3774bf368e76bbe02b6"}, - {file = "coverage-7.2.7-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8d13c64ee2d33eccf7437961b6ea7ad8673e2be040b4f7fd4fd4d4d28d9ccb1e"}, - {file = "coverage-7.2.7-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:b7aa5f8a41217360e600da646004f878250a0d6738bcdc11a0a39928d7dc2050"}, - {file = "coverage-7.2.7-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:8fa03bce9bfbeeef9f3b160a8bed39a221d82308b4152b27d82d8daa7041fee5"}, - {file = "coverage-7.2.7-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:245167dd26180ab4c91d5e1496a30be4cd721a5cf2abf52974f965f10f11419f"}, - {file = "coverage-7.2.7-cp38-cp38-win32.whl", hash = "sha256:d2c2db7fd82e9b72937969bceac4d6ca89660db0a0967614ce2481e81a0b771e"}, - {file = "coverage-7.2.7-cp38-cp38-win_amd64.whl", hash = "sha256:2e07b54284e381531c87f785f613b833569c14ecacdcb85d56b25c4622c16c3c"}, - {file = "coverage-7.2.7-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:537891ae8ce59ef63d0123f7ac9e2ae0fc8b72c7ccbe5296fec45fd68967b6c9"}, - {file = "coverage-7.2.7-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:06fb182e69f33f6cd1d39a6c597294cff3143554b64b9825d1dc69d18cc2fff2"}, - {file = "coverage-7.2.7-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:201e7389591af40950a6480bd9edfa8ed04346ff80002cec1a66cac4549c1ad7"}, - {file = "coverage-7.2.7-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f6951407391b639504e3b3be51b7ba5f3528adbf1a8ac3302b687ecababf929e"}, - {file = "coverage-7.2.7-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6f48351d66575f535669306aa7d6d6f71bc43372473b54a832222803eb956fd1"}, - {file = "coverage-7.2.7-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:b29019c76039dc3c0fd815c41392a044ce555d9bcdd38b0fb60fb4cd8e475ba9"}, - {file = "coverage-7.2.7-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:81c13a1fc7468c40f13420732805a4c38a105d89848b7c10af65a90beff25250"}, - {file = "coverage-7.2.7-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:975d70ab7e3c80a3fe86001d8751f6778905ec723f5b110aed1e450da9d4b7f2"}, - {file = "coverage-7.2.7-cp39-cp39-win32.whl", hash = "sha256:7ee7d9d4822c8acc74a5e26c50604dff824710bc8de424904c0982e25c39c6cb"}, - {file = "coverage-7.2.7-cp39-cp39-win_amd64.whl", hash = "sha256:eb393e5ebc85245347950143969b241d08b52b88a3dc39479822e073a1a8eb27"}, - {file = "coverage-7.2.7-pp37.pp38.pp39-none-any.whl", hash = "sha256:b7b4c971f05e6ae490fef852c218b0e79d4e52f79ef0c8475566584a8fb3e01d"}, - {file = "coverage-7.2.7.tar.gz", hash = "sha256:924d94291ca674905fe9481f12294eb11f2d3d3fd1adb20314ba89e94f44ed59"}, + {file = "coverage-7.3.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d872145f3a3231a5f20fd48500274d7df222e291d90baa2026cc5152b7ce86bf"}, + {file = "coverage-7.3.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:310b3bb9c91ea66d59c53fa4989f57d2436e08f18fb2f421a1b0b6b8cc7fffda"}, + {file = "coverage-7.3.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f47d39359e2c3779c5331fc740cf4bce6d9d680a7b4b4ead97056a0ae07cb49a"}, + {file = "coverage-7.3.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:aa72dbaf2c2068404b9870d93436e6d23addd8bbe9295f49cbca83f6e278179c"}, + {file = "coverage-7.3.2-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:beaa5c1b4777f03fc63dfd2a6bd820f73f036bfb10e925fce067b00a340d0f3f"}, + {file = "coverage-7.3.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:dbc1b46b92186cc8074fee9d9fbb97a9dd06c6cbbef391c2f59d80eabdf0faa6"}, + {file = "coverage-7.3.2-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:315a989e861031334d7bee1f9113c8770472db2ac484e5b8c3173428360a9148"}, + {file = "coverage-7.3.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:d1bc430677773397f64a5c88cb522ea43175ff16f8bfcc89d467d974cb2274f9"}, + {file = "coverage-7.3.2-cp310-cp310-win32.whl", hash = "sha256:a889ae02f43aa45032afe364c8ae84ad3c54828c2faa44f3bfcafecb5c96b02f"}, + {file = "coverage-7.3.2-cp310-cp310-win_amd64.whl", hash = "sha256:c0ba320de3fb8c6ec16e0be17ee1d3d69adcda99406c43c0409cb5c41788a611"}, + {file = "coverage-7.3.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:ac8c802fa29843a72d32ec56d0ca792ad15a302b28ca6203389afe21f8fa062c"}, + {file = "coverage-7.3.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:89a937174104339e3a3ffcf9f446c00e3a806c28b1841c63edb2b369310fd074"}, + {file = "coverage-7.3.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e267e9e2b574a176ddb983399dec325a80dbe161f1a32715c780b5d14b5f583a"}, + {file = "coverage-7.3.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2443cbda35df0d35dcfb9bf8f3c02c57c1d6111169e3c85fc1fcc05e0c9f39a3"}, + {file = "coverage-7.3.2-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4175e10cc8dda0265653e8714b3174430b07c1dca8957f4966cbd6c2b1b8065a"}, + {file = "coverage-7.3.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:0cbf38419fb1a347aaf63481c00f0bdc86889d9fbf3f25109cf96c26b403fda1"}, + {file = "coverage-7.3.2-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:5c913b556a116b8d5f6ef834038ba983834d887d82187c8f73dec21049abd65c"}, + {file = "coverage-7.3.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:1981f785239e4e39e6444c63a98da3a1db8e971cb9ceb50a945ba6296b43f312"}, + {file = "coverage-7.3.2-cp311-cp311-win32.whl", hash = "sha256:43668cabd5ca8258f5954f27a3aaf78757e6acf13c17604d89648ecc0cc66640"}, + {file = "coverage-7.3.2-cp311-cp311-win_amd64.whl", hash = "sha256:e10c39c0452bf6e694511c901426d6b5ac005acc0f78ff265dbe36bf81f808a2"}, + {file = "coverage-7.3.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:4cbae1051ab791debecc4a5dcc4a1ff45fc27b91b9aee165c8a27514dd160836"}, + {file = "coverage-7.3.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:12d15ab5833a997716d76f2ac1e4b4d536814fc213c85ca72756c19e5a6b3d63"}, + {file = "coverage-7.3.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3c7bba973ebee5e56fe9251300c00f1579652587a9f4a5ed8404b15a0471f216"}, + {file = "coverage-7.3.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:fe494faa90ce6381770746077243231e0b83ff3f17069d748f645617cefe19d4"}, + {file = "coverage-7.3.2-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f6e9589bd04d0461a417562649522575d8752904d35c12907d8c9dfeba588faf"}, + {file = "coverage-7.3.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:d51ac2a26f71da1b57f2dc81d0e108b6ab177e7d30e774db90675467c847bbdf"}, + {file = "coverage-7.3.2-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:99b89d9f76070237975b315b3d5f4d6956ae354a4c92ac2388a5695516e47c84"}, + {file = "coverage-7.3.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:fa28e909776dc69efb6ed975a63691bc8172b64ff357e663a1bb06ff3c9b589a"}, + {file = "coverage-7.3.2-cp312-cp312-win32.whl", hash = "sha256:289fe43bf45a575e3ab10b26d7b6f2ddb9ee2dba447499f5401cfb5ecb8196bb"}, + {file = "coverage-7.3.2-cp312-cp312-win_amd64.whl", hash = "sha256:7dbc3ed60e8659bc59b6b304b43ff9c3ed858da2839c78b804973f613d3e92ed"}, + {file = "coverage-7.3.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:f94b734214ea6a36fe16e96a70d941af80ff3bfd716c141300d95ebc85339738"}, + {file = "coverage-7.3.2-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:af3d828d2c1cbae52d34bdbb22fcd94d1ce715d95f1a012354a75e5913f1bda2"}, + {file = "coverage-7.3.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:630b13e3036e13c7adc480ca42fa7afc2a5d938081d28e20903cf7fd687872e2"}, + {file = "coverage-7.3.2-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c9eacf273e885b02a0273bb3a2170f30e2d53a6d53b72dbe02d6701b5296101c"}, + {file = "coverage-7.3.2-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d8f17966e861ff97305e0801134e69db33b143bbfb36436efb9cfff6ec7b2fd9"}, + {file = "coverage-7.3.2-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:b4275802d16882cf9c8b3d057a0839acb07ee9379fa2749eca54efbce1535b82"}, + {file = "coverage-7.3.2-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:72c0cfa5250f483181e677ebc97133ea1ab3eb68645e494775deb6a7f6f83901"}, + {file = "coverage-7.3.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:cb536f0dcd14149425996821a168f6e269d7dcd2c273a8bff8201e79f5104e76"}, + {file = "coverage-7.3.2-cp38-cp38-win32.whl", hash = "sha256:307adb8bd3abe389a471e649038a71b4eb13bfd6b7dd9a129fa856f5c695cf92"}, + {file = "coverage-7.3.2-cp38-cp38-win_amd64.whl", hash = "sha256:88ed2c30a49ea81ea3b7f172e0269c182a44c236eb394718f976239892c0a27a"}, + {file = "coverage-7.3.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:b631c92dfe601adf8f5ebc7fc13ced6bb6e9609b19d9a8cd59fa47c4186ad1ce"}, + {file = "coverage-7.3.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:d3d9df4051c4a7d13036524b66ecf7a7537d14c18a384043f30a303b146164e9"}, + {file = "coverage-7.3.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5f7363d3b6a1119ef05015959ca24a9afc0ea8a02c687fe7e2d557705375c01f"}, + {file = "coverage-7.3.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2f11cc3c967a09d3695d2a6f03fb3e6236622b93be7a4b5dc09166a861be6d25"}, + {file = "coverage-7.3.2-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:149de1d2401ae4655c436a3dced6dd153f4c3309f599c3d4bd97ab172eaf02d9"}, + {file = "coverage-7.3.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:3a4006916aa6fee7cd38db3bfc95aa9c54ebb4ffbfc47c677c8bba949ceba0a6"}, + {file = "coverage-7.3.2-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:9028a3871280110d6e1aa2df1afd5ef003bab5fb1ef421d6dc748ae1c8ef2ebc"}, + {file = "coverage-7.3.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:9f805d62aec8eb92bab5b61c0f07329275b6f41c97d80e847b03eb894f38d083"}, + {file = "coverage-7.3.2-cp39-cp39-win32.whl", hash = "sha256:d1c88ec1a7ff4ebca0219f5b1ef863451d828cccf889c173e1253aa84b1e07ce"}, + {file = "coverage-7.3.2-cp39-cp39-win_amd64.whl", hash = "sha256:b4767da59464bb593c07afceaddea61b154136300881844768037fd5e859353f"}, + {file = "coverage-7.3.2-pp38.pp39.pp310-none-any.whl", hash = "sha256:ae97af89f0fbf373400970c0a21eef5aa941ffeed90aee43650b81f7d7f47637"}, + {file = "coverage-7.3.2.tar.gz", hash = "sha256:be32ad29341b0170e795ca590e1c07e81fc061cb5b10c74ce7203491484404ef"}, ] [package.extras] @@ -188,75 +180,72 @@ sqlite = ["aiosqlite"] [[package]] name = "greenlet" -version = "2.0.2" +version = "3.0.1" description = "Lightweight in-process concurrent programming" optional = false -python-versions = ">=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*" +python-versions = ">=3.7" files = [ - {file = "greenlet-2.0.2-cp27-cp27m-macosx_10_14_x86_64.whl", hash = "sha256:bdfea8c661e80d3c1c99ad7c3ff74e6e87184895bbaca6ee8cc61209f8b9b85d"}, - {file = "greenlet-2.0.2-cp27-cp27m-manylinux2010_x86_64.whl", hash = "sha256:9d14b83fab60d5e8abe587d51c75b252bcc21683f24699ada8fb275d7712f5a9"}, - {file = "greenlet-2.0.2-cp27-cp27m-win32.whl", hash = "sha256:6c3acb79b0bfd4fe733dff8bc62695283b57949ebcca05ae5c129eb606ff2d74"}, - {file = "greenlet-2.0.2-cp27-cp27m-win_amd64.whl", hash = "sha256:283737e0da3f08bd637b5ad058507e578dd462db259f7f6e4c5c365ba4ee9343"}, - {file = "greenlet-2.0.2-cp27-cp27mu-manylinux2010_x86_64.whl", hash = "sha256:d27ec7509b9c18b6d73f2f5ede2622441de812e7b1a80bbd446cb0633bd3d5ae"}, - {file = "greenlet-2.0.2-cp310-cp310-macosx_11_0_x86_64.whl", hash = "sha256:30bcf80dda7f15ac77ba5af2b961bdd9dbc77fd4ac6105cee85b0d0a5fcf74df"}, - {file = "greenlet-2.0.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:26fbfce90728d82bc9e6c38ea4d038cba20b7faf8a0ca53a9c07b67318d46088"}, - {file = "greenlet-2.0.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9190f09060ea4debddd24665d6804b995a9c122ef5917ab26e1566dcc712ceeb"}, - {file = "greenlet-2.0.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d75209eed723105f9596807495d58d10b3470fa6732dd6756595e89925ce2470"}, - {file = "greenlet-2.0.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:3a51c9751078733d88e013587b108f1b7a1fb106d402fb390740f002b6f6551a"}, - {file = "greenlet-2.0.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:76ae285c8104046b3a7f06b42f29c7b73f77683df18c49ab5af7983994c2dd91"}, - {file = "greenlet-2.0.2-cp310-cp310-win_amd64.whl", hash = "sha256:2d4686f195e32d36b4d7cf2d166857dbd0ee9f3d20ae349b6bf8afc8485b3645"}, - {file = "greenlet-2.0.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:c4302695ad8027363e96311df24ee28978162cdcdd2006476c43970b384a244c"}, - {file = "greenlet-2.0.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c48f54ef8e05f04d6eff74b8233f6063cb1ed960243eacc474ee73a2ea8573ca"}, - {file = "greenlet-2.0.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a1846f1b999e78e13837c93c778dcfc3365902cfb8d1bdb7dd73ead37059f0d0"}, - {file = "greenlet-2.0.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3a06ad5312349fec0ab944664b01d26f8d1f05009566339ac6f63f56589bc1a2"}, - {file = "greenlet-2.0.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:eff4eb9b7eb3e4d0cae3d28c283dc16d9bed6b193c2e1ace3ed86ce48ea8df19"}, - {file = "greenlet-2.0.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:5454276c07d27a740c5892f4907c86327b632127dd9abec42ee62e12427ff7e3"}, - {file = "greenlet-2.0.2-cp311-cp311-win_amd64.whl", hash = "sha256:7cafd1208fdbe93b67c7086876f061f660cfddc44f404279c1585bbf3cdc64c5"}, - {file = "greenlet-2.0.2-cp35-cp35m-macosx_10_14_x86_64.whl", hash = "sha256:910841381caba4f744a44bf81bfd573c94e10b3045ee00de0cbf436fe50673a6"}, - {file = "greenlet-2.0.2-cp35-cp35m-manylinux2010_x86_64.whl", hash = "sha256:18a7f18b82b52ee85322d7a7874e676f34ab319b9f8cce5de06067384aa8ff43"}, - {file = "greenlet-2.0.2-cp35-cp35m-win32.whl", hash = "sha256:03a8f4f3430c3b3ff8d10a2a86028c660355ab637cee9333d63d66b56f09d52a"}, - {file = "greenlet-2.0.2-cp35-cp35m-win_amd64.whl", hash = "sha256:4b58adb399c4d61d912c4c331984d60eb66565175cdf4a34792cd9600f21b394"}, - {file = "greenlet-2.0.2-cp36-cp36m-macosx_10_14_x86_64.whl", hash = "sha256:703f18f3fda276b9a916f0934d2fb6d989bf0b4fb5a64825260eb9bfd52d78f0"}, - {file = "greenlet-2.0.2-cp36-cp36m-manylinux2010_x86_64.whl", hash = "sha256:32e5b64b148966d9cccc2c8d35a671409e45f195864560829f395a54226408d3"}, - {file = "greenlet-2.0.2-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2dd11f291565a81d71dab10b7033395b7a3a5456e637cf997a6f33ebdf06f8db"}, - {file = "greenlet-2.0.2-cp36-cp36m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e0f72c9ddb8cd28532185f54cc1453f2c16fb417a08b53a855c4e6a418edd099"}, - {file = "greenlet-2.0.2-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cd021c754b162c0fb55ad5d6b9d960db667faad0fa2ff25bb6e1301b0b6e6a75"}, - {file = "greenlet-2.0.2-cp36-cp36m-musllinux_1_1_aarch64.whl", hash = "sha256:3c9b12575734155d0c09d6c3e10dbd81665d5c18e1a7c6597df72fd05990c8cf"}, - {file = "greenlet-2.0.2-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:b9ec052b06a0524f0e35bd8790686a1da006bd911dd1ef7d50b77bfbad74e292"}, - {file = "greenlet-2.0.2-cp36-cp36m-win32.whl", hash = "sha256:dbfcfc0218093a19c252ca8eb9aee3d29cfdcb586df21049b9d777fd32c14fd9"}, - {file = "greenlet-2.0.2-cp36-cp36m-win_amd64.whl", hash = "sha256:9f35ec95538f50292f6d8f2c9c9f8a3c6540bbfec21c9e5b4b751e0a7c20864f"}, - {file = "greenlet-2.0.2-cp37-cp37m-macosx_10_15_x86_64.whl", hash = "sha256:d5508f0b173e6aa47273bdc0a0b5ba055b59662ba7c7ee5119528f466585526b"}, - {file = "greenlet-2.0.2-cp37-cp37m-manylinux2010_x86_64.whl", hash = "sha256:f82d4d717d8ef19188687aa32b8363e96062911e63ba22a0cff7802a8e58e5f1"}, - {file = "greenlet-2.0.2-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c9c59a2120b55788e800d82dfa99b9e156ff8f2227f07c5e3012a45a399620b7"}, - {file = "greenlet-2.0.2-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2780572ec463d44c1d3ae850239508dbeb9fed38e294c68d19a24d925d9223ca"}, - {file = "greenlet-2.0.2-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:937e9020b514ceedb9c830c55d5c9872abc90f4b5862f89c0887033ae33c6f73"}, - {file = "greenlet-2.0.2-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:36abbf031e1c0f79dd5d596bfaf8e921c41df2bdf54ee1eed921ce1f52999a86"}, - {file = "greenlet-2.0.2-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:18e98fb3de7dba1c0a852731c3070cf022d14f0d68b4c87a19cc1016f3bb8b33"}, - {file = "greenlet-2.0.2-cp37-cp37m-win32.whl", hash = "sha256:3f6ea9bd35eb450837a3d80e77b517ea5bc56b4647f5502cd28de13675ee12f7"}, - {file = "greenlet-2.0.2-cp37-cp37m-win_amd64.whl", hash = "sha256:7492e2b7bd7c9b9916388d9df23fa49d9b88ac0640db0a5b4ecc2b653bf451e3"}, - {file = "greenlet-2.0.2-cp38-cp38-macosx_10_15_x86_64.whl", hash = "sha256:b864ba53912b6c3ab6bcb2beb19f19edd01a6bfcbdfe1f37ddd1778abfe75a30"}, - {file = "greenlet-2.0.2-cp38-cp38-manylinux2010_x86_64.whl", hash = "sha256:ba2956617f1c42598a308a84c6cf021a90ff3862eddafd20c3333d50f0edb45b"}, - {file = "greenlet-2.0.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fc3a569657468b6f3fb60587e48356fe512c1754ca05a564f11366ac9e306526"}, - {file = "greenlet-2.0.2-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8eab883b3b2a38cc1e050819ef06a7e6344d4a990d24d45bc6f2cf959045a45b"}, - {file = "greenlet-2.0.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:acd2162a36d3de67ee896c43effcd5ee3de247eb00354db411feb025aa319857"}, - {file = "greenlet-2.0.2-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:0bf60faf0bc2468089bdc5edd10555bab6e85152191df713e2ab1fcc86382b5a"}, - {file = "greenlet-2.0.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:b0ef99cdbe2b682b9ccbb964743a6aca37905fda5e0452e5ee239b1654d37f2a"}, - {file = "greenlet-2.0.2-cp38-cp38-win32.whl", hash = "sha256:b80f600eddddce72320dbbc8e3784d16bd3fb7b517e82476d8da921f27d4b249"}, - {file = "greenlet-2.0.2-cp38-cp38-win_amd64.whl", hash = "sha256:4d2e11331fc0c02b6e84b0d28ece3a36e0548ee1a1ce9ddde03752d9b79bba40"}, - {file = "greenlet-2.0.2-cp39-cp39-macosx_11_0_x86_64.whl", hash = "sha256:88d9ab96491d38a5ab7c56dd7a3cc37d83336ecc564e4e8816dbed12e5aaefc8"}, - {file = "greenlet-2.0.2-cp39-cp39-manylinux2010_x86_64.whl", hash = "sha256:561091a7be172ab497a3527602d467e2b3fbe75f9e783d8b8ce403fa414f71a6"}, - {file = "greenlet-2.0.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:971ce5e14dc5e73715755d0ca2975ac88cfdaefcaab078a284fea6cfabf866df"}, - {file = "greenlet-2.0.2-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:be4ed120b52ae4d974aa40215fcdfde9194d63541c7ded40ee12eb4dda57b76b"}, - {file = "greenlet-2.0.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:94c817e84245513926588caf1152e3b559ff794d505555211ca041f032abbb6b"}, - {file = "greenlet-2.0.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:1a819eef4b0e0b96bb0d98d797bef17dc1b4a10e8d7446be32d1da33e095dbb8"}, - {file = "greenlet-2.0.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:7efde645ca1cc441d6dc4b48c0f7101e8d86b54c8530141b09fd31cef5149ec9"}, - {file = "greenlet-2.0.2-cp39-cp39-win32.whl", hash = "sha256:ea9872c80c132f4663822dd2a08d404073a5a9b5ba6155bea72fb2a79d1093b5"}, - {file = "greenlet-2.0.2-cp39-cp39-win_amd64.whl", hash = "sha256:db1a39669102a1d8d12b57de2bb7e2ec9066a6f2b3da35ae511ff93b01b5d564"}, - {file = "greenlet-2.0.2.tar.gz", hash = "sha256:e7c8dc13af7db097bed64a051d2dd49e9f0af495c26995c00a9ee842690d34c0"}, + {file = "greenlet-3.0.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:f89e21afe925fcfa655965ca8ea10f24773a1791400989ff32f467badfe4a064"}, + {file = "greenlet-3.0.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:28e89e232c7593d33cac35425b58950789962011cc274aa43ef8865f2e11f46d"}, + {file = "greenlet-3.0.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b8ba29306c5de7717b5761b9ea74f9c72b9e2b834e24aa984da99cbfc70157fd"}, + {file = "greenlet-3.0.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:19bbdf1cce0346ef7341705d71e2ecf6f41a35c311137f29b8a2dc2341374565"}, + {file = "greenlet-3.0.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:599daf06ea59bfedbec564b1692b0166a0045f32b6f0933b0dd4df59a854caf2"}, + {file = "greenlet-3.0.1-cp310-cp310-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b641161c302efbb860ae6b081f406839a8b7d5573f20a455539823802c655f63"}, + {file = "greenlet-3.0.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:d57e20ba591727da0c230ab2c3f200ac9d6d333860d85348816e1dca4cc4792e"}, + {file = "greenlet-3.0.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:5805e71e5b570d490938d55552f5a9e10f477c19400c38bf1d5190d760691846"}, + {file = "greenlet-3.0.1-cp310-cp310-win_amd64.whl", hash = "sha256:52e93b28db27ae7d208748f45d2db8a7b6a380e0d703f099c949d0f0d80b70e9"}, + {file = "greenlet-3.0.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:f7bfb769f7efa0eefcd039dd19d843a4fbfbac52f1878b1da2ed5793ec9b1a65"}, + {file = "greenlet-3.0.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:91e6c7db42638dc45cf2e13c73be16bf83179f7859b07cfc139518941320be96"}, + {file = "greenlet-3.0.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1757936efea16e3f03db20efd0cd50a1c86b06734f9f7338a90c4ba85ec2ad5a"}, + {file = "greenlet-3.0.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:19075157a10055759066854a973b3d1325d964d498a805bb68a1f9af4aaef8ec"}, + {file = "greenlet-3.0.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e9d21aaa84557d64209af04ff48e0ad5e28c5cca67ce43444e939579d085da72"}, + {file = "greenlet-3.0.1-cp311-cp311-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2847e5d7beedb8d614186962c3d774d40d3374d580d2cbdab7f184580a39d234"}, + {file = "greenlet-3.0.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:97e7ac860d64e2dcba5c5944cfc8fa9ea185cd84061c623536154d5a89237884"}, + {file = "greenlet-3.0.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:b2c02d2ad98116e914d4f3155ffc905fd0c025d901ead3f6ed07385e19122c94"}, + {file = "greenlet-3.0.1-cp311-cp311-win_amd64.whl", hash = "sha256:22f79120a24aeeae2b4471c711dcf4f8c736a2bb2fabad2a67ac9a55ea72523c"}, + {file = "greenlet-3.0.1-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:100f78a29707ca1525ea47388cec8a049405147719f47ebf3895e7509c6446aa"}, + {file = "greenlet-3.0.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:60d5772e8195f4e9ebf74046a9121bbb90090f6550f81d8956a05387ba139353"}, + {file = "greenlet-3.0.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:daa7197b43c707462f06d2c693ffdbb5991cbb8b80b5b984007de431493a319c"}, + {file = "greenlet-3.0.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ea6b8aa9e08eea388c5f7a276fabb1d4b6b9d6e4ceb12cc477c3d352001768a9"}, + {file = "greenlet-3.0.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8d11ebbd679e927593978aa44c10fc2092bc454b7d13fdc958d3e9d508aba7d0"}, + {file = "greenlet-3.0.1-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:dbd4c177afb8a8d9ba348d925b0b67246147af806f0b104af4d24f144d461cd5"}, + {file = "greenlet-3.0.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:20107edf7c2c3644c67c12205dc60b1bb11d26b2610b276f97d666110d1b511d"}, + {file = "greenlet-3.0.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:8bef097455dea90ffe855286926ae02d8faa335ed8e4067326257cb571fc1445"}, + {file = "greenlet-3.0.1-cp312-cp312-win_amd64.whl", hash = "sha256:b2d3337dcfaa99698aa2377c81c9ca72fcd89c07e7eb62ece3f23a3fe89b2ce4"}, + {file = "greenlet-3.0.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:80ac992f25d10aaebe1ee15df45ca0d7571d0f70b645c08ec68733fb7a020206"}, + {file = "greenlet-3.0.1-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:337322096d92808f76ad26061a8f5fccb22b0809bea39212cd6c406f6a7060d2"}, + {file = "greenlet-3.0.1-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b9934adbd0f6e476f0ecff3c94626529f344f57b38c9a541f87098710b18af0a"}, + {file = "greenlet-3.0.1-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dc4d815b794fd8868c4d67602692c21bf5293a75e4b607bb92a11e821e2b859a"}, + {file = "greenlet-3.0.1-cp37-cp37m-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:41bdeeb552d814bcd7fb52172b304898a35818107cc8778b5101423c9017b3de"}, + {file = "greenlet-3.0.1-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:6e6061bf1e9565c29002e3c601cf68569c450be7fc3f7336671af7ddb4657166"}, + {file = "greenlet-3.0.1-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:fa24255ae3c0ab67e613556375a4341af04a084bd58764731972bcbc8baeba36"}, + {file = "greenlet-3.0.1-cp37-cp37m-win32.whl", hash = "sha256:b489c36d1327868d207002391f662a1d163bdc8daf10ab2e5f6e41b9b96de3b1"}, + {file = "greenlet-3.0.1-cp37-cp37m-win_amd64.whl", hash = "sha256:f33f3258aae89da191c6ebaa3bc517c6c4cbc9b9f689e5d8452f7aedbb913fa8"}, + {file = "greenlet-3.0.1-cp38-cp38-macosx_11_0_universal2.whl", hash = "sha256:d2905ce1df400360463c772b55d8e2518d0e488a87cdea13dd2c71dcb2a1fa16"}, + {file = "greenlet-3.0.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0a02d259510b3630f330c86557331a3b0e0c79dac3d166e449a39363beaae174"}, + {file = "greenlet-3.0.1-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:55d62807f1c5a1682075c62436702aaba941daa316e9161e4b6ccebbbf38bda3"}, + {file = "greenlet-3.0.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3fcc780ae8edbb1d050d920ab44790201f027d59fdbd21362340a85c79066a74"}, + {file = "greenlet-3.0.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4eddd98afc726f8aee1948858aed9e6feeb1758889dfd869072d4465973f6bfd"}, + {file = "greenlet-3.0.1-cp38-cp38-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:eabe7090db68c981fca689299c2d116400b553f4b713266b130cfc9e2aa9c5a9"}, + {file = "greenlet-3.0.1-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:f2f6d303f3dee132b322a14cd8765287b8f86cdc10d2cb6a6fae234ea488888e"}, + {file = "greenlet-3.0.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:d923ff276f1c1f9680d32832f8d6c040fe9306cbfb5d161b0911e9634be9ef0a"}, + {file = "greenlet-3.0.1-cp38-cp38-win32.whl", hash = "sha256:0b6f9f8ca7093fd4433472fd99b5650f8a26dcd8ba410e14094c1e44cd3ceddd"}, + {file = "greenlet-3.0.1-cp38-cp38-win_amd64.whl", hash = "sha256:990066bff27c4fcf3b69382b86f4c99b3652bab2a7e685d968cd4d0cfc6f67c6"}, + {file = "greenlet-3.0.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:ce85c43ae54845272f6f9cd8320d034d7a946e9773c693b27d620edec825e376"}, + {file = "greenlet-3.0.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:89ee2e967bd7ff85d84a2de09df10e021c9b38c7d91dead95b406ed6350c6997"}, + {file = "greenlet-3.0.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:87c8ceb0cf8a5a51b8008b643844b7f4a8264a2c13fcbcd8a8316161725383fe"}, + {file = "greenlet-3.0.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d6a8c9d4f8692917a3dc7eb25a6fb337bff86909febe2f793ec1928cd97bedfc"}, + {file = "greenlet-3.0.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9fbc5b8f3dfe24784cee8ce0be3da2d8a79e46a276593db6868382d9c50d97b1"}, + {file = "greenlet-3.0.1-cp39-cp39-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:85d2b77e7c9382f004b41d9c72c85537fac834fb141b0296942d52bf03fe4a3d"}, + {file = "greenlet-3.0.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:696d8e7d82398e810f2b3622b24e87906763b6ebfd90e361e88eb85b0e554dc8"}, + {file = "greenlet-3.0.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:329c5a2e5a0ee942f2992c5e3ff40be03e75f745f48847f118a3cfece7a28546"}, + {file = "greenlet-3.0.1-cp39-cp39-win32.whl", hash = "sha256:cf868e08690cb89360eebc73ba4be7fb461cfbc6168dd88e2fbbe6f31812cd57"}, + {file = "greenlet-3.0.1-cp39-cp39-win_amd64.whl", hash = "sha256:ac4a39d1abae48184d420aa8e5e63efd1b75c8444dd95daa3e03f6c6310e9619"}, + {file = "greenlet-3.0.1.tar.gz", hash = "sha256:816bd9488a94cba78d93e1abb58000e8266fa9cc2aa9ccdd6eb0696acb24005b"}, ] [package.extras] -docs = ["Sphinx", "docutils (<0.18)"] +docs = ["Sphinx"] test = ["objgraph", "psutil"] [[package]] @@ -354,13 +343,13 @@ socks = ["socksio (==1.*)"] [[package]] name = "idna" -version = "3.4" +version = "3.6" description = "Internationalized Domain Names in Applications (IDNA)" optional = false python-versions = ">=3.5" files = [ - {file = "idna-3.4-py3-none-any.whl", hash = "sha256:90b77e79eaa3eba6de819a0c442c0b4ceefc341a7a2ab77d7562bf49f425c5c2"}, - {file = "idna-3.4.tar.gz", hash = "sha256:814f528e8dead7d329833b91c5faa87d60bf71824cd12a7530b5526063d02cb4"}, + {file = "idna-3.6-py3-none-any.whl", hash = "sha256:c05567e9c24a6b9faaa835c4821bad0590fbb9d5779e7caa6e1cc4978e7eb24f"}, + {file = "idna-3.6.tar.gz", hash = "sha256:9ecdbbd083b06798ae1e86adcbfe8ab1479cf864e4ee30fe4e46a003d12491ca"}, ] [[package]] @@ -390,24 +379,24 @@ setuptools = "*" [[package]] name = "packaging" -version = "23.1" +version = "23.2" description = "Core utilities for Python packages" optional = false python-versions = ">=3.7" files = [ - {file = "packaging-23.1-py3-none-any.whl", hash = "sha256:994793af429502c4ea2ebf6bf664629d07c1a9fe974af92966e4b8d2df7edc61"}, - {file = "packaging-23.1.tar.gz", hash = "sha256:a392980d2b6cffa644431898be54b0045151319d1e7ec34f0cfed48767dd334f"}, + {file = "packaging-23.2-py3-none-any.whl", hash = "sha256:8c491190033a9af7e1d931d0b5dacc2ef47509b34dd0de67ed209b5203fc88c7"}, + {file = "packaging-23.2.tar.gz", hash = "sha256:048fb0e9405036518eaaf48a55953c750c11e1a1b68e0dd1a9d62ed0c092cfc5"}, ] [[package]] name = "pluggy" -version = "1.2.0" +version = "1.3.0" description = "plugin and hook calling mechanisms for python" optional = false -python-versions = ">=3.7" +python-versions = ">=3.8" files = [ - {file = "pluggy-1.2.0-py3-none-any.whl", hash = "sha256:c2fd55a7d7a3863cba1a013e4e2414658b1d07b6bc57b3919e0c63c9abb99849"}, - {file = "pluggy-1.2.0.tar.gz", hash = "sha256:d12f0c4b579b15f5e054301bb226ee85eeeba08ffec228092f8defbaa3a4c4b3"}, + {file = "pluggy-1.3.0-py3-none-any.whl", hash = "sha256:d89c696a773f8bd377d18e5ecda92b7a3793cbe66c87060a6fb58c7b6e1061f7"}, + {file = "pluggy-1.3.0.tar.gz", hash = "sha256:cf61ae8f126ac6f7c451172cf30e3e43d3ca77615509771b3a984a0730651e12"}, ] [package.extras] @@ -416,13 +405,13 @@ testing = ["pytest", "pytest-benchmark"] [[package]] name = "pyright" -version = "1.1.318" +version = "1.1.337" description = "Command line wrapper for pyright" optional = false python-versions = ">=3.7" files = [ - {file = "pyright-1.1.318-py3-none-any.whl", hash = "sha256:056c1b2e711c3526e32919de1684ae599d34b7ec27e94398858a43f56ac9ba9b"}, - {file = "pyright-1.1.318.tar.gz", hash = "sha256:69dcf9c32d5be27d531750de627e76a7cadc741d333b547c09044278b508db7b"}, + {file = "pyright-1.1.337-py3-none-any.whl", hash = "sha256:8cbd4ef71797258f816a8393a758c9c91213479f472082d0e3a735ef7ab5f65a"}, + {file = "pyright-1.1.337.tar.gz", hash = "sha256:81d81f839d1750385390c4c4a7b84b062ece2f9a078f87055d4d2a5914ef2a08"}, ] [package.dependencies] @@ -434,13 +423,13 @@ dev = ["twine (>=3.4.1)"] [[package]] name = "pytest" -version = "7.4.0" +version = "7.4.3" description = "pytest: simple powerful testing with Python" optional = false python-versions = ">=3.7" files = [ - {file = "pytest-7.4.0-py3-none-any.whl", hash = "sha256:78bf16451a2eb8c7a2ea98e32dc119fd2aa758f1d5d66dbf0a59d69a3969df32"}, - {file = "pytest-7.4.0.tar.gz", hash = "sha256:b4bf8c45bd59934ed84001ad51e11b4ee40d40a1229d2c79f9c592b0a3f6bd8a"}, + {file = "pytest-7.4.3-py3-none-any.whl", hash = "sha256:0d009c083ea859a71b76adf7c1d502e4bc170b80a8ef002da5806527b9591fac"}, + {file = "pytest-7.4.3.tar.gz", hash = "sha256:d989d136982de4e3b29dabcc838ad581c64e8ed52c11fbe86ddebd9da0818cd5"}, ] [package.dependencies] @@ -516,19 +505,19 @@ files = [ [[package]] name = "setuptools" -version = "68.0.0" +version = "69.0.2" description = "Easily download, build, install, upgrade, and uninstall Python packages" optional = false -python-versions = ">=3.7" +python-versions = ">=3.8" files = [ - {file = "setuptools-68.0.0-py3-none-any.whl", hash = "sha256:11e52c67415a381d10d6b462ced9cfb97066179f0e871399e006c4ab101fc85f"}, - {file = "setuptools-68.0.0.tar.gz", hash = "sha256:baf1fdb41c6da4cd2eae722e135500da913332ab3f2f5c7d33af9b492acb5235"}, + {file = "setuptools-69.0.2-py3-none-any.whl", hash = "sha256:1e8fdff6797d3865f37397be788a4e3cba233608e9b509382a2777d25ebde7f2"}, + {file = "setuptools-69.0.2.tar.gz", hash = "sha256:735896e78a4742605974de002ac60562d286fa8051a7e2299445e8e8fbb01aa6"}, ] [package.extras] -docs = ["furo", "jaraco.packaging (>=9)", "jaraco.tidelift (>=1.4)", "pygments-github-lexers (==0.0.5)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-favicon", "sphinx-hoverxref (<2)", "sphinx-inline-tabs", "sphinx-lint", "sphinx-notfound-page (==0.8.3)", "sphinx-reredirects", "sphinxcontrib-towncrier"] -testing = ["build[virtualenv]", "filelock (>=3.4.0)", "flake8-2020", "ini2toml[lite] (>=0.9)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "pip (>=19.1)", "pip-run (>=8.8)", "pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=1.3)", "pytest-mypy (>=0.9.1)", "pytest-perf", "pytest-ruff", "pytest-timeout", "pytest-xdist", "tomli-w (>=1.0.0)", "virtualenv (>=13.0.0)", "wheel"] -testing-integration = ["build[virtualenv]", "filelock (>=3.4.0)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "pytest", "pytest-enabler", "pytest-xdist", "tomli", "virtualenv (>=13.0.0)", "wheel"] +docs = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "pygments-github-lexers (==0.0.5)", "rst.linker (>=1.9)", "sphinx (<7.2.5)", "sphinx (>=3.5)", "sphinx-favicon", "sphinx-inline-tabs", "sphinx-lint", "sphinx-notfound-page (>=1,<2)", "sphinx-reredirects", "sphinxcontrib-towncrier"] +testing = ["build[virtualenv]", "filelock (>=3.4.0)", "flake8-2020", "ini2toml[lite] (>=0.9)", "jaraco.develop (>=7.21)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "pip (>=19.1)", "pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-mypy (>=0.9.1)", "pytest-perf", "pytest-ruff", "pytest-timeout", "pytest-xdist", "tomli-w (>=1.0.0)", "virtualenv (>=13.0.0)", "wheel"] +testing-integration = ["build[virtualenv] (>=1.0.3)", "filelock (>=3.4.0)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "packaging (>=23.1)", "pytest", "pytest-enabler", "pytest-xdist", "tomli", "virtualenv (>=13.0.0)", "wheel"] [[package]] name = "six" @@ -554,60 +543,47 @@ files = [ [[package]] name = "soupsieve" -version = "2.4.1" +version = "2.5" description = "A modern CSS selector implementation for Beautiful Soup." optional = false -python-versions = ">=3.7" +python-versions = ">=3.8" files = [ - {file = "soupsieve-2.4.1-py3-none-any.whl", hash = "sha256:1c1bfee6819544a3447586c889157365a27e10d88cde3ad3da0cf0ddf646feb8"}, - {file = "soupsieve-2.4.1.tar.gz", hash = "sha256:89d12b2d5dfcd2c9e8c22326da9d9aa9cb3dfab0a83a024f05704076ee8d35ea"}, + {file = "soupsieve-2.5-py3-none-any.whl", hash = "sha256:eaa337ff55a1579b6549dc679565eac1e3d000563bcb1c8ab0d0fefbc0c2cdc7"}, + {file = "soupsieve-2.5.tar.gz", hash = "sha256:5663d5a7b3bfaeee0bc4372e7fc48f9cff4940b3eec54a6451cc5299f1097690"}, ] [[package]] name = "sqlalchemy" -version = "1.4.49" +version = "1.4.50" description = "Database Abstraction Library" optional = false python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,>=2.7" files = [ - {file = "SQLAlchemy-1.4.49-cp27-cp27m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:2e126cf98b7fd38f1e33c64484406b78e937b1a280e078ef558b95bf5b6895f6"}, - {file = "SQLAlchemy-1.4.49-cp27-cp27mu-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:03db81b89fe7ef3857b4a00b63dedd632d6183d4ea5a31c5d8a92e000a41fc71"}, - {file = "SQLAlchemy-1.4.49-cp310-cp310-macosx_11_0_x86_64.whl", hash = "sha256:95b9df9afd680b7a3b13b38adf6e3a38995da5e162cc7524ef08e3be4e5ed3e1"}, - {file = "SQLAlchemy-1.4.49-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a63e43bf3f668c11bb0444ce6e809c1227b8f067ca1068898f3008a273f52b09"}, - {file = "SQLAlchemy-1.4.49-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:f835c050ebaa4e48b18403bed2c0fda986525896efd76c245bdd4db995e51a4c"}, - {file = "SQLAlchemy-1.4.49-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9c21b172dfb22e0db303ff6419451f0cac891d2e911bb9fbf8003d717f1bcf91"}, - {file = "SQLAlchemy-1.4.49-cp310-cp310-win32.whl", hash = "sha256:5fb1ebdfc8373b5a291485757bd6431de8d7ed42c27439f543c81f6c8febd729"}, - {file = "SQLAlchemy-1.4.49-cp310-cp310-win_amd64.whl", hash = "sha256:f8a65990c9c490f4651b5c02abccc9f113a7f56fa482031ac8cb88b70bc8ccaa"}, - {file = "SQLAlchemy-1.4.49-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:8923dfdf24d5aa8a3adb59723f54118dd4fe62cf59ed0d0d65d940579c1170a4"}, - {file = "SQLAlchemy-1.4.49-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a9ab2c507a7a439f13ca4499db6d3f50423d1d65dc9b5ed897e70941d9e135b0"}, - {file = "SQLAlchemy-1.4.49-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5debe7d49b8acf1f3035317e63d9ec8d5e4d904c6e75a2a9246a119f5f2fdf3d"}, - {file = "SQLAlchemy-1.4.49-cp311-cp311-win32.whl", hash = "sha256:82b08e82da3756765c2e75f327b9bf6b0f043c9c3925fb95fb51e1567fa4ee87"}, - {file = "SQLAlchemy-1.4.49-cp311-cp311-win_amd64.whl", hash = "sha256:171e04eeb5d1c0d96a544caf982621a1711d078dbc5c96f11d6469169bd003f1"}, - {file = "SQLAlchemy-1.4.49-cp36-cp36m-macosx_10_14_x86_64.whl", hash = "sha256:36e58f8c4fe43984384e3fbe6341ac99b6b4e083de2fe838f0fdb91cebe9e9cb"}, - {file = "SQLAlchemy-1.4.49-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b31e67ff419013f99ad6f8fc73ee19ea31585e1e9fe773744c0f3ce58c039c30"}, - {file = "SQLAlchemy-1.4.49-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:c14b29d9e1529f99efd550cd04dbb6db6ba5d690abb96d52de2bff4ed518bc95"}, - {file = "SQLAlchemy-1.4.49-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c40f3470e084d31247aea228aa1c39bbc0904c2b9ccbf5d3cfa2ea2dac06f26d"}, - {file = "SQLAlchemy-1.4.49-cp36-cp36m-win32.whl", hash = "sha256:706bfa02157b97c136547c406f263e4c6274a7b061b3eb9742915dd774bbc264"}, - {file = "SQLAlchemy-1.4.49-cp36-cp36m-win_amd64.whl", hash = "sha256:a7f7b5c07ae5c0cfd24c2db86071fb2a3d947da7bd487e359cc91e67ac1c6d2e"}, - {file = "SQLAlchemy-1.4.49-cp37-cp37m-macosx_11_0_x86_64.whl", hash = "sha256:4afbbf5ef41ac18e02c8dc1f86c04b22b7a2125f2a030e25bbb4aff31abb224b"}, - {file = "SQLAlchemy-1.4.49-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:24e300c0c2147484a002b175f4e1361f102e82c345bf263242f0449672a4bccf"}, - {file = "SQLAlchemy-1.4.49-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:201de072b818f8ad55c80d18d1a788729cccf9be6d9dc3b9d8613b053cd4836d"}, - {file = "SQLAlchemy-1.4.49-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7653ed6817c710d0c95558232aba799307d14ae084cc9b1f4c389157ec50df5c"}, - {file = "SQLAlchemy-1.4.49-cp37-cp37m-win32.whl", hash = "sha256:647e0b309cb4512b1f1b78471fdaf72921b6fa6e750b9f891e09c6e2f0e5326f"}, - {file = "SQLAlchemy-1.4.49-cp37-cp37m-win_amd64.whl", hash = "sha256:ab73ed1a05ff539afc4a7f8cf371764cdf79768ecb7d2ec691e3ff89abbc541e"}, - {file = "SQLAlchemy-1.4.49-cp38-cp38-macosx_11_0_x86_64.whl", hash = "sha256:37ce517c011560d68f1ffb28af65d7e06f873f191eb3a73af5671e9c3fada08a"}, - {file = "SQLAlchemy-1.4.49-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a1878ce508edea4a879015ab5215546c444233881301e97ca16fe251e89f1c55"}, - {file = "SQLAlchemy-1.4.49-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:0e8e608983e6f85d0852ca61f97e521b62e67969e6e640fe6c6b575d4db68557"}, - {file = "SQLAlchemy-1.4.49-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ccf956da45290df6e809ea12c54c02ace7f8ff4d765d6d3dfb3655ee876ce58d"}, - {file = "SQLAlchemy-1.4.49-cp38-cp38-win32.whl", hash = "sha256:f167c8175ab908ce48bd6550679cc6ea20ae169379e73c7720a28f89e53aa532"}, - {file = "SQLAlchemy-1.4.49-cp38-cp38-win_amd64.whl", hash = "sha256:45806315aae81a0c202752558f0df52b42d11dd7ba0097bf71e253b4215f34f4"}, - {file = "SQLAlchemy-1.4.49-cp39-cp39-macosx_11_0_x86_64.whl", hash = "sha256:b6d0c4b15d65087738a6e22e0ff461b407533ff65a73b818089efc8eb2b3e1de"}, - {file = "SQLAlchemy-1.4.49-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a843e34abfd4c797018fd8d00ffffa99fd5184c421f190b6ca99def4087689bd"}, - {file = "SQLAlchemy-1.4.49-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:1c890421651b45a681181301b3497e4d57c0d01dc001e10438a40e9a9c25ee77"}, - {file = "SQLAlchemy-1.4.49-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d26f280b8f0a8f497bc10573849ad6dc62e671d2468826e5c748d04ed9e670d5"}, - {file = "SQLAlchemy-1.4.49-cp39-cp39-win32.whl", hash = "sha256:ec2268de67f73b43320383947e74700e95c6770d0c68c4e615e9897e46296294"}, - {file = "SQLAlchemy-1.4.49-cp39-cp39-win_amd64.whl", hash = "sha256:bbdf16372859b8ed3f4d05f925a984771cd2abd18bd187042f24be4886c2a15f"}, - {file = "SQLAlchemy-1.4.49.tar.gz", hash = "sha256:06ff25cbae30c396c4b7737464f2a7fc37a67b7da409993b182b024cec80aed9"}, + {file = "SQLAlchemy-1.4.50-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d00665725063692c42badfd521d0c4392e83c6c826795d38eb88fb108e5660e5"}, + {file = "SQLAlchemy-1.4.50-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:85292ff52ddf85a39367057c3d7968a12ee1fb84565331a36a8fead346f08796"}, + {file = "SQLAlchemy-1.4.50-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:d0fed0f791d78e7767c2db28d34068649dfeea027b83ed18c45a423f741425cb"}, + {file = "SQLAlchemy-1.4.50-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:db4db3c08ffbb18582f856545f058a7a5e4ab6f17f75795ca90b3c38ee0a8ba4"}, + {file = "SQLAlchemy-1.4.50-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:14b0cacdc8a4759a1e1bd47dc3ee3f5db997129eb091330beda1da5a0e9e5bd7"}, + {file = "SQLAlchemy-1.4.50-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1fb9cb60e0f33040e4f4681e6658a7eb03b5cb4643284172f91410d8c493dace"}, + {file = "SQLAlchemy-1.4.50-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c4cb501d585aa74a0f86d0ea6263b9c5e1d1463f8f9071392477fd401bd3c7cc"}, + {file = "SQLAlchemy-1.4.50-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8a7a66297e46f85a04d68981917c75723e377d2e0599d15fbe7a56abed5e2d75"}, + {file = "SQLAlchemy-1.4.50-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c1db0221cb26d66294f4ca18c533e427211673ab86c1fbaca8d6d9ff78654293"}, + {file = "SQLAlchemy-1.4.50-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0b7dbe6369677a2bea68fe9812c6e4bbca06ebfa4b5cde257b2b0bf208709131"}, + {file = "SQLAlchemy-1.4.50-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:a9bddb60566dc45c57fd0a5e14dd2d9e5f106d2241e0a2dc0c1da144f9444516"}, + {file = "SQLAlchemy-1.4.50-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:82dd4131d88395df7c318eeeef367ec768c2a6fe5bd69423f7720c4edb79473c"}, + {file = "SQLAlchemy-1.4.50-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:273505fcad22e58cc67329cefab2e436006fc68e3c5423056ee0513e6523268a"}, + {file = "SQLAlchemy-1.4.50-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a3257a6e09626d32b28a0c5b4f1a97bced585e319cfa90b417f9ab0f6145c33c"}, + {file = "SQLAlchemy-1.4.50-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:d69738d582e3a24125f0c246ed8d712b03bd21e148268421e4a4d09c34f521a5"}, + {file = "SQLAlchemy-1.4.50-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:34e1c5d9cd3e6bf3d1ce56971c62a40c06bfc02861728f368dcfec8aeedb2814"}, + {file = "SQLAlchemy-1.4.50-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f1fcee5a2c859eecb4ed179edac5ffbc7c84ab09a5420219078ccc6edda45436"}, + {file = "SQLAlchemy-1.4.50-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fbaf6643a604aa17e7a7afd74f665f9db882df5c297bdd86c38368f2c471f37d"}, + {file = "SQLAlchemy-1.4.50-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:2e70e0673d7d12fa6cd363453a0d22dac0d9978500aa6b46aa96e22690a55eab"}, + {file = "SQLAlchemy-1.4.50-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8b881ac07d15fb3e4f68c5a67aa5cdaf9eb8f09eb5545aaf4b0a5f5f4659be18"}, + {file = "SQLAlchemy-1.4.50-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3f6997da81114daef9203d30aabfa6b218a577fc2bd797c795c9c88c9eb78d49"}, + {file = "SQLAlchemy-1.4.50-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bdb77e1789e7596b77fd48d99ec1d2108c3349abd20227eea0d48d3f8cf398d9"}, + {file = "SQLAlchemy-1.4.50-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:128a948bd40780667114b0297e2cc6d657b71effa942e0a368d8cc24293febb3"}, + {file = "SQLAlchemy-1.4.50-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f2d526aeea1bd6a442abc7c9b4b00386fd70253b80d54a0930c0a216230a35be"}, + {file = "SQLAlchemy-1.4.50.tar.gz", hash = "sha256:3b97ddf509fc21e10b09403b5219b06c5b558b27fc2453150274fa4e70707dbf"}, ] [package.dependencies] @@ -616,7 +592,7 @@ greenlet = {version = "!=0.4.17", optional = true, markers = "python_version >= typing-extensions = {version = "!=3.10.0.1", optional = true, markers = "extra == \"aiosqlite\""} [package.extras] -aiomysql = ["aiomysql", "greenlet (!=0.4.17)"] +aiomysql = ["aiomysql (>=0.2.0)", "greenlet (!=0.4.17)"] aiosqlite = ["aiosqlite", "greenlet (!=0.4.17)", "typing-extensions (!=3.10.0.1)"] asyncio = ["greenlet (!=0.4.17)"] asyncmy = ["asyncmy (>=0.2.3,!=0.2.4)", "greenlet (!=0.4.17)"] @@ -655,13 +631,13 @@ full = ["httpx (>=0.22.0)", "itsdangerous", "jinja2", "python-multipart", "pyyam [[package]] name = "typing-extensions" -version = "4.7.1" -description = "Backported and Experimental Type Hints for Python 3.7+" +version = "4.8.0" +description = "Backported and Experimental Type Hints for Python 3.8+" optional = false -python-versions = ">=3.7" +python-versions = ">=3.8" files = [ - {file = "typing_extensions-4.7.1-py3-none-any.whl", hash = "sha256:440d5dd3af93b060174bf433bccd69b0babc3b15b1a8dca43789fd7f61514b36"}, - {file = "typing_extensions-4.7.1.tar.gz", hash = "sha256:b75ddc264f0ba5615db7ba217daeb99701ad295353c45f9e95963337ceeeffb2"}, + {file = "typing_extensions-4.8.0-py3-none-any.whl", hash = "sha256:8f92fc8806f9a6b641eaa5318da32b44d401efaac0f6678c9bc448ba3605faa0"}, + {file = "typing_extensions-4.8.0.tar.gz", hash = "sha256:df8e4339e9cb77357558cbdbceca33c303714cf861d1eef15e1070055ae8b7ef"}, ] [[package]] @@ -677,13 +653,13 @@ files = [ [[package]] name = "uvicorn" -version = "0.23.1" +version = "0.23.2" description = "The lightning-fast ASGI server." optional = false python-versions = ">=3.8" files = [ - {file = "uvicorn-0.23.1-py3-none-any.whl", hash = "sha256:1d55d46b83ee4ce82b4e82f621f2050adb3eb7b5481c13f9af1744951cae2f1f"}, - {file = "uvicorn-0.23.1.tar.gz", hash = "sha256:da9b0c8443b2d7ee9db00a345f1eee6db7317432c9d4400f5049cc8d358383be"}, + {file = "uvicorn-0.23.2-py3-none-any.whl", hash = "sha256:1f9be6558f01239d4fdf22ef8126c39cb1ad0addf76c40e760549d2c2f43ab53"}, + {file = "uvicorn-0.23.2.tar.gz", hash = "sha256:4d3cc12d7727ba72b64d12d3cc7743124074c0a69f7b201512fc50c3e3f1569a"}, ] [package.dependencies] @@ -706,5 +682,5 @@ files = [ [metadata] lock-version = "2.0" -python-versions = "^3.11" -content-hash = "17bf1e1330a25f59ffec4ff3aa70d8e9d9e93553a81f72c0823d76125f6ccd08" +python-versions = "^3.12" +content-hash = "8d0ddcdcd96f4736bb3608df11678d78776f5cf7c6883474b61b158c99ac4732" diff --git a/pyproject.toml b/pyproject.toml index 73eab62..92b2b9d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -6,7 +6,7 @@ authors = ["ducklet "] license = "LOL" [tool.poetry.dependencies] -python = "^3.11" +python = "^3.12" beautifulsoup4 = "^4.9.3" html5lib = "^1.1" starlette = "^0.30" @@ -32,9 +32,9 @@ requires = ["poetry-core>=1.0.0"] build-backend = "poetry.core.masonry.api" [tool.pyright] -pythonVersion = "3.11" +pythonVersion = "3.12" [tool.ruff] -target-version = "py311" +target-version = "py312" ignore-init-module-imports = true select = ["I", "F401", "F601", "F602", "F841"] diff --git a/unwind/models.py b/unwind/models.py index 13af462..609614a 100644 --- a/unwind/models.py +++ b/unwind/models.py @@ -207,7 +207,7 @@ def validate(o: object) -> None: def utcnow(): - return datetime.utcnow().replace(tzinfo=timezone.utc) + return datetime.now(timezone.utc) @mapper_registry.mapped From 22ea553f48ea9d398f0f6bf24eb46db289214c20 Mon Sep 17 00:00:00 2001 From: ducklet Date: Sun, 26 Nov 2023 18:28:17 +0100 Subject: [PATCH 27/31] improve typing --- unwind/db.py | 12 ++++++------ unwind/imdb.py | 27 ++++++++++++++++++++------- unwind/models.py | 6 +++--- 3 files changed, 29 insertions(+), 16 deletions(-) diff --git a/unwind/db.py b/unwind/db.py index 7a0169d..278c0c0 100644 --- a/unwind/db.py +++ b/unwind/db.py @@ -3,7 +3,7 @@ import contextlib import logging import threading from pathlib import Path -from typing import Any, Iterable, Literal, Type, TypeVar +from typing import Any, AsyncGenerator, Iterable, Literal, Type, TypeVar import sqlalchemy as sa from databases import Database @@ -472,12 +472,12 @@ async def find_ratings( .limit(limit_rows) ) async with locked_connection() as conn: - rows = conn.iterate(query) - movie_ids = [r.movie_id async for r in rows] + rating_rows: AsyncGenerator[Rating, None] = conn.iterate(query) # type: ignore + movie_ids = [r.movie_id async for r in rating_rows] if include_unrated and len(movie_ids) < limit_rows: query = ( - sa.select(movies.c.id.label("movie_id")) + sa.select(movies.c.id) .distinct() .where(movies.c.id.not_in(movie_ids), *conditions) .order_by( @@ -488,8 +488,8 @@ async def find_ratings( .limit(limit_rows - len(movie_ids)) ) async with locked_connection() as conn: - rows = conn.iterate(query) - movie_ids += [r.movie_id async for r in rows] + movie_rows: AsyncGenerator[Movie, None] = conn.iterate(query) # type: ignore + movie_ids += [r.id async for r in movie_rows] return await ratings_for_movie_ids(ids=movie_ids) diff --git a/unwind/imdb.py b/unwind/imdb.py index 477ec64..6858fc7 100644 --- a/unwind/imdb.py +++ b/unwind/imdb.py @@ -4,6 +4,8 @@ from collections import namedtuple from datetime import datetime from urllib.parse import urljoin +import bs4 + from . import db from .models import Movie, Rating, User from .request import asession, asoup_from_url, cache_path @@ -43,7 +45,7 @@ async def refresh_user_ratings_from_imdb(stop_on_dupe: bool = True): try: async for rating, is_updated in load_ratings(user.imdb_id): - assert rating.user.id == user.id + assert rating.user is not None and rating.user.id == user.id if stop_on_dupe and not is_updated: break @@ -154,13 +156,18 @@ async def parse_page(url: str) -> tuple[list[Rating], str | None]: soup = await asoup_from_url(url) - meta = soup.find("meta", property="pageId") - headline = soup.h1 - assert meta is not None and headline is not None + if (meta := soup.find("meta", property="pageId")) is None: + raise RuntimeError("No pageId found.") + assert isinstance(meta, bs4.Tag) imdb_id = meta["content"] + assert isinstance(imdb_id, str) user = await db.get(User, imdb_id=imdb_id) or User( imdb_id=imdb_id, name="", secret="" ) + + if (headline := soup.h1) is None: + raise RuntimeError("No headline found.") + assert isinstance(headline.string, str) if match := find_name(headline.string): user.name = match["name"] @@ -184,9 +191,15 @@ async def parse_page(url: str) -> tuple[list[Rating], str | None]: ratings.append(rating) - footer = soup.find("div", "footer") - assert footer is not None - next_url = urljoin(url, footer.find(string=re.compile(r"Next")).parent["href"]) + next_url = None + if (footer := soup.find("div", "footer")) is None: + raise RuntimeError("No footer found.") + assert isinstance(footer, bs4.Tag) + if (next_link := footer.find("a", string="Next")) is not None: + assert isinstance(next_link, bs4.Tag) + next_href = next_link["href"] + assert isinstance(next_href, str) + next_url = urljoin(url, next_href) return (ratings, next_url if url != next_url else None) diff --git a/unwind/models.py b/unwind/models.py index 609614a..ff961fc 100644 --- a/unwind/models.py +++ b/unwind/models.py @@ -40,7 +40,7 @@ metadata = mapper_registry.metadata def annotations(tp: Type) -> tuple | None: - return tp.__metadata__ if hasattr(tp, "__metadata__") else None + return tp.__metadata__ if hasattr(tp, "__metadata__") else None # type: ignore def fields(class_or_instance): @@ -125,7 +125,7 @@ def asplain( if filter_fields is not None and f.name not in filter_fields: continue - target = f.type + target: Any = f.type # XXX this doesn't properly support any kind of nested types if (otype := optional_type(f.type)) is not None: target = otype @@ -169,7 +169,7 @@ def fromplain(cls: Type[T], d: Mapping, *, serialized: bool = False) -> T: dd: JSONObject = {} for f in fields(cls): - target = f.type + target: Any = f.type otype = optional_type(f.type) is_opt = otype is not None if is_opt: From 1f425384817efe07f37510e3b0b78a11a262dfdb Mon Sep 17 00:00:00 2001 From: ducklet Date: Sun, 26 Nov 2023 18:41:32 +0100 Subject: [PATCH 28/31] make the shared connection internal to the db module This should make it easier to refactor the code for removing the databases package. --- tests/conftest.py | 2 +- unwind/db.py | 46 +++++++++++++++++++++++++--------------------- unwind/web.py | 2 +- 3 files changed, 27 insertions(+), 23 deletions(-) diff --git a/tests/conftest.py b/tests/conftest.py index e57d3e1..470bc4d 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -17,7 +17,7 @@ def event_loop(): @pytest_asyncio.fixture(scope="session") async def shared_conn(): - c = db.shared_connection() + c = db._shared_connection() await c.connect() await db.apply_db_patches(c) diff --git a/unwind/db.py b/unwind/db.py index 278c0c0..51e66d5 100644 --- a/unwind/db.py +++ b/unwind/db.py @@ -31,7 +31,7 @@ from .types import ULID log = logging.getLogger(__name__) T = TypeVar("T") -_shared_connection: Database | None = None +_database: Database | None = None async def open_connection_pool() -> None: @@ -39,7 +39,7 @@ async def open_connection_pool() -> None: This function needs to be called before any access to the database can happen. """ - db = shared_connection() + db = _shared_connection() await db.connect() await db.execute(sa.text("PRAGMA journal_mode=WAL")) @@ -53,7 +53,7 @@ async def close_connection_pool() -> None: This function should be called before the app shuts down to ensure all data has been flushed to the database. """ - db = shared_connection() + db = _shared_connection() # Run automatic ANALYZE prior to closing the db, # see https://sqlite.com/lang_analyze.html. @@ -205,23 +205,27 @@ async def single_threaded(): @contextlib.asynccontextmanager -async def locked_connection(): +async def _locked_connection(): async with single_threaded(): - yield shared_connection() + yield _shared_connection() -def shared_connection() -> Database: - global _shared_connection +def _shared_connection() -> Database: + global _database - if _shared_connection is None: + if _database is None: uri = f"sqlite:///{config.storage_path}" # uri = f"sqlite+aiosqlite:///{config.storage_path}" - _shared_connection = Database(uri) + _database = Database(uri) engine = sa.create_engine(uri, future=True) metadata.create_all(engine, tables=[db_patches]) - return _shared_connection + return _database + + +def transaction(): + return _shared_connection().transaction() async def add(item: Model) -> None: @@ -233,7 +237,7 @@ async def add(item: Model) -> None: table: sa.Table = item.__table__ values = asplain(item, serialize=True) stmt = table.insert().values(values) - async with locked_connection() as conn: + async with _locked_connection() as conn: await conn.execute(stmt) @@ -264,7 +268,7 @@ async def get( query = query.order_by( order_col.asc() if order_dir == "asc" else order_col.desc() ) - async with locked_connection() as conn: + async with _locked_connection() as conn: row = await conn.fetch_one(query) return fromplain(model, row._mapping, serialized=True) if row else None @@ -284,7 +288,7 @@ async def get_many( table: sa.Table = model.__table__ query = sa.select(model).where(*(table.c[k].in_(v) for k, v in field_sets.items())) - async with locked_connection() as conn: + async with _locked_connection() as conn: rows = await conn.fetch_all(query) return (fromplain(model, row._mapping, serialized=True) for row in rows) @@ -298,7 +302,7 @@ async def get_all(model: Type[ModelType], **field_values) -> Iterable[ModelType] query = sa.select(model).where( *(table.c[k] == v for k, v in field_values.items() if v is not None) ) - async with locked_connection() as conn: + async with _locked_connection() as conn: rows = await conn.fetch_all(query) return (fromplain(model, row._mapping, serialized=True) for row in rows) @@ -312,7 +316,7 @@ async def update(item: Model) -> None: table: sa.Table = item.__table__ values = asplain(item, serialize=True) stmt = table.update().where(table.c.id == values["id"]).values(values) - async with locked_connection() as conn: + async with _locked_connection() as conn: await conn.execute(stmt) @@ -320,7 +324,7 @@ async def remove(item: Model) -> None: table: sa.Table = item.__table__ values = asplain(item, filter_fields={"id"}, serialize=True) stmt = table.delete().where(table.c.id == values["id"]) - async with locked_connection() as conn: + async with _locked_connection() as conn: await conn.execute(stmt) @@ -471,7 +475,7 @@ async def find_ratings( ) .limit(limit_rows) ) - async with locked_connection() as conn: + async with _locked_connection() as conn: rating_rows: AsyncGenerator[Rating, None] = conn.iterate(query) # type: ignore movie_ids = [r.movie_id async for r in rating_rows] @@ -487,7 +491,7 @@ async def find_ratings( ) .limit(limit_rows - len(movie_ids)) ) - async with locked_connection() as conn: + async with _locked_connection() as conn: movie_rows: AsyncGenerator[Movie, None] = conn.iterate(query) # type: ignore movie_ids += [r.id async for r in movie_rows] @@ -523,7 +527,7 @@ async def ratings_for_movie_ids( .outerjoin_from(movies, ratings, movies.c.id == ratings.c.movie_id) .where(sa.or_(*conds)) ) - async with locked_connection() as conn: + async with _locked_connection() as conn: rows = await conn.fetch_all(query) return tuple(dict(r._mapping) for r in rows) @@ -538,7 +542,7 @@ async def ratings_for_movies( query = sa.select(ratings).where(*conditions) - async with locked_connection() as conn: + async with _locked_connection() as conn: rows = await conn.fetch_all(query) return (fromplain(Rating, row._mapping, serialized=True) for row in rows) @@ -602,7 +606,7 @@ async def find_movies( .offset(skip_rows) ) - async with locked_connection() as conn: + async with _locked_connection() as conn: rows = await conn.fetch_all(query) movies_ = [fromplain(Movie, row._mapping, serialized=True) for row in rows] diff --git a/unwind/web.py b/unwind/web.py index 3ebbcdc..bddd54d 100644 --- a/unwind/web.py +++ b/unwind/web.py @@ -445,7 +445,7 @@ async def remove_user(request): if not user: return not_found() - async with db.shared_connection().transaction(): + async with db.transaction(): # XXX remove user refs from groups and ratings await db.remove(user) From c63bee072f502be5c00c936a9816506ccb8ec56b Mon Sep 17 00:00:00 2001 From: ducklet Date: Sun, 26 Nov 2023 19:43:56 +0100 Subject: [PATCH 29/31] respect API_HOST & API_PORT env vars for dev Using Vite's proxy option allows us to avoid CORS issues when the host for Uvicorn doesn't match the host for Vite, e.g. localhost vs. 127.0.0.1. --- scripts/dev | 6 ++++++ scripts/dev-server | 7 ++++++- scripts/server | 3 ++- unwind-ui/vite.config.ts | 24 ++++++++++++++++++++---- 4 files changed, 34 insertions(+), 6 deletions(-) diff --git a/scripts/dev b/scripts/dev index dee1b94..0381aa4 100755 --- a/scripts/dev +++ b/scripts/dev @@ -2,6 +2,12 @@ cd "$RUN_DIR" +# Make Uvicorn defaults explicit. +: "${API_PORT:=8000}" +: "${API_HOST:=127.0.0.1}" +export API_PORT +export API_HOST + [ -z "${DEBUG:-}" ] || set -x exec honcho start diff --git a/scripts/dev-server b/scripts/dev-server index 7d3e2ef..c99ac4b 100755 --- a/scripts/dev-server +++ b/scripts/dev-server @@ -4,4 +4,9 @@ cd "$RUN_DIR" [ -z "${DEBUG:-}" ] || set -x -exec uvicorn unwind:create_app --factory --reload +exec uvicorn \ + --host "$API_HOST" \ + --port "$API_PORT" \ + --reload \ + --factory \ + unwind:create_app diff --git a/scripts/server b/scripts/server index 599cb7f..5f236ed 100755 --- a/scripts/server +++ b/scripts/server @@ -11,4 +11,5 @@ export UNWIND_PORT exec uvicorn \ --host 0.0.0.0 \ --port "$UNWIND_PORT" \ - --factory unwind:create_app + --factory \ + unwind:create_app diff --git a/unwind-ui/vite.config.ts b/unwind-ui/vite.config.ts index 84d4068..99d078b 100644 --- a/unwind-ui/vite.config.ts +++ b/unwind-ui/vite.config.ts @@ -1,13 +1,29 @@ import { defineConfig } from "vite" import vue from "@vitejs/plugin-vue" +// Vite defaults. +const vite_host = "localhost" +const vite_port = 3000 + +const base = process.env.BASE_URL || "/" +const proxied_api_url = `http://${vite_host}:${vite_port}/api/` +const real_api_url = `http://${process.env.API_HOST}:${process.env.API_PORT}/api/` + // https://vitejs.dev/config/ export default defineConfig({ - base: process.env.BASE_URL || "/", + base, define: { - "process.env.API_URL": JSON.stringify( - process.env.API_URL || "http://localhost:8000/api/", - ), + "process.env.API_URL": JSON.stringify(process.env.API_URL || proxied_api_url), + }, + server: { + host: vite_host, + port: vite_port, + proxy: { + [`${base}api`]: { + target: real_api_url, + prependPath: false, + }, + }, }, plugins: [vue()], }) From 4981de4a045194e450178a2741f87d543928e059 Mon Sep 17 00:00:00 2001 From: ducklet Date: Mon, 27 Nov 2023 23:24:35 +0100 Subject: [PATCH 30/31] remove databases, use SQLAlechemy 2.0 instead Among the many changes we switch to using SQLAlchemy's connection pool, which means we are no longer required to guard against multiple threads working on the database. All db funcs now receive a connection to use as their first argument, this allows the caller to control transaction & rollback behavior. --- poetry.lock | 129 ++++----- pyproject.toml | 3 +- scripts/tests | 2 +- tests/conftest.py | 17 +- tests/test_db.py | 627 +++++++++++++++++++++--------------------- tests/test_models.py | 11 + tests/test_web.py | 287 ++++++++++--------- unwind/db.py | 305 +++++++++++--------- unwind/imdb.py | 28 +- unwind/imdb_import.py | 21 +- unwind/models.py | 87 +++--- unwind/web.py | 124 +++++---- 12 files changed, 876 insertions(+), 765 deletions(-) create mode 100644 tests/test_models.py diff --git a/poetry.lock b/poetry.lock index 9dd4032..df69612 100644 --- a/poetry.lock +++ b/poetry.lock @@ -153,31 +153,6 @@ files = [ [package.extras] toml = ["tomli"] -[[package]] -name = "databases" -version = "0.7.0" -description = "Async database support for Python." -optional = false -python-versions = ">=3.7" -files = [ - {file = "databases-0.7.0-py3-none-any.whl", hash = "sha256:cf5da4b8a3e3cd038c459529725ebb64931cbbb7a091102664f20ef8f6cefd0d"}, - {file = "databases-0.7.0.tar.gz", hash = "sha256:ea2d419d3d2eb80595b7ceb8f282056f080af62efe2fb9bcd83562f93ec4b674"}, -] - -[package.dependencies] -aiosqlite = {version = "*", optional = true, markers = "extra == \"sqlite\""} -sqlalchemy = ">=1.4.42,<1.5" - -[package.extras] -aiomysql = ["aiomysql"] -aiopg = ["aiopg"] -aiosqlite = ["aiosqlite"] -asyncmy = ["asyncmy"] -asyncpg = ["asyncpg"] -mysql = ["aiomysql"] -postgresql = ["asyncpg"] -sqlite = ["aiosqlite"] - [[package]] name = "greenlet" version = "3.0.1" @@ -554,62 +529,90 @@ files = [ [[package]] name = "sqlalchemy" -version = "1.4.50" +version = "2.0.23" description = "Database Abstraction Library" optional = false -python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,>=2.7" +python-versions = ">=3.7" files = [ - {file = "SQLAlchemy-1.4.50-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d00665725063692c42badfd521d0c4392e83c6c826795d38eb88fb108e5660e5"}, - {file = "SQLAlchemy-1.4.50-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:85292ff52ddf85a39367057c3d7968a12ee1fb84565331a36a8fead346f08796"}, - {file = "SQLAlchemy-1.4.50-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:d0fed0f791d78e7767c2db28d34068649dfeea027b83ed18c45a423f741425cb"}, - {file = "SQLAlchemy-1.4.50-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:db4db3c08ffbb18582f856545f058a7a5e4ab6f17f75795ca90b3c38ee0a8ba4"}, - {file = "SQLAlchemy-1.4.50-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:14b0cacdc8a4759a1e1bd47dc3ee3f5db997129eb091330beda1da5a0e9e5bd7"}, - {file = "SQLAlchemy-1.4.50-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1fb9cb60e0f33040e4f4681e6658a7eb03b5cb4643284172f91410d8c493dace"}, - {file = "SQLAlchemy-1.4.50-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c4cb501d585aa74a0f86d0ea6263b9c5e1d1463f8f9071392477fd401bd3c7cc"}, - {file = "SQLAlchemy-1.4.50-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8a7a66297e46f85a04d68981917c75723e377d2e0599d15fbe7a56abed5e2d75"}, - {file = "SQLAlchemy-1.4.50-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c1db0221cb26d66294f4ca18c533e427211673ab86c1fbaca8d6d9ff78654293"}, - {file = "SQLAlchemy-1.4.50-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0b7dbe6369677a2bea68fe9812c6e4bbca06ebfa4b5cde257b2b0bf208709131"}, - {file = "SQLAlchemy-1.4.50-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:a9bddb60566dc45c57fd0a5e14dd2d9e5f106d2241e0a2dc0c1da144f9444516"}, - {file = "SQLAlchemy-1.4.50-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:82dd4131d88395df7c318eeeef367ec768c2a6fe5bd69423f7720c4edb79473c"}, - {file = "SQLAlchemy-1.4.50-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:273505fcad22e58cc67329cefab2e436006fc68e3c5423056ee0513e6523268a"}, - {file = "SQLAlchemy-1.4.50-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a3257a6e09626d32b28a0c5b4f1a97bced585e319cfa90b417f9ab0f6145c33c"}, - {file = "SQLAlchemy-1.4.50-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:d69738d582e3a24125f0c246ed8d712b03bd21e148268421e4a4d09c34f521a5"}, - {file = "SQLAlchemy-1.4.50-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:34e1c5d9cd3e6bf3d1ce56971c62a40c06bfc02861728f368dcfec8aeedb2814"}, - {file = "SQLAlchemy-1.4.50-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f1fcee5a2c859eecb4ed179edac5ffbc7c84ab09a5420219078ccc6edda45436"}, - {file = "SQLAlchemy-1.4.50-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fbaf6643a604aa17e7a7afd74f665f9db882df5c297bdd86c38368f2c471f37d"}, - {file = "SQLAlchemy-1.4.50-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:2e70e0673d7d12fa6cd363453a0d22dac0d9978500aa6b46aa96e22690a55eab"}, - {file = "SQLAlchemy-1.4.50-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8b881ac07d15fb3e4f68c5a67aa5cdaf9eb8f09eb5545aaf4b0a5f5f4659be18"}, - {file = "SQLAlchemy-1.4.50-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3f6997da81114daef9203d30aabfa6b218a577fc2bd797c795c9c88c9eb78d49"}, - {file = "SQLAlchemy-1.4.50-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bdb77e1789e7596b77fd48d99ec1d2108c3349abd20227eea0d48d3f8cf398d9"}, - {file = "SQLAlchemy-1.4.50-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:128a948bd40780667114b0297e2cc6d657b71effa942e0a368d8cc24293febb3"}, - {file = "SQLAlchemy-1.4.50-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f2d526aeea1bd6a442abc7c9b4b00386fd70253b80d54a0930c0a216230a35be"}, - {file = "SQLAlchemy-1.4.50.tar.gz", hash = "sha256:3b97ddf509fc21e10b09403b5219b06c5b558b27fc2453150274fa4e70707dbf"}, + {file = "SQLAlchemy-2.0.23-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:638c2c0b6b4661a4fd264f6fb804eccd392745c5887f9317feb64bb7cb03b3ea"}, + {file = "SQLAlchemy-2.0.23-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:e3b5036aa326dc2df50cba3c958e29b291a80f604b1afa4c8ce73e78e1c9f01d"}, + {file = "SQLAlchemy-2.0.23-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:787af80107fb691934a01889ca8f82a44adedbf5ef3d6ad7d0f0b9ac557e0c34"}, + {file = "SQLAlchemy-2.0.23-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c14eba45983d2f48f7546bb32b47937ee2cafae353646295f0e99f35b14286ab"}, + {file = "SQLAlchemy-2.0.23-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:0666031df46b9badba9bed00092a1ffa3aa063a5e68fa244acd9f08070e936d3"}, + {file = "SQLAlchemy-2.0.23-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:89a01238fcb9a8af118eaad3ffcc5dedaacbd429dc6fdc43fe430d3a941ff965"}, + {file = "SQLAlchemy-2.0.23-cp310-cp310-win32.whl", hash = "sha256:cabafc7837b6cec61c0e1e5c6d14ef250b675fa9c3060ed8a7e38653bd732ff8"}, + {file = "SQLAlchemy-2.0.23-cp310-cp310-win_amd64.whl", hash = "sha256:87a3d6b53c39cd173990de2f5f4b83431d534a74f0e2f88bd16eabb5667e65c6"}, + {file = "SQLAlchemy-2.0.23-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:d5578e6863eeb998980c212a39106ea139bdc0b3f73291b96e27c929c90cd8e1"}, + {file = "SQLAlchemy-2.0.23-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:62d9e964870ea5ade4bc870ac4004c456efe75fb50404c03c5fd61f8bc669a72"}, + {file = "SQLAlchemy-2.0.23-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c80c38bd2ea35b97cbf7c21aeb129dcbebbf344ee01a7141016ab7b851464f8e"}, + {file = "SQLAlchemy-2.0.23-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:75eefe09e98043cff2fb8af9796e20747ae870c903dc61d41b0c2e55128f958d"}, + {file = "SQLAlchemy-2.0.23-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:bd45a5b6c68357578263d74daab6ff9439517f87da63442d244f9f23df56138d"}, + {file = "SQLAlchemy-2.0.23-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:a86cb7063e2c9fb8e774f77fbf8475516d270a3e989da55fa05d08089d77f8c4"}, + {file = "SQLAlchemy-2.0.23-cp311-cp311-win32.whl", hash = "sha256:b41f5d65b54cdf4934ecede2f41b9c60c9f785620416e8e6c48349ab18643855"}, + {file = "SQLAlchemy-2.0.23-cp311-cp311-win_amd64.whl", hash = "sha256:9ca922f305d67605668e93991aaf2c12239c78207bca3b891cd51a4515c72e22"}, + {file = "SQLAlchemy-2.0.23-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:d0f7fb0c7527c41fa6fcae2be537ac137f636a41b4c5a4c58914541e2f436b45"}, + {file = "SQLAlchemy-2.0.23-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:7c424983ab447dab126c39d3ce3be5bee95700783204a72549c3dceffe0fc8f4"}, + {file = "SQLAlchemy-2.0.23-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f508ba8f89e0a5ecdfd3761f82dda2a3d7b678a626967608f4273e0dba8f07ac"}, + {file = "SQLAlchemy-2.0.23-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6463aa765cf02b9247e38b35853923edbf2f6fd1963df88706bc1d02410a5577"}, + {file = "SQLAlchemy-2.0.23-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:e599a51acf3cc4d31d1a0cf248d8f8d863b6386d2b6782c5074427ebb7803bda"}, + {file = "SQLAlchemy-2.0.23-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:fd54601ef9cc455a0c61e5245f690c8a3ad67ddb03d3b91c361d076def0b4c60"}, + {file = "SQLAlchemy-2.0.23-cp312-cp312-win32.whl", hash = "sha256:42d0b0290a8fb0165ea2c2781ae66e95cca6e27a2fbe1016ff8db3112ac1e846"}, + {file = "SQLAlchemy-2.0.23-cp312-cp312-win_amd64.whl", hash = "sha256:227135ef1e48165f37590b8bfc44ed7ff4c074bf04dc8d6f8e7f1c14a94aa6ca"}, + {file = "SQLAlchemy-2.0.23-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:14aebfe28b99f24f8a4c1346c48bc3d63705b1f919a24c27471136d2f219f02d"}, + {file = "SQLAlchemy-2.0.23-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3e983fa42164577d073778d06d2cc5d020322425a509a08119bdcee70ad856bf"}, + {file = "SQLAlchemy-2.0.23-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7e0dc9031baa46ad0dd5a269cb7a92a73284d1309228be1d5935dac8fb3cae24"}, + {file = "SQLAlchemy-2.0.23-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:5f94aeb99f43729960638e7468d4688f6efccb837a858b34574e01143cf11f89"}, + {file = "SQLAlchemy-2.0.23-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:63bfc3acc970776036f6d1d0e65faa7473be9f3135d37a463c5eba5efcdb24c8"}, + {file = "SQLAlchemy-2.0.23-cp37-cp37m-win32.whl", hash = "sha256:f48ed89dd11c3c586f45e9eec1e437b355b3b6f6884ea4a4c3111a3358fd0c18"}, + {file = "SQLAlchemy-2.0.23-cp37-cp37m-win_amd64.whl", hash = "sha256:1e018aba8363adb0599e745af245306cb8c46b9ad0a6fc0a86745b6ff7d940fc"}, + {file = "SQLAlchemy-2.0.23-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:64ac935a90bc479fee77f9463f298943b0e60005fe5de2aa654d9cdef46c54df"}, + {file = "SQLAlchemy-2.0.23-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:c4722f3bc3c1c2fcc3702dbe0016ba31148dd6efcd2a2fd33c1b4897c6a19693"}, + {file = "SQLAlchemy-2.0.23-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4af79c06825e2836de21439cb2a6ce22b2ca129bad74f359bddd173f39582bf5"}, + {file = "SQLAlchemy-2.0.23-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:683ef58ca8eea4747737a1c35c11372ffeb84578d3aab8f3e10b1d13d66f2bc4"}, + {file = "SQLAlchemy-2.0.23-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:d4041ad05b35f1f4da481f6b811b4af2f29e83af253bf37c3c4582b2c68934ab"}, + {file = "SQLAlchemy-2.0.23-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:aeb397de65a0a62f14c257f36a726945a7f7bb60253462e8602d9b97b5cbe204"}, + {file = "SQLAlchemy-2.0.23-cp38-cp38-win32.whl", hash = "sha256:42ede90148b73fe4ab4a089f3126b2cfae8cfefc955c8174d697bb46210c8306"}, + {file = "SQLAlchemy-2.0.23-cp38-cp38-win_amd64.whl", hash = "sha256:964971b52daab357d2c0875825e36584d58f536e920f2968df8d581054eada4b"}, + {file = "SQLAlchemy-2.0.23-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:616fe7bcff0a05098f64b4478b78ec2dfa03225c23734d83d6c169eb41a93e55"}, + {file = "SQLAlchemy-2.0.23-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:0e680527245895aba86afbd5bef6c316831c02aa988d1aad83c47ffe92655e74"}, + {file = "SQLAlchemy-2.0.23-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9585b646ffb048c0250acc7dad92536591ffe35dba624bb8fd9b471e25212a35"}, + {file = "SQLAlchemy-2.0.23-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4895a63e2c271ffc7a81ea424b94060f7b3b03b4ea0cd58ab5bb676ed02f4221"}, + {file = "SQLAlchemy-2.0.23-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:cc1d21576f958c42d9aec68eba5c1a7d715e5fc07825a629015fe8e3b0657fb0"}, + {file = "SQLAlchemy-2.0.23-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:967c0b71156f793e6662dd839da54f884631755275ed71f1539c95bbada9aaab"}, + {file = "SQLAlchemy-2.0.23-cp39-cp39-win32.whl", hash = "sha256:0a8c6aa506893e25a04233bc721c6b6cf844bafd7250535abb56cb6cc1368884"}, + {file = "SQLAlchemy-2.0.23-cp39-cp39-win_amd64.whl", hash = "sha256:f3420d00d2cb42432c1d0e44540ae83185ccbbc67a6054dcc8ab5387add6620b"}, + {file = "SQLAlchemy-2.0.23-py3-none-any.whl", hash = "sha256:31952bbc527d633b9479f5f81e8b9dfada00b91d6baba021a869095f1a97006d"}, + {file = "SQLAlchemy-2.0.23.tar.gz", hash = "sha256:c1bda93cbbe4aa2aa0aa8655c5aeda505cd219ff3e8da91d1d329e143e4aff69"}, ] [package.dependencies] -aiosqlite = {version = "*", optional = true, markers = "python_version >= \"3\" and extra == \"aiosqlite\""} -greenlet = {version = "!=0.4.17", optional = true, 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\" or extra == \"aiosqlite\")"} -typing-extensions = {version = "!=3.10.0.1", optional = true, markers = "extra == \"aiosqlite\""} +aiosqlite = {version = "*", optional = true, markers = "extra == \"aiosqlite\""} +greenlet = {version = "!=0.4.17", optional = true, markers = "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\" or extra == \"aiosqlite\""} +typing-extensions = {version = ">=4.2.0", optional = true, markers = "extra == \"aiosqlite\""} [package.extras] aiomysql = ["aiomysql (>=0.2.0)", "greenlet (!=0.4.17)"] +aioodbc = ["aioodbc", "greenlet (!=0.4.17)"] aiosqlite = ["aiosqlite", "greenlet (!=0.4.17)", "typing-extensions (!=3.10.0.1)"] asyncio = ["greenlet (!=0.4.17)"] -asyncmy = ["asyncmy (>=0.2.3,!=0.2.4)", "greenlet (!=0.4.17)"] -mariadb-connector = ["mariadb (>=1.0.1,!=1.1.2)"] +asyncmy = ["asyncmy (>=0.2.3,!=0.2.4,!=0.2.6)", "greenlet (!=0.4.17)"] +mariadb-connector = ["mariadb (>=1.0.1,!=1.1.2,!=1.1.5)"] mssql = ["pyodbc"] mssql-pymssql = ["pymssql"] mssql-pyodbc = ["pyodbc"] -mypy = ["mypy (>=0.910)", "sqlalchemy2-stubs"] -mysql = ["mysqlclient (>=1.4.0)", "mysqlclient (>=1.4.0,<2)"] +mypy = ["mypy (>=0.910)"] +mysql = ["mysqlclient (>=1.4.0)"] mysql-connector = ["mysql-connector-python"] -oracle = ["cx-oracle (>=7)", "cx-oracle (>=7,<8)"] +oracle = ["cx-oracle (>=8)"] +oracle-oracledb = ["oracledb (>=1.0.1)"] postgresql = ["psycopg2 (>=2.7)"] postgresql-asyncpg = ["asyncpg", "greenlet (!=0.4.17)"] -postgresql-pg8000 = ["pg8000 (>=1.16.6,!=1.29.0)"] +postgresql-pg8000 = ["pg8000 (>=1.29.1)"] +postgresql-psycopg = ["psycopg (>=3.0.7)"] postgresql-psycopg2binary = ["psycopg2-binary"] postgresql-psycopg2cffi = ["psycopg2cffi"] -pymysql = ["pymysql", "pymysql (<1)"] +postgresql-psycopgbinary = ["psycopg[binary] (>=3.0.7)"] +pymysql = ["pymysql"] sqlcipher = ["sqlcipher3-binary"] [[package]] @@ -683,4 +686,4 @@ files = [ [metadata] lock-version = "2.0" python-versions = "^3.12" -content-hash = "8d0ddcdcd96f4736bb3608df11678d78776f5cf7c6883474b61b158c99ac4732" +content-hash = "fc07028820963701634eb55b42ea12962fd7c6fc25ef76ddadf30f2c74544b5f" diff --git a/pyproject.toml b/pyproject.toml index 92b2b9d..f7c1b6b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -11,10 +11,9 @@ beautifulsoup4 = "^4.9.3" html5lib = "^1.1" starlette = "^0.30" ulid-py = "^1.1.0" -databases = {extras = ["sqlite"], version = "^0.7.0"} uvicorn = "^0.23" httpx = "^0.24" -sqlalchemy = {version = "^1.4", extras = ["aiosqlite"]} +sqlalchemy = {version = "^2.0", extras = ["aiosqlite"]} [tool.poetry.group.dev] optional = true diff --git a/scripts/tests b/scripts/tests index 8261f1e..5eefc57 100755 --- a/scripts/tests +++ b/scripts/tests @@ -6,7 +6,7 @@ dbfile="${UNWIND_DATA:-./data}/tests.sqlite" # Rollback in Databases is currently broken, so we have to rebuild the database # each time; see https://github.com/encode/databases/issues/403 -trap 'rm "$dbfile"' EXIT TERM INT QUIT +trap 'rm "$dbfile" "${dbfile}-shm" "${dbfile}-wal"' EXIT TERM INT QUIT [ -z "${DEBUG:-}" ] || set -x diff --git a/tests/conftest.py b/tests/conftest.py index 470bc4d..17ce01a 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -17,16 +17,19 @@ def event_loop(): @pytest_asyncio.fixture(scope="session") async def shared_conn(): - c = db._shared_connection() - await c.connect() + """A database connection, ready to use.""" + await db.open_connection_pool() - await db.apply_db_patches(c) - yield c + async with db.new_connection() as c: + db._test_connection = c + yield c + db._test_connection = None - await c.disconnect() + await db.close_connection_pool() @pytest_asyncio.fixture -async def conn(shared_conn): - async with shared_conn.transaction(force_rollback=True): +async def conn(shared_conn: db.Connection): + """A transacted database connection, will be rolled back after use.""" + async with db.transacted(shared_conn, force_rollback=True): yield shared_conn diff --git a/tests/test_db.py b/tests/test_db.py index cd5f295..3619497 100644 --- a/tests/test_db.py +++ b/tests/test_db.py @@ -4,7 +4,7 @@ import pytest from unwind import db, models, web_models -_movie_imdb_id = 1234567 +_movie_imdb_id = 1230000 def a_movie(**kwds) -> models.Movie: @@ -21,394 +21,399 @@ def a_movie(**kwds) -> models.Movie: @pytest.mark.asyncio -async def test_current_patch_level(shared_conn: db.Database): - async with shared_conn.transaction(force_rollback=True): - patch_level = "some-patch-level" - assert patch_level != await db.current_patch_level(shared_conn) - await db.set_current_patch_level(shared_conn, patch_level) - assert patch_level == await db.current_patch_level(shared_conn) +async def test_current_patch_level(conn: db.Connection): + patch_level = "some-patch-level" + assert patch_level != await db.current_patch_level(conn) + await db.set_current_patch_level(conn, patch_level) + assert patch_level == await db.current_patch_level(conn) @pytest.mark.asyncio -async def test_get(shared_conn: db.Database): - async with shared_conn.transaction(force_rollback=True): - m1 = a_movie() - await db.add(m1) +async def test_get(conn: db.Connection): + m1 = a_movie() + await db.add(conn, m1) - m2 = a_movie(release_year=m1.release_year + 1) - await db.add(m2) + m2 = a_movie(release_year=m1.release_year + 1) + await db.add(conn, m2) - assert None is await db.get(models.Movie) - assert None is await db.get(models.Movie, id="blerp") - assert m1 == await db.get(models.Movie, id=str(m1.id)) - assert m2 == await db.get(models.Movie, release_year=m2.release_year) - assert None is await db.get( - models.Movie, id=str(m1.id), release_year=m2.release_year - ) - assert m2 == await db.get( - models.Movie, id=str(m2.id), release_year=m2.release_year - ) - assert m1 == await db.get( - models.Movie, - media_type=m1.media_type, - order_by=(models.movies.c.release_year, "asc"), - ) - assert m2 == await db.get( - models.Movie, - media_type=m1.media_type, - order_by=(models.movies.c.release_year, "desc"), - ) + assert None is await db.get(conn, models.Movie) + assert None is await db.get(conn, models.Movie, id="blerp") + assert m1 == await db.get(conn, models.Movie, id=str(m1.id)) + assert m2 == await db.get(conn, models.Movie, release_year=m2.release_year) + assert None is await db.get( + conn, models.Movie, id=str(m1.id), release_year=m2.release_year + ) + assert m2 == await db.get( + conn, models.Movie, id=str(m2.id), release_year=m2.release_year + ) + assert m1 == await db.get( + conn, + models.Movie, + media_type=m1.media_type, + order_by=(models.movies.c.release_year, "asc"), + ) + assert m2 == await db.get( + conn, + models.Movie, + media_type=m1.media_type, + order_by=(models.movies.c.release_year, "desc"), + ) @pytest.mark.asyncio -async def test_get_all(shared_conn: db.Database): - async with shared_conn.transaction(force_rollback=True): - m1 = a_movie() - await db.add(m1) +async def test_get_all(conn: db.Connection): + m1 = a_movie() + await db.add(conn, m1) - m2 = a_movie(release_year=m1.release_year) - await db.add(m2) + m2 = a_movie(release_year=m1.release_year) + await db.add(conn, m2) - m3 = a_movie(release_year=m1.release_year + 1) - await db.add(m3) + m3 = a_movie(release_year=m1.release_year + 1) + await db.add(conn, m3) - assert [] == list(await db.get_all(models.Movie, id="blerp")) - assert [m1] == list(await db.get_all(models.Movie, id=str(m1.id))) - assert [m1, m2] == list( - await db.get_all(models.Movie, release_year=m1.release_year) - ) - assert [m1, m2, m3] == list(await db.get_all(models.Movie)) + assert [] == list(await db.get_all(conn, models.Movie, id="blerp")) + assert [m1] == list(await db.get_all(conn, models.Movie, id=str(m1.id))) + assert [m1, m2] == list( + await db.get_all(conn, models.Movie, release_year=m1.release_year) + ) + assert [m1, m2, m3] == list(await db.get_all(conn, models.Movie)) @pytest.mark.asyncio -async def test_get_many(shared_conn: db.Database): - async with shared_conn.transaction(force_rollback=True): - m1 = a_movie() - await db.add(m1) +async def test_get_many(conn: db.Connection): + m1 = a_movie() + await db.add(conn, m1) - m2 = a_movie(release_year=m1.release_year) - await db.add(m2) + m2 = a_movie(release_year=m1.release_year) + await db.add(conn, m2) - m3 = a_movie(release_year=m1.release_year + 1) - await db.add(m3) + m3 = a_movie(release_year=m1.release_year + 1) + await db.add(conn, m3) - assert [] == list(await db.get_many(models.Movie)), "selected nothing" - assert [m1] == list(await db.get_many(models.Movie, id=[str(m1.id)])) - assert [m1] == list(await db.get_many(models.Movie, id={str(m1.id)})) - assert [m1, m2] == list( - await db.get_many(models.Movie, release_year=[m1.release_year]) - ) - assert [m1, m2, m3] == list( - await db.get_many( - models.Movie, release_year=[m1.release_year, m3.release_year] - ) + assert [] == list(await db.get_many(conn, models.Movie)), "selected nothing" + assert [m1] == list(await db.get_many(conn, models.Movie, id=[str(m1.id)])) + assert [m1] == list(await db.get_many(conn, models.Movie, id={str(m1.id)})) + assert [m1, m2] == list( + await db.get_many(conn, models.Movie, release_year=[m1.release_year]) + ) + assert [m1, m2, m3] == list( + await db.get_many( + conn, models.Movie, release_year=[m1.release_year, m3.release_year] ) + ) @pytest.mark.asyncio -async def test_add_and_get(shared_conn: db.Database): - async with shared_conn.transaction(force_rollback=True): - m1 = a_movie() - await db.add(m1) +async def test_add_and_get(conn: db.Connection): + m1 = a_movie() + await db.add(conn, m1) - m2 = a_movie() - await db.add(m2) + m2 = a_movie() + await db.add(conn, m2) - assert m1 == await db.get(models.Movie, id=str(m1.id)) - assert m2 == await db.get(models.Movie, id=str(m2.id)) + assert m1 == await db.get(conn, models.Movie, id=str(m1.id)) + assert m2 == await db.get(conn, models.Movie, id=str(m2.id)) @pytest.mark.asyncio -async def test_update(shared_conn: db.Database): - async with shared_conn.transaction(force_rollback=True): - m = a_movie() - await db.add(m) +async def test_update(conn: db.Connection): + m = a_movie() + await db.add(conn, m) - assert m == await db.get(models.Movie, id=str(m.id)) - m.title += "something else" - assert m != await db.get(models.Movie, id=str(m.id)) + assert m == await db.get(conn, models.Movie, id=str(m.id)) + m.title += "something else" + assert m != await db.get(conn, models.Movie, id=str(m.id)) - await db.update(m) - assert m == await db.get(models.Movie, id=str(m.id)) + await db.update(conn, m) + assert m == await db.get(conn, models.Movie, id=str(m.id)) @pytest.mark.asyncio -async def test_remove(shared_conn: db.Database): - async with shared_conn.transaction(force_rollback=True): - m1 = a_movie() - await db.add(m1) - assert m1 == await db.get(models.Movie, id=str(m1.id)) +async def test_remove(conn: db.Connection): + m1 = a_movie() + await db.add(conn, m1) + assert m1 == await db.get(conn, models.Movie, id=str(m1.id)) - await db.remove(m1) - assert None is await db.get(models.Movie, id=str(m1.id)) + await db.remove(conn, m1) + assert None is await db.get(conn, models.Movie, id=str(m1.id)) @pytest.mark.asyncio -async def test_find_ratings(shared_conn: db.Database): - async with shared_conn.transaction(force_rollback=True): - m1 = a_movie( - title="test movie", - release_year=2013, - genres={"genre-1"}, - ) - await db.add(m1) +async def test_find_ratings(conn: db.Connection): + m1 = a_movie( + title="test movie", + release_year=2013, + genres={"genre-1"}, + ) + await db.add(conn, m1) - m2 = a_movie( - title="it's anöther Movie, Part 2", - release_year=2015, - genres={"genre-2"}, - ) - await db.add(m2) + m2 = a_movie( + title="it's anöther Movie, Part 2", + release_year=2015, + genres={"genre-2"}, + ) + await db.add(conn, m2) - m3 = a_movie( - title="movie it's, Part 3", - release_year=m2.release_year, - genres=m2.genres, - ) - await db.add(m3) + m3 = a_movie( + title="movie it's, Part 3", + release_year=m2.release_year, + genres=m2.genres, + ) + await db.add(conn, m3) - u1 = models.User( - imdb_id="u00001", - name="User1", - secret="secret1", - ) - await db.add(u1) + u1 = models.User( + imdb_id="u00001", + name="User1", + secret="secret1", + ) + await db.add(conn, u1) - u2 = models.User( - imdb_id="u00002", - name="User2", - secret="secret2", - ) - await db.add(u2) + u2 = models.User( + imdb_id="u00002", + name="User2", + secret="secret2", + ) + await db.add(conn, u2) - r1 = models.Rating( - movie_id=m2.id, - movie=m2, - user_id=u1.id, - user=u1, - score=66, - rating_date=datetime.now(), - ) - await db.add(r1) + r1 = models.Rating( + movie_id=m2.id, + movie=m2, + user_id=u1.id, + user=u1, + score=66, + rating_date=datetime.now(), + ) + await db.add(conn, r1) - r2 = models.Rating( - movie_id=m2.id, - movie=m2, - user_id=u2.id, - user=u2, - score=77, - rating_date=datetime.now(), - ) - await db.add(r2) + r2 = models.Rating( + movie_id=m2.id, + movie=m2, + user_id=u2.id, + user=u2, + score=77, + rating_date=datetime.now(), + ) + await db.add(conn, r2) - # --- + # --- - rows = await db.find_ratings( - title=m1.title, - media_type=m1.media_type, - exact=True, - ignore_tv_episodes=True, - include_unrated=True, - yearcomp=("=", m1.release_year), - limit_rows=3, - user_ids=[], - ) - ratings = (web_models.Rating(**r) for r in rows) - assert (web_models.RatingAggregate.from_movie(m1),) == tuple( - web_models.aggregate_ratings(ratings, user_ids=[]) - ) + rows = await db.find_ratings( + conn, + title=m1.title, + media_type=m1.media_type, + exact=True, + ignore_tv_episodes=True, + include_unrated=True, + yearcomp=("=", m1.release_year), + limit_rows=3, + user_ids=[], + ) + ratings = (web_models.Rating(**r) for r in rows) + assert (web_models.RatingAggregate.from_movie(m1),) == tuple( + web_models.aggregate_ratings(ratings, user_ids=[]) + ) - rows = await db.find_ratings(title="movie", include_unrated=False) - ratings = tuple(web_models.Rating(**r) for r in rows) - assert ( - web_models.Rating.from_movie(m2, rating=r1), - web_models.Rating.from_movie(m2, rating=r2), - ) == ratings + rows = await db.find_ratings(conn, title="movie", include_unrated=False) + ratings = tuple(web_models.Rating(**r) for r in rows) + assert ( + web_models.Rating.from_movie(m2, rating=r1), + web_models.Rating.from_movie(m2, rating=r2), + ) == ratings - rows = await db.find_ratings(title="movie", include_unrated=True) - ratings = tuple(web_models.Rating(**r) for r in rows) - assert ( - web_models.Rating.from_movie(m1), - web_models.Rating.from_movie(m2, rating=r1), - web_models.Rating.from_movie(m2, rating=r2), - web_models.Rating.from_movie(m3), - ) == ratings + rows = await db.find_ratings(conn, title="movie", include_unrated=True) + ratings = tuple(web_models.Rating(**r) for r in rows) + assert ( + web_models.Rating.from_movie(m1), + web_models.Rating.from_movie(m2, rating=r1), + web_models.Rating.from_movie(m2, rating=r2), + web_models.Rating.from_movie(m3), + ) == ratings - aggr = web_models.aggregate_ratings(ratings, user_ids=[]) - assert tuple( - web_models.RatingAggregate.from_movie(m) for m in [m1, m2, m3] - ) == tuple(aggr) + aggr = web_models.aggregate_ratings(ratings, user_ids=[]) + assert tuple( + web_models.RatingAggregate.from_movie(m) for m in [m1, m2, m3] + ) == tuple(aggr) - aggr = web_models.aggregate_ratings(ratings, user_ids=[str(u1.id)]) - assert ( - web_models.RatingAggregate.from_movie(m1), - web_models.RatingAggregate.from_movie(m2, ratings=[r1]), - web_models.RatingAggregate.from_movie(m3), - ) == tuple(aggr) + aggr = web_models.aggregate_ratings(ratings, user_ids=[str(u1.id)]) + assert ( + web_models.RatingAggregate.from_movie(m1), + web_models.RatingAggregate.from_movie(m2, ratings=[r1]), + web_models.RatingAggregate.from_movie(m3), + ) == tuple(aggr) - aggr = web_models.aggregate_ratings(ratings, user_ids=[str(u1.id), str(u2.id)]) - assert ( - web_models.RatingAggregate.from_movie(m1), - web_models.RatingAggregate.from_movie(m2, ratings=[r1, r2]), - web_models.RatingAggregate.from_movie(m3), - ) == tuple(aggr) + aggr = web_models.aggregate_ratings(ratings, user_ids=[str(u1.id), str(u2.id)]) + assert ( + web_models.RatingAggregate.from_movie(m1), + web_models.RatingAggregate.from_movie(m2, ratings=[r1, r2]), + web_models.RatingAggregate.from_movie(m3), + ) == tuple(aggr) - rows = await db.find_ratings(title="movie", include_unrated=True) - ratings = (web_models.Rating(**r) for r in rows) - aggr = web_models.aggregate_ratings(ratings, user_ids=[]) - assert tuple( - web_models.RatingAggregate.from_movie(m) for m in [m1, m2, m3] - ) == tuple(aggr) + rows = await db.find_ratings(conn, title="movie", include_unrated=True) + ratings = (web_models.Rating(**r) for r in rows) + aggr = web_models.aggregate_ratings(ratings, user_ids=[]) + assert tuple( + web_models.RatingAggregate.from_movie(m) for m in [m1, m2, m3] + ) == tuple(aggr) - rows = await db.find_ratings(title="test", include_unrated=True) - ratings = tuple(web_models.Rating(**r) for r in rows) - assert (web_models.Rating.from_movie(m1),) == ratings + rows = await db.find_ratings(conn, title="test", include_unrated=True) + ratings = tuple(web_models.Rating(**r) for r in rows) + assert (web_models.Rating.from_movie(m1),) == ratings @pytest.mark.asyncio -async def test_ratings_for_movies(shared_conn: db.Database): - async with shared_conn.transaction(force_rollback=True): - m1 = a_movie() - await db.add(m1) +async def test_ratings_for_movies(conn: db.Connection): + m1 = a_movie() + await db.add(conn, m1) - m2 = a_movie() - await db.add(m2) + m2 = a_movie() + await db.add(conn, m2) - u1 = models.User( - imdb_id="u00001", - name="User1", - secret="secret1", - ) - await db.add(u1) + u1 = models.User( + imdb_id="u00001", + name="User1", + secret="secret1", + ) + await db.add(conn, u1) - u2 = models.User( - imdb_id="u00002", - name="User2", - secret="secret2", - ) - await db.add(u2) + u2 = models.User( + imdb_id="u00002", + name="User2", + secret="secret2", + ) + await db.add(conn, u2) - r1 = models.Rating( - movie_id=m2.id, - movie=m2, - user_id=u1.id, - user=u1, - score=66, - rating_date=datetime.now(), - ) - await db.add(r1) + r1 = models.Rating( + movie_id=m2.id, + movie=m2, + user_id=u1.id, + user=u1, + score=66, + rating_date=datetime.now(), + ) + await db.add(conn, r1) - # --- + # --- - movie_ids = [m1.id] - user_ids = [] - assert tuple() == tuple( - await db.ratings_for_movies(movie_ids=movie_ids, user_ids=user_ids) - ) + movie_ids = [m1.id] + user_ids = [] + assert tuple() == tuple( + await db.ratings_for_movies(conn, movie_ids=movie_ids, user_ids=user_ids) + ) - movie_ids = [m2.id] - user_ids = [] - assert (r1,) == tuple( - await db.ratings_for_movies(movie_ids=movie_ids, user_ids=user_ids) - ) + movie_ids = [m2.id] + user_ids = [] + assert (r1,) == tuple( + await db.ratings_for_movies(conn, movie_ids=movie_ids, user_ids=user_ids) + ) - movie_ids = [m2.id] - user_ids = [u2.id] - assert tuple() == tuple( - await db.ratings_for_movies(movie_ids=movie_ids, user_ids=user_ids) - ) + movie_ids = [m2.id] + user_ids = [u2.id] + assert tuple() == tuple( + await db.ratings_for_movies(conn, movie_ids=movie_ids, user_ids=user_ids) + ) - movie_ids = [m2.id] - user_ids = [u1.id] - assert (r1,) == tuple( - await db.ratings_for_movies(movie_ids=movie_ids, user_ids=user_ids) - ) + movie_ids = [m2.id] + user_ids = [u1.id] + assert (r1,) == tuple( + await db.ratings_for_movies(conn, movie_ids=movie_ids, user_ids=user_ids) + ) - movie_ids = [m1.id, m2.id] - user_ids = [u1.id, u2.id] - assert (r1,) == tuple( - await db.ratings_for_movies(movie_ids=movie_ids, user_ids=user_ids) - ) + movie_ids = [m1.id, m2.id] + user_ids = [u1.id, u2.id] + assert (r1,) == tuple( + await db.ratings_for_movies(conn, movie_ids=movie_ids, user_ids=user_ids) + ) @pytest.mark.asyncio -async def test_find_movies(shared_conn: db.Database): - async with shared_conn.transaction(force_rollback=True): - m1 = a_movie(title="movie one") - await db.add(m1) +async def test_find_movies(conn: db.Connection): + m1 = a_movie(title="movie one") + await db.add(conn, m1) - m2 = a_movie(title="movie two", imdb_score=33, release_year=m1.release_year + 1) - await db.add(m2) + m2 = a_movie(title="movie two", imdb_score=33, release_year=m1.release_year + 1) + await db.add(conn, m2) - u1 = models.User( - imdb_id="u00001", - name="User1", - secret="secret1", - ) - await db.add(u1) + u1 = models.User( + imdb_id="u00001", + name="User1", + secret="secret1", + ) + await db.add(conn, u1) - u2 = models.User( - imdb_id="u00002", - name="User2", - secret="secret2", - ) - await db.add(u2) + u2 = models.User( + imdb_id="u00002", + name="User2", + secret="secret2", + ) + await db.add(conn, u2) - r1 = models.Rating( - movie_id=m2.id, - movie=m2, - user_id=u1.id, - user=u1, - score=66, - rating_date=datetime.now(), - ) - await db.add(r1) + r1 = models.Rating( + movie_id=m2.id, + movie=m2, + user_id=u1.id, + user=u1, + score=66, + rating_date=datetime.now(), + ) + await db.add(conn, r1) - # --- + # --- - assert () == tuple(await db.find_movies(title=m1.title, include_unrated=False)) - assert ((m1, []),) == tuple( - await db.find_movies(title=m1.title, include_unrated=True) - ) + assert () == tuple( + await db.find_movies(conn, title=m1.title, include_unrated=False) + ) + assert ((m1, []),) == tuple( + await db.find_movies(conn, title=m1.title, include_unrated=True) + ) - assert ((m1, []),) == tuple( - await db.find_movies(title="mo on", exact=False, include_unrated=True) - ) - assert ((m1, []),) == tuple( - await db.find_movies(title="movie one", exact=True, include_unrated=True) - ) - assert () == tuple( - await db.find_movies(title="mo on", exact=True, include_unrated=True) - ) + assert ((m1, []),) == tuple( + await db.find_movies(conn, title="mo on", exact=False, include_unrated=True) + ) + assert ((m1, []),) == tuple( + await db.find_movies(conn, title="movie one", exact=True, include_unrated=True) + ) + assert () == tuple( + await db.find_movies(conn, title="mo on", exact=True, include_unrated=True) + ) - assert ((m2, []),) == tuple( - await db.find_movies(title="movie", exact=False, include_unrated=False) - ) - assert ((m2, []), (m1, [])) == tuple( - await db.find_movies(title="movie", exact=False, include_unrated=True) - ) + assert ((m2, []),) == tuple( + await db.find_movies(conn, title="movie", exact=False, include_unrated=False) + ) + assert ((m2, []), (m1, [])) == tuple( + await db.find_movies(conn, title="movie", exact=False, include_unrated=True) + ) - assert ((m1, []),) == tuple( - await db.find_movies(include_unrated=True, yearcomp=("=", m1.release_year)) + assert ((m1, []),) == tuple( + await db.find_movies( + conn, include_unrated=True, yearcomp=("=", m1.release_year) ) - assert ((m2, []),) == tuple( - await db.find_movies(include_unrated=True, yearcomp=("=", m2.release_year)) + ) + assert ((m2, []),) == tuple( + await db.find_movies( + conn, include_unrated=True, yearcomp=("=", m2.release_year) ) - assert ((m1, []),) == tuple( - await db.find_movies(include_unrated=True, yearcomp=("<", m2.release_year)) + ) + assert ((m1, []),) == tuple( + await db.find_movies( + conn, include_unrated=True, yearcomp=("<", m2.release_year) ) - assert ((m2, []),) == tuple( - await db.find_movies(include_unrated=True, yearcomp=(">", m1.release_year)) + ) + assert ((m2, []),) == tuple( + await db.find_movies( + conn, include_unrated=True, yearcomp=(">", m1.release_year) ) + ) - assert ((m2, []), (m1, [])) == tuple(await db.find_movies(include_unrated=True)) - assert ((m2, []),) == tuple( - await db.find_movies(include_unrated=True, limit_rows=1) - ) - assert ((m1, []),) == tuple( - await db.find_movies(include_unrated=True, skip_rows=1) - ) + assert ((m2, []), (m1, [])) == tuple( + await db.find_movies(conn, include_unrated=True) + ) + assert ((m2, []),) == tuple( + await db.find_movies(conn, include_unrated=True, limit_rows=1) + ) + assert ((m1, []),) == tuple( + await db.find_movies(conn, include_unrated=True, skip_rows=1) + ) - assert ((m2, [r1]), (m1, [])) == tuple( - await db.find_movies(include_unrated=True, user_ids=[u1.id, u2.id]) - ) + assert ((m2, [r1]), (m1, [])) == tuple( + await db.find_movies(conn, include_unrated=True, user_ids=[u1.id, u2.id]) + ) diff --git a/tests/test_models.py b/tests/test_models.py new file mode 100644 index 0000000..f4bd7b6 --- /dev/null +++ b/tests/test_models.py @@ -0,0 +1,11 @@ +import pytest + +from unwind import models + + +@pytest.mark.parametrize("mapper", models.mapper_registry.mappers) +def test_fields(mapper): + """Test that models.fields() matches exactly all table columns.""" + dcfields = {f.name for f in models.fields(mapper.class_)} + mfields = {c.name for c in mapper.columns} + assert dcfields == mfields diff --git a/tests/test_web.py b/tests/test_web.py index 5a4c3c5..0444406 100644 --- a/tests/test_web.py +++ b/tests/test_web.py @@ -34,7 +34,7 @@ def admin_client() -> TestClient: @pytest.mark.asyncio async def test_get_ratings_for_group( - shared_conn: db.Database, unauthorized_client: TestClient + conn: db.Connection, unauthorized_client: TestClient ): user = models.User( imdb_id="ur12345678", @@ -48,201 +48,196 @@ async def test_get_ratings_for_group( ) user.groups = [models.UserGroup(id=str(group.id), access="r")] path = app.url_path_for("get_ratings_for_group", group_id=str(group.id)) - async with shared_conn.transaction(force_rollback=True): - resp = unauthorized_client.get(path) - assert resp.status_code == 404, "Group does not exist (yet)" - await db.add(user) - await db.add(group) + resp = unauthorized_client.get(path) + assert resp.status_code == 404, "Group does not exist (yet)" - resp = unauthorized_client.get(path) - assert resp.status_code == 200 - assert resp.json() == [] + await db.add(conn, user) + await db.add(conn, group) - movie = models.Movie( - title="test movie", - release_year=2013, - media_type="Movie", - imdb_id="tt12345678", - genres={"genre-1"}, - ) - await db.add(movie) + resp = unauthorized_client.get(path) + assert resp.status_code == 200 + assert resp.json() == [] - rating = models.Rating( - movie_id=movie.id, user_id=user.id, score=66, rating_date=datetime.now() - ) - await db.add(rating) + movie = models.Movie( + title="test movie", + release_year=2013, + media_type="Movie", + imdb_id="tt12345678", + genres={"genre-1"}, + ) + await db.add(conn, movie) - rating_aggregate = { - "canonical_title": movie.title, - "imdb_score": movie.imdb_score, - "imdb_votes": movie.imdb_votes, - "link": imdb.movie_url(movie.imdb_id), - "media_type": movie.media_type, - "original_title": movie.original_title, - "user_scores": [rating.score], - "year": movie.release_year, - } + rating = models.Rating( + movie_id=movie.id, user_id=user.id, score=66, rating_date=datetime.now() + ) + await db.add(conn, rating) - resp = unauthorized_client.get(path) + rating_aggregate = { + "canonical_title": movie.title, + "imdb_score": movie.imdb_score, + "imdb_votes": movie.imdb_votes, + "link": imdb.movie_url(movie.imdb_id), + "media_type": movie.media_type, + "original_title": movie.original_title, + "user_scores": [rating.score], + "year": movie.release_year, + } + + resp = unauthorized_client.get(path) + assert resp.status_code == 200 + assert resp.json() == [rating_aggregate] + + filters = { + "imdb_id": movie.imdb_id, + "unwind_id": str(movie.id), + "title": movie.title, + "media_type": movie.media_type, + "year": movie.release_year, + } + for k, v in filters.items(): + resp = unauthorized_client.get(path, params={k: v}) assert resp.status_code == 200 assert resp.json() == [rating_aggregate] - filters = { - "imdb_id": movie.imdb_id, - "unwind_id": str(movie.id), - "title": movie.title, - "media_type": movie.media_type, - "year": movie.release_year, - } - for k, v in filters.items(): - resp = unauthorized_client.get(path, params={k: v}) - assert resp.status_code == 200 - assert resp.json() == [rating_aggregate] + resp = unauthorized_client.get(path, params={"title": "no such thing"}) + assert resp.status_code == 200 + assert resp.json() == [] - resp = unauthorized_client.get(path, params={"title": "no such thing"}) - assert resp.status_code == 200 - assert resp.json() == [] + # Test "exact" query param. + resp = unauthorized_client.get( + path, params={"title": "test movie", "exact": "true"} + ) + assert resp.status_code == 200 + assert resp.json() == [rating_aggregate] + resp = unauthorized_client.get(path, params={"title": "te mo", "exact": "false"}) + assert resp.status_code == 200 + assert resp.json() == [rating_aggregate] + resp = unauthorized_client.get(path, params={"title": "te mo", "exact": "true"}) + assert resp.status_code == 200 + assert resp.json() == [] - # Test "exact" query param. - resp = unauthorized_client.get( - path, params={"title": "test movie", "exact": "true"} - ) - assert resp.status_code == 200 - assert resp.json() == [rating_aggregate] - resp = unauthorized_client.get( - path, params={"title": "te mo", "exact": "false"} - ) - assert resp.status_code == 200 - assert resp.json() == [rating_aggregate] - resp = unauthorized_client.get(path, params={"title": "te mo", "exact": "true"}) - assert resp.status_code == 200 - assert resp.json() == [] - - # XXX Test "ignore_tv_episodes" query param. - # XXX Test "include_unrated" query param. - # XXX Test "per_page" query param. + # XXX Test "ignore_tv_episodes" query param. + # XXX Test "include_unrated" query param. + # XXX Test "per_page" query param. @pytest.mark.asyncio async def test_list_movies( - shared_conn: db.Database, + conn: db.Connection, unauthorized_client: TestClient, authorized_client: TestClient, ): path = app.url_path_for("list_movies") - async with shared_conn.transaction(force_rollback=True): - response = unauthorized_client.get(path) - assert response.status_code == 403 + response = unauthorized_client.get(path) + assert response.status_code == 403 - response = authorized_client.get(path) - assert response.status_code == 200 - assert response.json() == [] + response = authorized_client.get(path) + assert response.status_code == 200 + assert response.json() == [] - m = models.Movie( - title="test movie", - release_year=2013, - media_type="Movie", - imdb_id="tt12345678", - genres={"genre-1"}, - ) - await db.add(m) + m = models.Movie( + title="test movie", + release_year=2013, + media_type="Movie", + imdb_id="tt12345678", + genres={"genre-1"}, + ) + await db.add(conn, m) - response = authorized_client.get(path, params={"include_unrated": 1}) - assert response.status_code == 200 - assert response.json() == [{**models.asplain(m), "user_scores": []}] + response = authorized_client.get(path, params={"include_unrated": 1}) + assert response.status_code == 200 + assert response.json() == [{**models.asplain(m), "user_scores": []}] - m_plain = { - "canonical_title": m.title, - "imdb_score": m.imdb_score, - "imdb_votes": m.imdb_votes, - "link": imdb.movie_url(m.imdb_id), - "media_type": m.media_type, - "original_title": m.original_title, - "user_scores": [], - "year": m.release_year, - } + m_plain = { + "canonical_title": m.title, + "imdb_score": m.imdb_score, + "imdb_votes": m.imdb_votes, + "link": imdb.movie_url(m.imdb_id), + "media_type": m.media_type, + "original_title": m.original_title, + "user_scores": [], + "year": m.release_year, + } - response = authorized_client.get(path, params={"imdb_id": m.imdb_id}) - assert response.status_code == 200 - assert response.json() == [m_plain] + response = authorized_client.get(path, params={"imdb_id": m.imdb_id}) + assert response.status_code == 200 + assert response.json() == [m_plain] - response = authorized_client.get(path, params={"unwind_id": str(m.id)}) - assert response.status_code == 200 - assert response.json() == [m_plain] + response = authorized_client.get(path, params={"unwind_id": str(m.id)}) + assert response.status_code == 200 + assert response.json() == [m_plain] @pytest.mark.asyncio async def test_list_users( - shared_conn: db.Database, + conn: db.Connection, unauthorized_client: TestClient, authorized_client: TestClient, admin_client: TestClient, ): path = app.url_path_for("list_users") - async with shared_conn.transaction(force_rollback=True): - response = unauthorized_client.get(path) - assert response.status_code == 403 + response = unauthorized_client.get(path) + assert response.status_code == 403 - response = authorized_client.get(path) - assert response.status_code == 403 + response = authorized_client.get(path) + assert response.status_code == 403 - response = admin_client.get(path) - assert response.status_code == 200 - assert response.json() == [] + response = admin_client.get(path) + assert response.status_code == 200 + assert response.json() == [] - m = models.User( - imdb_id="ur12345678", - name="user-1", - secret="secret-1", - groups=[], - ) - await db.add(m) + m = models.User( + imdb_id="ur12345678", + name="user-1", + secret="secret-1", + groups=[], + ) + await db.add(conn, m) - m_plain = { - "groups": m.groups, - "id": m.id, - "imdb_id": m.imdb_id, - "name": m.name, - "secret": m.secret, - } + m_plain = { + "groups": m.groups, + "id": m.id, + "imdb_id": m.imdb_id, + "name": m.name, + "secret": m.secret, + } - response = admin_client.get(path) - assert response.status_code == 200 - assert response.json() == [m_plain] + response = admin_client.get(path) + assert response.status_code == 200 + assert response.json() == [m_plain] @pytest.mark.asyncio async def test_list_groups( - shared_conn: db.Database, + conn: db.Connection, unauthorized_client: TestClient, authorized_client: TestClient, admin_client: TestClient, ): path = app.url_path_for("list_groups") - async with shared_conn.transaction(force_rollback=True): - response = unauthorized_client.get(path) - assert response.status_code == 403 + response = unauthorized_client.get(path) + assert response.status_code == 403 - response = authorized_client.get(path) - assert response.status_code == 403 + response = authorized_client.get(path) + assert response.status_code == 403 - response = admin_client.get(path) - assert response.status_code == 200 - assert response.json() == [] + response = admin_client.get(path) + assert response.status_code == 200 + assert response.json() == [] - m = models.Group( - name="group-1", - users=[models.GroupUser(id="123", name="itsa-me")], - ) - await db.add(m) + m = models.Group( + name="group-1", + users=[models.GroupUser(id="123", name="itsa-me")], + ) + await db.add(conn, m) - m_plain = { - "users": m.users, - "id": m.id, - "name": m.name, - } + m_plain = { + "users": m.users, + "id": m.id, + "name": m.name, + } - response = admin_client.get(path) - assert response.status_code == 200 - assert response.json() == [m_plain] + response = admin_client.get(path) + assert response.status_code == 200 + assert response.json() == [m_plain] diff --git a/unwind/db.py b/unwind/db.py index 51e66d5..3ebca66 100644 --- a/unwind/db.py +++ b/unwind/db.py @@ -1,13 +1,11 @@ -import asyncio import contextlib import logging -import threading from pathlib import Path -from typing import Any, AsyncGenerator, Iterable, Literal, Type, TypeVar +from typing import Any, AsyncGenerator, Iterable, Literal, Sequence, Type, TypeVar import sqlalchemy as sa -from databases import Database from sqlalchemy.dialects.sqlite import insert +from sqlalchemy.ext.asyncio import AsyncConnection, AsyncEngine, create_async_engine from . import config from .models import ( @@ -31,7 +29,9 @@ from .types import ULID log = logging.getLogger(__name__) T = TypeVar("T") -_database: Database | None = None +_engine: AsyncEngine | None = None + +type Connection = AsyncConnection async def open_connection_pool() -> None: @@ -39,12 +39,13 @@ async def open_connection_pool() -> None: This function needs to be called before any access to the database can happen. """ - db = _shared_connection() - await db.connect() + async with transaction() as conn: + await conn.execute(sa.text("PRAGMA journal_mode=WAL")) - await db.execute(sa.text("PRAGMA journal_mode=WAL")) + await conn.run_sync(metadata.create_all, tables=[db_patches]) - await apply_db_patches(db) + async with new_connection() as conn: + await apply_db_patches(conn) async def close_connection_pool() -> None: @@ -53,32 +54,33 @@ async def close_connection_pool() -> None: This function should be called before the app shuts down to ensure all data has been flushed to the database. """ - db = _shared_connection() + engine = _shared_engine() - # Run automatic ANALYZE prior to closing the db, - # see https://sqlite.com/lang_analyze.html. - await db.execute(sa.text("PRAGMA analysis_limit=400")) - await db.execute(sa.text("PRAGMA optimize")) + async with engine.begin() as conn: + # Run automatic ANALYZE prior to closing the db, + # see https://sqlite.com/lang_analyze.html. + await conn.execute(sa.text("PRAGMA analysis_limit=400")) + await conn.execute(sa.text("PRAGMA optimize")) - await db.disconnect() + await engine.dispose() -async def current_patch_level(db: Database) -> str: +async def current_patch_level(conn: Connection, /) -> str: query = sa.select(db_patches.c.current) - current = await db.fetch_val(query) + current = await conn.scalar(query) return current or "" -async def set_current_patch_level(db: Database, current: str) -> None: +async def set_current_patch_level(conn: Connection, /, current: str) -> None: stmt = insert(db_patches).values(id=1, current=current) stmt = stmt.on_conflict_do_update(set_={"current": stmt.excluded.current}) - await db.execute(stmt) + await conn.execute(stmt) db_patches_dir = Path(__file__).parent / "sql" -async def apply_db_patches(db: Database) -> None: +async def apply_db_patches(conn: Connection, /) -> None: """Apply all remaining patches to the database. Beware that patches will be applied in lexicographical order, @@ -90,7 +92,7 @@ async def apply_db_patches(db: Database) -> None: using two consecutive semi-colons (;). Failing to do so will result in an error. """ - applied_lvl = await current_patch_level(db) + applied_lvl = await current_patch_level(conn) did_patch = False @@ -109,31 +111,52 @@ async def apply_db_patches(db: Database) -> None: ) raise RuntimeError("No statement found.") - async with db.transaction(): + async with transacted(conn): for query in queries: - await db.execute(sa.text(query)) + await conn.execute(sa.text(query)) - await set_current_patch_level(db, patch_lvl) + await set_current_patch_level(conn, patch_lvl) did_patch = True if did_patch: - await db.execute(sa.text("vacuum")) + await _vacuum(conn) -async def get_import_progress() -> Progress | None: +async def _vacuum(conn: Connection, /) -> None: + """Vacuum the database. + + This function cannot be run on a connection with an open transaction. + """ + # With SQLAlchemy's "autobegin" behavior we need to switch the connection + # to "autocommit" first to keep it from automatically starting a transaction, + # as VACUUM cannot be run inside a transaction for most databases. + await conn.commit() + isolation_level = await conn.get_isolation_level() + log.debug("Previous isolation_level: %a", isolation_level) + await conn.execution_options(isolation_level="AUTOCOMMIT") + try: + await conn.execute(sa.text("vacuum")) + await conn.commit() + finally: + await conn.execution_options(isolation_level=isolation_level) + + +async def get_import_progress(conn: Connection, /) -> Progress | None: """Return the latest import progress.""" return await get( - Progress, type="import-imdb-movies", order_by=(progress.c.started, "desc") + conn, Progress, type="import-imdb-movies", order_by=(progress.c.started, "desc") ) -async def stop_import_progress(*, error: BaseException | None = None) -> None: +async def stop_import_progress( + conn: Connection, /, *, error: BaseException | None = None +) -> None: """Stop the current import. If an error is given, it will be logged to the progress state. """ - current = await get_import_progress() + current = await get_import_progress(conn) is_running = current and current.stopped is None if not is_running: @@ -144,17 +167,17 @@ async def stop_import_progress(*, error: BaseException | None = None) -> None: current.error = repr(error) current.stopped = utcnow().isoformat() - await update(current) + await update(conn, current) -async def set_import_progress(progress: float) -> Progress: +async def set_import_progress(conn: Connection, /, progress: float) -> Progress: """Set the current import progress percentage. If no import is currently running, this will create a new one. """ progress = min(max(0.0, progress), 100.0) # clamp to 0 <= progress <= 100 - current = await get_import_progress() + current = await get_import_progress(conn) is_running = current and current.stopped is None if not is_running: @@ -164,71 +187,88 @@ async def set_import_progress(progress: float) -> Progress: current.percent = progress if is_running: - await update(current) + await update(conn, current) else: - await add(current) + await add(conn, current) return current -_lock = threading.Lock() -_prelock = threading.Lock() +def _new_engine() -> AsyncEngine: + uri = f"sqlite+aiosqlite:///{config.storage_path}" + + return create_async_engine( + uri, + isolation_level="SERIALIZABLE", + ) + + +def _shared_engine() -> AsyncEngine: + global _engine + + if _engine is None: + _engine = _new_engine() + + return _engine + + +def _new_connection() -> Connection: + return _shared_engine().connect() @contextlib.asynccontextmanager -async def single_threaded(): - """Ensure the nested code is run only by a single thread at a time.""" - wait = 1e-5 # XXX not sure if there's a better magic value here +async def transaction( + *, force_rollback: bool = False +) -> AsyncGenerator[Connection, None]: + async with new_connection() as conn: + yield conn - # The pre-lock (a lock for the lock) allows for multiple threads to hand of - # the main lock. - # With only a single lock the contending thread will spend most of its time - # in the asyncio.sleep and the reigning thread will have time to finish - # whatever it's doing and simply acquire the lock again before the other - # thread has had a change to try. - # By having another lock (and the same sleep time!) the contending thread - # will always have a chance to acquire the main lock. - while not _prelock.acquire(blocking=False): - await asyncio.sleep(wait) + if not force_rollback: + await conn.commit() - try: - while not _lock.acquire(blocking=False): - await asyncio.sleep(wait) - finally: - _prelock.release() - try: - yield - - finally: - _lock.release() +# The _test_connection allows pinning a connection that will be shared across the app. +# This can (and should only) be used when running tests, NOT IN PRODUCTION! +_test_connection: Connection | None = None @contextlib.asynccontextmanager -async def _locked_connection(): - async with single_threaded(): - yield _shared_connection() +async def new_connection() -> AsyncGenerator[Connection, None]: + """Return a new connection. + + Any changes will be rolled back, unless `.commit()` is called on the + connection. + + If you want to commit changes, consider using `transaction()` instead. + """ + conn = _test_connection or _new_connection() + + # Support reusing the same connection for _test_connection. + is_started = conn.sync_connection is not None + if is_started: + yield conn + return + + async with conn: + yield conn -def _shared_connection() -> Database: - global _database +@contextlib.asynccontextmanager +async def transacted( + conn: Connection, /, *, force_rollback: bool = False +) -> AsyncGenerator[None, None]: + transaction = contextlib.nullcontext() if conn.in_transaction() else conn.begin() - if _database is None: - uri = f"sqlite:///{config.storage_path}" - # uri = f"sqlite+aiosqlite:///{config.storage_path}" - _database = Database(uri) + async with transaction: + try: + yield - engine = sa.create_engine(uri, future=True) - metadata.create_all(engine, tables=[db_patches]) - - return _database + finally: + if force_rollback: + await conn.rollback() -def transaction(): - return _shared_connection().transaction() - - -async def add(item: Model) -> None: +async def add(conn: Connection, /, item: Model) -> None: # Support late initializing - used for optimization. if getattr(item, "_is_lazy", False): assert hasattr(item, "_lazy_init") @@ -237,14 +277,29 @@ async def add(item: Model) -> None: table: sa.Table = item.__table__ values = asplain(item, serialize=True) stmt = table.insert().values(values) - async with _locked_connection() as conn: - await conn.execute(stmt) + await conn.execute(stmt) + + +async def fetch_all( + conn: Connection, /, query: sa.Executable, values: "dict | None" = None +) -> Sequence[sa.Row]: + result = await conn.execute(query, values) + return result.all() + + +async def fetch_one( + conn: Connection, /, query: sa.Executable, values: "dict | None" = None +) -> sa.Row | None: + result = await conn.execute(query, values) + return result.first() ModelType = TypeVar("ModelType", bound=Model) async def get( + conn: Connection, + /, model: Type[ModelType], *, order_by: tuple[sa.Column, Literal["asc", "desc"]] | None = None, @@ -268,13 +323,12 @@ async def get( query = query.order_by( order_col.asc() if order_dir == "asc" else order_col.desc() ) - async with _locked_connection() as conn: - row = await conn.fetch_one(query) + row = await fetch_one(conn, query) return fromplain(model, row._mapping, serialized=True) if row else None async def get_many( - model: Type[ModelType], **field_sets: set | list + conn: Connection, /, model: Type[ModelType], **field_sets: set | list ) -> Iterable[ModelType]: """Return the items with any values matching all given field sets. @@ -288,12 +342,13 @@ async def get_many( table: sa.Table = model.__table__ query = sa.select(model).where(*(table.c[k].in_(v) for k, v in field_sets.items())) - async with _locked_connection() as conn: - rows = await conn.fetch_all(query) + rows = await fetch_all(conn, query) return (fromplain(model, row._mapping, serialized=True) for row in rows) -async def get_all(model: Type[ModelType], **field_values) -> Iterable[ModelType]: +async def get_all( + conn: Connection, /, model: Type[ModelType], **field_values +) -> Iterable[ModelType]: """Filter all items by comparing all given field values. If no filters are given, all items will be returned. @@ -302,12 +357,11 @@ async def get_all(model: Type[ModelType], **field_values) -> Iterable[ModelType] query = sa.select(model).where( *(table.c[k] == v for k, v in field_values.items() if v is not None) ) - async with _locked_connection() as conn: - rows = await conn.fetch_all(query) + rows = await fetch_all(conn, query) return (fromplain(model, row._mapping, serialized=True) for row in rows) -async def update(item: Model) -> None: +async def update(conn: Connection, /, item: Model) -> None: # Support late initializing - used for optimization. if getattr(item, "_is_lazy", False): assert hasattr(item, "_lazy_init") @@ -316,30 +370,28 @@ async def update(item: Model) -> None: table: sa.Table = item.__table__ values = asplain(item, serialize=True) stmt = table.update().where(table.c.id == values["id"]).values(values) - async with _locked_connection() as conn: - await conn.execute(stmt) + await conn.execute(stmt) -async def remove(item: Model) -> None: +async def remove(conn: Connection, /, item: Model) -> None: table: sa.Table = item.__table__ values = asplain(item, filter_fields={"id"}, serialize=True) stmt = table.delete().where(table.c.id == values["id"]) - async with _locked_connection() as conn: - await conn.execute(stmt) + await conn.execute(stmt) -async def add_or_update_user(user: User) -> None: - db_user = await get(User, imdb_id=user.imdb_id) +async def add_or_update_user(conn: Connection, /, user: User) -> None: + db_user = await get(conn, User, imdb_id=user.imdb_id) if not db_user: - await add(user) + await add(conn, user) else: user.id = db_user.id if user != db_user: - await update(user) + await update(conn, user) -async def add_or_update_many_movies(movies: list[Movie]) -> None: +async def add_or_update_many_movies(conn: Connection, /, movies: list[Movie]) -> None: """Add or update Movies in the database. This is an optimized version of `add_or_update_movie` for the purpose @@ -348,12 +400,13 @@ async def add_or_update_many_movies(movies: list[Movie]) -> None: # for movie in movies: # await add_or_update_movie(movie) db_movies = { - m.imdb_id: m for m in await get_many(Movie, imdb_id=[m.imdb_id for m in movies]) + m.imdb_id: m + for m in await get_many(conn, Movie, imdb_id=[m.imdb_id for m in movies]) } for movie in movies: # XXX optimize bulk add & update as well if movie.imdb_id not in db_movies: - await add(movie) + await add(conn, movie) else: db_movie = db_movies[movie.imdb_id] movie.id = db_movie.id @@ -366,10 +419,10 @@ async def add_or_update_many_movies(movies: list[Movie]) -> None: if movie.updated <= db_movie.updated: return - await update(movie) + await update(conn, movie) -async def add_or_update_movie(movie: Movie) -> None: +async def add_or_update_movie(conn: Connection, /, movie: Movie) -> None: """Add or update a Movie in the database. This is an upsert operation, but it will also update the Movie you pass @@ -377,9 +430,9 @@ async def add_or_update_movie(movie: Movie) -> None: set all optional values on your Movie that might be unset but exist in the database. It's a bidirectional sync. """ - db_movie = await get(Movie, imdb_id=movie.imdb_id) + db_movie = await get(conn, Movie, imdb_id=movie.imdb_id) if not db_movie: - await add(movie) + await add(conn, movie) else: movie.id = db_movie.id @@ -391,23 +444,23 @@ async def add_or_update_movie(movie: Movie) -> None: if movie.updated <= db_movie.updated: return - await update(movie) + await update(conn, movie) -async def add_or_update_rating(rating: Rating) -> bool: +async def add_or_update_rating(conn: Connection, /, rating: Rating) -> bool: db_rating = await get( - Rating, movie_id=str(rating.movie_id), user_id=str(rating.user_id) + conn, Rating, movie_id=str(rating.movie_id), user_id=str(rating.user_id) ) if not db_rating: - await add(rating) + await add(conn, rating) return True else: rating.id = db_rating.id if rating != db_rating: - await update(rating) + await update(conn, rating) return True return False @@ -418,6 +471,8 @@ def sql_escape(s: str, char: str = "#") -> str: async def find_ratings( + conn: Connection, + /, *, title: str | None = None, media_type: str | None = None, @@ -475,9 +530,8 @@ async def find_ratings( ) .limit(limit_rows) ) - async with _locked_connection() as conn: - rating_rows: AsyncGenerator[Rating, None] = conn.iterate(query) # type: ignore - movie_ids = [r.movie_id async for r in rating_rows] + rating_rows: sa.CursorResult[Rating] = await conn.execute(query) + movie_ids = [r.movie_id for r in rating_rows] if include_unrated and len(movie_ids) < limit_rows: query = ( @@ -491,15 +545,17 @@ async def find_ratings( ) .limit(limit_rows - len(movie_ids)) ) - async with _locked_connection() as conn: - movie_rows: AsyncGenerator[Movie, None] = conn.iterate(query) # type: ignore - movie_ids += [r.id async for r in movie_rows] + movie_rows: sa.CursorResult[Movie] = await conn.execute(query) + movie_ids += [r.id for r in movie_rows] - return await ratings_for_movie_ids(ids=movie_ids) + return await ratings_for_movie_ids(conn, ids=movie_ids) async def ratings_for_movie_ids( - ids: Iterable[ULID | str] = [], imdb_ids: Iterable[str] = [] + conn: Connection, + /, + ids: Iterable[ULID | str] = [], + imdb_ids: Iterable[str] = [], ) -> Iterable[dict[str, Any]]: conds = [] @@ -527,13 +583,12 @@ async def ratings_for_movie_ids( .outerjoin_from(movies, ratings, movies.c.id == ratings.c.movie_id) .where(sa.or_(*conds)) ) - async with _locked_connection() as conn: - rows = await conn.fetch_all(query) + rows = await fetch_all(conn, query) return tuple(dict(r._mapping) for r in rows) async def ratings_for_movies( - movie_ids: Iterable[ULID], user_ids: Iterable[ULID] = [] + conn: Connection, /, movie_ids: Iterable[ULID], user_ids: Iterable[ULID] = [] ) -> Iterable[Rating]: conditions = [ratings.c.movie_id.in_(str(x) for x in movie_ids)] @@ -542,13 +597,14 @@ async def ratings_for_movies( query = sa.select(ratings).where(*conditions) - async with _locked_connection() as conn: - rows = await conn.fetch_all(query) + rows = await fetch_all(conn, query) return (fromplain(Rating, row._mapping, serialized=True) for row in rows) async def find_movies( + conn: Connection, + /, *, title: str | None = None, media_type: str | None = None, @@ -606,15 +662,14 @@ async def find_movies( .offset(skip_rows) ) - async with _locked_connection() as conn: - rows = await conn.fetch_all(query) + rows = await fetch_all(conn, query) movies_ = [fromplain(Movie, row._mapping, serialized=True) for row in rows] if not user_ids: return ((m, []) for m in movies_) - ratings = await ratings_for_movies((m.id for m in movies_), user_ids) + ratings = await ratings_for_movies(conn, (m.id for m in movies_), user_ids) aggreg: dict[ULID, tuple[Movie, list[Rating]]] = {m.id: (m, []) for m in movies_} for rating in ratings: diff --git a/unwind/imdb.py b/unwind/imdb.py index 6858fc7..631a088 100644 --- a/unwind/imdb.py +++ b/unwind/imdb.py @@ -40,7 +40,9 @@ async def refresh_user_ratings_from_imdb(stop_on_dupe: bool = True): async with asession() as s: s.headers["Accept-Language"] = "en-US, en;q=0.5" - for user in await db.get_all(User): + async with db.new_connection() as conn: + users = list(await db.get_all(conn, User)) + for user in users: log.info("⚡️ Loading data for %s ...", user.name) try: @@ -96,7 +98,7 @@ find_year = re.compile( find_movie_id = re.compile(r"/title/(?Ptt\d+)/").search -def movie_and_rating_from_item(item) -> tuple[Movie, Rating]: +def movie_and_rating_from_item(item: bs4.Tag) -> tuple[Movie, Rating]: genres = (genre := item.find("span", "genre")) and genre.string or "" movie = Movie( title=item.h3.a.string.strip(), @@ -161,9 +163,10 @@ async def parse_page(url: str) -> tuple[list[Rating], str | None]: assert isinstance(meta, bs4.Tag) imdb_id = meta["content"] assert isinstance(imdb_id, str) - user = await db.get(User, imdb_id=imdb_id) or User( - imdb_id=imdb_id, name="", secret="" - ) + async with db.new_connection() as conn: + user = await db.get(conn, User, imdb_id=imdb_id) or User( + imdb_id=imdb_id, name="", secret="" + ) if (headline := soup.h1) is None: raise RuntimeError("No headline found.") @@ -213,14 +216,15 @@ async def load_ratings(user_id: str): for i, rating in enumerate(ratings): assert rating.user and rating.movie - if i == 0: - # All rating objects share the same user. - await db.add_or_update_user(rating.user) - rating.user_id = rating.user.id + async with db.transaction() as conn: + if i == 0: + # All rating objects share the same user. + await db.add_or_update_user(conn, rating.user) + rating.user_id = rating.user.id - await db.add_or_update_movie(rating.movie) - rating.movie_id = rating.movie.id + await db.add_or_update_movie(conn, rating.movie) + rating.movie_id = rating.movie.id - is_updated = await db.add_or_update_rating(rating) + is_updated = await db.add_or_update_rating(conn, rating) yield rating, is_updated diff --git a/unwind/imdb_import.py b/unwind/imdb_import.py index 705db2f..dad419e 100644 --- a/unwind/imdb_import.py +++ b/unwind/imdb_import.py @@ -209,7 +209,8 @@ async def import_from_file(*, basics_path: Path, ratings_path: Path): for i, m in enumerate(read_basics(basics_path)): perc = 100 * i / total if perc >= perc_next_report: - await db.set_import_progress(perc) + async with db.transaction() as conn: + await db.set_import_progress(conn, perc) log.info("⏳ Imported %s%%", round(perc, 1)) perc_next_report += perc_step @@ -233,15 +234,18 @@ async def import_from_file(*, basics_path: Path, ratings_path: Path): chunk.append(m) if len(chunk) > 1000: - await add_or_update_many_movies(chunk) + async with db.transaction() as conn: + await add_or_update_many_movies(conn, chunk) chunk = [] if chunk: - await add_or_update_many_movies(chunk) + async with db.transaction() as conn: + await add_or_update_many_movies(conn, chunk) chunk = [] log.info("👍 Imported 100%") - await db.set_import_progress(100) + async with db.transaction() as conn: + await db.set_import_progress(conn, 100) async def download_datasets(*, basics_path: Path, ratings_path: Path) -> None: @@ -270,7 +274,8 @@ async def load_from_web(*, force: bool = False) -> None: See https://www.imdb.com/interfaces/ and https://datasets.imdbws.com/ for more information on the IMDb database dumps. """ - await db.set_import_progress(0) + async with db.transaction() as conn: + await db.set_import_progress(conn, 0) try: ratings_file = config.datadir / "imdb/title.ratings.tsv.gz" @@ -290,8 +295,10 @@ async def load_from_web(*, force: bool = False) -> None: await import_from_file(basics_path=basics_file, ratings_path=ratings_file) except BaseException as err: - await db.stop_import_progress(error=err) + async with db.transaction() as conn: + await db.stop_import_progress(conn, error=err) raise else: - await db.stop_import_progress() + async with db.transaction() as conn: + await db.stop_import_progress(conn) diff --git a/unwind/models.py b/unwind/models.py index ff961fc..3e52225 100644 --- a/unwind/models.py +++ b/unwind/models.py @@ -354,49 +354,6 @@ The contents of the Relation are ignored or discarded when using Relation = Annotated[T | None, _RelationSentinel] -@mapper_registry.mapped -@dataclass -class Rating: - __table__: ClassVar[Table] = Table( - "ratings", - metadata, - Column("id", String, primary_key=True), # ULID - Column("movie_id", ForeignKey("movies.id"), nullable=False), # ULID - Column("user_id", ForeignKey("users.id"), nullable=False), # ULID - Column("score", Integer, nullable=False), - Column("rating_date", String, nullable=False), # datetime - Column("favorite", Integer), # bool - Column("finished", Integer), # bool - ) - - id: ULID = field(default_factory=ULID) - - movie_id: ULID = None - movie: Relation[Movie] = None - - user_id: ULID = None - user: Relation["User"] = None - - score: int = None # range: [0,100] - rating_date: datetime = None - favorite: bool | None = None - finished: bool | None = None - - def __eq__(self, other): - """Return wether two Ratings are equal. - - This operation compares all fields as expected, except that it - ignores any field marked as Relation. - """ - if type(other) is not type(self): - return False - return all( - getattr(self, f.name) == getattr(other, f.name) for f in fields(self) - ) - - -ratings = Rating.__table__ - Access = Literal[ "r", # read "i", # index @@ -442,6 +399,50 @@ class User: self.groups.append({"id": group_id, "access": access}) +@mapper_registry.mapped +@dataclass +class Rating: + __table__: ClassVar[Table] = Table( + "ratings", + metadata, + Column("id", String, primary_key=True), # ULID + Column("movie_id", ForeignKey("movies.id"), nullable=False), # ULID + Column("user_id", ForeignKey("users.id"), nullable=False), # ULID + Column("score", Integer, nullable=False), + Column("rating_date", String, nullable=False), # datetime + Column("favorite", Integer), # bool + Column("finished", Integer), # bool + ) + + id: ULID = field(default_factory=ULID) + + movie_id: ULID = None + movie: Relation[Movie] = None + + user_id: ULID = None + user: Relation[User] = None + + score: int = None # range: [0,100] + rating_date: datetime = None + favorite: bool | None = None + finished: bool | None = None + + def __eq__(self, other): + """Return wether two Ratings are equal. + + This operation compares all fields as expected, except that it + ignores any field marked as Relation. + """ + if type(other) is not type(self): + return False + return all( + getattr(self, f.name) == getattr(other, f.name) for f in fields(self) + ) + + +ratings = Rating.__table__ + + class GroupUser(TypedDict): id: str name: str diff --git a/unwind/web.py b/unwind/web.py index bddd54d..8b02863 100644 --- a/unwind/web.py +++ b/unwind/web.py @@ -168,7 +168,8 @@ async def auth_user(request) -> User | None: if not isinstance(request.user, AuthedUser): return - user = await db.get(User, id=request.user.user_id) + async with db.new_connection() as conn: + user = await db.get(conn, User, id=request.user.user_id) if not user: return @@ -195,8 +196,9 @@ def route(path: str, *, methods: list[str] | None = None, **kwds): async def get_ratings_for_group(request): group_id = as_ulid(request.path_params["group_id"]) - if (group := await db.get(Group, id=str(group_id))) is None: - return not_found() + async with db.new_connection() as conn: + if (group := await db.get(conn, Group, id=str(group_id))) is None: + return not_found() user_ids = {u["id"] for u in group.users} @@ -207,22 +209,26 @@ async def get_ratings_for_group(request): # if (imdb_id or unwind_id) and (movie := await db.get(Movie, id=unwind_id, imdb_id=imdb_id)): if unwind_id: - rows = await db.ratings_for_movie_ids(ids=[unwind_id]) + async with db.new_connection() as conn: + rows = await db.ratings_for_movie_ids(conn, ids=[unwind_id]) elif imdb_id: - rows = await db.ratings_for_movie_ids(imdb_ids=[imdb_id]) + async with db.new_connection() as conn: + rows = await db.ratings_for_movie_ids(conn, imdb_ids=[imdb_id]) else: - rows = await find_ratings( - title=params.get("title"), - media_type=params.get("media_type"), - exact=truthy(params.get("exact")), - ignore_tv_episodes=truthy(params.get("ignore_tv_episodes")), - include_unrated=truthy(params.get("include_unrated")), - yearcomp=yearcomp(params["year"]) if "year" in params else None, - limit_rows=as_int(params.get("per_page"), max=10, default=5), - user_ids=user_ids, - ) + async with db.new_connection() as conn: + rows = await find_ratings( + conn, + title=params.get("title"), + media_type=params.get("media_type"), + exact=truthy(params.get("exact")), + ignore_tv_episodes=truthy(params.get("ignore_tv_episodes")), + include_unrated=truthy(params.get("include_unrated")), + yearcomp=yearcomp(params["year"]) if "year" in params else None, + limit_rows=as_int(params.get("per_page"), max=10, default=5), + user_ids=user_ids, + ) ratings = (web_models.Rating(**r) for r in rows) @@ -261,7 +267,8 @@ async def list_movies(request): if group_id := params.get("group_id"): group_id = as_ulid(group_id) - group = await db.get(Group, id=str(group_id)) + async with db.new_connection() as conn: + group = await db.get(conn, Group, id=str(group_id)) if not group: return not_found("Group not found.") @@ -286,26 +293,31 @@ async def list_movies(request): if imdb_id or unwind_id: # XXX missing support for user_ids and user_scores - movies = ( - [m] if (m := await db.get(Movie, id=unwind_id, imdb_id=imdb_id)) else [] - ) + async with db.new_connection() as conn: + movies = ( + [m] + if (m := await db.get(conn, Movie, id=unwind_id, imdb_id=imdb_id)) + else [] + ) resp = [asplain(web_models.RatingAggregate.from_movie(m)) for m in movies] else: per_page = as_int(params.get("per_page"), max=1000, default=5) page = as_int(params.get("page"), min=1, default=1) - movieratings = await find_movies( - title=params.get("title"), - media_type=params.get("media_type"), - exact=truthy(params.get("exact")), - ignore_tv_episodes=truthy(params.get("ignore_tv_episodes")), - include_unrated=truthy(params.get("include_unrated")), - yearcomp=yearcomp(params["year"]) if "year" in params else None, - limit_rows=per_page, - skip_rows=(page - 1) * per_page, - user_ids=list(user_ids), - ) + async with db.new_connection() as conn: + movieratings = await find_movies( + conn, + title=params.get("title"), + media_type=params.get("media_type"), + exact=truthy(params.get("exact")), + ignore_tv_episodes=truthy(params.get("ignore_tv_episodes")), + include_unrated=truthy(params.get("include_unrated")), + yearcomp=yearcomp(params["year"]) if "year" in params else None, + limit_rows=per_page, + skip_rows=(page - 1) * per_page, + user_ids=list(user_ids), + ) resp = [] for movie, ratings in movieratings: @@ -325,7 +337,8 @@ async def add_movie(request): @route("/movies/_reload_imdb", methods=["GET"]) @requires(["authenticated", "admin"]) async def progress_for_load_imdb_movies(request): - progress = await db.get_import_progress() + async with db.new_connection() as conn: + progress = await db.get_import_progress(conn) if not progress: return JSONResponse({"status": "No import exists."}, status_code=404) @@ -364,14 +377,16 @@ async def load_imdb_movies(request): force = truthy(params.get("force")) async with _import_lock: - progress = await db.get_import_progress() + async with db.new_connection() as conn: + progress = await db.get_import_progress(conn) if progress and not progress.stopped: return JSONResponse( {"status": "Import is running.", "progress": progress.percent}, status_code=409, ) - await db.set_import_progress(0) + async with db.transaction() as conn: + await db.set_import_progress(conn, 0) task = BackgroundTask(imdb_import.load_from_web, force=force) return JSONResponse( @@ -382,7 +397,8 @@ async def load_imdb_movies(request): @route("/users") @requires(["authenticated", "admin"]) async def list_users(request): - users = await db.get_all(User) + async with db.new_connection() as conn: + users = await db.get_all(conn, User) return JSONResponse([asplain(u) for u in users]) @@ -398,7 +414,8 @@ async def add_user(request): secret = secrets.token_bytes() user = User(name=name, imdb_id=imdb_id, secret=phc_scrypt(secret)) - await db.add(user) + async with db.transaction() as conn: + await db.add(conn, user) return JSONResponse( { @@ -414,7 +431,8 @@ async def show_user(request): user_id = as_ulid(request.path_params["user_id"]) if is_admin(request): - user = await db.get(User, id=str(user_id)) + async with db.new_connection() as conn: + user = await db.get(conn, User, id=str(user_id)) else: user = await auth_user(request) @@ -441,14 +459,15 @@ async def show_user(request): async def remove_user(request): user_id = as_ulid(request.path_params["user_id"]) - user = await db.get(User, id=str(user_id)) + async with db.new_connection() as conn: + user = await db.get(conn, User, id=str(user_id)) if not user: return not_found() - async with db.transaction(): + async with db.transaction() as conn: # XXX remove user refs from groups and ratings - await db.remove(user) + await db.remove(conn, user) return JSONResponse(asplain(user)) @@ -459,7 +478,8 @@ async def modify_user(request): user_id = as_ulid(request.path_params["user_id"]) if is_admin(request): - user = await db.get(User, id=str(user_id)) + async with db.new_connection() as conn: + user = await db.get(conn, User, id=str(user_id)) else: user = await auth_user(request) @@ -495,7 +515,8 @@ async def modify_user(request): user.secret = phc_scrypt(secret) - await db.update(user) + async with db.transaction() as conn: + await db.update(conn, user) return JSONResponse(asplain(user)) @@ -505,13 +526,15 @@ async def modify_user(request): async def add_group_to_user(request): user_id = as_ulid(request.path_params["user_id"]) - user = await db.get(User, id=str(user_id)) + async with db.new_connection() as conn: + user = await db.get(conn, User, id=str(user_id)) if not user: return not_found("User not found") (group_id, access) = await json_from_body(request, ["group", "access"]) - group = await db.get(Group, id=str(group_id)) + async with db.new_connection() as conn: + group = await db.get(conn, Group, id=str(group_id)) if not group: return not_found("Group not found") @@ -519,7 +542,8 @@ async def add_group_to_user(request): raise HTTPException(422, f"Invalid access level.") user.set_access(group_id, access) - await db.update(user) + async with db.transaction() as conn: + await db.update(conn, user) return JSONResponse(asplain(user)) @@ -547,7 +571,8 @@ async def load_imdb_user_ratings(request): @route("/groups") @requires(["authenticated", "admin"]) async def list_groups(request): - groups = await db.get_all(Group) + async with db.new_connection() as conn: + groups = await db.get_all(conn, Group) return JSONResponse([asplain(g) for g in groups]) @@ -560,7 +585,8 @@ async def add_group(request): # XXX restrict name group = Group(name=name) - await db.add(group) + async with db.transaction() as conn: + await db.add(conn, group) return JSONResponse(asplain(group)) @@ -569,7 +595,8 @@ async def add_group(request): @requires(["authenticated"]) async def add_user_to_group(request): group_id = as_ulid(request.path_params["group_id"]) - group = await db.get(Group, id=str(group_id)) + async with db.new_connection() as conn: + group = await db.get(conn, Group, id=str(group_id)) if not group: return not_found() @@ -596,7 +623,8 @@ async def add_user_to_group(request): else: group.users.append({"name": name, "id": user_id}) - await db.update(group) + async with db.transaction() as conn: + await db.update(conn, group) return JSONResponse(asplain(group)) From 78b531ad8cc3caa761aefbd6607d813a9f8e0623 Mon Sep 17 00:00:00 2001 From: ducklet Date: Wed, 29 Nov 2023 18:01:01 +0100 Subject: [PATCH 31/31] add scripts to build & run with Docker Or set DOCKER_BIN=podman to use Podman. --- .gitignore | 2 +- poetry.lock | 2 +- pyproject.toml | 9 ++++++++- scripts/docker-build | 25 +++++++++++++++++++++++++ scripts/docker-run | 18 ++++++++++++++++++ 5 files changed, 53 insertions(+), 3 deletions(-) create mode 100755 scripts/docker-build create mode 100755 scripts/docker-run diff --git a/.gitignore b/.gitignore index ba432e1..578559d 100644 --- a/.gitignore +++ b/.gitignore @@ -2,5 +2,5 @@ *.pyc /.cache /.pytest_cache +/build /data/* -/requirements.txt diff --git a/poetry.lock b/poetry.lock index df69612..bbd1764 100644 --- a/poetry.lock +++ b/poetry.lock @@ -686,4 +686,4 @@ files = [ [metadata] lock-version = "2.0" python-versions = "^3.12" -content-hash = "fc07028820963701634eb55b42ea12962fd7c6fc25ef76ddadf30f2c74544b5f" +content-hash = "ba28f3acc8701a53b35b1c8ea15169e151c74c277bad095f52e19e3f65be9ed7" diff --git a/pyproject.toml b/pyproject.toml index f7c1b6b..417764e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "unwind" -version = "0.1.0" +version = "0" description = "" authors = ["ducklet "] license = "LOL" @@ -15,6 +15,13 @@ uvicorn = "^0.23" httpx = "^0.24" sqlalchemy = {version = "^2.0", extras = ["aiosqlite"]} +[tool.poetry.group.build.dependencies] +# When we run poetry export, typing-extensions is a transient dependency via +# sqlalchemy, but the hash won't be included in the requirements.txt. +# By making it a direct dependency we can fix this issue, otherwise this could +# be removed. +typing-extensions = "*" + [tool.poetry.group.dev] optional = true diff --git a/scripts/docker-build b/scripts/docker-build new file mode 100755 index 0000000..8f4c093 --- /dev/null +++ b/scripts/docker-build @@ -0,0 +1,25 @@ +#!/bin/sh -eu + +: "${DOCKER_BIN:=docker}" + +cd "$RUN_DIR" + +builddir=build + +[ -z "${DEBUG:-}" ] || set -x + +mkdir -p "$builddir" + +poetry export \ + --with=build \ + --output="$builddir"/requirements.txt + +githash=$(git rev-parse --short HEAD) +today=$(date -u '+%Y.%m.%d') +version="${today}+${githash}" +echo "$version" >"$builddir"/version + +$DOCKER_BIN build \ + --pull \ + --tag "code.dumpr.org/ducklet/unwind":"$version" \ + . diff --git a/scripts/docker-run b/scripts/docker-run new file mode 100755 index 0000000..407b199 --- /dev/null +++ b/scripts/docker-run @@ -0,0 +1,18 @@ +#!/bin/sh -eu + +: "${DOCKER_BIN:=docker}" + +cd "$RUN_DIR" + +[ -z "${DEBUG:-}" ] || set -x + +version=$(cat build/version) + +$DOCKER_BIN run \ + --init \ + -it --rm \ + --read-only \ + --memory '500m' \ + --publish 127.0.0.1:8000:8000 \ + --volume "$RUN_DIR"/data:/data \ + "code.dumpr.org/ducklet/unwind":"$version"