use native union type syntax
This commit is contained in:
parent
13b65103fd
commit
3320d53eda
8 changed files with 43 additions and 45 deletions
12
unwind/db.py
12
unwind/db.py
|
|
@ -4,7 +4,7 @@ import logging
|
||||||
import re
|
import re
|
||||||
import threading
|
import threading
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Any, Iterable, Literal, Optional, Type, TypeVar, Union
|
from typing import Any, Iterable, Literal, Type, TypeVar
|
||||||
|
|
||||||
import sqlalchemy
|
import sqlalchemy
|
||||||
from databases import Database
|
from databases import Database
|
||||||
|
|
@ -26,7 +26,7 @@ from .types import ULID
|
||||||
log = logging.getLogger(__name__)
|
log = logging.getLogger(__name__)
|
||||||
T = TypeVar("T")
|
T = TypeVar("T")
|
||||||
|
|
||||||
_shared_connection: Optional[Database] = None
|
_shared_connection: Database | None = None
|
||||||
|
|
||||||
|
|
||||||
async def open_connection_pool() -> None:
|
async def open_connection_pool() -> None:
|
||||||
|
|
@ -131,7 +131,7 @@ async def apply_db_patches(db: Database):
|
||||||
await db.execute("vacuum")
|
await db.execute("vacuum")
|
||||||
|
|
||||||
|
|
||||||
async def get_import_progress() -> Optional[Progress]:
|
async def get_import_progress() -> Progress | None:
|
||||||
"""Return the latest import progress."""
|
"""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")
|
||||||
|
|
||||||
|
|
@ -244,7 +244,7 @@ ModelType = TypeVar("ModelType")
|
||||||
|
|
||||||
async def get(
|
async def get(
|
||||||
model: Type[ModelType], *, order_by: str = None, **kwds
|
model: Type[ModelType], *, order_by: str = None, **kwds
|
||||||
) -> Optional[ModelType]:
|
) -> ModelType | None:
|
||||||
"""Load a model instance from the database.
|
"""Load a model instance from the database.
|
||||||
|
|
||||||
Passing `kwds` allows to filter the instance to load. You have to encode the
|
Passing `kwds` allows to filter the instance to load. You have to encode the
|
||||||
|
|
@ -415,7 +415,7 @@ async def find_ratings(
|
||||||
limit_rows: int = 10,
|
limit_rows: int = 10,
|
||||||
user_ids: Iterable[str] = [],
|
user_ids: Iterable[str] = [],
|
||||||
):
|
):
|
||||||
values: dict[str, Union[int, str]] = {
|
values: dict[str, int | str] = {
|
||||||
"limit_rows": limit_rows,
|
"limit_rows": limit_rows,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -598,7 +598,7 @@ async def find_movies(
|
||||||
include_unrated: bool = False,
|
include_unrated: bool = False,
|
||||||
user_ids: list[ULID] = [],
|
user_ids: list[ULID] = [],
|
||||||
) -> Iterable[tuple[Movie, list[Rating]]]:
|
) -> Iterable[tuple[Movie, list[Rating]]]:
|
||||||
values: dict[str, Union[int, str]] = {
|
values: dict[str, int | str] = {
|
||||||
"limit_rows": limit_rows,
|
"limit_rows": limit_rows,
|
||||||
"skip_rows": skip_rows,
|
"skip_rows": skip_rows,
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,6 @@ import logging
|
||||||
import re
|
import re
|
||||||
from collections import namedtuple
|
from collections import namedtuple
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from typing import Optional, Tuple
|
|
||||||
from urllib.parse import urljoin
|
from urllib.parse import urljoin
|
||||||
|
|
||||||
from . import db
|
from . import db
|
||||||
|
|
@ -153,7 +152,7 @@ def movie_and_rating_from_item(item) -> tuple[Movie, Rating]:
|
||||||
ForgedRequest = namedtuple("ForgedRequest", "url headers")
|
ForgedRequest = namedtuple("ForgedRequest", "url headers")
|
||||||
|
|
||||||
|
|
||||||
async def parse_page(url) -> Tuple[list[Rating], Optional[str]]:
|
async def parse_page(url) -> tuple[list[Rating], str | None]:
|
||||||
ratings = []
|
ratings = []
|
||||||
|
|
||||||
soup = soup_from_url(url)
|
soup = soup_from_url(url)
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,7 @@ import logging
|
||||||
from dataclasses import dataclass, fields
|
from dataclasses import dataclass, fields
|
||||||
from datetime import datetime, timezone
|
from datetime import datetime, timezone
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Generator, Literal, Optional, Type, TypeVar, overload
|
from typing import Generator, Literal, Type, TypeVar, overload
|
||||||
|
|
||||||
from . import config, db, request
|
from . import config, db, request
|
||||||
from .db import add_or_update_many_movies
|
from .db import add_or_update_many_movies
|
||||||
|
|
@ -27,10 +27,10 @@ class BasicRow:
|
||||||
primaryTitle: str
|
primaryTitle: str
|
||||||
originalTitle: str
|
originalTitle: str
|
||||||
isAdult: bool
|
isAdult: bool
|
||||||
startYear: Optional[int]
|
startYear: int | None
|
||||||
endYear: Optional[int]
|
endYear: int | None
|
||||||
runtimeMinutes: Optional[int]
|
runtimeMinutes: int | None
|
||||||
genres: Optional[set[str]]
|
genres: set[str] | None
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def from_row(cls, row):
|
def from_row(cls, row):
|
||||||
|
|
|
||||||
|
|
@ -9,7 +9,6 @@ from typing import (
|
||||||
ClassVar,
|
ClassVar,
|
||||||
Container,
|
Container,
|
||||||
Literal,
|
Literal,
|
||||||
Optional,
|
|
||||||
Type,
|
Type,
|
||||||
TypeVar,
|
TypeVar,
|
||||||
Union,
|
Union,
|
||||||
|
|
@ -25,7 +24,7 @@ JSONObject = dict[str, JSON]
|
||||||
T = TypeVar("T")
|
T = TypeVar("T")
|
||||||
|
|
||||||
|
|
||||||
def annotations(tp: Type) -> Optional[tuple]:
|
def annotations(tp: Type) -> tuple | None:
|
||||||
return tp.__metadata__ if hasattr(tp, "__metadata__") else None
|
return tp.__metadata__ if hasattr(tp, "__metadata__") else None
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -61,7 +60,7 @@ def is_optional(tp: Type) -> bool:
|
||||||
return len(args) == 2 and type(None) in args
|
return len(args) == 2 and type(None) in args
|
||||||
|
|
||||||
|
|
||||||
def optional_type(tp: Type) -> Optional[Type]:
|
def optional_type(tp: Type) -> Type | None:
|
||||||
"""Return the wrapped type from an optional type.
|
"""Return the wrapped type from an optional type.
|
||||||
|
|
||||||
For example this will return `int` for `Optional[int]`.
|
For example this will return `int` for `Optional[int]`.
|
||||||
|
|
@ -206,7 +205,7 @@ class Progress:
|
||||||
type: str = None
|
type: str = None
|
||||||
state: str = None
|
state: str = None
|
||||||
started: datetime = field(default_factory=utcnow)
|
started: datetime = field(default_factory=utcnow)
|
||||||
stopped: Optional[str] = None
|
stopped: str | None = None
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def _state(self) -> dict:
|
def _state(self) -> dict:
|
||||||
|
|
@ -243,15 +242,15 @@ class Movie:
|
||||||
|
|
||||||
id: ULID = field(default_factory=ULID)
|
id: ULID = field(default_factory=ULID)
|
||||||
title: str = None # canonical title (usually English)
|
title: str = None # canonical title (usually English)
|
||||||
original_title: Optional[
|
original_title: str | None = (
|
||||||
str
|
None # original title (usually transscribed to latin script)
|
||||||
] = None # original title (usually transscribed to latin script)
|
)
|
||||||
release_year: int = None # canonical release date
|
release_year: int = None # canonical release date
|
||||||
media_type: str = None
|
media_type: str = None
|
||||||
imdb_id: str = None
|
imdb_id: str = None
|
||||||
imdb_score: Optional[int] = None # range: [0,100]
|
imdb_score: int | None = None # range: [0,100]
|
||||||
imdb_votes: Optional[int] = None
|
imdb_votes: int | None = None
|
||||||
runtime: Optional[int] = None # minutes
|
runtime: int | None = None # minutes
|
||||||
genres: set[str] = None
|
genres: set[str] = None
|
||||||
created: datetime = field(default_factory=utcnow)
|
created: datetime = field(default_factory=utcnow)
|
||||||
updated: datetime = field(default_factory=utcnow)
|
updated: datetime = field(default_factory=utcnow)
|
||||||
|
|
@ -292,7 +291,7 @@ dataclass containing the ID of the linked data.
|
||||||
The contents of the Relation are ignored or discarded when using
|
The contents of the Relation are ignored or discarded when using
|
||||||
`asplain`, `fromplain`, and `validate`.
|
`asplain`, `fromplain`, and `validate`.
|
||||||
"""
|
"""
|
||||||
Relation = Annotated[Optional[T], _RelationSentinel]
|
Relation = Annotated[T | None, _RelationSentinel]
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
|
|
@ -309,8 +308,8 @@ class Rating:
|
||||||
|
|
||||||
score: int = None # range: [0,100]
|
score: int = None # range: [0,100]
|
||||||
rating_date: datetime = None
|
rating_date: datetime = None
|
||||||
favorite: Optional[bool] = None
|
favorite: bool | None = None
|
||||||
finished: Optional[bool] = None
|
finished: bool | None = None
|
||||||
|
|
||||||
def __eq__(self, other):
|
def __eq__(self, other):
|
||||||
"""Return wether two Ratings are equal.
|
"""Return wether two Ratings are equal.
|
||||||
|
|
@ -342,11 +341,11 @@ class User:
|
||||||
secret: str = None
|
secret: str = None
|
||||||
groups: list[dict[str, str]] = field(default_factory=list)
|
groups: list[dict[str, str]] = field(default_factory=list)
|
||||||
|
|
||||||
def has_access(self, group_id: Union[ULID, str], access: Access = "r"):
|
def has_access(self, group_id: ULID | str, access: Access = "r"):
|
||||||
group_id = group_id if isinstance(group_id, str) else str(group_id)
|
group_id = group_id if isinstance(group_id, str) else str(group_id)
|
||||||
return any(g["id"] == group_id and access == g["access"] for g in self.groups)
|
return any(g["id"] == group_id and access == g["access"] for g in self.groups)
|
||||||
|
|
||||||
def set_access(self, group_id: Union[ULID, str], access: Access):
|
def set_access(self, group_id: ULID | str, access: Access):
|
||||||
group_id = group_id if isinstance(group_id, str) else str(group_id)
|
group_id = group_id if isinstance(group_id, str) else str(group_id)
|
||||||
for g in self.groups:
|
for g in self.groups:
|
||||||
if g["id"] == group_id:
|
if g["id"] == group_id:
|
||||||
|
|
|
||||||
|
|
@ -11,7 +11,7 @@ from hashlib import md5
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from random import random
|
from random import random
|
||||||
from time import sleep, time
|
from time import sleep, time
|
||||||
from typing import Callable, Optional, Union
|
from typing import Callable
|
||||||
|
|
||||||
import bs4
|
import bs4
|
||||||
import requests
|
import requests
|
||||||
|
|
@ -142,7 +142,7 @@ class RedirectError(RuntimeError):
|
||||||
super().__init__(f"Redirected: {from_url} -> {to_url}")
|
super().__init__(f"Redirected: {from_url} -> {to_url}")
|
||||||
|
|
||||||
|
|
||||||
def cache_path(req) -> Optional[Path]:
|
def cache_path(req) -> Path | None:
|
||||||
if not config.cachedir:
|
if not config.cachedir:
|
||||||
return
|
return
|
||||||
sig = repr(req.url) # + repr(sorted(req.headers.items()))
|
sig = repr(req.url) # + repr(sorted(req.headers.items()))
|
||||||
|
|
@ -215,7 +215,7 @@ def last_modified_from_file(path: Path):
|
||||||
|
|
||||||
def download(
|
def download(
|
||||||
url: str,
|
url: str,
|
||||||
file_path: Union[Path, str] = None,
|
file_path: Path | str = None,
|
||||||
*,
|
*,
|
||||||
replace_existing: bool = None,
|
replace_existing: bool = None,
|
||||||
only_if_newer: bool = False,
|
only_if_newer: bool = False,
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
import re
|
import re
|
||||||
from typing import Union, cast
|
from typing import cast
|
||||||
|
|
||||||
import ulid
|
import ulid
|
||||||
from ulid.hints import Buffer
|
from ulid.hints import Buffer
|
||||||
|
|
@ -16,7 +16,7 @@ class ULID(ulid.ULID):
|
||||||
|
|
||||||
_pattern = re.compile(r"^[0-9A-HJKMNP-TV-Z]{26}$")
|
_pattern = re.compile(r"^[0-9A-HJKMNP-TV-Z]{26}$")
|
||||||
|
|
||||||
def __init__(self, buffer: Union[Buffer, ulid.ULID, str, None] = None):
|
def __init__(self, buffer: Buffer | ulid.ULID | str | None = None):
|
||||||
if isinstance(buffer, str):
|
if isinstance(buffer, str):
|
||||||
if not self._pattern.search(buffer):
|
if not self._pattern.search(buffer):
|
||||||
raise ValueError("Invalid ULID.")
|
raise ValueError("Invalid ULID.")
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,7 @@ import asyncio
|
||||||
import logging
|
import logging
|
||||||
import secrets
|
import secrets
|
||||||
from json.decoder import JSONDecodeError
|
from json.decoder import JSONDecodeError
|
||||||
from typing import Literal, Optional, overload
|
from typing import Literal, overload
|
||||||
|
|
||||||
from starlette.applications import Starlette
|
from starlette.applications import Starlette
|
||||||
from starlette.authentication import (
|
from starlette.authentication import (
|
||||||
|
|
@ -97,7 +97,7 @@ def yearcomp(s: str):
|
||||||
return comp, int(s)
|
return comp, int(s)
|
||||||
|
|
||||||
|
|
||||||
def as_int(x, *, max: int = None, min: Optional[int] = 1, default: int = None):
|
def as_int(x, *, max: int = None, min: int | None = 1, default: int = None):
|
||||||
try:
|
try:
|
||||||
if not isinstance(x, int):
|
if not isinstance(x, int):
|
||||||
x = int(x)
|
x = int(x)
|
||||||
|
|
@ -158,7 +158,7 @@ def is_admin(request):
|
||||||
return "admin" in request.auth.scopes
|
return "admin" in request.auth.scopes
|
||||||
|
|
||||||
|
|
||||||
async def auth_user(request) -> Optional[User]:
|
async def auth_user(request) -> User | None:
|
||||||
if not isinstance(request.user, AuthedUser):
|
if not isinstance(request.user, AuthedUser):
|
||||||
return
|
return
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
from typing import Container, Iterable, Optional
|
from typing import Container, Iterable
|
||||||
|
|
||||||
from . import imdb, models
|
from . import imdb, models
|
||||||
|
|
||||||
|
|
@ -10,14 +10,14 @@ Score100 = int # [0, 100]
|
||||||
@dataclass
|
@dataclass
|
||||||
class Rating:
|
class Rating:
|
||||||
canonical_title: str
|
canonical_title: str
|
||||||
imdb_score: Optional[Score100]
|
imdb_score: Score100 | None
|
||||||
imdb_votes: Optional[int]
|
imdb_votes: int | None
|
||||||
media_type: str
|
media_type: str
|
||||||
movie_imdb_id: str
|
movie_imdb_id: str
|
||||||
original_title: Optional[str]
|
original_title: str | None
|
||||||
release_year: int
|
release_year: int
|
||||||
user_id: Optional[str]
|
user_id: str | None
|
||||||
user_score: Optional[Score100]
|
user_score: Score100 | None
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def from_movie(cls, movie: models.Movie, *, rating: models.Rating = None):
|
def from_movie(cls, movie: models.Movie, *, rating: models.Rating = None):
|
||||||
|
|
@ -37,11 +37,11 @@ class Rating:
|
||||||
@dataclass
|
@dataclass
|
||||||
class RatingAggregate:
|
class RatingAggregate:
|
||||||
canonical_title: str
|
canonical_title: str
|
||||||
imdb_score: Optional[Score100]
|
imdb_score: Score100 | None
|
||||||
imdb_votes: Optional[int]
|
imdb_votes: int | None
|
||||||
link: URL
|
link: URL
|
||||||
media_type: str
|
media_type: str
|
||||||
original_title: Optional[str]
|
original_title: str | None
|
||||||
user_scores: list[Score100]
|
user_scores: list[Score100]
|
||||||
year: int
|
year: int
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue