servicephilosophy 0.1.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.
- servicephilosophy-0.1.0/LICENSE +21 -0
- servicephilosophy-0.1.0/MANIFEST.in +1 -0
- servicephilosophy-0.1.0/PKG-INFO +176 -0
- servicephilosophy-0.1.0/README.md +146 -0
- servicephilosophy-0.1.0/pyproject.toml +93 -0
- servicephilosophy-0.1.0/setup.cfg +4 -0
- servicephilosophy-0.1.0/src/servicephilosophy/__init__.py +18 -0
- servicephilosophy-0.1.0/src/servicephilosophy/exceptions.py +9 -0
- servicephilosophy-0.1.0/src/servicephilosophy/protocols.py +29 -0
- servicephilosophy-0.1.0/src/servicephilosophy/py.typed +0 -0
- servicephilosophy-0.1.0/src/servicephilosophy/repository.py +33 -0
- servicephilosophy-0.1.0/src/servicephilosophy.egg-info/PKG-INFO +176 -0
- servicephilosophy-0.1.0/src/servicephilosophy.egg-info/SOURCES.txt +14 -0
- servicephilosophy-0.1.0/src/servicephilosophy.egg-info/dependency_links.txt +1 -0
- servicephilosophy-0.1.0/src/servicephilosophy.egg-info/requires.txt +9 -0
- servicephilosophy-0.1.0/src/servicephilosophy.egg-info/top_level.txt +1 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) Josh Martin
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
prune tests
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: servicephilosophy
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Typed factory-aware base for service and repository components without a domain model.
|
|
5
|
+
Author-email: Josh Martin <denverprogrammer@gmail.com>
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/SignalSafeSoftware/servicephilosophy
|
|
8
|
+
Project-URL: Repository, https://github.com/SignalSafeSoftware/servicephilosophy
|
|
9
|
+
Project-URL: Documentation, https://github.com/SignalSafeSoftware/servicephilosophy#readme
|
|
10
|
+
Project-URL: Issues, https://github.com/SignalSafeSoftware/servicephilosophy/issues
|
|
11
|
+
Keywords: repository-pattern,service-layer,factory,typing
|
|
12
|
+
Classifier: Development Status :: 3 - Alpha
|
|
13
|
+
Classifier: Intended Audience :: Developers
|
|
14
|
+
Classifier: Programming Language :: Python :: 3
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
17
|
+
Classifier: Typing :: Typed
|
|
18
|
+
Requires-Python: <4.0,>=3.12
|
|
19
|
+
Description-Content-Type: text/markdown
|
|
20
|
+
License-File: LICENSE
|
|
21
|
+
Provides-Extra: dev
|
|
22
|
+
Requires-Dist: pytest>=8; extra == "dev"
|
|
23
|
+
Requires-Dist: pytest-cov>=6; extra == "dev"
|
|
24
|
+
Requires-Dist: ruff>=0.15; extra == "dev"
|
|
25
|
+
Requires-Dist: mypy>=1.11; extra == "dev"
|
|
26
|
+
Requires-Dist: build>=1.2; extra == "dev"
|
|
27
|
+
Requires-Dist: twine>=5; extra == "dev"
|
|
28
|
+
Requires-Dist: bandit>=1.7; extra == "dev"
|
|
29
|
+
Dynamic: license-file
|
|
30
|
+
|
|
31
|
+
# servicePhilosophy
|
|
32
|
+
|
|
33
|
+
A small typed foundation for factory-aware repository-style components.
|
|
34
|
+
|
|
35
|
+
| | |
|
|
36
|
+
|---|---|
|
|
37
|
+
| **Python** | 3.12+ |
|
|
38
|
+
| **Runtime deps** | none |
|
|
39
|
+
| **License** | MIT |
|
|
40
|
+
|
|
41
|
+
## Core idea
|
|
42
|
+
|
|
43
|
+
```text
|
|
44
|
+
ServiceRepository[FactoryT]
|
|
45
|
+
= a neutral base class for objects that need factory access.
|
|
46
|
+
It does not require a model.
|
|
47
|
+
It does not know about SQL, HTTP, controllers, or frameworks.
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
`ServiceRepository` stores an optional factory and exposes it through:
|
|
51
|
+
|
|
52
|
+
- **`factory`** — returns the factory, or raises `FactoryRequiredError` when missing
|
|
53
|
+
- **`maybe_factory`** — returns the factory or `None`
|
|
54
|
+
- **`has_factory`** — `True` when a factory was provided at construction
|
|
55
|
+
|
|
56
|
+
Use **`ServiceRepositoryProtocol`** when you want to type against that shape without inheriting from the base class. Use **`RepositoryFactoryProtocol`** as a minimal marker for factory types that downstream packages extend.
|
|
57
|
+
|
|
58
|
+
## What this is (and is not)
|
|
59
|
+
|
|
60
|
+
- **`ServiceRepository` is not a SQL repository.**
|
|
61
|
+
- **`ServiceRepository` is not an API client.**
|
|
62
|
+
- It is a **shared factory-aware base**.
|
|
63
|
+
- **[sqlPhilosophy](https://github.com/SignalSafeSoftware/sqlphilosophy)** can extend it with model-bound persistence.
|
|
64
|
+
- **apiPhilosophy** can extend it with HTTP/resource clients.
|
|
65
|
+
- **Application service repositories** can extend it directly for business logic with no model at all.
|
|
66
|
+
|
|
67
|
+
This package has zero runtime dependencies. It does not include SQLAlchemy, HTTP clients, FastAPI, Pydantic, or Django.
|
|
68
|
+
|
|
69
|
+
## Basic service repository
|
|
70
|
+
|
|
71
|
+
```python
|
|
72
|
+
from servicephilosophy import ServiceRepository
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
class ServiceFactory:
|
|
76
|
+
def greeting(self) -> str:
|
|
77
|
+
return "hello"
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
class GreetingService(ServiceRepository[ServiceFactory]):
|
|
81
|
+
def greet(self) -> str:
|
|
82
|
+
return self.factory.greeting()
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
## SQL specialization in another package
|
|
86
|
+
|
|
87
|
+
```python
|
|
88
|
+
from typing import Generic, TypeVar
|
|
89
|
+
|
|
90
|
+
from servicephilosophy import ServiceRepository
|
|
91
|
+
|
|
92
|
+
ModelT = TypeVar("ModelT")
|
|
93
|
+
FactoryT = TypeVar("FactoryT")
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
class BaseRepository(ServiceRepository[FactoryT], Generic[ModelT, FactoryT]):
|
|
97
|
+
model: type[ModelT]
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
## API specialization in another package
|
|
101
|
+
|
|
102
|
+
```python
|
|
103
|
+
from typing import Generic, TypeVar
|
|
104
|
+
|
|
105
|
+
from servicephilosophy import ServiceRepository
|
|
106
|
+
|
|
107
|
+
ResourceT = TypeVar("ResourceT")
|
|
108
|
+
FactoryT = TypeVar("FactoryT")
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
class BaseApiRepository(ServiceRepository[FactoryT], Generic[ResourceT, FactoryT]):
|
|
112
|
+
pass
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
## Business logic with no model
|
|
116
|
+
|
|
117
|
+
```python
|
|
118
|
+
from servicephilosophy import ServiceRepository
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
class PermissionServiceRepository(ServiceRepository[ServiceFactory]):
|
|
122
|
+
def has_permission(self, actor_id: int, permission: str) -> bool:
|
|
123
|
+
return True
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
## Recommended ecosystem
|
|
127
|
+
|
|
128
|
+
```text
|
|
129
|
+
servicePhilosophy
|
|
130
|
+
ServiceRepository[FactoryT]
|
|
131
|
+
|
|
132
|
+
sqlPhilosophy
|
|
133
|
+
BaseRepository[ModelT, FactoryT]
|
|
134
|
+
|
|
135
|
+
apiPhilosophy
|
|
136
|
+
BaseApiRepository[ResourceT, FactoryT]
|
|
137
|
+
|
|
138
|
+
application
|
|
139
|
+
PermissionServiceRepository(ServiceRepository[ServiceFactory])
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
Each layer adds its own concern. `servicePhilosophy` only handles factory wiring; specialization lives in the package or application that needs it.
|
|
143
|
+
|
|
144
|
+
## Install
|
|
145
|
+
|
|
146
|
+
```bash
|
|
147
|
+
pip install servicephilosophy
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
Development:
|
|
151
|
+
|
|
152
|
+
```bash
|
|
153
|
+
uv sync --extra dev
|
|
154
|
+
uv run pytest
|
|
155
|
+
uv run ruff check src tests
|
|
156
|
+
uv run mypy src
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
## Public API
|
|
160
|
+
|
|
161
|
+
```python
|
|
162
|
+
from servicephilosophy import (
|
|
163
|
+
FactoryRequiredError,
|
|
164
|
+
RepositoryFactoryProtocol,
|
|
165
|
+
ServiceRepository,
|
|
166
|
+
ServiceRepositoryProtocol,
|
|
167
|
+
)
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
Or import from submodules:
|
|
171
|
+
|
|
172
|
+
```python
|
|
173
|
+
from servicephilosophy.repository import ServiceRepository
|
|
174
|
+
from servicephilosophy.protocols import RepositoryFactoryProtocol, ServiceRepositoryProtocol
|
|
175
|
+
from servicephilosophy.exceptions import FactoryRequiredError
|
|
176
|
+
```
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
# servicePhilosophy
|
|
2
|
+
|
|
3
|
+
A small typed foundation for factory-aware repository-style components.
|
|
4
|
+
|
|
5
|
+
| | |
|
|
6
|
+
|---|---|
|
|
7
|
+
| **Python** | 3.12+ |
|
|
8
|
+
| **Runtime deps** | none |
|
|
9
|
+
| **License** | MIT |
|
|
10
|
+
|
|
11
|
+
## Core idea
|
|
12
|
+
|
|
13
|
+
```text
|
|
14
|
+
ServiceRepository[FactoryT]
|
|
15
|
+
= a neutral base class for objects that need factory access.
|
|
16
|
+
It does not require a model.
|
|
17
|
+
It does not know about SQL, HTTP, controllers, or frameworks.
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
`ServiceRepository` stores an optional factory and exposes it through:
|
|
21
|
+
|
|
22
|
+
- **`factory`** — returns the factory, or raises `FactoryRequiredError` when missing
|
|
23
|
+
- **`maybe_factory`** — returns the factory or `None`
|
|
24
|
+
- **`has_factory`** — `True` when a factory was provided at construction
|
|
25
|
+
|
|
26
|
+
Use **`ServiceRepositoryProtocol`** when you want to type against that shape without inheriting from the base class. Use **`RepositoryFactoryProtocol`** as a minimal marker for factory types that downstream packages extend.
|
|
27
|
+
|
|
28
|
+
## What this is (and is not)
|
|
29
|
+
|
|
30
|
+
- **`ServiceRepository` is not a SQL repository.**
|
|
31
|
+
- **`ServiceRepository` is not an API client.**
|
|
32
|
+
- It is a **shared factory-aware base**.
|
|
33
|
+
- **[sqlPhilosophy](https://github.com/SignalSafeSoftware/sqlphilosophy)** can extend it with model-bound persistence.
|
|
34
|
+
- **apiPhilosophy** can extend it with HTTP/resource clients.
|
|
35
|
+
- **Application service repositories** can extend it directly for business logic with no model at all.
|
|
36
|
+
|
|
37
|
+
This package has zero runtime dependencies. It does not include SQLAlchemy, HTTP clients, FastAPI, Pydantic, or Django.
|
|
38
|
+
|
|
39
|
+
## Basic service repository
|
|
40
|
+
|
|
41
|
+
```python
|
|
42
|
+
from servicephilosophy import ServiceRepository
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
class ServiceFactory:
|
|
46
|
+
def greeting(self) -> str:
|
|
47
|
+
return "hello"
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
class GreetingService(ServiceRepository[ServiceFactory]):
|
|
51
|
+
def greet(self) -> str:
|
|
52
|
+
return self.factory.greeting()
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
## SQL specialization in another package
|
|
56
|
+
|
|
57
|
+
```python
|
|
58
|
+
from typing import Generic, TypeVar
|
|
59
|
+
|
|
60
|
+
from servicephilosophy import ServiceRepository
|
|
61
|
+
|
|
62
|
+
ModelT = TypeVar("ModelT")
|
|
63
|
+
FactoryT = TypeVar("FactoryT")
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
class BaseRepository(ServiceRepository[FactoryT], Generic[ModelT, FactoryT]):
|
|
67
|
+
model: type[ModelT]
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
## API specialization in another package
|
|
71
|
+
|
|
72
|
+
```python
|
|
73
|
+
from typing import Generic, TypeVar
|
|
74
|
+
|
|
75
|
+
from servicephilosophy import ServiceRepository
|
|
76
|
+
|
|
77
|
+
ResourceT = TypeVar("ResourceT")
|
|
78
|
+
FactoryT = TypeVar("FactoryT")
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
class BaseApiRepository(ServiceRepository[FactoryT], Generic[ResourceT, FactoryT]):
|
|
82
|
+
pass
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
## Business logic with no model
|
|
86
|
+
|
|
87
|
+
```python
|
|
88
|
+
from servicephilosophy import ServiceRepository
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
class PermissionServiceRepository(ServiceRepository[ServiceFactory]):
|
|
92
|
+
def has_permission(self, actor_id: int, permission: str) -> bool:
|
|
93
|
+
return True
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
## Recommended ecosystem
|
|
97
|
+
|
|
98
|
+
```text
|
|
99
|
+
servicePhilosophy
|
|
100
|
+
ServiceRepository[FactoryT]
|
|
101
|
+
|
|
102
|
+
sqlPhilosophy
|
|
103
|
+
BaseRepository[ModelT, FactoryT]
|
|
104
|
+
|
|
105
|
+
apiPhilosophy
|
|
106
|
+
BaseApiRepository[ResourceT, FactoryT]
|
|
107
|
+
|
|
108
|
+
application
|
|
109
|
+
PermissionServiceRepository(ServiceRepository[ServiceFactory])
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
Each layer adds its own concern. `servicePhilosophy` only handles factory wiring; specialization lives in the package or application that needs it.
|
|
113
|
+
|
|
114
|
+
## Install
|
|
115
|
+
|
|
116
|
+
```bash
|
|
117
|
+
pip install servicephilosophy
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
Development:
|
|
121
|
+
|
|
122
|
+
```bash
|
|
123
|
+
uv sync --extra dev
|
|
124
|
+
uv run pytest
|
|
125
|
+
uv run ruff check src tests
|
|
126
|
+
uv run mypy src
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
## Public API
|
|
130
|
+
|
|
131
|
+
```python
|
|
132
|
+
from servicephilosophy import (
|
|
133
|
+
FactoryRequiredError,
|
|
134
|
+
RepositoryFactoryProtocol,
|
|
135
|
+
ServiceRepository,
|
|
136
|
+
ServiceRepositoryProtocol,
|
|
137
|
+
)
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
Or import from submodules:
|
|
141
|
+
|
|
142
|
+
```python
|
|
143
|
+
from servicephilosophy.repository import ServiceRepository
|
|
144
|
+
from servicephilosophy.protocols import RepositoryFactoryProtocol, ServiceRepositoryProtocol
|
|
145
|
+
from servicephilosophy.exceptions import FactoryRequiredError
|
|
146
|
+
```
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=69", "wheel"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "servicephilosophy"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "Typed factory-aware base for service and repository components without a domain model."
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.12,<4.0"
|
|
11
|
+
license = "MIT"
|
|
12
|
+
license-files = ["LICENSE"]
|
|
13
|
+
authors = [
|
|
14
|
+
{ name = "Josh Martin", email = "denverprogrammer@gmail.com" }
|
|
15
|
+
]
|
|
16
|
+
keywords = ["repository-pattern", "service-layer", "factory", "typing"]
|
|
17
|
+
classifiers = [
|
|
18
|
+
"Development Status :: 3 - Alpha",
|
|
19
|
+
"Intended Audience :: Developers",
|
|
20
|
+
"Programming Language :: Python :: 3",
|
|
21
|
+
"Programming Language :: Python :: 3.12",
|
|
22
|
+
"Programming Language :: Python :: 3.13",
|
|
23
|
+
"Typing :: Typed",
|
|
24
|
+
]
|
|
25
|
+
dependencies = []
|
|
26
|
+
|
|
27
|
+
[project.optional-dependencies]
|
|
28
|
+
dev = [
|
|
29
|
+
"pytest>=8",
|
|
30
|
+
"pytest-cov>=6",
|
|
31
|
+
"ruff>=0.15",
|
|
32
|
+
"mypy>=1.11",
|
|
33
|
+
"build>=1.2",
|
|
34
|
+
"twine>=5",
|
|
35
|
+
"bandit>=1.7",
|
|
36
|
+
]
|
|
37
|
+
|
|
38
|
+
[project.urls]
|
|
39
|
+
Homepage = "https://github.com/SignalSafeSoftware/servicephilosophy"
|
|
40
|
+
Repository = "https://github.com/SignalSafeSoftware/servicephilosophy"
|
|
41
|
+
Documentation = "https://github.com/SignalSafeSoftware/servicephilosophy#readme"
|
|
42
|
+
Issues = "https://github.com/SignalSafeSoftware/servicephilosophy/issues"
|
|
43
|
+
|
|
44
|
+
[tool.setuptools.packages.find]
|
|
45
|
+
where = ["src"]
|
|
46
|
+
|
|
47
|
+
[tool.setuptools.package-data]
|
|
48
|
+
servicephilosophy = ["py.typed"]
|
|
49
|
+
|
|
50
|
+
[tool.pytest.ini_options]
|
|
51
|
+
testpaths = ["tests"]
|
|
52
|
+
addopts = "--cov=servicephilosophy --cov-report=term-missing --cov-fail-under=100"
|
|
53
|
+
|
|
54
|
+
[tool.coverage.run]
|
|
55
|
+
source = ["servicephilosophy"]
|
|
56
|
+
|
|
57
|
+
[tool.coverage.report]
|
|
58
|
+
fail_under = 100
|
|
59
|
+
show_missing = true
|
|
60
|
+
|
|
61
|
+
[tool.mypy]
|
|
62
|
+
python_version = "3.12"
|
|
63
|
+
warn_unused_ignores = true
|
|
64
|
+
strict = true
|
|
65
|
+
|
|
66
|
+
[tool.ruff]
|
|
67
|
+
target-version = "py312"
|
|
68
|
+
line-length = 120
|
|
69
|
+
src = ["src", "tests"]
|
|
70
|
+
|
|
71
|
+
[tool.ruff.lint]
|
|
72
|
+
select = [
|
|
73
|
+
"E",
|
|
74
|
+
"F",
|
|
75
|
+
"W",
|
|
76
|
+
"I",
|
|
77
|
+
"B",
|
|
78
|
+
"UP",
|
|
79
|
+
"SIM",
|
|
80
|
+
"PT",
|
|
81
|
+
"RUF",
|
|
82
|
+
]
|
|
83
|
+
ignore = [
|
|
84
|
+
"E203",
|
|
85
|
+
]
|
|
86
|
+
|
|
87
|
+
[tool.ruff.lint.per-file-ignores]
|
|
88
|
+
"tests/**/*.py" = [
|
|
89
|
+
"PT011",
|
|
90
|
+
]
|
|
91
|
+
|
|
92
|
+
[tool.uv]
|
|
93
|
+
package = true
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
"""Factory-aware service repository foundation."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from servicephilosophy.exceptions import FactoryRequiredError, ServicePhilosophyError
|
|
6
|
+
from servicephilosophy.protocols import RepositoryFactoryProtocol, ServiceRepositoryProtocol
|
|
7
|
+
from servicephilosophy.repository import ServiceRepository
|
|
8
|
+
|
|
9
|
+
__all__ = [
|
|
10
|
+
"FactoryRequiredError",
|
|
11
|
+
"RepositoryFactoryProtocol",
|
|
12
|
+
"ServicePhilosophyError",
|
|
13
|
+
"ServiceRepository",
|
|
14
|
+
"ServiceRepositoryProtocol",
|
|
15
|
+
"__version__",
|
|
16
|
+
]
|
|
17
|
+
|
|
18
|
+
__version__ = "0.1.0"
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
"""Shared exceptions for servicephilosophy."""
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class ServicePhilosophyError(Exception):
|
|
5
|
+
"""Base error for servicephilosophy."""
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class FactoryRequiredError(ServicePhilosophyError):
|
|
9
|
+
"""Raised when an operation requires a factory but none was configured."""
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
"""Protocols for factory-aware service repositories."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Protocol, TypeVar
|
|
6
|
+
|
|
7
|
+
FactoryT = TypeVar("FactoryT", covariant=True)
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class ServiceRepositoryProtocol(Protocol[FactoryT]):
|
|
11
|
+
"""Structural typing surface for factory-aware repositories."""
|
|
12
|
+
|
|
13
|
+
@property
|
|
14
|
+
def factory(self) -> FactoryT: ...
|
|
15
|
+
|
|
16
|
+
@property
|
|
17
|
+
def maybe_factory(self) -> FactoryT | None: ...
|
|
18
|
+
|
|
19
|
+
@property
|
|
20
|
+
def has_factory(self) -> bool: ...
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class RepositoryFactoryProtocol(Protocol):
|
|
24
|
+
"""Marker protocol for repository-scoped factories.
|
|
25
|
+
|
|
26
|
+
Intentionally empty so downstream packages can extend it with
|
|
27
|
+
SQL-, API-, or application-specific methods without coupling this
|
|
28
|
+
package to those concerns.
|
|
29
|
+
"""
|
|
File without changes
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
"""Factory-aware base class for service repositories without a domain model."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from abc import ABC
|
|
6
|
+
|
|
7
|
+
from servicephilosophy.exceptions import FactoryRequiredError
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class ServiceRepository[FactoryT](ABC): # noqa: B024 — extension point for service subclasses
|
|
11
|
+
"""Neutral factory-aware base for service-layer repositories.
|
|
12
|
+
|
|
13
|
+
Unlike data repositories, this type does **not** bind to a model or
|
|
14
|
+
persistence backend. Subclasses add domain methods and access the shared
|
|
15
|
+
factory through ``factory``, ``maybe_factory``, or ``has_factory``.
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
def __init__(self, factory: FactoryT | None = None) -> None:
|
|
19
|
+
self._factory: FactoryT | None = factory
|
|
20
|
+
|
|
21
|
+
@property
|
|
22
|
+
def factory(self) -> FactoryT:
|
|
23
|
+
if self._factory is None:
|
|
24
|
+
raise FactoryRequiredError("factory is required for this operation")
|
|
25
|
+
return self._factory
|
|
26
|
+
|
|
27
|
+
@property
|
|
28
|
+
def maybe_factory(self) -> FactoryT | None:
|
|
29
|
+
return self._factory
|
|
30
|
+
|
|
31
|
+
@property
|
|
32
|
+
def has_factory(self) -> bool:
|
|
33
|
+
return self._factory is not None
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: servicephilosophy
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Typed factory-aware base for service and repository components without a domain model.
|
|
5
|
+
Author-email: Josh Martin <denverprogrammer@gmail.com>
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/SignalSafeSoftware/servicephilosophy
|
|
8
|
+
Project-URL: Repository, https://github.com/SignalSafeSoftware/servicephilosophy
|
|
9
|
+
Project-URL: Documentation, https://github.com/SignalSafeSoftware/servicephilosophy#readme
|
|
10
|
+
Project-URL: Issues, https://github.com/SignalSafeSoftware/servicephilosophy/issues
|
|
11
|
+
Keywords: repository-pattern,service-layer,factory,typing
|
|
12
|
+
Classifier: Development Status :: 3 - Alpha
|
|
13
|
+
Classifier: Intended Audience :: Developers
|
|
14
|
+
Classifier: Programming Language :: Python :: 3
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
17
|
+
Classifier: Typing :: Typed
|
|
18
|
+
Requires-Python: <4.0,>=3.12
|
|
19
|
+
Description-Content-Type: text/markdown
|
|
20
|
+
License-File: LICENSE
|
|
21
|
+
Provides-Extra: dev
|
|
22
|
+
Requires-Dist: pytest>=8; extra == "dev"
|
|
23
|
+
Requires-Dist: pytest-cov>=6; extra == "dev"
|
|
24
|
+
Requires-Dist: ruff>=0.15; extra == "dev"
|
|
25
|
+
Requires-Dist: mypy>=1.11; extra == "dev"
|
|
26
|
+
Requires-Dist: build>=1.2; extra == "dev"
|
|
27
|
+
Requires-Dist: twine>=5; extra == "dev"
|
|
28
|
+
Requires-Dist: bandit>=1.7; extra == "dev"
|
|
29
|
+
Dynamic: license-file
|
|
30
|
+
|
|
31
|
+
# servicePhilosophy
|
|
32
|
+
|
|
33
|
+
A small typed foundation for factory-aware repository-style components.
|
|
34
|
+
|
|
35
|
+
| | |
|
|
36
|
+
|---|---|
|
|
37
|
+
| **Python** | 3.12+ |
|
|
38
|
+
| **Runtime deps** | none |
|
|
39
|
+
| **License** | MIT |
|
|
40
|
+
|
|
41
|
+
## Core idea
|
|
42
|
+
|
|
43
|
+
```text
|
|
44
|
+
ServiceRepository[FactoryT]
|
|
45
|
+
= a neutral base class for objects that need factory access.
|
|
46
|
+
It does not require a model.
|
|
47
|
+
It does not know about SQL, HTTP, controllers, or frameworks.
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
`ServiceRepository` stores an optional factory and exposes it through:
|
|
51
|
+
|
|
52
|
+
- **`factory`** — returns the factory, or raises `FactoryRequiredError` when missing
|
|
53
|
+
- **`maybe_factory`** — returns the factory or `None`
|
|
54
|
+
- **`has_factory`** — `True` when a factory was provided at construction
|
|
55
|
+
|
|
56
|
+
Use **`ServiceRepositoryProtocol`** when you want to type against that shape without inheriting from the base class. Use **`RepositoryFactoryProtocol`** as a minimal marker for factory types that downstream packages extend.
|
|
57
|
+
|
|
58
|
+
## What this is (and is not)
|
|
59
|
+
|
|
60
|
+
- **`ServiceRepository` is not a SQL repository.**
|
|
61
|
+
- **`ServiceRepository` is not an API client.**
|
|
62
|
+
- It is a **shared factory-aware base**.
|
|
63
|
+
- **[sqlPhilosophy](https://github.com/SignalSafeSoftware/sqlphilosophy)** can extend it with model-bound persistence.
|
|
64
|
+
- **apiPhilosophy** can extend it with HTTP/resource clients.
|
|
65
|
+
- **Application service repositories** can extend it directly for business logic with no model at all.
|
|
66
|
+
|
|
67
|
+
This package has zero runtime dependencies. It does not include SQLAlchemy, HTTP clients, FastAPI, Pydantic, or Django.
|
|
68
|
+
|
|
69
|
+
## Basic service repository
|
|
70
|
+
|
|
71
|
+
```python
|
|
72
|
+
from servicephilosophy import ServiceRepository
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
class ServiceFactory:
|
|
76
|
+
def greeting(self) -> str:
|
|
77
|
+
return "hello"
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
class GreetingService(ServiceRepository[ServiceFactory]):
|
|
81
|
+
def greet(self) -> str:
|
|
82
|
+
return self.factory.greeting()
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
## SQL specialization in another package
|
|
86
|
+
|
|
87
|
+
```python
|
|
88
|
+
from typing import Generic, TypeVar
|
|
89
|
+
|
|
90
|
+
from servicephilosophy import ServiceRepository
|
|
91
|
+
|
|
92
|
+
ModelT = TypeVar("ModelT")
|
|
93
|
+
FactoryT = TypeVar("FactoryT")
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
class BaseRepository(ServiceRepository[FactoryT], Generic[ModelT, FactoryT]):
|
|
97
|
+
model: type[ModelT]
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
## API specialization in another package
|
|
101
|
+
|
|
102
|
+
```python
|
|
103
|
+
from typing import Generic, TypeVar
|
|
104
|
+
|
|
105
|
+
from servicephilosophy import ServiceRepository
|
|
106
|
+
|
|
107
|
+
ResourceT = TypeVar("ResourceT")
|
|
108
|
+
FactoryT = TypeVar("FactoryT")
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
class BaseApiRepository(ServiceRepository[FactoryT], Generic[ResourceT, FactoryT]):
|
|
112
|
+
pass
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
## Business logic with no model
|
|
116
|
+
|
|
117
|
+
```python
|
|
118
|
+
from servicephilosophy import ServiceRepository
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
class PermissionServiceRepository(ServiceRepository[ServiceFactory]):
|
|
122
|
+
def has_permission(self, actor_id: int, permission: str) -> bool:
|
|
123
|
+
return True
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
## Recommended ecosystem
|
|
127
|
+
|
|
128
|
+
```text
|
|
129
|
+
servicePhilosophy
|
|
130
|
+
ServiceRepository[FactoryT]
|
|
131
|
+
|
|
132
|
+
sqlPhilosophy
|
|
133
|
+
BaseRepository[ModelT, FactoryT]
|
|
134
|
+
|
|
135
|
+
apiPhilosophy
|
|
136
|
+
BaseApiRepository[ResourceT, FactoryT]
|
|
137
|
+
|
|
138
|
+
application
|
|
139
|
+
PermissionServiceRepository(ServiceRepository[ServiceFactory])
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
Each layer adds its own concern. `servicePhilosophy` only handles factory wiring; specialization lives in the package or application that needs it.
|
|
143
|
+
|
|
144
|
+
## Install
|
|
145
|
+
|
|
146
|
+
```bash
|
|
147
|
+
pip install servicephilosophy
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
Development:
|
|
151
|
+
|
|
152
|
+
```bash
|
|
153
|
+
uv sync --extra dev
|
|
154
|
+
uv run pytest
|
|
155
|
+
uv run ruff check src tests
|
|
156
|
+
uv run mypy src
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
## Public API
|
|
160
|
+
|
|
161
|
+
```python
|
|
162
|
+
from servicephilosophy import (
|
|
163
|
+
FactoryRequiredError,
|
|
164
|
+
RepositoryFactoryProtocol,
|
|
165
|
+
ServiceRepository,
|
|
166
|
+
ServiceRepositoryProtocol,
|
|
167
|
+
)
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
Or import from submodules:
|
|
171
|
+
|
|
172
|
+
```python
|
|
173
|
+
from servicephilosophy.repository import ServiceRepository
|
|
174
|
+
from servicephilosophy.protocols import RepositoryFactoryProtocol, ServiceRepositoryProtocol
|
|
175
|
+
from servicephilosophy.exceptions import FactoryRequiredError
|
|
176
|
+
```
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
LICENSE
|
|
2
|
+
MANIFEST.in
|
|
3
|
+
README.md
|
|
4
|
+
pyproject.toml
|
|
5
|
+
src/servicephilosophy/__init__.py
|
|
6
|
+
src/servicephilosophy/exceptions.py
|
|
7
|
+
src/servicephilosophy/protocols.py
|
|
8
|
+
src/servicephilosophy/py.typed
|
|
9
|
+
src/servicephilosophy/repository.py
|
|
10
|
+
src/servicephilosophy.egg-info/PKG-INFO
|
|
11
|
+
src/servicephilosophy.egg-info/SOURCES.txt
|
|
12
|
+
src/servicephilosophy.egg-info/dependency_links.txt
|
|
13
|
+
src/servicephilosophy.egg-info/requires.txt
|
|
14
|
+
src/servicephilosophy.egg-info/top_level.txt
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
servicephilosophy
|