python-corekit 0.1.0__py3-none-any.whl → 0.2.0__py3-none-any.whl
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.
- corekit/api/__init__.py +18 -3
- corekit/api/application.py +237 -0
- corekit/api/lifespan.py +210 -0
- corekit/api/middleware.py +93 -0
- corekit/api/routers.py +109 -1
- corekit/concurrency/worker.py +65 -65
- corekit/config/settings.py +3 -3
- corekit/connections/sql/__init__.py +31 -3
- corekit/connections/sql/connection.py +19 -0
- corekit/connections/sql/migration/__init__.py +5 -5
- corekit/connections/sql/migration/base.py +3 -3
- corekit/connections/sql/migration/operations.py +66 -42
- corekit/connections/sql/migration/registry.py +2 -2
- corekit/connections/sql/operations/__init__.py +24 -0
- corekit/connections/sql/operations/base.py +102 -0
- corekit/connections/sql/operations/statements.py +150 -0
- corekit/connections/sql/query.py +4 -62
- corekit/connections/sql/table.py +30 -4
- corekit/constants.py +45 -45
- corekit/crypto/constants.py +4 -4
- corekit/data/__init__.py +8 -0
- corekit/data/expressions/__init__.py +10 -2
- corekit/data/expressions/comparison.py +184 -104
- corekit/data/expressions/expression.py +103 -98
- corekit/data/expressions/operator.py +54 -0
- corekit/data/expressions/target.py +21 -0
- corekit/data/record.py +147 -147
- corekit/data/stats.py +159 -157
- corekit/decorators/__init__.py +2 -2
- corekit/decorators/exception_handling.py +2 -1
- corekit/etl/connection.py +44 -44
- corekit/events/websocket.py +3 -2
- corekit/exceptions/__init__.py +18 -0
- corekit/http/__init__.py +13 -0
- corekit/jobs/__init__.py +26 -0
- corekit/jobs/registry.py +87 -0
- corekit/jobs/runner.py +69 -0
- corekit/jobs/task.py +152 -0
- corekit/observability/__init__.py +5 -3
- corekit/observability/request_context.py +135 -0
- corekit/registry/__init__.py +11 -6
- corekit/registry/ordered.py +86 -0
- corekit/schemas/__init__.py +10 -0
- corekit/schemas/enum.py +49 -49
- corekit/schemas/models/arbitrary.py +11 -11
- corekit/schemas/pydantic/fields.py +35 -35
- corekit/schemas/types.py +40 -40
- corekit/serialization/__init__.py +22 -0
- corekit/serialization/serializer.py +1 -1
- corekit/utils/__init__.py +59 -5
- corekit/utils/coercion.py +118 -0
- corekit/utils/collections.py +115 -0
- corekit/utils/ids.py +61 -5
- corekit/utils/payload.py +100 -0
- corekit/utils/raise_exc.py +8 -8
- corekit/utils/text.py +56 -0
- corekit/utils/time.py +74 -21
- corekit/utils/validators.py +15 -15
- corekit/utils/void.py +8 -8
- {python_corekit-0.1.0.dist-info → python_corekit-0.2.0.dist-info}/METADATA +105 -100
- {python_corekit-0.1.0.dist-info → python_corekit-0.2.0.dist-info}/RECORD +64 -46
- {python_corekit-0.1.0.dist-info → python_corekit-0.2.0.dist-info}/WHEEL +0 -0
- {python_corekit-0.1.0.dist-info → python_corekit-0.2.0.dist-info}/licenses/LICENSE +0 -0
- {python_corekit-0.1.0.dist-info → python_corekit-0.2.0.dist-info}/top_level.txt +0 -0
corekit/jobs/runner.py
ADDED
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
"""
|
|
2
|
+
The worker entrypoint.
|
|
3
|
+
|
|
4
|
+
``run_task`` is what a queue calls. It takes a name and a JSON payload, builds
|
|
5
|
+
the task from the worker's own registry and runs it -- so a queue entry selects
|
|
6
|
+
among the tasks this worker already has, and cannot introduce code.
|
|
7
|
+
|
|
8
|
+
Point the queue at this function rather than at a bound method::
|
|
9
|
+
|
|
10
|
+
queue.enqueue(run_task, "NightlyBackup", task.payload("db"))
|
|
11
|
+
|
|
12
|
+
Enqueueing a bound method makes the queue serialize the instance, which is the
|
|
13
|
+
thing worth avoiding.
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
import logging
|
|
17
|
+
from typing import Any
|
|
18
|
+
|
|
19
|
+
from corekit.jobs.registry import task_registry
|
|
20
|
+
from corekit.utils.payload import decode_payload
|
|
21
|
+
|
|
22
|
+
__all__ = ["run_task"]
|
|
23
|
+
|
|
24
|
+
logger = logging.getLogger(__name__)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def run_task(task_name: str, payload: str | bytes | None = None) -> Any:
|
|
28
|
+
"""
|
|
29
|
+
Build the named task and run it.
|
|
30
|
+
|
|
31
|
+
``success`` runs after ``task_function`` returns; ``failure`` runs if it
|
|
32
|
+
raises, and the original exception is re-raised so the queue records the
|
|
33
|
+
job as failed. A hook that raises does not mask the original error.
|
|
34
|
+
|
|
35
|
+
Args:
|
|
36
|
+
task_name: The registered name of the task to run.
|
|
37
|
+
payload: JSON arguments from ``Task.payload``. Empty means no arguments.
|
|
38
|
+
|
|
39
|
+
Returns:
|
|
40
|
+
Whatever ``task_function`` returns.
|
|
41
|
+
|
|
42
|
+
Raises:
|
|
43
|
+
TaskNotRegisteredError: If no task is registered under that name.
|
|
44
|
+
ValueError: If the payload is not the expected JSON object.
|
|
45
|
+
"""
|
|
46
|
+
args, kwargs = decode_payload(payload)
|
|
47
|
+
task = task_registry.build(task_name)
|
|
48
|
+
|
|
49
|
+
try:
|
|
50
|
+
result = task.task_function(*args, **kwargs)
|
|
51
|
+
except Exception:
|
|
52
|
+
_run_hook(task, "failure", args, kwargs)
|
|
53
|
+
raise
|
|
54
|
+
|
|
55
|
+
_run_hook(task, "success", args, kwargs)
|
|
56
|
+
return result
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def _run_hook(task: Any, hook_name: str, args: list[Any], kwargs: dict[str, Any]) -> None:
|
|
60
|
+
"""
|
|
61
|
+
Run a success or failure hook without letting it replace the real outcome.
|
|
62
|
+
|
|
63
|
+
A raising ``failure`` hook would otherwise mask the exception that caused
|
|
64
|
+
it, leaving the job recorded under the wrong error.
|
|
65
|
+
"""
|
|
66
|
+
try:
|
|
67
|
+
getattr(task, hook_name)(*args, **kwargs)
|
|
68
|
+
except Exception:
|
|
69
|
+
logger.exception(f"Task {task.name} '{hook_name}' hook raised; the task's own outcome stands")
|
corekit/jobs/task.py
ADDED
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Background tasks, independent of any queue.
|
|
3
|
+
|
|
4
|
+
``Task`` describes work and how to name it; nothing here imports a queue
|
|
5
|
+
library, so the same task runs under RQ, Celery, a cron entry or a test with no
|
|
6
|
+
queue at all. An adapter binds it to whichever backend an application uses --
|
|
7
|
+
see ``corekit.jobs.runner``.
|
|
8
|
+
|
|
9
|
+
A task is identified by name and constructed by the worker from its own code::
|
|
10
|
+
|
|
11
|
+
class NightlyBackup(ScheduledTask):
|
|
12
|
+
interval = 86400
|
|
13
|
+
|
|
14
|
+
def task_function(self, target: str) -> None:
|
|
15
|
+
...
|
|
16
|
+
|
|
17
|
+
The queue carries ``("NightlyBackup", '{"args": ["db"], "kwargs": {}}')`` and
|
|
18
|
+
never a serialized object, so writing to the queue cannot run arbitrary code in
|
|
19
|
+
a worker.
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
import abc
|
|
23
|
+
import logging
|
|
24
|
+
from datetime import datetime, timedelta
|
|
25
|
+
from typing import Any
|
|
26
|
+
|
|
27
|
+
from corekit.jobs.registry import task_registry
|
|
28
|
+
from corekit.utils.payload import encode_payload
|
|
29
|
+
|
|
30
|
+
__all__ = ["ScheduledTask", "Task"]
|
|
31
|
+
|
|
32
|
+
DEFAULT_TASK_TIMEOUT = 180
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def _declared_abstract_methods(cls: type) -> tuple[str, ...]:
|
|
36
|
+
"""
|
|
37
|
+
Names marked abstract anywhere in the class's ancestry.
|
|
38
|
+
|
|
39
|
+
Needed because ABCMeta has not yet populated ``__abstractmethods__`` when
|
|
40
|
+
``__init_subclass__`` runs on the class being defined.
|
|
41
|
+
"""
|
|
42
|
+
names = set()
|
|
43
|
+
for klass in cls.__mro__:
|
|
44
|
+
for name, value in vars(klass).items():
|
|
45
|
+
if getattr(value, "__isabstractmethod__", False):
|
|
46
|
+
names.add(name)
|
|
47
|
+
return tuple(names)
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
class Task(abc.ABC):
|
|
51
|
+
"""
|
|
52
|
+
A unit of background work.
|
|
53
|
+
|
|
54
|
+
Subclasses implement ``task_function`` and are registered by class name when
|
|
55
|
+
the class is defined, so a worker can rebuild one from a queued name.
|
|
56
|
+
"""
|
|
57
|
+
|
|
58
|
+
#: Overrides the class name as the registered identity.
|
|
59
|
+
task_name: str | None = None
|
|
60
|
+
|
|
61
|
+
def __init_subclass__(cls, **kwargs: Any) -> None:
|
|
62
|
+
"""
|
|
63
|
+
Register every concrete subclass under its name.
|
|
64
|
+
|
|
65
|
+
Abstract subclasses are skipped -- an intermediate base with methods
|
|
66
|
+
still unimplemented is a shared parent, not a task a worker can run.
|
|
67
|
+
``__abstractmethods__`` is set by ABCMeta *after* this hook runs, so the
|
|
68
|
+
class's own namespace is inspected instead.
|
|
69
|
+
"""
|
|
70
|
+
super().__init_subclass__(**kwargs)
|
|
71
|
+
if not cls._is_abstract():
|
|
72
|
+
task_registry.register(cls)
|
|
73
|
+
|
|
74
|
+
@classmethod
|
|
75
|
+
def _is_abstract(cls) -> bool:
|
|
76
|
+
"""
|
|
77
|
+
Whether this class still leaves an abstract method unimplemented.
|
|
78
|
+
"""
|
|
79
|
+
for name in getattr(cls, "__abstractmethods__", ()) or _declared_abstract_methods(cls):
|
|
80
|
+
attribute = getattr(cls, name, None)
|
|
81
|
+
if getattr(attribute, "__isabstractmethod__", False):
|
|
82
|
+
return True
|
|
83
|
+
return False
|
|
84
|
+
|
|
85
|
+
def __init__(self, name: str | None = None) -> None:
|
|
86
|
+
"""
|
|
87
|
+
Build the task. ``name`` overrides the registered identity for logging.
|
|
88
|
+
"""
|
|
89
|
+
self.name = name or self.task_name or type(self).__name__
|
|
90
|
+
self.logger = logging.getLogger(type(self).__module__)
|
|
91
|
+
|
|
92
|
+
@abc.abstractmethod
|
|
93
|
+
def task_function(self, *args: Any, **kwargs: Any) -> None:
|
|
94
|
+
"""
|
|
95
|
+
The work to perform. Arguments arrive as JSON-decoded data.
|
|
96
|
+
"""
|
|
97
|
+
raise NotImplementedError
|
|
98
|
+
|
|
99
|
+
def success(self, *args: Any, **kwargs: Any) -> None:
|
|
100
|
+
"""
|
|
101
|
+
Called after ``task_function`` returns. Override to add behaviour.
|
|
102
|
+
"""
|
|
103
|
+
self.logger.info(f"Task {self.name} completed successfully")
|
|
104
|
+
|
|
105
|
+
def failure(self, *args: Any, **kwargs: Any) -> None:
|
|
106
|
+
"""
|
|
107
|
+
Called when ``task_function`` raises. Override to add behaviour.
|
|
108
|
+
"""
|
|
109
|
+
self.logger.error(f"Task {self.name} failed")
|
|
110
|
+
|
|
111
|
+
def payload(self, *args: Any, **kwargs: Any) -> str:
|
|
112
|
+
"""
|
|
113
|
+
Encode arguments for the queue, raising now if they cannot cross it.
|
|
114
|
+
"""
|
|
115
|
+
return encode_payload(args, kwargs)
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
class ScheduledTask(Task):
|
|
119
|
+
"""
|
|
120
|
+
A task that runs on a schedule.
|
|
121
|
+
|
|
122
|
+
The timing attributes are read by whichever adapter schedules the task;
|
|
123
|
+
nothing here talks to a scheduler itself.
|
|
124
|
+
"""
|
|
125
|
+
|
|
126
|
+
interval: int | None = None
|
|
127
|
+
repeat: int | None = None
|
|
128
|
+
timeout: int = DEFAULT_TASK_TIMEOUT
|
|
129
|
+
schedule_delay: dict[str, int] = {"hours": 1}
|
|
130
|
+
start_time: datetime | None = None
|
|
131
|
+
|
|
132
|
+
#: Default arguments, encoded into the payload when the task is scheduled.
|
|
133
|
+
args: list[Any] | None = None
|
|
134
|
+
kwargs: dict[str, Any] | None = None
|
|
135
|
+
|
|
136
|
+
def get_start_time(self) -> datetime:
|
|
137
|
+
"""
|
|
138
|
+
When the first run should happen.
|
|
139
|
+
|
|
140
|
+
``start_time`` if set, otherwise ``schedule_delay`` from now. Timezone
|
|
141
|
+
handling is the scheduler's business, so this returns local time to
|
|
142
|
+
match what schedulers expect.
|
|
143
|
+
"""
|
|
144
|
+
if self.start_time is not None:
|
|
145
|
+
return self.start_time
|
|
146
|
+
return datetime.now() + timedelta(**self.schedule_delay)
|
|
147
|
+
|
|
148
|
+
def scheduled_payload(self) -> str:
|
|
149
|
+
"""
|
|
150
|
+
The encoded arguments this task is scheduled with.
|
|
151
|
+
"""
|
|
152
|
+
return encode_payload(self.args or [], self.kwargs or {})
|
|
@@ -2,8 +2,9 @@
|
|
|
2
2
|
Logging and performance measurement.
|
|
3
3
|
|
|
4
4
|
``Loggable`` gives a class a logger named after itself; ``Benchmarkable`` adds
|
|
5
|
-
split timing on top.
|
|
6
|
-
|
|
5
|
+
split timing on top. ``BaseRequestContext`` carries per-request metadata so a
|
|
6
|
+
log line can say which request it belongs to. They live together because they
|
|
7
|
+
are the same concern -- knowing what a running system is doing.
|
|
7
8
|
|
|
8
9
|
class Importer(Benchmarkable):
|
|
9
10
|
def run(self) -> None:
|
|
@@ -15,7 +16,8 @@ knowing what a running system is doing.
|
|
|
15
16
|
|
|
16
17
|
from corekit.observability.benchmarkable import Benchmarkable
|
|
17
18
|
from corekit.observability.loggable import Loggable
|
|
19
|
+
from corekit.observability.request_context import BaseRequestContext
|
|
18
20
|
from corekit.observability.timing.split import Split
|
|
19
21
|
from corekit.observability.timing.timer import Timer
|
|
20
22
|
|
|
21
|
-
__all__ = ["Benchmarkable", "Loggable", "Split", "Timer"]
|
|
23
|
+
__all__ = ["BaseRequestContext", "Benchmarkable", "Loggable", "Split", "Timer"]
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Request-scoped metadata.
|
|
3
|
+
|
|
4
|
+
Small values that belong to one request -- which surface initiated it, a request
|
|
5
|
+
id, a correlation id -- carried in a ``ContextVar`` so they follow the work
|
|
6
|
+
rather than being threaded through every call signature.
|
|
7
|
+
|
|
8
|
+
A ``ContextVar`` rather than ``threading.local``: coroutines that ``await``
|
|
9
|
+
may resume on a different thread, and several tasks may share one, so
|
|
10
|
+
thread-local state either leaks between requests or vanishes mid-request.
|
|
11
|
+
Context variables follow the logical task instead.
|
|
12
|
+
|
|
13
|
+
This is not for connections or anything that must be closed. It holds
|
|
14
|
+
immutable-per-request facts; see ``ThreadLocalRegistry`` for resources with a
|
|
15
|
+
lifecycle.
|
|
16
|
+
|
|
17
|
+
Subclass it with the fields an application actually wants::
|
|
18
|
+
|
|
19
|
+
class RequestContext(BaseRequestContext):
|
|
20
|
+
surface: Surface
|
|
21
|
+
request_id: str | None = None
|
|
22
|
+
|
|
23
|
+
with RequestContext(surface=Surface.API).active():
|
|
24
|
+
... # RequestContext.current() sees it
|
|
25
|
+
|
|
26
|
+
Each subclass gets its own storage, so one application's context cannot be read
|
|
27
|
+
through another's class.
|
|
28
|
+
"""
|
|
29
|
+
|
|
30
|
+
from contextlib import contextmanager
|
|
31
|
+
from contextvars import ContextVar, Token
|
|
32
|
+
from typing import Any, Iterator, Self
|
|
33
|
+
|
|
34
|
+
from pydantic import BaseModel
|
|
35
|
+
|
|
36
|
+
__all__ = ["BaseRequestContext"]
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
class BaseRequestContext(BaseModel):
|
|
40
|
+
"""
|
|
41
|
+
Base for request-scoped metadata carried in a ``ContextVar``.
|
|
42
|
+
|
|
43
|
+
Prefer ``active()`` or ``active_if_absent()`` over the manual
|
|
44
|
+
``activate``/``deactivate`` pair: an early return or a raised exception
|
|
45
|
+
between them leaks the context into whatever runs next on that task.
|
|
46
|
+
"""
|
|
47
|
+
|
|
48
|
+
def __init_subclass__(cls, **kwargs: Any) -> None:
|
|
49
|
+
"""
|
|
50
|
+
Give every subclass its own ContextVar.
|
|
51
|
+
"""
|
|
52
|
+
super().__init_subclass__(**kwargs)
|
|
53
|
+
cls._context_var = ContextVar(f"{cls.__module__}.{cls.__name__}", default=None)
|
|
54
|
+
|
|
55
|
+
@classmethod
|
|
56
|
+
def _var(cls) -> ContextVar[Any]:
|
|
57
|
+
"""
|
|
58
|
+
This class's context variable.
|
|
59
|
+
|
|
60
|
+
Raises:
|
|
61
|
+
TypeError: If called on ``BaseRequestContext`` itself, which holds
|
|
62
|
+
no storage -- subclass it with the fields you need.
|
|
63
|
+
"""
|
|
64
|
+
# Set by __init_subclass__, so each subclass reads and writes its own
|
|
65
|
+
# variable rather than sharing one with its siblings.
|
|
66
|
+
var = cls.__dict__.get("_context_var") or getattr(cls, "_context_var", None)
|
|
67
|
+
if not isinstance(var, ContextVar):
|
|
68
|
+
raise TypeError(
|
|
69
|
+
f"{cls.__name__} has no context storage. Subclass BaseRequestContext "
|
|
70
|
+
f"with the fields your application needs rather than using it directly."
|
|
71
|
+
)
|
|
72
|
+
return var
|
|
73
|
+
|
|
74
|
+
@classmethod
|
|
75
|
+
def current(cls) -> Self | None:
|
|
76
|
+
"""
|
|
77
|
+
The active context, or ``None`` if nothing is active.
|
|
78
|
+
"""
|
|
79
|
+
return cls._var().get()
|
|
80
|
+
|
|
81
|
+
def activate(self) -> Token:
|
|
82
|
+
"""
|
|
83
|
+
Make this the active context, returning a token for ``deactivate``.
|
|
84
|
+
|
|
85
|
+
Prefer ``active()``, which cannot leak.
|
|
86
|
+
"""
|
|
87
|
+
return self._var().set(self)
|
|
88
|
+
|
|
89
|
+
def activate_if_absent(self) -> Token | None:
|
|
90
|
+
"""
|
|
91
|
+
Activate only if nothing is active yet, else return ``None``.
|
|
92
|
+
|
|
93
|
+
Lets an inner call establish a default without overriding the surface an
|
|
94
|
+
outer caller already set. The result, ``None`` included, goes to
|
|
95
|
+
``deactivate``.
|
|
96
|
+
"""
|
|
97
|
+
if self._var().get() is not None:
|
|
98
|
+
return None
|
|
99
|
+
return self._var().set(self)
|
|
100
|
+
|
|
101
|
+
@classmethod
|
|
102
|
+
def deactivate(cls, token: Token | None) -> None:
|
|
103
|
+
"""
|
|
104
|
+
Restore whatever was active before. ``None`` is a no-op.
|
|
105
|
+
"""
|
|
106
|
+
if token is not None:
|
|
107
|
+
cls._var().reset(token)
|
|
108
|
+
|
|
109
|
+
@contextmanager
|
|
110
|
+
def active(self) -> Iterator[Self]:
|
|
111
|
+
"""
|
|
112
|
+
Activate for the duration of the block, restoring on the way out.
|
|
113
|
+
|
|
114
|
+
Restores even when the block raises, which the manual pair does not do
|
|
115
|
+
unless every caller remembers the ``finally``.
|
|
116
|
+
"""
|
|
117
|
+
token = self.activate()
|
|
118
|
+
try:
|
|
119
|
+
yield self
|
|
120
|
+
finally:
|
|
121
|
+
type(self).deactivate(token)
|
|
122
|
+
|
|
123
|
+
@contextmanager
|
|
124
|
+
def active_if_absent(self) -> Iterator[Self | None]:
|
|
125
|
+
"""
|
|
126
|
+
Activate for the block only if nothing is active yet.
|
|
127
|
+
|
|
128
|
+
Yields the context that ends up active -- this one, or the one that was
|
|
129
|
+
already there -- so a caller can read it either way.
|
|
130
|
+
"""
|
|
131
|
+
token = self.activate_if_absent()
|
|
132
|
+
try:
|
|
133
|
+
yield type(self).current()
|
|
134
|
+
finally:
|
|
135
|
+
type(self).deactivate(token)
|
corekit/registry/__init__.py
CHANGED
|
@@ -1,12 +1,17 @@
|
|
|
1
1
|
"""
|
|
2
|
-
|
|
2
|
+
Registries.
|
|
3
3
|
|
|
4
|
-
``SmartRegistry`` folds case, trims whitespace and
|
|
5
|
-
as hyphens, so ``"AI Conversation Handler"``,
|
|
6
|
-
``"ai-conversation-handler"`` all address the
|
|
7
|
-
matters wherever a human -- or a language model -- supplies
|
|
4
|
+
``SmartRegistry`` is keyed and forgiving: it folds case, trims whitespace and
|
|
5
|
+
treats spaces and underscores as hyphens, so ``"AI Conversation Handler"``,
|
|
6
|
+
``"ai_conversation_handler"`` and ``"ai-conversation-handler"`` all address the
|
|
7
|
+
same entry. That matters wherever a human -- or a language model -- supplies
|
|
8
|
+
the key.
|
|
9
|
+
|
|
10
|
+
``OrderedRegistry`` is unkeyed: it preserves registration order and allows
|
|
11
|
+
duplicates. Reach for it when position is what matters and no name is needed.
|
|
8
12
|
"""
|
|
9
13
|
|
|
14
|
+
from corekit.registry.ordered import OrderedRegistry
|
|
10
15
|
from corekit.registry.registry import SmartRegistry
|
|
11
16
|
|
|
12
|
-
__all__ = ["SmartRegistry"]
|
|
17
|
+
__all__ = ["OrderedRegistry", "SmartRegistry"]
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Insertion-ordered registry.
|
|
3
|
+
|
|
4
|
+
Where ``SmartRegistry`` is a dict keyed by a forgiving name, ``OrderedRegistry``
|
|
5
|
+
is a list: it keeps what was registered, in the order it was registered, and
|
|
6
|
+
lets callers filter it. Use it when order is the point and names are not --
|
|
7
|
+
mount order, execution order, pipeline stages -- and when two entries may
|
|
8
|
+
legitimately share a name.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from typing import Callable, Generic, Iterator, TypeVar
|
|
12
|
+
|
|
13
|
+
__all__ = ["OrderedRegistry"]
|
|
14
|
+
|
|
15
|
+
T = TypeVar("T")
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class OrderedRegistry(Generic[T]):
|
|
19
|
+
"""
|
|
20
|
+
Everything registered in this process, in construction order.
|
|
21
|
+
|
|
22
|
+
Deliberately not keyed. A keyed registry silently evicts a collision, which
|
|
23
|
+
for order-sensitive collections means an entry vanishing with no error --
|
|
24
|
+
so duplicates are kept, and callers select with ``where`` instead.
|
|
25
|
+
"""
|
|
26
|
+
|
|
27
|
+
__slots__ = ("_entries",)
|
|
28
|
+
|
|
29
|
+
def __init__(self) -> None:
|
|
30
|
+
"""
|
|
31
|
+
Initialize an empty registry.
|
|
32
|
+
"""
|
|
33
|
+
self._entries: list[T] = []
|
|
34
|
+
|
|
35
|
+
def register(self, entry: T) -> T:
|
|
36
|
+
"""
|
|
37
|
+
Append an entry, returning it so this can be used inline.
|
|
38
|
+
"""
|
|
39
|
+
self._entries.append(entry)
|
|
40
|
+
return entry
|
|
41
|
+
|
|
42
|
+
def where(self, predicate: Callable[[T], bool]) -> list[T]:
|
|
43
|
+
"""
|
|
44
|
+
Return the entries matching ``predicate``, in registration order.
|
|
45
|
+
"""
|
|
46
|
+
return [entry for entry in self._entries if predicate(entry)]
|
|
47
|
+
|
|
48
|
+
def clear(self) -> None:
|
|
49
|
+
"""
|
|
50
|
+
Forget every entry. For tests that build more than one of whatever this holds.
|
|
51
|
+
"""
|
|
52
|
+
self._entries.clear()
|
|
53
|
+
|
|
54
|
+
@property
|
|
55
|
+
def entries(self) -> list[T]:
|
|
56
|
+
"""
|
|
57
|
+
A copy of the entries, in registration order.
|
|
58
|
+
|
|
59
|
+
A copy, so a caller iterating the result cannot be tripped up by
|
|
60
|
+
something registering mid-loop.
|
|
61
|
+
"""
|
|
62
|
+
return list(self._entries)
|
|
63
|
+
|
|
64
|
+
def __len__(self) -> int:
|
|
65
|
+
"""
|
|
66
|
+
Return the number of registered entries.
|
|
67
|
+
"""
|
|
68
|
+
return len(self._entries)
|
|
69
|
+
|
|
70
|
+
def __iter__(self) -> Iterator[T]:
|
|
71
|
+
"""
|
|
72
|
+
Iterate the entries in registration order.
|
|
73
|
+
"""
|
|
74
|
+
return iter(self._entries)
|
|
75
|
+
|
|
76
|
+
def __contains__(self, entry: object) -> bool:
|
|
77
|
+
"""
|
|
78
|
+
Check whether an entry has been registered, by identity or equality.
|
|
79
|
+
"""
|
|
80
|
+
return entry in self._entries
|
|
81
|
+
|
|
82
|
+
def __repr__(self) -> str:
|
|
83
|
+
"""
|
|
84
|
+
Return a formal representation of the registry.
|
|
85
|
+
"""
|
|
86
|
+
return f"{type(self).__name__}({self._entries!r})"
|
corekit/schemas/__init__.py
CHANGED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Shared types.
|
|
3
|
+
|
|
4
|
+
The enum bases validate their members on construction, so an unknown value
|
|
5
|
+
raises where it is introduced rather than somewhere further along.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from corekit.schemas.enum import IntegerEnum, StringEnum, ValidatingEnum
|
|
9
|
+
|
|
10
|
+
__all__ = ["IntegerEnum", "StringEnum", "ValidatingEnum"]
|
corekit/schemas/enum.py
CHANGED
|
@@ -1,49 +1,49 @@
|
|
|
1
|
-
from enum import Enum
|
|
2
|
-
from typing import Any
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
class ValidatingEnum(Enum):
|
|
6
|
-
"""
|
|
7
|
-
A base class for enums that validates values against a set of allowed values.
|
|
8
|
-
"""
|
|
9
|
-
|
|
10
|
-
@classmethod
|
|
11
|
-
def is_member(cls, value: Any) -> bool:
|
|
12
|
-
"""
|
|
13
|
-
Check if the value is a member of the enum.
|
|
14
|
-
"""
|
|
15
|
-
return value in cls._value2member_map_
|
|
16
|
-
|
|
17
|
-
@classmethod
|
|
18
|
-
def validate_and_create(cls, value: Any) -> "ValidatingEnum":
|
|
19
|
-
if cls.is_member(value):
|
|
20
|
-
return cls(value)
|
|
21
|
-
raise ValueError(f"{cls.__name__} does not contain {cls}")
|
|
22
|
-
|
|
23
|
-
@classmethod
|
|
24
|
-
def get_all_members(cls) -> list["ValidatingEnum"]:
|
|
25
|
-
"""
|
|
26
|
-
Get all members of the enum.
|
|
27
|
-
"""
|
|
28
|
-
return list(cls._value2member_map_.values()) # type: ignore[return-value]
|
|
29
|
-
|
|
30
|
-
def display_name(self) -> str:
|
|
31
|
-
return self.name.replace("_", " ").title()
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
class StringEnum(str, ValidatingEnum):
|
|
35
|
-
@classmethod
|
|
36
|
-
def from_string(cls, value: str) -> "StringEnum":
|
|
37
|
-
"""
|
|
38
|
-
Convert a string to the corresponding enum member.
|
|
39
|
-
"""
|
|
40
|
-
return cls.validate_and_create(value)
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
class IntegerEnum(int, ValidatingEnum):
|
|
44
|
-
@classmethod
|
|
45
|
-
def from_int(cls, value: int) -> "IntegerEnum":
|
|
46
|
-
"""
|
|
47
|
-
Convert an integer to the corresponding enum member.
|
|
48
|
-
"""
|
|
49
|
-
return cls.validate_and_create(value)
|
|
1
|
+
from enum import Enum
|
|
2
|
+
from typing import Any
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
class ValidatingEnum(Enum):
|
|
6
|
+
"""
|
|
7
|
+
A base class for enums that validates values against a set of allowed values.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
@classmethod
|
|
11
|
+
def is_member(cls, value: Any) -> bool:
|
|
12
|
+
"""
|
|
13
|
+
Check if the value is a member of the enum.
|
|
14
|
+
"""
|
|
15
|
+
return value in cls._value2member_map_
|
|
16
|
+
|
|
17
|
+
@classmethod
|
|
18
|
+
def validate_and_create(cls, value: Any) -> "ValidatingEnum":
|
|
19
|
+
if cls.is_member(value):
|
|
20
|
+
return cls(value)
|
|
21
|
+
raise ValueError(f"{cls.__name__} does not contain {cls}")
|
|
22
|
+
|
|
23
|
+
@classmethod
|
|
24
|
+
def get_all_members(cls) -> list["ValidatingEnum"]:
|
|
25
|
+
"""
|
|
26
|
+
Get all members of the enum.
|
|
27
|
+
"""
|
|
28
|
+
return list(cls._value2member_map_.values()) # type: ignore[return-value]
|
|
29
|
+
|
|
30
|
+
def display_name(self) -> str:
|
|
31
|
+
return self.name.replace("_", " ").title()
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class StringEnum(str, ValidatingEnum):
|
|
35
|
+
@classmethod
|
|
36
|
+
def from_string(cls, value: str) -> "StringEnum":
|
|
37
|
+
"""
|
|
38
|
+
Convert a string to the corresponding enum member.
|
|
39
|
+
"""
|
|
40
|
+
return cls.validate_and_create(value)
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
class IntegerEnum(int, ValidatingEnum):
|
|
44
|
+
@classmethod
|
|
45
|
+
def from_int(cls, value: int) -> "IntegerEnum":
|
|
46
|
+
"""
|
|
47
|
+
Convert an integer to the corresponding enum member.
|
|
48
|
+
"""
|
|
49
|
+
return cls.validate_and_create(value)
|
|
@@ -1,11 +1,11 @@
|
|
|
1
|
-
from pydantic import BaseModel
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
class ArbitraryModel(BaseModel):
|
|
5
|
-
"""
|
|
6
|
-
Base model for arbitrary data. This model is used to take unstructured data and convert it into
|
|
7
|
-
a structured format that can be easily mapped to a model. This model is capable of parsing any type of data
|
|
8
|
-
and converting it into a structured format that can be easily accessed and manipulated.
|
|
9
|
-
"""
|
|
10
|
-
|
|
11
|
-
pass
|
|
1
|
+
from pydantic import BaseModel
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class ArbitraryModel(BaseModel):
|
|
5
|
+
"""
|
|
6
|
+
Base model for arbitrary data. This model is used to take unstructured data and convert it into
|
|
7
|
+
a structured format that can be easily mapped to a model. This model is capable of parsing any type of data
|
|
8
|
+
and converting it into a structured format that can be easily accessed and manipulated.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
pass
|