apexdevkit 1.30.4__tar.gz → 1.32.1__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {apexdevkit-1.30.4 → apexdevkit-1.32.1}/PKG-INFO +3 -4
- {apexdevkit-1.30.4 → apexdevkit-1.32.1}/apexdevkit/fastapi/__init__.py +3 -0
- {apexdevkit-1.30.4 → apexdevkit-1.32.1}/apexdevkit/fastapi/resource.py +1 -1
- {apexdevkit-1.30.4 → apexdevkit-1.32.1}/apexdevkit/fastapi/router.py +34 -1
- {apexdevkit-1.30.4 → apexdevkit-1.32.1}/apexdevkit/key_fn.py +3 -0
- apexdevkit-1.32.1/apexdevkit/sync/__init__.py +19 -0
- apexdevkit-1.32.1/apexdevkit/sync/observer.py +89 -0
- apexdevkit-1.32.1/apexdevkit/sync/source.py +122 -0
- apexdevkit-1.32.1/apexdevkit/sync/sync.py +78 -0
- apexdevkit-1.32.1/apexdevkit/sync/target.py +82 -0
- {apexdevkit-1.30.4 → apexdevkit-1.32.1}/pyproject.toml +13 -13
- {apexdevkit-1.30.4 → apexdevkit-1.32.1}/LICENSE +0 -0
- {apexdevkit-1.30.4 → apexdevkit-1.32.1}/README.md +0 -0
- {apexdevkit-1.30.4 → apexdevkit-1.32.1}/apexdevkit/__init__.py +0 -0
- {apexdevkit-1.30.4 → apexdevkit-1.32.1}/apexdevkit/annotation/__init__.py +0 -0
- {apexdevkit-1.30.4 → apexdevkit-1.32.1}/apexdevkit/annotation/deprecate.py +0 -0
- {apexdevkit-1.30.4 → apexdevkit-1.32.1}/apexdevkit/date.py +0 -0
- {apexdevkit-1.30.4 → apexdevkit-1.32.1}/apexdevkit/environment.py +0 -0
- {apexdevkit-1.30.4 → apexdevkit-1.32.1}/apexdevkit/error.py +0 -0
- {apexdevkit-1.30.4 → apexdevkit-1.32.1}/apexdevkit/fastapi/builder.py +0 -0
- {apexdevkit-1.30.4 → apexdevkit-1.32.1}/apexdevkit/fastapi/dependable.py +0 -0
- {apexdevkit-1.30.4 → apexdevkit-1.32.1}/apexdevkit/fastapi/docs.py +0 -0
- {apexdevkit-1.30.4 → apexdevkit-1.32.1}/apexdevkit/fastapi/name.py +0 -0
- {apexdevkit-1.30.4 → apexdevkit-1.32.1}/apexdevkit/fastapi/response.py +0 -0
- {apexdevkit-1.30.4 → apexdevkit-1.32.1}/apexdevkit/fastapi/schema.py +0 -0
- {apexdevkit-1.30.4 → apexdevkit-1.32.1}/apexdevkit/fastapi/service.py +0 -0
- {apexdevkit-1.30.4 → apexdevkit-1.32.1}/apexdevkit/fluent.py +0 -0
- {apexdevkit-1.30.4 → apexdevkit-1.32.1}/apexdevkit/formatter.py +0 -0
- {apexdevkit-1.30.4 → apexdevkit-1.32.1}/apexdevkit/http/__init__.py +0 -0
- {apexdevkit-1.30.4 → apexdevkit-1.32.1}/apexdevkit/http/fake.py +0 -0
- {apexdevkit-1.30.4 → apexdevkit-1.32.1}/apexdevkit/http/fluent.py +0 -0
- {apexdevkit-1.30.4 → apexdevkit-1.32.1}/apexdevkit/http/httpx/__init__.py +0 -0
- {apexdevkit-1.30.4 → apexdevkit-1.32.1}/apexdevkit/http/httpx/client.py +0 -0
- {apexdevkit-1.30.4 → apexdevkit-1.32.1}/apexdevkit/http/httpx/hooks.py +0 -0
- {apexdevkit-1.30.4 → apexdevkit-1.32.1}/apexdevkit/http/json.py +0 -0
- {apexdevkit-1.30.4 → apexdevkit-1.32.1}/apexdevkit/http/url.py +0 -0
- {apexdevkit-1.30.4 → apexdevkit-1.32.1}/apexdevkit/py.typed +0 -0
- {apexdevkit-1.30.4 → apexdevkit-1.32.1}/apexdevkit/query/__init__.py +0 -0
- {apexdevkit-1.30.4 → apexdevkit-1.32.1}/apexdevkit/query/generator.py +0 -0
- {apexdevkit-1.30.4 → apexdevkit-1.32.1}/apexdevkit/query/query.py +0 -0
- {apexdevkit-1.30.4 → apexdevkit-1.32.1}/apexdevkit/repository/__init__.py +0 -0
- {apexdevkit-1.30.4 → apexdevkit-1.32.1}/apexdevkit/repository/core/__init__.py +0 -0
- {apexdevkit-1.30.4 → apexdevkit-1.32.1}/apexdevkit/repository/core/database.py +0 -0
- {apexdevkit-1.30.4 → apexdevkit-1.32.1}/apexdevkit/repository/core/decorator.py +0 -0
- {apexdevkit-1.30.4 → apexdevkit-1.32.1}/apexdevkit/repository/core/interface.py +0 -0
- {apexdevkit-1.30.4 → apexdevkit-1.32.1}/apexdevkit/repository/core/mixin.py +0 -0
- {apexdevkit-1.30.4 → apexdevkit-1.32.1}/apexdevkit/repository/core/multi.py +0 -0
- {apexdevkit-1.30.4 → apexdevkit-1.32.1}/apexdevkit/repository/in_memory/__init__.py +0 -0
- {apexdevkit-1.30.4 → apexdevkit-1.32.1}/apexdevkit/repository/in_memory/mixin.py +0 -0
- {apexdevkit-1.30.4 → apexdevkit-1.32.1}/apexdevkit/repository/in_memory/repository.py +0 -0
- {apexdevkit-1.30.4 → apexdevkit-1.32.1}/apexdevkit/repository/in_memory/store.py +0 -0
- {apexdevkit-1.30.4 → apexdevkit-1.32.1}/apexdevkit/repository/sql/__init__.py +0 -0
- {apexdevkit-1.30.4 → apexdevkit-1.32.1}/apexdevkit/repository/sql/connector.py +0 -0
- {apexdevkit-1.30.4 → apexdevkit-1.32.1}/apexdevkit/repository/sql/field.py +0 -0
- {apexdevkit-1.30.4 → apexdevkit-1.32.1}/apexdevkit/repository/sql/mssql.py +0 -0
- {apexdevkit-1.30.4 → apexdevkit-1.32.1}/apexdevkit/repository/sql/sqlite.py +0 -0
- {apexdevkit-1.30.4 → apexdevkit-1.32.1}/apexdevkit/security/__init__.py +0 -0
- {apexdevkit-1.30.4 → apexdevkit-1.32.1}/apexdevkit/security/core.py +0 -0
- {apexdevkit-1.30.4 → apexdevkit-1.32.1}/apexdevkit/security/hashing.py +0 -0
- {apexdevkit-1.30.4 → apexdevkit-1.32.1}/apexdevkit/server.py +0 -0
- {apexdevkit-1.30.4 → apexdevkit-1.32.1}/apexdevkit/testing/__init__.py +0 -0
- {apexdevkit-1.30.4 → apexdevkit-1.32.1}/apexdevkit/testing/database.py +0 -0
- {apexdevkit-1.30.4 → apexdevkit-1.32.1}/apexdevkit/testing/fake.py +0 -0
- {apexdevkit-1.30.4 → apexdevkit-1.32.1}/apexdevkit/testing/rest.py +0 -0
- {apexdevkit-1.30.4 → apexdevkit-1.32.1}/apexdevkit/value.py +0 -0
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: apexdevkit
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.32.1
|
|
4
4
|
Summary: Apex Development Tools for python.
|
|
5
5
|
License-File: LICENSE
|
|
6
6
|
Author: Apex Dev
|
|
7
7
|
Author-email: dev@apex.ge
|
|
8
|
-
Requires-Python: >=3.11
|
|
8
|
+
Requires-Python: >=3.11,<4
|
|
9
9
|
Classifier: Programming Language :: Python :: 3
|
|
10
10
|
Classifier: Programming Language :: Python :: 3.11
|
|
11
11
|
Classifier: Programming Language :: Python :: 3.12
|
|
@@ -13,11 +13,10 @@ Classifier: Programming Language :: Python :: 3.13
|
|
|
13
13
|
Classifier: Programming Language :: Python :: 3.14
|
|
14
14
|
Requires-Dist: fastapi
|
|
15
15
|
Requires-Dist: httpx
|
|
16
|
+
Requires-Dist: pydantic
|
|
16
17
|
Requires-Dist: pymssql (==2.*)
|
|
17
18
|
Requires-Dist: python-dotenv
|
|
18
19
|
Requires-Dist: sentry-sdk[fastapi]
|
|
19
|
-
Requires-Dist: tomlkit
|
|
20
|
-
Requires-Dist: typer
|
|
21
20
|
Requires-Dist: uvicorn
|
|
22
21
|
Description-Content-Type: text/markdown
|
|
23
22
|
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
from apexdevkit.fastapi.builder import FastApiBuilder, RestfulServiceBuilder
|
|
2
2
|
from apexdevkit.fastapi.dependable import inject
|
|
3
|
+
from apexdevkit.fastapi.router import RestfulRouter, RouterWithHiddenUnderscoreRoutes
|
|
3
4
|
from apexdevkit.fastapi.service import RestfulRepository
|
|
4
5
|
|
|
5
6
|
__all__ = [
|
|
@@ -7,4 +8,6 @@ __all__ = [
|
|
|
7
8
|
"RestfulServiceBuilder",
|
|
8
9
|
"inject",
|
|
9
10
|
"RestfulRepository",
|
|
11
|
+
"RestfulRouter",
|
|
12
|
+
"RouterWithHiddenUnderscoreRoutes",
|
|
10
13
|
]
|
|
@@ -3,8 +3,8 @@ from collections.abc import Callable
|
|
|
3
3
|
from dataclasses import dataclass
|
|
4
4
|
from typing import Any, Union
|
|
5
5
|
|
|
6
|
+
from fastapi.responses import JSONResponse
|
|
6
7
|
from pydantic import BaseModel
|
|
7
|
-
from starlette.responses import JSONResponse
|
|
8
8
|
|
|
9
9
|
from apexdevkit.error import DoesNotExistError, ExistsError, ForbiddenError
|
|
10
10
|
from apexdevkit.fastapi.response import RestfulResponse
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
from collections.abc import Callable
|
|
1
2
|
from dataclasses import dataclass, field
|
|
2
3
|
from enum import Enum
|
|
3
4
|
from typing import Annotated, Any, Protocol, Self, TypeVar
|
|
@@ -12,6 +13,38 @@ from apexdevkit.fastapi.schema import RestfulSchema, Schema, SchemaFields
|
|
|
12
13
|
from apexdevkit.fastapi.service import RawCollection, RawItem, RestfulService
|
|
13
14
|
from apexdevkit.fluent import FluentDict
|
|
14
15
|
|
|
16
|
+
|
|
17
|
+
class RouterWithHiddenUnderscoreRoutes(APIRouter):
|
|
18
|
+
def add_api_route(
|
|
19
|
+
self,
|
|
20
|
+
path: str,
|
|
21
|
+
endpoint: Callable[..., Any],
|
|
22
|
+
**kwargs: Any,
|
|
23
|
+
) -> None:
|
|
24
|
+
super().add_api_route(path, endpoint, **kwargs)
|
|
25
|
+
self._add_underscored_api_route(path, endpoint, kwargs.copy())
|
|
26
|
+
|
|
27
|
+
def _add_underscored_api_route(
|
|
28
|
+
self,
|
|
29
|
+
path: str,
|
|
30
|
+
endpoint: Callable[..., Any],
|
|
31
|
+
kwargs: dict[str, Any],
|
|
32
|
+
) -> None:
|
|
33
|
+
if "-" not in path:
|
|
34
|
+
return
|
|
35
|
+
|
|
36
|
+
op_id = kwargs.get("operation_id", endpoint.__name__) or endpoint.__name__
|
|
37
|
+
kwargs["operation_id"] = f"{op_id}_alias"
|
|
38
|
+
kwargs["include_in_schema"] = False
|
|
39
|
+
|
|
40
|
+
# Register the underscored route pointing to the same endpoint
|
|
41
|
+
super().add_api_route(
|
|
42
|
+
path=path.replace("-", "_"),
|
|
43
|
+
endpoint=endpoint,
|
|
44
|
+
**kwargs,
|
|
45
|
+
)
|
|
46
|
+
|
|
47
|
+
|
|
15
48
|
_Response = JSONResponse | dict[str, Any]
|
|
16
49
|
|
|
17
50
|
T = TypeVar("T")
|
|
@@ -27,7 +60,7 @@ class RestfulRouter:
|
|
|
27
60
|
name: RestfulName
|
|
28
61
|
|
|
29
62
|
parent: RestfulName | None = None
|
|
30
|
-
router: APIRouter = field(default_factory=
|
|
63
|
+
router: APIRouter = field(default_factory=RouterWithHiddenUnderscoreRoutes)
|
|
31
64
|
|
|
32
65
|
_schema: RestfulSchema = field(init=False)
|
|
33
66
|
_dependency: Dependency | None = field(init=False, default=None)
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
from .observer import SyncObserver
|
|
2
|
+
from .source import EmptySource, Source, SourceDecorator, SourceFailing, SourcePreSet
|
|
3
|
+
from .sync import Sync, SyncProbe
|
|
4
|
+
from .target import NoTarget, Target, TargetDecorator, TargetFailing
|
|
5
|
+
|
|
6
|
+
__all__ = [
|
|
7
|
+
"Sync",
|
|
8
|
+
"SyncObserver",
|
|
9
|
+
"SyncProbe",
|
|
10
|
+
"EmptySource",
|
|
11
|
+
"Source",
|
|
12
|
+
"SourceDecorator",
|
|
13
|
+
"SourceFailing",
|
|
14
|
+
"SourcePreSet",
|
|
15
|
+
"NoTarget",
|
|
16
|
+
"Target",
|
|
17
|
+
"TargetDecorator",
|
|
18
|
+
"TargetFailing",
|
|
19
|
+
]
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from collections.abc import Callable
|
|
4
|
+
from dataclasses import dataclass, field
|
|
5
|
+
from typing import Protocol, Self
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
@dataclass(frozen=True)
|
|
9
|
+
class ObservableSync:
|
|
10
|
+
observers: list[SyncObserver] = field(default_factory=list)
|
|
11
|
+
|
|
12
|
+
def attach(self, observer: SyncObserver, when: bool = True) -> Self:
|
|
13
|
+
if when:
|
|
14
|
+
self.observers.append(observer)
|
|
15
|
+
|
|
16
|
+
return self
|
|
17
|
+
|
|
18
|
+
def before_prune(self, n_items: int) -> None:
|
|
19
|
+
for observer in self.observers:
|
|
20
|
+
observer.before_prune(n_items)
|
|
21
|
+
|
|
22
|
+
def after_prune(self, n_items: int) -> None:
|
|
23
|
+
_ = n_items
|
|
24
|
+
|
|
25
|
+
for observer in self.observers:
|
|
26
|
+
observer.after_prune()
|
|
27
|
+
|
|
28
|
+
def before_load(self, n_items: int) -> None:
|
|
29
|
+
for observer in self.observers:
|
|
30
|
+
observer.before_load(n_items)
|
|
31
|
+
|
|
32
|
+
def after_load(self, n_items: int) -> None:
|
|
33
|
+
_ = n_items
|
|
34
|
+
|
|
35
|
+
for observer in self.observers:
|
|
36
|
+
observer.after_load()
|
|
37
|
+
|
|
38
|
+
def before_update(self, n_items: int) -> None:
|
|
39
|
+
for observer in self.observers:
|
|
40
|
+
observer.before_update(n_items)
|
|
41
|
+
|
|
42
|
+
def after_update(self, n_items: int) -> None:
|
|
43
|
+
_ = n_items
|
|
44
|
+
|
|
45
|
+
for observer in self.observers:
|
|
46
|
+
observer.after_update()
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
class SyncObserver(Protocol): # pragma: no cover
|
|
50
|
+
def before_prune(self, n_items: int) -> None:
|
|
51
|
+
pass
|
|
52
|
+
|
|
53
|
+
def after_prune(self) -> None:
|
|
54
|
+
pass
|
|
55
|
+
|
|
56
|
+
def before_load(self, n_items: int) -> None:
|
|
57
|
+
pass
|
|
58
|
+
|
|
59
|
+
def after_load(self) -> None:
|
|
60
|
+
pass
|
|
61
|
+
|
|
62
|
+
def before_update(self, n_items: int) -> None:
|
|
63
|
+
pass
|
|
64
|
+
|
|
65
|
+
def after_update(self) -> None:
|
|
66
|
+
pass
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
@dataclass(frozen=True)
|
|
70
|
+
class DefaultLog:
|
|
71
|
+
echo: Callable[[str], None] = print
|
|
72
|
+
|
|
73
|
+
def before_prune(self, n_items: int) -> None:
|
|
74
|
+
self.echo(f"Total # of absent items: {n_items}")
|
|
75
|
+
|
|
76
|
+
def after_prune(self) -> None:
|
|
77
|
+
self.echo("Pruning completed")
|
|
78
|
+
|
|
79
|
+
def before_load(self, n_items: int) -> None:
|
|
80
|
+
self.echo(f"Total # of new items: {n_items}")
|
|
81
|
+
|
|
82
|
+
def after_load(self) -> None:
|
|
83
|
+
self.echo("Loading completed")
|
|
84
|
+
|
|
85
|
+
def before_update(self, n_items: int) -> None:
|
|
86
|
+
self.echo(f"Total # of updates: {n_items}")
|
|
87
|
+
|
|
88
|
+
def after_update(self) -> None:
|
|
89
|
+
self.echo("Updating completed")
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from collections.abc import Iterable
|
|
4
|
+
from dataclasses import dataclass, field
|
|
5
|
+
from typing import Generic, Protocol, TypeVar
|
|
6
|
+
|
|
7
|
+
from apexdevkit.key_fn import AttributeKey, KeyFn
|
|
8
|
+
|
|
9
|
+
T = TypeVar("T")
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class Source(Protocol): # pragma: no cover
|
|
13
|
+
def absent(self) -> Iterable[T]:
|
|
14
|
+
pass
|
|
15
|
+
|
|
16
|
+
def new(self) -> Iterable[T]:
|
|
17
|
+
pass
|
|
18
|
+
|
|
19
|
+
def updates(self) -> Iterable[T]:
|
|
20
|
+
pass
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
@dataclass(frozen=True)
|
|
24
|
+
class EmptySource: # pragma: no cover
|
|
25
|
+
def absent(self) -> Iterable[T]:
|
|
26
|
+
return []
|
|
27
|
+
|
|
28
|
+
def new(self) -> Iterable[T]:
|
|
29
|
+
return []
|
|
30
|
+
|
|
31
|
+
def updates(self) -> Iterable[T]:
|
|
32
|
+
return []
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
@dataclass(frozen=True, kw_only=True)
|
|
36
|
+
class SourceDecorator: # pragma: no cover
|
|
37
|
+
inner: Source = field(default_factory=EmptySource)
|
|
38
|
+
|
|
39
|
+
def absent(self) -> Iterable[T]:
|
|
40
|
+
return self.inner.absent()
|
|
41
|
+
|
|
42
|
+
def new(self) -> Iterable[T]:
|
|
43
|
+
return self.inner.new()
|
|
44
|
+
|
|
45
|
+
def updates(self) -> Iterable[T]:
|
|
46
|
+
return self.inner.updates()
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
@dataclass(frozen=True, kw_only=True)
|
|
50
|
+
class SourcePreSet(Generic[T]):
|
|
51
|
+
removals: Iterable[T]
|
|
52
|
+
additions: Iterable[T]
|
|
53
|
+
changes: Iterable[T]
|
|
54
|
+
|
|
55
|
+
def absent(self) -> Iterable[T]:
|
|
56
|
+
return self.removals
|
|
57
|
+
|
|
58
|
+
def new(self) -> Iterable[T]:
|
|
59
|
+
return self.additions
|
|
60
|
+
|
|
61
|
+
def updates(self) -> Iterable[T]:
|
|
62
|
+
return self.changes
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
class SourceFailing:
|
|
66
|
+
@staticmethod
|
|
67
|
+
def on_everything(using: Source | None = None) -> Source:
|
|
68
|
+
return SourceFailing.on_absent(
|
|
69
|
+
using=SourceFailing.on_new(
|
|
70
|
+
using=SourceFailing.on_update(
|
|
71
|
+
using=using or EmptySource(),
|
|
72
|
+
),
|
|
73
|
+
),
|
|
74
|
+
)
|
|
75
|
+
|
|
76
|
+
@staticmethod
|
|
77
|
+
def on_absent(using: Source | None = None) -> Source:
|
|
78
|
+
return SourceFailing.OnAbsent(inner=using or EmptySource())
|
|
79
|
+
|
|
80
|
+
@dataclass(frozen=True, kw_only=True)
|
|
81
|
+
class OnAbsent(SourceDecorator):
|
|
82
|
+
def absent(self) -> Iterable[T]: # pragma: no cover
|
|
83
|
+
raise RuntimeError("Should not request absent!")
|
|
84
|
+
|
|
85
|
+
@staticmethod
|
|
86
|
+
def on_new(using: Source | None = None) -> Source:
|
|
87
|
+
return SourceFailing.OnNew(inner=using or EmptySource())
|
|
88
|
+
|
|
89
|
+
@dataclass(frozen=True, kw_only=True)
|
|
90
|
+
class OnNew(SourceDecorator):
|
|
91
|
+
def new(self) -> Iterable[T]: # pragma: no cover
|
|
92
|
+
raise RuntimeError("Should not request new!")
|
|
93
|
+
|
|
94
|
+
@staticmethod
|
|
95
|
+
def on_update(using: Source | None = None) -> Source:
|
|
96
|
+
return SourceFailing.OnUpdate(inner=using or EmptySource())
|
|
97
|
+
|
|
98
|
+
@dataclass(frozen=True, kw_only=True)
|
|
99
|
+
class OnUpdate(SourceDecorator):
|
|
100
|
+
def updates(self) -> Iterable[T]: # pragma: no cover
|
|
101
|
+
raise RuntimeError("Should not request updates!")
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
@dataclass(frozen=True, kw_only=True)
|
|
105
|
+
class SourceDiscriminator(Generic[T]):
|
|
106
|
+
current: Iterable[T]
|
|
107
|
+
latest: Iterable[T]
|
|
108
|
+
|
|
109
|
+
key_fn: KeyFn = AttributeKey("id")
|
|
110
|
+
|
|
111
|
+
def absent(self) -> Iterable[T]:
|
|
112
|
+
index = {self.key_fn(item) for item in self.latest}
|
|
113
|
+
|
|
114
|
+
return [item for item in self.current if self.key_fn(item) not in index]
|
|
115
|
+
|
|
116
|
+
def new(self) -> Iterable[T]:
|
|
117
|
+
index = {self.key_fn(item) for item in self.current}
|
|
118
|
+
|
|
119
|
+
return [item for item in self.latest if self.key_fn(item) not in index]
|
|
120
|
+
|
|
121
|
+
def updates(self) -> Iterable[T]:
|
|
122
|
+
return set(self.latest).difference(self.current)
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from collections.abc import Callable, Collection, Iterable
|
|
4
|
+
from dataclasses import dataclass
|
|
5
|
+
from typing import Any, Self
|
|
6
|
+
|
|
7
|
+
from .observer import DefaultLog, ObservableSync
|
|
8
|
+
from .source import Source
|
|
9
|
+
from .target import Target
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
@dataclass(frozen=True, kw_only=True)
|
|
13
|
+
class Sync(ObservableSync):
|
|
14
|
+
source: Source
|
|
15
|
+
target: Target
|
|
16
|
+
|
|
17
|
+
dry_run: bool = False
|
|
18
|
+
|
|
19
|
+
def with_log(self, using: Callable[[str], None]) -> Self:
|
|
20
|
+
return self.attach(DefaultLog(echo=using))
|
|
21
|
+
|
|
22
|
+
def run(self, prune: bool = True, load: bool = True, update: bool = True) -> None:
|
|
23
|
+
if prune:
|
|
24
|
+
self.prune()
|
|
25
|
+
|
|
26
|
+
if load:
|
|
27
|
+
self.load()
|
|
28
|
+
|
|
29
|
+
if update:
|
|
30
|
+
self.update()
|
|
31
|
+
|
|
32
|
+
def prune(self) -> None:
|
|
33
|
+
(
|
|
34
|
+
SyncProbe(source=set(self.source.absent()))
|
|
35
|
+
.disable(when=self.dry_run)
|
|
36
|
+
.notify(using=self.before_prune)
|
|
37
|
+
.send(using=self.target.prune)
|
|
38
|
+
.notify(using=self.after_prune)
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
def load(self) -> None:
|
|
42
|
+
(
|
|
43
|
+
SyncProbe(source=set(self.source.new()))
|
|
44
|
+
.disable(when=self.dry_run)
|
|
45
|
+
.notify(using=self.before_load)
|
|
46
|
+
.send(using=self.target.load)
|
|
47
|
+
.notify(using=self.after_load)
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
def update(self) -> None:
|
|
51
|
+
(
|
|
52
|
+
SyncProbe(source=set(self.source.updates()))
|
|
53
|
+
.disable(when=self.dry_run)
|
|
54
|
+
.notify(using=self.before_update)
|
|
55
|
+
.send(using=self.target.renew)
|
|
56
|
+
.notify(using=self.after_update)
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
@dataclass(frozen=True, kw_only=True)
|
|
61
|
+
class SyncProbe:
|
|
62
|
+
source: Collection[Any]
|
|
63
|
+
|
|
64
|
+
is_enabled: bool = True
|
|
65
|
+
|
|
66
|
+
def disable(self, when: bool) -> SyncProbe:
|
|
67
|
+
return SyncProbe(source=self.source, is_enabled=not when)
|
|
68
|
+
|
|
69
|
+
def notify(self, using: Callable[[int], None]) -> SyncProbe:
|
|
70
|
+
using(len(self.source))
|
|
71
|
+
|
|
72
|
+
return self
|
|
73
|
+
|
|
74
|
+
def send(self, using: Callable[[Iterable[Any]], None]) -> SyncProbe:
|
|
75
|
+
if self.is_enabled and len(self.source) > 0:
|
|
76
|
+
using(self.source)
|
|
77
|
+
|
|
78
|
+
return self
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from collections.abc import Iterable
|
|
4
|
+
from dataclasses import dataclass, field
|
|
5
|
+
from typing import Protocol, TypeVar
|
|
6
|
+
|
|
7
|
+
T = TypeVar("T")
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class Target(Protocol): # pragma: no cover
|
|
11
|
+
def prune(self, source: Iterable[T]) -> None:
|
|
12
|
+
pass
|
|
13
|
+
|
|
14
|
+
def load(self, source: Iterable[T]) -> None:
|
|
15
|
+
pass
|
|
16
|
+
|
|
17
|
+
def renew(self, source: Iterable[T]) -> None:
|
|
18
|
+
pass
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class NoTarget: # pragma: no cover
|
|
22
|
+
def prune(self, source: Iterable[T]) -> None:
|
|
23
|
+
pass
|
|
24
|
+
|
|
25
|
+
def load(self, source: Iterable[T]) -> None:
|
|
26
|
+
pass
|
|
27
|
+
|
|
28
|
+
def renew(self, source: Iterable[T]) -> None:
|
|
29
|
+
pass
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
@dataclass(frozen=True, kw_only=True)
|
|
33
|
+
class TargetDecorator:
|
|
34
|
+
inner: Target = field(default_factory=NoTarget)
|
|
35
|
+
|
|
36
|
+
def prune(self, source: Iterable[T]) -> None:
|
|
37
|
+
self.inner.prune(source)
|
|
38
|
+
|
|
39
|
+
def load(self, source: Iterable[T]) -> None:
|
|
40
|
+
self.inner.load(source)
|
|
41
|
+
|
|
42
|
+
def renew(self, source: Iterable[T]) -> None:
|
|
43
|
+
self.inner.renew(source)
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
class TargetFailing:
|
|
47
|
+
@staticmethod
|
|
48
|
+
def on_everything(using: Target | None = None) -> Target:
|
|
49
|
+
return TargetFailing.on_prune(
|
|
50
|
+
using=TargetFailing.on_load(
|
|
51
|
+
using=TargetFailing.on_update(
|
|
52
|
+
using=using or NoTarget(),
|
|
53
|
+
),
|
|
54
|
+
),
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
@staticmethod
|
|
58
|
+
def on_prune(using: Target | None = None) -> Target:
|
|
59
|
+
return TargetFailing.OnPrune(inner=using or NoTarget())
|
|
60
|
+
|
|
61
|
+
@dataclass(frozen=True, kw_only=True)
|
|
62
|
+
class OnPrune(TargetDecorator):
|
|
63
|
+
def prune(self, source: Iterable[T]) -> None: # pragma: no cover
|
|
64
|
+
raise RuntimeError(source)
|
|
65
|
+
|
|
66
|
+
@staticmethod
|
|
67
|
+
def on_load(using: Target | None = None) -> Target:
|
|
68
|
+
return TargetFailing.OnLoad(inner=using or NoTarget())
|
|
69
|
+
|
|
70
|
+
@dataclass(frozen=True, kw_only=True)
|
|
71
|
+
class OnLoad(TargetDecorator):
|
|
72
|
+
def load(self, source: Iterable[T]) -> None: # pragma: no cover
|
|
73
|
+
raise RuntimeError(source)
|
|
74
|
+
|
|
75
|
+
@staticmethod
|
|
76
|
+
def on_update(using: Target | None = None) -> Target:
|
|
77
|
+
return TargetFailing.OnUpdate(inner=using or NoTarget())
|
|
78
|
+
|
|
79
|
+
@dataclass(frozen=True, kw_only=True)
|
|
80
|
+
class OnUpdate(TargetDecorator):
|
|
81
|
+
def renew(self, source: Iterable[T]) -> None: # pragma: no cover
|
|
82
|
+
raise RuntimeError(source)
|
|
@@ -1,38 +1,33 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "apexdevkit"
|
|
3
|
-
version = "1.
|
|
3
|
+
version = "1.32.1"
|
|
4
4
|
description = "Apex Development Tools for python."
|
|
5
5
|
readme = "README.md"
|
|
6
6
|
authors = [
|
|
7
7
|
{ name = "Apex Dev", email = "dev@apex.ge" }
|
|
8
8
|
]
|
|
9
9
|
dynamic = ["dependencies"]
|
|
10
|
-
requires-python = ">=3.11"
|
|
10
|
+
requires-python = ">=3.11,<4"
|
|
11
11
|
|
|
12
12
|
[tool.poetry.dependencies]
|
|
13
|
-
httpx = "*"
|
|
14
13
|
fastapi = "*"
|
|
15
|
-
|
|
16
|
-
sentry-sdk = { extras = ["fastapi"], version = "*" }
|
|
14
|
+
httpx = "*"
|
|
17
15
|
python-dotenv = "*"
|
|
18
16
|
pymssql = "2.*"
|
|
19
|
-
|
|
20
|
-
|
|
17
|
+
pydantic = "*"
|
|
18
|
+
sentry-sdk = { extras = ["fastapi"], version = "*" }
|
|
19
|
+
uvicorn = "*"
|
|
21
20
|
|
|
22
21
|
[tool.poetry.group.dev.dependencies]
|
|
23
|
-
|
|
22
|
+
apexcodexpy = "0.6.5"
|
|
24
23
|
pytest = "*"
|
|
25
24
|
pytest-approvaltests = "*"
|
|
26
25
|
pytest-cov = "*"
|
|
27
|
-
pytest-env = "
|
|
26
|
+
pytest-env = "*"
|
|
28
27
|
pytest-recording = "*"
|
|
29
28
|
coverage = "*"
|
|
30
29
|
faker = "*"
|
|
31
30
|
|
|
32
|
-
|
|
33
|
-
[tool.poetry.group.lint.dependencies]
|
|
34
|
-
apexcodexpy = "0.4.1"
|
|
35
|
-
|
|
36
31
|
[tool.mypy]
|
|
37
32
|
ignore_missing_imports = true
|
|
38
33
|
strict = true
|
|
@@ -43,6 +38,11 @@ exclude = [
|
|
|
43
38
|
show_error_codes = true
|
|
44
39
|
pretty = true
|
|
45
40
|
|
|
41
|
+
[tool.deptry]
|
|
42
|
+
extend_exclude = [
|
|
43
|
+
"apexdevkit/testing",
|
|
44
|
+
]
|
|
45
|
+
|
|
46
46
|
[tool.ruff.lint]
|
|
47
47
|
select = [
|
|
48
48
|
"A",
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|