mirror of
https://github.com/malmeloo/FindMy.py.git
synced 2026-04-21 08:15:36 +02:00
refactor(reports): improve Serializable base class
This commit is contained in:
@@ -10,17 +10,49 @@ from abc import ABC, abstractmethod
|
|||||||
from datetime import datetime, timezone
|
from datetime import datetime, timezone
|
||||||
from io import BytesIO
|
from io import BytesIO
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import BinaryIO
|
from typing import BinaryIO, Literal, TypedDict, Union
|
||||||
|
|
||||||
from anisette import Anisette, AnisetteHeaders
|
from anisette import Anisette, AnisetteHeaders
|
||||||
from typing_extensions import override
|
from typing_extensions import override
|
||||||
|
|
||||||
from findmy.util.abc import Closable, Serializable
|
from findmy.util.abc import Closable, Serializable
|
||||||
|
from findmy.util.files import read_data_json, save_and_return_json
|
||||||
from findmy.util.http import HttpSession
|
from findmy.util.http import HttpSession
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class RemoteAnisetteMapping(TypedDict):
|
||||||
|
"""JSON mapping representing state of a remote Anisette provider."""
|
||||||
|
|
||||||
|
type: Literal["aniRemote"]
|
||||||
|
url: str
|
||||||
|
|
||||||
|
|
||||||
|
class LocalAnisetteMapping(TypedDict):
|
||||||
|
"""JSON mapping representing state of a local Anisette provider."""
|
||||||
|
|
||||||
|
type: Literal["aniLocal"]
|
||||||
|
prov_data: str
|
||||||
|
|
||||||
|
|
||||||
|
AnisetteMapping = Union[RemoteAnisetteMapping, LocalAnisetteMapping]
|
||||||
|
|
||||||
|
|
||||||
|
def get_provider_from_mapping(
|
||||||
|
mapping: AnisetteMapping,
|
||||||
|
*,
|
||||||
|
libs_path: str | Path | None = None,
|
||||||
|
) -> RemoteAnisetteProvider | LocalAnisetteProvider:
|
||||||
|
"""Get the correct Anisette provider instance from saved JSON data."""
|
||||||
|
if mapping["type"] == "aniRemote":
|
||||||
|
return RemoteAnisetteProvider.from_json(mapping)
|
||||||
|
if mapping["type"] == "aniLocal":
|
||||||
|
return LocalAnisetteProvider.from_json(mapping, libs_path=libs_path)
|
||||||
|
msg = f"Unknown anisette type: {mapping['type']}"
|
||||||
|
raise ValueError(msg)
|
||||||
|
|
||||||
|
|
||||||
class BaseAnisetteProvider(Closable, Serializable, ABC):
|
class BaseAnisetteProvider(Closable, Serializable, ABC):
|
||||||
"""
|
"""
|
||||||
Abstract base class for Anisette providers.
|
Abstract base class for Anisette providers.
|
||||||
@@ -173,20 +205,25 @@ class RemoteAnisetteProvider(BaseAnisetteProvider):
|
|||||||
self._anisette_data_expires_at: float = 0
|
self._anisette_data_expires_at: float = 0
|
||||||
|
|
||||||
@override
|
@override
|
||||||
def serialize(self) -> dict:
|
def to_json(self, dst: str | Path | None = None, /) -> RemoteAnisetteMapping:
|
||||||
"""See `BaseAnisetteProvider.serialize`."""
|
"""See `BaseAnisetteProvider.serialize`."""
|
||||||
return {
|
return save_and_return_json(
|
||||||
"type": "aniRemote",
|
{
|
||||||
"url": self._server_url,
|
"type": "aniRemote",
|
||||||
}
|
"url": self._server_url,
|
||||||
|
},
|
||||||
|
dst,
|
||||||
|
)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
@override
|
@override
|
||||||
def deserialize(cls, data: dict) -> RemoteAnisetteProvider:
|
def from_json(cls, val: str | Path | RemoteAnisetteMapping) -> RemoteAnisetteProvider:
|
||||||
"""See `BaseAnisetteProvider.deserialize`."""
|
"""See `BaseAnisetteProvider.deserialize`."""
|
||||||
assert data["type"] == "aniRemote"
|
val = read_data_json(val)
|
||||||
|
|
||||||
server_url = data["url"]
|
assert val["type"] == "aniRemote"
|
||||||
|
|
||||||
|
server_url = val["url"]
|
||||||
|
|
||||||
return cls(server_url)
|
return cls(server_url)
|
||||||
|
|
||||||
@@ -276,24 +313,34 @@ class LocalAnisetteProvider(BaseAnisetteProvider):
|
|||||||
)
|
)
|
||||||
|
|
||||||
@override
|
@override
|
||||||
def serialize(self) -> dict:
|
def to_json(self, dst: str | Path | None = None, /) -> LocalAnisetteMapping:
|
||||||
"""See `BaseAnisetteProvider.serialize`."""
|
"""See `BaseAnisetteProvider.serialize`."""
|
||||||
with BytesIO() as buf:
|
with BytesIO() as buf:
|
||||||
self._ani.save_provisioning(buf)
|
self._ani.save_provisioning(buf)
|
||||||
prov_data = base64.b64encode(buf.getvalue()).decode("utf-8")
|
prov_data = base64.b64encode(buf.getvalue()).decode("utf-8")
|
||||||
|
|
||||||
return {
|
return save_and_return_json(
|
||||||
"type": "aniLocal",
|
{
|
||||||
"prov_data": prov_data,
|
"type": "aniLocal",
|
||||||
}
|
"prov_data": prov_data,
|
||||||
|
},
|
||||||
|
dst,
|
||||||
|
)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
@override
|
@override
|
||||||
def deserialize(cls, data: dict, libs_path: str | Path | None = None) -> LocalAnisetteProvider:
|
def from_json(
|
||||||
|
cls,
|
||||||
|
val: str | Path | LocalAnisetteMapping,
|
||||||
|
*,
|
||||||
|
libs_path: str | Path | None = None,
|
||||||
|
) -> LocalAnisetteProvider:
|
||||||
"""See `BaseAnisetteProvider.deserialize`."""
|
"""See `BaseAnisetteProvider.deserialize`."""
|
||||||
assert data["type"] == "aniLocal"
|
val = read_data_json(val)
|
||||||
|
|
||||||
state_blob = BytesIO(base64.b64decode(data["prov_data"]))
|
assert val["type"] == "aniLocal"
|
||||||
|
|
||||||
|
state_blob = BytesIO(base64.b64decode(val["prov_data"]))
|
||||||
|
|
||||||
return cls(state_blob=state_blob, libs_path=libs_path)
|
return cls(state_blob=state_blob, libs_path=libs_path)
|
||||||
|
|
||||||
|
|||||||
@@ -5,6 +5,10 @@ from __future__ import annotations
|
|||||||
import asyncio
|
import asyncio
|
||||||
import logging
|
import logging
|
||||||
from abc import ABC, abstractmethod
|
from abc import ABC, abstractmethod
|
||||||
|
from typing import TYPE_CHECKING, Generic, Self, TypeVar
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
logging.getLogger(__name__)
|
logging.getLogger(__name__)
|
||||||
|
|
||||||
@@ -38,16 +42,37 @@ class Closable(ABC):
|
|||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
class Serializable(ABC):
|
T = TypeVar("T", bound=dict)
|
||||||
|
|
||||||
|
|
||||||
|
class Serializable(Generic[T], ABC):
|
||||||
"""ABC for serializable classes."""
|
"""ABC for serializable classes."""
|
||||||
|
|
||||||
@abstractmethod
|
@abstractmethod
|
||||||
def serialize(self) -> dict:
|
def to_json(self, dst: str | Path | None = None, /) -> T:
|
||||||
"""Serialize the object to a JSON-serializable dictionary."""
|
"""
|
||||||
|
Export the current state of the object as a JSON-serializable dictionary.
|
||||||
|
|
||||||
|
If an argument is provided, the output will also be written to that file.
|
||||||
|
|
||||||
|
The output of this method is guaranteed to be JSON-serializable, and passing
|
||||||
|
the return value of this function as an argument to `Serializable.from_json`
|
||||||
|
will always result in an exact copy of the internal state as it was when exported.
|
||||||
|
|
||||||
|
You are encouraged to save and load object states to and from disk whenever possible,
|
||||||
|
to prevent unnecessary API calls or otherwise unexpected behavior.
|
||||||
|
"""
|
||||||
raise NotImplementedError
|
raise NotImplementedError
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
@abstractmethod
|
@abstractmethod
|
||||||
def deserialize(cls, data: dict) -> Serializable:
|
def from_json(cls, val: str | Path | T, /) -> Self:
|
||||||
"""Deserialize the object from a JSON-serializable dictionary."""
|
"""
|
||||||
|
Restore state from a previous `Closable.to_json` export.
|
||||||
|
|
||||||
|
If given a str or Path, it must point to a json file from `Serializable.to_json`.
|
||||||
|
Otherwise, it should be the Mapping itself.
|
||||||
|
|
||||||
|
See `Serializable.to_json` for more information.
|
||||||
|
"""
|
||||||
raise NotImplementedError
|
raise NotImplementedError
|
||||||
|
|||||||
34
findmy/util/files.py
Normal file
34
findmy/util/files.py
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
"""Utilities to simplify reading and writing data from and to files."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import json
|
||||||
|
from collections.abc import Mapping
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import TypeVar, cast
|
||||||
|
|
||||||
|
T = TypeVar("T", bound=Mapping)
|
||||||
|
|
||||||
|
|
||||||
|
def save_and_return_json(data: T, dst: str | Path | None) -> T:
|
||||||
|
"""Save and return a JSON-serializable data structure."""
|
||||||
|
if dst is None:
|
||||||
|
return data
|
||||||
|
|
||||||
|
if isinstance(dst, str):
|
||||||
|
dst = Path(dst)
|
||||||
|
|
||||||
|
dst.write_text(json.dumps(data, indent=4))
|
||||||
|
|
||||||
|
return data
|
||||||
|
|
||||||
|
|
||||||
|
def read_data_json(val: str | Path | T) -> T:
|
||||||
|
"""Read JSON data from a file if a path is passed, or return the argument itself."""
|
||||||
|
if isinstance(val, str):
|
||||||
|
val = Path(val)
|
||||||
|
|
||||||
|
if isinstance(val, Path):
|
||||||
|
val = cast("T", json.loads(val.read_text()))
|
||||||
|
|
||||||
|
return val
|
||||||
Reference in New Issue
Block a user