opendataframework 0.1.0__tar.gz → 0.2.0__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.
- {opendataframework-0.1.0 → opendataframework-0.2.0}/PKG-INFO +4 -6
- {opendataframework-0.1.0 → opendataframework-0.2.0}/README.md +3 -5
- {opendataframework-0.1.0 → opendataframework-0.2.0}/pyproject.toml +1 -1
- {opendataframework-0.1.0 → opendataframework-0.2.0}/src/opendataframework/component.py +46 -0
- {opendataframework-0.1.0 → opendataframework-0.2.0}/src/opendataframework/config.py +15 -8
- {opendataframework-0.1.0 → opendataframework-0.2.0}/src/opendataframework/namespace.py +16 -2
- {opendataframework-0.1.0 → opendataframework-0.2.0}/src/opendataframework/project.py +17 -0
- {opendataframework-0.1.0 → opendataframework-0.2.0}/src/opendataframework/view.py +2 -2
- {opendataframework-0.1.0 → opendataframework-0.2.0}/LICENSE +0 -0
- {opendataframework-0.1.0 → opendataframework-0.2.0}/src/opendataframework/__init__.py +0 -0
- {opendataframework-0.1.0 → opendataframework-0.2.0}/src/opendataframework/context.py +0 -0
- {opendataframework-0.1.0 → opendataframework-0.2.0}/src/opendataframework/entity.py +0 -0
- {opendataframework-0.1.0 → opendataframework-0.2.0}/src/opendataframework/layer.py +0 -0
- {opendataframework-0.1.0 → opendataframework-0.2.0}/src/opendataframework/logger.py +0 -0
- {opendataframework-0.1.0 → opendataframework-0.2.0}/src/opendataframework/pipeline.py +0 -0
- {opendataframework-0.1.0 → opendataframework-0.2.0}/src/opendataframework/py.typed +0 -0
- {opendataframework-0.1.0 → opendataframework-0.2.0}/src/opendataframework/repository.py +0 -0
- {opendataframework-0.1.0 → opendataframework-0.2.0}/src/opendataframework/service.py +0 -0
- {opendataframework-0.1.0 → opendataframework-0.2.0}/src/opendataframework/task.py +0 -0
- {opendataframework-0.1.0 → opendataframework-0.2.0}/src/opendataframework/utils.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: opendataframework
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.2.0
|
|
4
4
|
Summary: Lightweight, config-driven dependency-injection framework for data applications.
|
|
5
5
|
License-Expression: MIT
|
|
6
6
|
License-File: LICENSE
|
|
@@ -82,9 +82,7 @@ class User:
|
|
|
82
82
|
class SQLite:
|
|
83
83
|
def __init__(self, config: Config):
|
|
84
84
|
self.conn = sqlite3.connect(config.sqlite.path)
|
|
85
|
-
self.conn.execute(
|
|
86
|
-
"CREATE TABLE IF NOT EXISTS users (id INTEGER PRIMARY KEY, name TEXT)"
|
|
87
|
-
)
|
|
85
|
+
self.conn.execute("CREATE TABLE IF NOT EXISTS users (id INTEGER PRIMARY KEY, name TEXT)")
|
|
88
86
|
|
|
89
87
|
|
|
90
88
|
@Repository(User)
|
|
@@ -281,7 +279,7 @@ No circular dependencies — restructure with a shared third collaborator
|
|
|
281
279
|
instead.
|
|
282
280
|
|
|
283
281
|
```python
|
|
284
|
-
component = project.context.get(UsersApi)
|
|
282
|
+
component = project.context.get(UsersApi) # typed, by class
|
|
285
283
|
```
|
|
286
284
|
|
|
287
285
|
## Project
|
|
@@ -334,7 +332,7 @@ Project owns everything.
|
|
|
334
332
|
See [`examples/`](examples/README.md) — small, focused projects each
|
|
335
333
|
isolating one core abstraction.
|
|
336
334
|
|
|
337
|
-
> For the CLI, UI, MCP server, and chat surface
|
|
335
|
+
> For the CLI, UI, MCP server, and chat surface
|
|
338
336
|
> built on top of this package, see `odf`.
|
|
339
337
|
|
|
340
338
|
---
|
|
@@ -66,9 +66,7 @@ class User:
|
|
|
66
66
|
class SQLite:
|
|
67
67
|
def __init__(self, config: Config):
|
|
68
68
|
self.conn = sqlite3.connect(config.sqlite.path)
|
|
69
|
-
self.conn.execute(
|
|
70
|
-
"CREATE TABLE IF NOT EXISTS users (id INTEGER PRIMARY KEY, name TEXT)"
|
|
71
|
-
)
|
|
69
|
+
self.conn.execute("CREATE TABLE IF NOT EXISTS users (id INTEGER PRIMARY KEY, name TEXT)")
|
|
72
70
|
|
|
73
71
|
|
|
74
72
|
@Repository(User)
|
|
@@ -265,7 +263,7 @@ No circular dependencies — restructure with a shared third collaborator
|
|
|
265
263
|
instead.
|
|
266
264
|
|
|
267
265
|
```python
|
|
268
|
-
component = project.context.get(UsersApi)
|
|
266
|
+
component = project.context.get(UsersApi) # typed, by class
|
|
269
267
|
```
|
|
270
268
|
|
|
271
269
|
## Project
|
|
@@ -318,7 +316,7 @@ Project owns everything.
|
|
|
318
316
|
See [`examples/`](examples/README.md) — small, focused projects each
|
|
319
317
|
isolating one core abstraction.
|
|
320
318
|
|
|
321
|
-
> For the CLI, UI, MCP server, and chat surface
|
|
319
|
+
> For the CLI, UI, MCP server, and chat surface
|
|
322
320
|
> built on top of this package, see `odf`.
|
|
323
321
|
|
|
324
322
|
---
|
|
@@ -6,6 +6,8 @@ injected by the ``Context`` with no execution contract of its own. The
|
|
|
6
6
|
implement to opt into lifecycle callbacks or inspection tooling.
|
|
7
7
|
"""
|
|
8
8
|
|
|
9
|
+
from collections.abc import Callable
|
|
10
|
+
from dataclasses import dataclass
|
|
9
11
|
from typing import Protocol, runtime_checkable
|
|
10
12
|
|
|
11
13
|
from opendataframework.namespace import Namespace
|
|
@@ -87,6 +89,50 @@ class ChartProtocol(Protocol):
|
|
|
87
89
|
...
|
|
88
90
|
|
|
89
91
|
|
|
92
|
+
@dataclass
|
|
93
|
+
class McpTool:
|
|
94
|
+
"""One MCP tool a Component exposes, returned from ``mcp_tools()``.
|
|
95
|
+
|
|
96
|
+
Attributes:
|
|
97
|
+
name: The tool's local name — namespaced by whatever builds the
|
|
98
|
+
tool surface (today, the sibling ``odf`` package's
|
|
99
|
+
``McpServer`` prefixes it with the component's kebab-case
|
|
100
|
+
class name) to avoid collisions with the fixed built-in
|
|
101
|
+
tools and with other components' tools.
|
|
102
|
+
description: Human-readable description shown to the MCP client.
|
|
103
|
+
handler: The bound method implementing the tool call.
|
|
104
|
+
structured_output: Whether the handler's return value should be
|
|
105
|
+
reported as structured (JSON) output. ``None`` defers to
|
|
106
|
+
the MCP server's own default behavior, mirroring how this
|
|
107
|
+
field is expected to be forwarded verbatim by whatever
|
|
108
|
+
registers the tool.
|
|
109
|
+
"""
|
|
110
|
+
|
|
111
|
+
name: str
|
|
112
|
+
description: str
|
|
113
|
+
handler: Callable[..., object]
|
|
114
|
+
structured_output: bool | None = None
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
@runtime_checkable
|
|
118
|
+
class McpToolsProtocol(Protocol):
|
|
119
|
+
"""Capability interface for a Component's optional custom MCP tools.
|
|
120
|
+
|
|
121
|
+
Independent of ``DetailsProtocol``/``ChartProtocol``/
|
|
122
|
+
``OnStartProtocol``/``OnStopProtocol``. Never invoked by ``Context``
|
|
123
|
+
— detected structurally via ``isinstance(instance, McpToolsProtocol)``
|
|
124
|
+
and consumed on demand by whatever builds an MCP tool surface. (Today,
|
|
125
|
+
that's the sibling ``odf`` package's ``McpServer``, which registers
|
|
126
|
+
each returned ``McpTool`` under
|
|
127
|
+
``<kebab(cls.__name__)>.<tool.name>``, alongside its six fixed
|
|
128
|
+
built-in tools.)
|
|
129
|
+
"""
|
|
130
|
+
|
|
131
|
+
def mcp_tools(self) -> list[McpTool]:
|
|
132
|
+
"""Return the component's custom MCP tools, in addition to the fixed built-in surface."""
|
|
133
|
+
...
|
|
134
|
+
|
|
135
|
+
|
|
90
136
|
class Component(Namespace):
|
|
91
137
|
"""Decorator that registers a class with the Context for dependency injection.
|
|
92
138
|
|
|
@@ -7,6 +7,7 @@ components declare it like any other constructor dependency.
|
|
|
7
7
|
"""
|
|
8
8
|
|
|
9
9
|
import tomllib
|
|
10
|
+
from copy import deepcopy
|
|
10
11
|
from pathlib import Path
|
|
11
12
|
|
|
12
13
|
from opendataframework.utils import normalize
|
|
@@ -37,8 +38,14 @@ class Config:
|
|
|
37
38
|
"""
|
|
38
39
|
|
|
39
40
|
def __init__(self, data: dict) -> None:
|
|
40
|
-
"""Wrap ``data``, a (possibly nested) configuration dict.
|
|
41
|
-
|
|
41
|
+
"""Wrap a deep copy of ``data``, a (possibly nested) configuration dict.
|
|
42
|
+
|
|
43
|
+
``data`` is copied rather than stored by reference, so mutating the
|
|
44
|
+
original dict after construction (e.g. one the caller keeps a
|
|
45
|
+
reference to and mutates after passing to ``Project.from_dict()``)
|
|
46
|
+
does not leak through this ``Config``.
|
|
47
|
+
"""
|
|
48
|
+
self._data = deepcopy(data)
|
|
42
49
|
|
|
43
50
|
def __getattr__(self, name: str) -> Config | object:
|
|
44
51
|
"""Return value by snake_case attribute name, normalised to kebab-case.
|
|
@@ -60,10 +67,10 @@ class Config:
|
|
|
60
67
|
return Config(value) if isinstance(value, dict) else value
|
|
61
68
|
|
|
62
69
|
def __getitem__(self, key: str) -> Config | object:
|
|
63
|
-
"""Return value by key (any name form accepted).
|
|
70
|
+
"""Return value by key, normalised to kebab-case (any name form accepted).
|
|
64
71
|
|
|
65
72
|
Args:
|
|
66
|
-
key: Config key
|
|
73
|
+
key: Config key (snake_case, kebab-case, or PascalCase).
|
|
67
74
|
|
|
68
75
|
Returns:
|
|
69
76
|
A ``Config`` wrapping the value if it is a dict, otherwise the
|
|
@@ -73,13 +80,13 @@ class Config:
|
|
|
73
80
|
KeyError: If the key is not present.
|
|
74
81
|
"""
|
|
75
82
|
try:
|
|
76
|
-
value = self._data[key]
|
|
83
|
+
value = self._data[normalize(key)]
|
|
77
84
|
except KeyError as err:
|
|
78
85
|
raise KeyError(key) from err
|
|
79
86
|
return Config(value) if isinstance(value, dict) else value
|
|
80
87
|
|
|
81
88
|
def get(self, key: str, default: object = None) -> Config | object | None:
|
|
82
|
-
"""Return value by key, or ``default`` if absent.
|
|
89
|
+
"""Return value by key (any name form accepted), or ``default`` if absent.
|
|
83
90
|
|
|
84
91
|
If ``default`` is not given and the key is absent, an empty
|
|
85
92
|
``Config`` is returned instead of ``None``, so callers can safely
|
|
@@ -91,7 +98,7 @@ class Config:
|
|
|
91
98
|
port = cfg.get("port", 5432) # safe fallback
|
|
92
99
|
|
|
93
100
|
Args:
|
|
94
|
-
key: Config key.
|
|
101
|
+
key: Config key (snake_case, kebab-case, or PascalCase).
|
|
95
102
|
default: Fallback value when the key is not present.
|
|
96
103
|
|
|
97
104
|
Returns:
|
|
@@ -100,7 +107,7 @@ class Config:
|
|
|
100
107
|
or an empty ``Config`` if the key is absent and no default was
|
|
101
108
|
given.
|
|
102
109
|
"""
|
|
103
|
-
value = self._data.get(key)
|
|
110
|
+
value = self._data.get(normalize(key))
|
|
104
111
|
if value is None:
|
|
105
112
|
return default if default is not None else Config({})
|
|
106
113
|
return Config(value) if isinstance(value, dict) else value
|
|
@@ -94,7 +94,14 @@ class Namespace:
|
|
|
94
94
|
# reveal_type(Classifier) -> type[Classifier], from __call__ below
|
|
95
95
|
"""
|
|
96
96
|
if isinstance(_, type):
|
|
97
|
-
|
|
97
|
+
key = kebab(_.__name__)
|
|
98
|
+
if key in cls._namespace:
|
|
99
|
+
existing = cls._namespace[key]
|
|
100
|
+
raise ValueError(
|
|
101
|
+
f"Duplicate registration for {key!r} in {cls.__name__}: "
|
|
102
|
+
f"{existing.__name__} and {_.__name__}"
|
|
103
|
+
)
|
|
104
|
+
cls._namespace[key] = _
|
|
98
105
|
return _
|
|
99
106
|
return super().__new__(cls)
|
|
100
107
|
|
|
@@ -129,7 +136,14 @@ class Namespace:
|
|
|
129
136
|
Returns:
|
|
130
137
|
The original class, unmodified.
|
|
131
138
|
"""
|
|
132
|
-
self.
|
|
139
|
+
key = self._name or kebab(wrapped.__name__)
|
|
140
|
+
if key in self.__class__._namespace:
|
|
141
|
+
existing = self.__class__._namespace[key]
|
|
142
|
+
raise ValueError(
|
|
143
|
+
f"Duplicate registration for {key!r} in {self.__class__.__name__}: "
|
|
144
|
+
f"{existing.__name__} and {wrapped.__name__}"
|
|
145
|
+
)
|
|
146
|
+
self.__class__._namespace[key] = wrapped
|
|
133
147
|
return wrapped
|
|
134
148
|
|
|
135
149
|
@classmethod
|
|
@@ -5,6 +5,8 @@ pure DI lifecycle boundary. It has no knowledge of any UI/CLI/MCP layer
|
|
|
5
5
|
— those are composed on top of it by consuming packages (e.g. ``odf``).
|
|
6
6
|
"""
|
|
7
7
|
|
|
8
|
+
import importlib
|
|
9
|
+
|
|
8
10
|
from opendataframework.config import load as load_config
|
|
9
11
|
from opendataframework.context import Context
|
|
10
12
|
|
|
@@ -143,6 +145,14 @@ class Project:
|
|
|
143
145
|
(default ``"logs"``, relative to the current working directory) —
|
|
144
146
|
see ``opendataframework.logger.LogManager``.
|
|
145
147
|
|
|
148
|
+
Imports the module named under ``[project] app`` (if present)
|
|
149
|
+
before constructing the ``Context`` — equivalent to writing
|
|
150
|
+
``import app`` by hand before calling ``from_config``/``from_dict``.
|
|
151
|
+
That module's own imports (e.g. an ``app/__init__.py`` that imports
|
|
152
|
+
``app.components``, ``app.repositories``, ...) are what actually
|
|
153
|
+
register framework-decorated classes; this just ensures the root
|
|
154
|
+
import happens before resolution needs it.
|
|
155
|
+
|
|
146
156
|
Args:
|
|
147
157
|
config: Mapping of configuration values, equivalent to what
|
|
148
158
|
``from_config`` would parse from a ``.toml`` file.
|
|
@@ -150,6 +160,10 @@ class Project:
|
|
|
150
160
|
Returns:
|
|
151
161
|
A new ``Project`` configured from the given dict.
|
|
152
162
|
|
|
163
|
+
Raises:
|
|
164
|
+
ImportError: If the module named under ``[project] app`` cannot
|
|
165
|
+
be imported.
|
|
166
|
+
|
|
153
167
|
Example:
|
|
154
168
|
::
|
|
155
169
|
|
|
@@ -159,5 +173,8 @@ class Project:
|
|
|
159
173
|
}
|
|
160
174
|
})
|
|
161
175
|
"""
|
|
176
|
+
app = config.get("project", {}).get("app")
|
|
177
|
+
if app is not None:
|
|
178
|
+
importlib.import_module(app)
|
|
162
179
|
log_dir = config.get("project", {}).get("log-dir", "logs")
|
|
163
180
|
return cls(context=Context(config=config, log_dir=log_dir), config=config)
|
|
@@ -178,12 +178,12 @@ class ReplayProtocol(Protocol):
|
|
|
178
178
|
``ChartProtocol``/``DetailsProtocol`` (``opendataframework.component``) and
|
|
179
179
|
``DataViewProtocol`` above. (Today, the sibling ``odf`` package's UI is
|
|
180
180
|
what offers a timeline scrubber for this.) A repository with no
|
|
181
|
-
``
|
|
181
|
+
``field()`` gets no such affordance — this only matters for
|
|
182
182
|
``LocationView``/``VideoView``/``AudioView``-backed repositories; a
|
|
183
183
|
``TimeseriesView`` repository is always replayable since it already
|
|
184
184
|
names its own timestamp via ``field``.
|
|
185
185
|
"""
|
|
186
186
|
|
|
187
|
-
def
|
|
187
|
+
def field(self) -> str:
|
|
188
188
|
"""Return the entity's timestamp field records can be scrubbed through over time by."""
|
|
189
189
|
...
|
|
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
|