pydantic-store 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.
- pydantic_store-0.1.0/LICENSE.md +19 -0
- pydantic_store-0.1.0/PKG-INFO +212 -0
- pydantic_store-0.1.0/README.md +190 -0
- pydantic_store-0.1.0/pyproject.toml +30 -0
- pydantic_store-0.1.0/src/pydantic_store/__init__.py +19 -0
- pydantic_store-0.1.0/src/pydantic_store/dbm.py +57 -0
- pydantic_store-0.1.0/src/pydantic_store/dbm_sqlite.py +165 -0
- pydantic_store-0.1.0/src/pydantic_store/models.py +387 -0
- pydantic_store-0.1.0/src/pydantic_store/py.typed +0 -0
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
Copyright (c) 2025 mySociety
|
|
2
|
+
|
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
4
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
5
|
+
in the Software without restriction, including without limitation the rights
|
|
6
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
7
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
8
|
+
furnished to do so, subject to the following conditions:
|
|
9
|
+
|
|
10
|
+
The above copyright notice and this permission notice shall be included in all
|
|
11
|
+
copies or substantial portions of the Software.
|
|
12
|
+
|
|
13
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
|
14
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
|
15
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
|
|
16
|
+
IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
|
|
17
|
+
DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
|
|
18
|
+
OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE
|
|
19
|
+
OR OTHER DEALINGS IN THE SOFTWARE.
|
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: pydantic-store
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Helper models and backports
|
|
5
|
+
License: MIT
|
|
6
|
+
License-File: LICENSE.md
|
|
7
|
+
Author: mySociety
|
|
8
|
+
Author-email: alex.parsons@mysociety.org
|
|
9
|
+
Requires-Python: >=3.9,<4.0
|
|
10
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
11
|
+
Classifier: Programming Language :: Python :: 3
|
|
12
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
18
|
+
Requires-Dist: pydantic (>=2.0)
|
|
19
|
+
Project-URL: Homepage, https://github.com/mysociety/pydantic-store
|
|
20
|
+
Project-URL: Repository, https://github.com/mysociety/pydantic-store
|
|
21
|
+
Description-Content-Type: text/markdown
|
|
22
|
+
|
|
23
|
+
# pydantic-store
|
|
24
|
+
|
|
25
|
+
A collection of Pydantic models and storage approaches for ease of reading to/from files or creating caches.
|
|
26
|
+
|
|
27
|
+
## Installation
|
|
28
|
+
|
|
29
|
+
```bash
|
|
30
|
+
pip install pydantic-store
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
### Optional Dependencies
|
|
34
|
+
|
|
35
|
+
Additional dependencies needed for storing as YAML or TOML (not included to keep the dependency limited to pydantic)
|
|
36
|
+
|
|
37
|
+
```bash
|
|
38
|
+
# For YAML support
|
|
39
|
+
pip install ruamel.yaml
|
|
40
|
+
|
|
41
|
+
# For TOML writing support
|
|
42
|
+
pip install tomli-w
|
|
43
|
+
|
|
44
|
+
# For TOML reading on Python < 3.11
|
|
45
|
+
pip install tomli
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
## Quick Start
|
|
49
|
+
|
|
50
|
+
```python
|
|
51
|
+
from pydantic_store import BaseModel, ListModel, DictModel, JsonStore, PydanticDBM
|
|
52
|
+
|
|
53
|
+
# Define your models
|
|
54
|
+
class User(BaseModel):
|
|
55
|
+
name: str
|
|
56
|
+
age: int
|
|
57
|
+
email: str
|
|
58
|
+
|
|
59
|
+
# Create and save to file
|
|
60
|
+
user = User(name="Alice", age=30, email="alice@example.com")
|
|
61
|
+
user.to_file("user.json")
|
|
62
|
+
|
|
63
|
+
# Load from file
|
|
64
|
+
loaded_user = User.from_file("user.json")
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
## Model Types
|
|
68
|
+
|
|
69
|
+
### BaseModel
|
|
70
|
+
|
|
71
|
+
Enhanced Pydantic BaseModel with file I/O capabilities.
|
|
72
|
+
|
|
73
|
+
```python
|
|
74
|
+
from pydantic_store import BaseModel
|
|
75
|
+
|
|
76
|
+
class Config(BaseModel):
|
|
77
|
+
database_url: str
|
|
78
|
+
debug: bool = False
|
|
79
|
+
max_connections: int = 10
|
|
80
|
+
|
|
81
|
+
# Save to different formats
|
|
82
|
+
config = Config(database_url="postgresql://localhost/mydb")
|
|
83
|
+
config.to_file("config.json") # JSON format
|
|
84
|
+
config.to_file("config.yaml") # YAML format
|
|
85
|
+
config.to_file("config.toml") # TOML format
|
|
86
|
+
|
|
87
|
+
# Load from file (format auto-detected by extension)
|
|
88
|
+
config = Config.from_file("config.yaml")
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
### RootModel
|
|
92
|
+
|
|
93
|
+
A generic root model for wrapping single values with validation.
|
|
94
|
+
|
|
95
|
+
```python
|
|
96
|
+
from pydantic_store import RootModel
|
|
97
|
+
|
|
98
|
+
class Port(RootModel[int]):
|
|
99
|
+
pass
|
|
100
|
+
|
|
101
|
+
port = Port(8080)
|
|
102
|
+
port.to_file("port.json") # Saves: 8080
|
|
103
|
+
loaded_port = Port.from_file("port.json")
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
### ListModel
|
|
107
|
+
|
|
108
|
+
A list-like model that behaves like a Python list while providing Pydantic validation.
|
|
109
|
+
|
|
110
|
+
```python
|
|
111
|
+
from pydantic_store import ListModel
|
|
112
|
+
|
|
113
|
+
TodoList = ListModel[str]
|
|
114
|
+
|
|
115
|
+
todos = TodoList(["Buy groceries", "Walk the dog"])
|
|
116
|
+
|
|
117
|
+
# Use like a regular list
|
|
118
|
+
todos.append("Read a book")
|
|
119
|
+
todos.extend(["Exercise", "Cook dinner"])
|
|
120
|
+
print(len(todos)) # 5
|
|
121
|
+
print(todos[0]) # "Buy groceries"
|
|
122
|
+
|
|
123
|
+
# Persist to file
|
|
124
|
+
todos.to_file("todos.json")
|
|
125
|
+
|
|
126
|
+
# Load from file
|
|
127
|
+
loaded_todos = TodoList.from_file("todos.json")
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
### DictModel
|
|
131
|
+
|
|
132
|
+
A dictionary-like model that behaves like a Python dict with validation.
|
|
133
|
+
|
|
134
|
+
```python
|
|
135
|
+
from pydantic_store import DictModel
|
|
136
|
+
|
|
137
|
+
Settings = DictModel[str, int]
|
|
138
|
+
|
|
139
|
+
settings = Settings({"timeout": 30, "retries": 3})
|
|
140
|
+
|
|
141
|
+
# Use like a regular dict
|
|
142
|
+
settings["max_workers"] = 4
|
|
143
|
+
settings.update({"cache_size": 1000})
|
|
144
|
+
print(settings.keys())
|
|
145
|
+
print(len(settings))
|
|
146
|
+
|
|
147
|
+
# Persist to file
|
|
148
|
+
settings.to_file("settings.yaml")
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
### PydanticDBM
|
|
152
|
+
|
|
153
|
+
A wrapper around the sqlite DBM backend (backported from 3.14) to store and retrieve pydantic models.
|
|
154
|
+
|
|
155
|
+
```python
|
|
156
|
+
from pydantic_store import PydanticDBM
|
|
157
|
+
from pydantic import BaseModel
|
|
158
|
+
|
|
159
|
+
class User(BaseModel):
|
|
160
|
+
name: str
|
|
161
|
+
age: int
|
|
162
|
+
|
|
163
|
+
UserDBM = PydanticDBM[User]
|
|
164
|
+
|
|
165
|
+
# Method 1: Type subscription
|
|
166
|
+
with UserDBM("users.db") as db:
|
|
167
|
+
user = User(name="Alice", age=30)
|
|
168
|
+
db["alice"] = user
|
|
169
|
+
retrieved_user = db["alice"] # Automatically validated as User
|
|
170
|
+
|
|
171
|
+
# Method 2: Explicit storage format
|
|
172
|
+
with PydanticDBM("users.db", storage_format=User) as db:
|
|
173
|
+
db["bob"] = User(name="Bob", age=25)
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
### JsonStore
|
|
177
|
+
|
|
178
|
+
A persistent dictionary that automatically saves changes to disk.
|
|
179
|
+
|
|
180
|
+
```python
|
|
181
|
+
from pydantic_store import JsonStore
|
|
182
|
+
from pathlib import Path
|
|
183
|
+
|
|
184
|
+
# Connect to a JSON file (creates if doesn't exist)
|
|
185
|
+
store = JsonStore[str].connect(Path("data.json"))
|
|
186
|
+
|
|
187
|
+
# Changes are automatically persisted
|
|
188
|
+
store["user:1"] = "Alice"
|
|
189
|
+
store["user:2"] = "Bob"
|
|
190
|
+
|
|
191
|
+
# Data is immediately written to data.json
|
|
192
|
+
print(store["user:1"]) # "Alice"
|
|
193
|
+
```
|
|
194
|
+
|
|
195
|
+
### Supported Formats
|
|
196
|
+
|
|
197
|
+
pydantic-store supports multiple file formats with automatic format detection:
|
|
198
|
+
|
|
199
|
+
- **JSON**: Always available
|
|
200
|
+
- **YAML**: Human-readable format (requires `ruamel.yaml`)
|
|
201
|
+
- **TOML**: Configuration-friendly format (requires `tomli` for reading, `tomli-w` for writing)
|
|
202
|
+
|
|
203
|
+
|
|
204
|
+
You can also specify the format:
|
|
205
|
+
|
|
206
|
+
```python
|
|
207
|
+
model.to_file("data.txt", file_format="json")
|
|
208
|
+
```
|
|
209
|
+
|
|
210
|
+
## Licence
|
|
211
|
+
|
|
212
|
+
MIT Licence - see LICENSE.md for details.
|
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
# pydantic-store
|
|
2
|
+
|
|
3
|
+
A collection of Pydantic models and storage approaches for ease of reading to/from files or creating caches.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
pip install pydantic-store
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
### Optional Dependencies
|
|
12
|
+
|
|
13
|
+
Additional dependencies needed for storing as YAML or TOML (not included to keep the dependency limited to pydantic)
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
# For YAML support
|
|
17
|
+
pip install ruamel.yaml
|
|
18
|
+
|
|
19
|
+
# For TOML writing support
|
|
20
|
+
pip install tomli-w
|
|
21
|
+
|
|
22
|
+
# For TOML reading on Python < 3.11
|
|
23
|
+
pip install tomli
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
## Quick Start
|
|
27
|
+
|
|
28
|
+
```python
|
|
29
|
+
from pydantic_store import BaseModel, ListModel, DictModel, JsonStore, PydanticDBM
|
|
30
|
+
|
|
31
|
+
# Define your models
|
|
32
|
+
class User(BaseModel):
|
|
33
|
+
name: str
|
|
34
|
+
age: int
|
|
35
|
+
email: str
|
|
36
|
+
|
|
37
|
+
# Create and save to file
|
|
38
|
+
user = User(name="Alice", age=30, email="alice@example.com")
|
|
39
|
+
user.to_file("user.json")
|
|
40
|
+
|
|
41
|
+
# Load from file
|
|
42
|
+
loaded_user = User.from_file("user.json")
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
## Model Types
|
|
46
|
+
|
|
47
|
+
### BaseModel
|
|
48
|
+
|
|
49
|
+
Enhanced Pydantic BaseModel with file I/O capabilities.
|
|
50
|
+
|
|
51
|
+
```python
|
|
52
|
+
from pydantic_store import BaseModel
|
|
53
|
+
|
|
54
|
+
class Config(BaseModel):
|
|
55
|
+
database_url: str
|
|
56
|
+
debug: bool = False
|
|
57
|
+
max_connections: int = 10
|
|
58
|
+
|
|
59
|
+
# Save to different formats
|
|
60
|
+
config = Config(database_url="postgresql://localhost/mydb")
|
|
61
|
+
config.to_file("config.json") # JSON format
|
|
62
|
+
config.to_file("config.yaml") # YAML format
|
|
63
|
+
config.to_file("config.toml") # TOML format
|
|
64
|
+
|
|
65
|
+
# Load from file (format auto-detected by extension)
|
|
66
|
+
config = Config.from_file("config.yaml")
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
### RootModel
|
|
70
|
+
|
|
71
|
+
A generic root model for wrapping single values with validation.
|
|
72
|
+
|
|
73
|
+
```python
|
|
74
|
+
from pydantic_store import RootModel
|
|
75
|
+
|
|
76
|
+
class Port(RootModel[int]):
|
|
77
|
+
pass
|
|
78
|
+
|
|
79
|
+
port = Port(8080)
|
|
80
|
+
port.to_file("port.json") # Saves: 8080
|
|
81
|
+
loaded_port = Port.from_file("port.json")
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
### ListModel
|
|
85
|
+
|
|
86
|
+
A list-like model that behaves like a Python list while providing Pydantic validation.
|
|
87
|
+
|
|
88
|
+
```python
|
|
89
|
+
from pydantic_store import ListModel
|
|
90
|
+
|
|
91
|
+
TodoList = ListModel[str]
|
|
92
|
+
|
|
93
|
+
todos = TodoList(["Buy groceries", "Walk the dog"])
|
|
94
|
+
|
|
95
|
+
# Use like a regular list
|
|
96
|
+
todos.append("Read a book")
|
|
97
|
+
todos.extend(["Exercise", "Cook dinner"])
|
|
98
|
+
print(len(todos)) # 5
|
|
99
|
+
print(todos[0]) # "Buy groceries"
|
|
100
|
+
|
|
101
|
+
# Persist to file
|
|
102
|
+
todos.to_file("todos.json")
|
|
103
|
+
|
|
104
|
+
# Load from file
|
|
105
|
+
loaded_todos = TodoList.from_file("todos.json")
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
### DictModel
|
|
109
|
+
|
|
110
|
+
A dictionary-like model that behaves like a Python dict with validation.
|
|
111
|
+
|
|
112
|
+
```python
|
|
113
|
+
from pydantic_store import DictModel
|
|
114
|
+
|
|
115
|
+
Settings = DictModel[str, int]
|
|
116
|
+
|
|
117
|
+
settings = Settings({"timeout": 30, "retries": 3})
|
|
118
|
+
|
|
119
|
+
# Use like a regular dict
|
|
120
|
+
settings["max_workers"] = 4
|
|
121
|
+
settings.update({"cache_size": 1000})
|
|
122
|
+
print(settings.keys())
|
|
123
|
+
print(len(settings))
|
|
124
|
+
|
|
125
|
+
# Persist to file
|
|
126
|
+
settings.to_file("settings.yaml")
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
### PydanticDBM
|
|
130
|
+
|
|
131
|
+
A wrapper around the sqlite DBM backend (backported from 3.14) to store and retrieve pydantic models.
|
|
132
|
+
|
|
133
|
+
```python
|
|
134
|
+
from pydantic_store import PydanticDBM
|
|
135
|
+
from pydantic import BaseModel
|
|
136
|
+
|
|
137
|
+
class User(BaseModel):
|
|
138
|
+
name: str
|
|
139
|
+
age: int
|
|
140
|
+
|
|
141
|
+
UserDBM = PydanticDBM[User]
|
|
142
|
+
|
|
143
|
+
# Method 1: Type subscription
|
|
144
|
+
with UserDBM("users.db") as db:
|
|
145
|
+
user = User(name="Alice", age=30)
|
|
146
|
+
db["alice"] = user
|
|
147
|
+
retrieved_user = db["alice"] # Automatically validated as User
|
|
148
|
+
|
|
149
|
+
# Method 2: Explicit storage format
|
|
150
|
+
with PydanticDBM("users.db", storage_format=User) as db:
|
|
151
|
+
db["bob"] = User(name="Bob", age=25)
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
### JsonStore
|
|
155
|
+
|
|
156
|
+
A persistent dictionary that automatically saves changes to disk.
|
|
157
|
+
|
|
158
|
+
```python
|
|
159
|
+
from pydantic_store import JsonStore
|
|
160
|
+
from pathlib import Path
|
|
161
|
+
|
|
162
|
+
# Connect to a JSON file (creates if doesn't exist)
|
|
163
|
+
store = JsonStore[str].connect(Path("data.json"))
|
|
164
|
+
|
|
165
|
+
# Changes are automatically persisted
|
|
166
|
+
store["user:1"] = "Alice"
|
|
167
|
+
store["user:2"] = "Bob"
|
|
168
|
+
|
|
169
|
+
# Data is immediately written to data.json
|
|
170
|
+
print(store["user:1"]) # "Alice"
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
### Supported Formats
|
|
174
|
+
|
|
175
|
+
pydantic-store supports multiple file formats with automatic format detection:
|
|
176
|
+
|
|
177
|
+
- **JSON**: Always available
|
|
178
|
+
- **YAML**: Human-readable format (requires `ruamel.yaml`)
|
|
179
|
+
- **TOML**: Configuration-friendly format (requires `tomli` for reading, `tomli-w` for writing)
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
You can also specify the format:
|
|
183
|
+
|
|
184
|
+
```python
|
|
185
|
+
model.to_file("data.txt", file_format="json")
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
## Licence
|
|
189
|
+
|
|
190
|
+
MIT Licence - see LICENSE.md for details.
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
[tool.poetry]
|
|
2
|
+
name = "pydantic-store"
|
|
3
|
+
version = "0.1.0"
|
|
4
|
+
description = "Helper models and backports"
|
|
5
|
+
authors = ["mySociety <alex.parsons@mysociety.org>"]
|
|
6
|
+
readme = "README.md"
|
|
7
|
+
license = "MIT"
|
|
8
|
+
homepage = "https://github.com/mysociety/pydantic-store"
|
|
9
|
+
repository = "https://github.com/mysociety/pydantic-store"
|
|
10
|
+
include = [
|
|
11
|
+
"LICENSE.md",
|
|
12
|
+
]
|
|
13
|
+
|
|
14
|
+
[tool.poetry_bumpversion.file."src/pydantic_store/__init__.py"]
|
|
15
|
+
|
|
16
|
+
[tool.poetry.dependencies]
|
|
17
|
+
python = "^3.9"
|
|
18
|
+
pydantic = ">=2.0"
|
|
19
|
+
|
|
20
|
+
[tool.poetry.group.dev.dependencies]
|
|
21
|
+
pytest = "^7.1.2"
|
|
22
|
+
pytest-cov = "^3.0.0"
|
|
23
|
+
pylint = "^2.12.2"
|
|
24
|
+
ruff = "^0.4.4"
|
|
25
|
+
pyright = "^1.1"
|
|
26
|
+
toml = "^0.10.2"
|
|
27
|
+
|
|
28
|
+
[build-system]
|
|
29
|
+
requires = ["poetry-core>=1.0.0"]
|
|
30
|
+
build-backend = "poetry.core.masonry.api"
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Helper models and backports
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from __future__ import annotations
|
|
6
|
+
|
|
7
|
+
__version__ = "0.1.0"
|
|
8
|
+
|
|
9
|
+
__all__ = [
|
|
10
|
+
"BaseModel",
|
|
11
|
+
"RootModel",
|
|
12
|
+
"ListModel",
|
|
13
|
+
"DictModel",
|
|
14
|
+
"JsonStore",
|
|
15
|
+
"PydanticDBM",
|
|
16
|
+
]
|
|
17
|
+
|
|
18
|
+
from .dbm import PydanticDBM
|
|
19
|
+
from .models import BaseModel, DictModel, JsonStore, ListModel, RootModel
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
from typing import Literal, Optional, TypeVar, Union
|
|
5
|
+
|
|
6
|
+
from pydantic import TypeAdapter
|
|
7
|
+
|
|
8
|
+
from .dbm_sqlite import _Database # type: ignore
|
|
9
|
+
|
|
10
|
+
T = TypeVar("T")
|
|
11
|
+
PathLike = Union[str, Path]
|
|
12
|
+
FlagOptions = Literal["r", "w", "c", "n"]
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class PydanticDBM(_Database[T]):
|
|
16
|
+
default_storage_format = None
|
|
17
|
+
|
|
18
|
+
def __class_getitem__(cls, storage_format: type[T]):
|
|
19
|
+
class _child(cls):
|
|
20
|
+
default_storage_format = storage_format
|
|
21
|
+
|
|
22
|
+
_child.__name__ = cls.__name__
|
|
23
|
+
|
|
24
|
+
return _child
|
|
25
|
+
|
|
26
|
+
def __init__(
|
|
27
|
+
self,
|
|
28
|
+
path: PathLike,
|
|
29
|
+
/,
|
|
30
|
+
*,
|
|
31
|
+
flag: FlagOptions = "c",
|
|
32
|
+
mode: int = 0o600,
|
|
33
|
+
storage_format: Optional[type[T]] = None,
|
|
34
|
+
):
|
|
35
|
+
super().__init__(path, flag=flag, mode=mode)
|
|
36
|
+
self.storage_format = storage_format or self.default_storage_format
|
|
37
|
+
if not self.storage_format:
|
|
38
|
+
raise ValueError(
|
|
39
|
+
"storage_format must be provided either as argument or class attribute"
|
|
40
|
+
)
|
|
41
|
+
self.type_adapter = TypeAdapter(self.storage_format)
|
|
42
|
+
|
|
43
|
+
def __getitem__(self, key: str) -> T:
|
|
44
|
+
return self.type_adapter.validate_json(super().__getitem__(key)) # type: ignore
|
|
45
|
+
|
|
46
|
+
def __setitem__(self, key: str, value: T) -> None:
|
|
47
|
+
super().__setitem__(key, self.type_adapter.dump_json(value)) # type: ignore
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def open(
|
|
51
|
+
filename: PathLike,
|
|
52
|
+
/,
|
|
53
|
+
flag: FlagOptions = "c",
|
|
54
|
+
mode: int = 0o600,
|
|
55
|
+
storage_format: type[T] = str,
|
|
56
|
+
) -> PydanticDBM[T]:
|
|
57
|
+
return PydanticDBM(filename, flag=flag, mode=mode, storage_format=storage_format)
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Backport of dbm.sqlite3 module for Pydantic IO.
|
|
3
|
+
Added type annotations and generics support.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
import os
|
|
9
|
+
import sqlite3
|
|
10
|
+
from collections.abc import Iterator, KeysView, MutableMapping
|
|
11
|
+
from contextlib import closing, suppress
|
|
12
|
+
from pathlib import Path
|
|
13
|
+
from types import TracebackType
|
|
14
|
+
from typing import Any, Literal, TypeVar, Union
|
|
15
|
+
|
|
16
|
+
BUILD_TABLE = """
|
|
17
|
+
CREATE TABLE IF NOT EXISTS Dict (
|
|
18
|
+
key BLOB UNIQUE NOT NULL,
|
|
19
|
+
value BLOB NOT NULL
|
|
20
|
+
)
|
|
21
|
+
"""
|
|
22
|
+
GET_SIZE = "SELECT COUNT (key) FROM Dict"
|
|
23
|
+
LOOKUP_KEY = "SELECT value FROM Dict WHERE key = CAST(? AS BLOB)"
|
|
24
|
+
STORE_KV = "REPLACE INTO Dict (key, value) VALUES (CAST(? AS BLOB), CAST(? AS BLOB))"
|
|
25
|
+
DELETE_KEY = "DELETE FROM Dict WHERE key = CAST(? AS BLOB)"
|
|
26
|
+
ITER_KEYS = "SELECT key FROM Dict"
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class error(OSError):
|
|
30
|
+
pass
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
_ERR_CLOSED = "DBM object has already been closed"
|
|
34
|
+
_ERR_REINIT = "DBM object does not support reinitialization"
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def _normalize_uri(path: Union[str, os.PathLike[str]]) -> str:
|
|
38
|
+
path = Path(path)
|
|
39
|
+
uri = path.absolute().as_uri()
|
|
40
|
+
while "//" in uri:
|
|
41
|
+
uri = uri.replace("//", "/")
|
|
42
|
+
return uri
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
T = TypeVar("T")
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
class _Database(MutableMapping[str, T]):
|
|
49
|
+
def __init__(
|
|
50
|
+
self, path: Union[str, os.PathLike[str]], /, *, flag: str, mode: int
|
|
51
|
+
) -> None:
|
|
52
|
+
if hasattr(self, "_cx"):
|
|
53
|
+
raise error(_ERR_REINIT)
|
|
54
|
+
|
|
55
|
+
path = os.fsdecode(path)
|
|
56
|
+
if flag == "r":
|
|
57
|
+
flag = "ro"
|
|
58
|
+
elif flag == "w":
|
|
59
|
+
flag = "rw"
|
|
60
|
+
elif flag == "c":
|
|
61
|
+
flag = "rwc"
|
|
62
|
+
Path(path).touch(mode=mode, exist_ok=True)
|
|
63
|
+
elif flag == "n":
|
|
64
|
+
flag = "rwc"
|
|
65
|
+
Path(path).unlink(missing_ok=True)
|
|
66
|
+
Path(path).touch(mode=mode)
|
|
67
|
+
else:
|
|
68
|
+
raise ValueError(f"Flag must be one of 'r', 'w', 'c', or 'n', not {flag!r}")
|
|
69
|
+
|
|
70
|
+
# We use the URI format when opening the database.
|
|
71
|
+
uri = _normalize_uri(path)
|
|
72
|
+
uri = f"{uri}?mode={flag}"
|
|
73
|
+
if flag == "ro":
|
|
74
|
+
# Add immutable=1 to allow read-only SQLite access even if wal/shm missing
|
|
75
|
+
uri += "&immutable=1"
|
|
76
|
+
|
|
77
|
+
try:
|
|
78
|
+
self._cx = sqlite3.connect(uri, isolation_level=None, uri=True)
|
|
79
|
+
except sqlite3.Error as exc:
|
|
80
|
+
raise error(str(exc))
|
|
81
|
+
|
|
82
|
+
if flag != "ro":
|
|
83
|
+
# This is an optimization only; it's ok if it fails.
|
|
84
|
+
with suppress(sqlite3.OperationalError):
|
|
85
|
+
self._cx.execute("PRAGMA journal_mode = wal")
|
|
86
|
+
|
|
87
|
+
if flag == "rwc":
|
|
88
|
+
self._execute(BUILD_TABLE)
|
|
89
|
+
|
|
90
|
+
def _execute(self, *args: Any, **kwargs: Any) -> closing[sqlite3.Cursor]:
|
|
91
|
+
if not self._cx:
|
|
92
|
+
raise error(_ERR_CLOSED)
|
|
93
|
+
try:
|
|
94
|
+
return closing(self._cx.execute(*args, **kwargs))
|
|
95
|
+
except sqlite3.Error as exc:
|
|
96
|
+
raise error(str(exc))
|
|
97
|
+
|
|
98
|
+
def __len__(self) -> int:
|
|
99
|
+
with self._execute(GET_SIZE) as cu:
|
|
100
|
+
row = cu.fetchone()
|
|
101
|
+
return row[0]
|
|
102
|
+
|
|
103
|
+
def __getitem__(self, key: str) -> T:
|
|
104
|
+
with self._execute(LOOKUP_KEY, (key,)) as cu:
|
|
105
|
+
row = cu.fetchone()
|
|
106
|
+
if not row:
|
|
107
|
+
raise KeyError(key)
|
|
108
|
+
return row[0]
|
|
109
|
+
|
|
110
|
+
def __setitem__(self, key: str, value: T) -> None:
|
|
111
|
+
self._execute(STORE_KV, (key, value))
|
|
112
|
+
|
|
113
|
+
def __delitem__(self, key: str) -> None:
|
|
114
|
+
with self._execute(DELETE_KEY, (key,)) as cu:
|
|
115
|
+
if not cu.rowcount:
|
|
116
|
+
raise KeyError(key)
|
|
117
|
+
|
|
118
|
+
def __iter__(self) -> Iterator[str]:
|
|
119
|
+
try:
|
|
120
|
+
with self._execute(ITER_KEYS) as cu:
|
|
121
|
+
for row in cu:
|
|
122
|
+
yield row[0].decode("utf-8")
|
|
123
|
+
except sqlite3.Error as exc:
|
|
124
|
+
raise error(str(exc))
|
|
125
|
+
|
|
126
|
+
def close(self) -> None:
|
|
127
|
+
if self._cx:
|
|
128
|
+
self._cx.close()
|
|
129
|
+
self._cx = None
|
|
130
|
+
|
|
131
|
+
def keys(self) -> KeysView[str]:
|
|
132
|
+
return super().keys()
|
|
133
|
+
|
|
134
|
+
def __enter__(self) -> _Database[T]:
|
|
135
|
+
return self
|
|
136
|
+
|
|
137
|
+
def __exit__(
|
|
138
|
+
self,
|
|
139
|
+
exc_type: type[BaseException] | None,
|
|
140
|
+
exc_val: BaseException | None,
|
|
141
|
+
exc_tb: TracebackType | None,
|
|
142
|
+
) -> None:
|
|
143
|
+
self.close()
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
def open(
|
|
147
|
+
filename: Union[str, os.PathLike[str]],
|
|
148
|
+
/,
|
|
149
|
+
flag: Literal["r", "w", "c", "n"] = "r",
|
|
150
|
+
mode: int = 0o666,
|
|
151
|
+
) -> _Database[bytes]:
|
|
152
|
+
"""Open a dbm.sqlite3 database and return the dbm object.
|
|
153
|
+
|
|
154
|
+
The 'filename' parameter is the name of the database file.
|
|
155
|
+
|
|
156
|
+
The optional 'flag' parameter can be one of ...:
|
|
157
|
+
'r' (default): open an existing database for read only access
|
|
158
|
+
'w': open an existing database for read/write access
|
|
159
|
+
'c': create a database if it does not exist; open for read/write access
|
|
160
|
+
'n': always create a new, empty database; open for read/write access
|
|
161
|
+
|
|
162
|
+
The optional 'mode' parameter is the Unix file access mode of the database;
|
|
163
|
+
only used when creating a new database. Default: 0o666.
|
|
164
|
+
"""
|
|
165
|
+
return _Database(filename, flag=flag, mode=mode)
|
|
@@ -0,0 +1,387 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Helper models and backports
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from __future__ import annotations
|
|
6
|
+
|
|
7
|
+
__version__ = "0.1.0"
|
|
8
|
+
|
|
9
|
+
import json
|
|
10
|
+
from collections.abc import MutableMapping, MutableSequence
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
from typing import (
|
|
13
|
+
Any,
|
|
14
|
+
Callable,
|
|
15
|
+
Iterable,
|
|
16
|
+
Literal,
|
|
17
|
+
Mapping,
|
|
18
|
+
Optional,
|
|
19
|
+
Protocol,
|
|
20
|
+
TypeVar,
|
|
21
|
+
Union,
|
|
22
|
+
overload,
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
from pydantic import BaseModel as PydanticBaseModel
|
|
26
|
+
from pydantic import Field, PrivateAttr
|
|
27
|
+
from pydantic import RootModel as PydanticRootModel
|
|
28
|
+
from typing_extensions import Self
|
|
29
|
+
|
|
30
|
+
# Optional imports for YAML and TOML support
|
|
31
|
+
try:
|
|
32
|
+
from ruamel.yaml import YAML # type: ignore
|
|
33
|
+
|
|
34
|
+
yaml = YAML()
|
|
35
|
+
yaml.preserve_quotes = True
|
|
36
|
+
yaml.width = 4096 # Avoid line wrapping
|
|
37
|
+
except ImportError:
|
|
38
|
+
yaml = None # type: ignore
|
|
39
|
+
|
|
40
|
+
try:
|
|
41
|
+
import tomllib # type: ignore
|
|
42
|
+
except ImportError:
|
|
43
|
+
try:
|
|
44
|
+
import tomli as tomllib # type: ignore
|
|
45
|
+
except ImportError:
|
|
46
|
+
tomllib = None # type: ignore
|
|
47
|
+
|
|
48
|
+
try:
|
|
49
|
+
import tomli_w # type: ignore
|
|
50
|
+
except ImportError:
|
|
51
|
+
tomli_w = None # type: ignore
|
|
52
|
+
|
|
53
|
+
ExtraValues = Literal["allow", "ignore", "forbid"]
|
|
54
|
+
FileFormats = Literal["json", "yaml", "toml", "auto"]
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
T = TypeVar("T")
|
|
58
|
+
K = TypeVar("K")
|
|
59
|
+
PathLike = Union[str, Path]
|
|
60
|
+
IncEx = Union[
|
|
61
|
+
set[int],
|
|
62
|
+
set[str],
|
|
63
|
+
Mapping[int, Union["IncEx", bool]],
|
|
64
|
+
Mapping[str, Union["IncEx", bool]],
|
|
65
|
+
]
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def file_format_from_file(file_path: Path) -> FileFormats:
|
|
69
|
+
ext = file_path.suffix.lower()
|
|
70
|
+
if ext == ".json":
|
|
71
|
+
return "json"
|
|
72
|
+
elif ext in {".yaml", ".yml"}:
|
|
73
|
+
return "yaml"
|
|
74
|
+
elif ext == ".toml":
|
|
75
|
+
return "toml"
|
|
76
|
+
else:
|
|
77
|
+
raise ValueError(f"Unsupported file extension: {ext}")
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
class BaseModelLike(Protocol):
|
|
81
|
+
def model_dump(
|
|
82
|
+
self,
|
|
83
|
+
*,
|
|
84
|
+
include: IncEx | None = None,
|
|
85
|
+
exclude: IncEx | None = None,
|
|
86
|
+
context: Any | None = None,
|
|
87
|
+
by_alias: bool = False,
|
|
88
|
+
exclude_unset: bool = False,
|
|
89
|
+
exclude_defaults: bool = False,
|
|
90
|
+
exclude_none: bool = False,
|
|
91
|
+
round_trip: bool = False,
|
|
92
|
+
warnings: bool = True,
|
|
93
|
+
serialize_as_any: bool = False,
|
|
94
|
+
) -> dict[str, Any]: ...
|
|
95
|
+
|
|
96
|
+
def model_dump_json(
|
|
97
|
+
self,
|
|
98
|
+
*,
|
|
99
|
+
indent: int | None = None,
|
|
100
|
+
ensure_ascii: bool = False,
|
|
101
|
+
include: IncEx | None = None,
|
|
102
|
+
exclude: IncEx | None = None,
|
|
103
|
+
context: Any | None = None,
|
|
104
|
+
by_alias: bool | None = None,
|
|
105
|
+
exclude_unset: bool = False,
|
|
106
|
+
exclude_defaults: bool = False,
|
|
107
|
+
exclude_none: bool = False,
|
|
108
|
+
exclude_computed_fields: bool = False,
|
|
109
|
+
round_trip: bool = False,
|
|
110
|
+
warnings: bool | Literal["none", "warn", "error"] = True,
|
|
111
|
+
fallback: Callable[[Any], Any] | None = None,
|
|
112
|
+
serialize_as_any: bool = False,
|
|
113
|
+
) -> str: ...
|
|
114
|
+
|
|
115
|
+
@classmethod
|
|
116
|
+
def model_validate(
|
|
117
|
+
cls,
|
|
118
|
+
obj: Any,
|
|
119
|
+
*,
|
|
120
|
+
strict: bool | None = None,
|
|
121
|
+
extra: ExtraValues | None = None,
|
|
122
|
+
from_attributes: bool | None = None,
|
|
123
|
+
context: Any | None = None,
|
|
124
|
+
by_alias: bool | None = None,
|
|
125
|
+
by_name: bool | None = None,
|
|
126
|
+
) -> Self: ...
|
|
127
|
+
|
|
128
|
+
@classmethod
|
|
129
|
+
def model_validate_json(
|
|
130
|
+
cls,
|
|
131
|
+
json_data: str | bytes | bytearray,
|
|
132
|
+
*,
|
|
133
|
+
strict: bool | None = None,
|
|
134
|
+
extra: ExtraValues | None = None,
|
|
135
|
+
context: Any | None = None,
|
|
136
|
+
by_alias: bool | None = None,
|
|
137
|
+
by_name: bool | None = None,
|
|
138
|
+
) -> Self: ...
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
TypeBaseModelLike = TypeVar("TypeBaseModelLike", bound=BaseModelLike)
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
def yaml_to_json(yaml_str: str) -> str:
|
|
145
|
+
"""
|
|
146
|
+
Convert a YAML string to a JSON string.
|
|
147
|
+
|
|
148
|
+
Args:
|
|
149
|
+
yaml_str: YAML string to convert
|
|
150
|
+
|
|
151
|
+
Returns:
|
|
152
|
+
JSON string representation
|
|
153
|
+
|
|
154
|
+
Raises:
|
|
155
|
+
ImportError: If ruamel.yaml is not available
|
|
156
|
+
"""
|
|
157
|
+
if yaml is None:
|
|
158
|
+
raise ImportError(
|
|
159
|
+
"ruamel.yaml is required for YAML support. Install it with: pip install ruamel.yaml"
|
|
160
|
+
)
|
|
161
|
+
parsed_data = yaml.load(yaml_str) # type: ignore
|
|
162
|
+
return json.dumps(parsed_data)
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
def toml_to_json(toml_str: str) -> str:
|
|
166
|
+
"""
|
|
167
|
+
Convert a TOML string to a JSON string.
|
|
168
|
+
|
|
169
|
+
Args:
|
|
170
|
+
toml_str: TOML string to convert
|
|
171
|
+
|
|
172
|
+
Returns:
|
|
173
|
+
JSON string representation
|
|
174
|
+
|
|
175
|
+
Raises:
|
|
176
|
+
ImportError: If tomllib/tomli is not available
|
|
177
|
+
"""
|
|
178
|
+
if tomllib is None:
|
|
179
|
+
raise ImportError(
|
|
180
|
+
"tomllib (Python 3.11+) or tomli is required for TOML reading support. "
|
|
181
|
+
"For Python < 3.11, install with: pip install tomli"
|
|
182
|
+
)
|
|
183
|
+
parsed_data = tomllib.loads(toml_str) # type: ignore
|
|
184
|
+
return json.dumps(parsed_data)
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
class IOMixin:
|
|
188
|
+
def to_file(
|
|
189
|
+
self: BaseModelLike, file_path: PathLike, file_format: FileFormats = "auto"
|
|
190
|
+
) -> None:
|
|
191
|
+
"""
|
|
192
|
+
Save the model to a file in the specified format.
|
|
193
|
+
|
|
194
|
+
Args:
|
|
195
|
+
file_path: Path to the output file.
|
|
196
|
+
file_format: Format to save the file in ("json", "yaml", or "toml").
|
|
197
|
+
|
|
198
|
+
Raises:
|
|
199
|
+
ImportError: If the required library for the format is not installed.
|
|
200
|
+
ValueError: If an unsupported file format is specified.
|
|
201
|
+
"""
|
|
202
|
+
file_path = Path(file_path)
|
|
203
|
+
file_path.parent.mkdir(parents=True, exist_ok=True)
|
|
204
|
+
|
|
205
|
+
if file_format == "auto":
|
|
206
|
+
file_format = file_format_from_file(file_path)
|
|
207
|
+
|
|
208
|
+
if file_format == "json":
|
|
209
|
+
txt = self.model_dump_json(indent=2)
|
|
210
|
+
elif file_format == "yaml":
|
|
211
|
+
if yaml is None:
|
|
212
|
+
raise ImportError(
|
|
213
|
+
"ruamel.yaml is required for YAML support. Install it with: pip install ruamel.yaml"
|
|
214
|
+
)
|
|
215
|
+
# Convert to dict first, then to YAML
|
|
216
|
+
data = self.model_dump()
|
|
217
|
+
from io import StringIO
|
|
218
|
+
|
|
219
|
+
stream = StringIO()
|
|
220
|
+
yaml.dump(data, stream) # type: ignore
|
|
221
|
+
txt = stream.getvalue()
|
|
222
|
+
elif file_format == "toml":
|
|
223
|
+
if tomli_w is None:
|
|
224
|
+
raise ImportError(
|
|
225
|
+
"tomli-w is required for TOML writing support. Install it with: pip install tomli-w"
|
|
226
|
+
)
|
|
227
|
+
# Convert to dict first, then to TOML
|
|
228
|
+
data = self.model_dump()
|
|
229
|
+
# TOML requires a table (dict) at the root level, not arrays
|
|
230
|
+
if isinstance(data, list):
|
|
231
|
+
raise ValueError(
|
|
232
|
+
"TOML format does not support arrays at the root level. "
|
|
233
|
+
"Use JSON or YAML format for list/array data."
|
|
234
|
+
)
|
|
235
|
+
txt = tomli_w.dumps(data) # type: ignore
|
|
236
|
+
else:
|
|
237
|
+
raise ValueError(f"Unsupported file format: {file_format}")
|
|
238
|
+
|
|
239
|
+
file_path.write_text(txt) # type: ignore
|
|
240
|
+
|
|
241
|
+
@classmethod
|
|
242
|
+
def from_file(
|
|
243
|
+
cls: type[TypeBaseModelLike],
|
|
244
|
+
file_path: PathLike,
|
|
245
|
+
file_format: FileFormats = "auto",
|
|
246
|
+
) -> TypeBaseModelLike:
|
|
247
|
+
"""
|
|
248
|
+
Load the model from a file in the specified format.
|
|
249
|
+
|
|
250
|
+
Args:
|
|
251
|
+
file_path: Path to the input file.
|
|
252
|
+
file_format: Format of the input file ("json", "yaml", or "toml").
|
|
253
|
+
|
|
254
|
+
Returns:
|
|
255
|
+
An instance of the model.
|
|
256
|
+
|
|
257
|
+
Raises:
|
|
258
|
+
ImportError: If the required library for the format is not installed.
|
|
259
|
+
ValueError: If an unsupported file format is specified.
|
|
260
|
+
"""
|
|
261
|
+
file_path = Path(file_path)
|
|
262
|
+
|
|
263
|
+
if file_format == "auto":
|
|
264
|
+
file_format = file_format_from_file(file_path)
|
|
265
|
+
if file_format == "json":
|
|
266
|
+
json_data = file_path.read_text()
|
|
267
|
+
elif file_format == "yaml":
|
|
268
|
+
yaml_data = file_path.read_text()
|
|
269
|
+
json_data = yaml_to_json(yaml_data)
|
|
270
|
+
elif file_format == "toml":
|
|
271
|
+
toml_data = file_path.read_text()
|
|
272
|
+
json_data = toml_to_json(toml_data)
|
|
273
|
+
else:
|
|
274
|
+
raise ValueError(f"Unsupported file format: {file_format}")
|
|
275
|
+
|
|
276
|
+
return cls.model_validate_json(json_data)
|
|
277
|
+
|
|
278
|
+
|
|
279
|
+
class BaseModel(PydanticBaseModel, IOMixin): ...
|
|
280
|
+
|
|
281
|
+
|
|
282
|
+
class RootModel(PydanticRootModel[T], IOMixin): ...
|
|
283
|
+
|
|
284
|
+
|
|
285
|
+
class ListModel(RootModel[list[T]], MutableSequence[T]):
|
|
286
|
+
root: list[T] = Field(default_factory=list) # type: ignore
|
|
287
|
+
|
|
288
|
+
def append(self, value: T) -> None:
|
|
289
|
+
self.root.append(value)
|
|
290
|
+
|
|
291
|
+
def extend(self, values: Iterable[T]) -> None:
|
|
292
|
+
self.root.extend(values)
|
|
293
|
+
|
|
294
|
+
@overload
|
|
295
|
+
def __getitem__(self, index: int) -> T: ...
|
|
296
|
+
|
|
297
|
+
@overload
|
|
298
|
+
def __getitem__(self, index: slice) -> list[T]: ...
|
|
299
|
+
|
|
300
|
+
def __getitem__(self, index: int | slice) -> T | list[T]:
|
|
301
|
+
return self.root[index]
|
|
302
|
+
|
|
303
|
+
@overload
|
|
304
|
+
def __setitem__(self, index: int, value: T) -> None: ...
|
|
305
|
+
|
|
306
|
+
@overload
|
|
307
|
+
def __setitem__(self, index: slice, value: Iterable[T]) -> None: ...
|
|
308
|
+
|
|
309
|
+
def __setitem__(self, index: int | slice, value: T | Iterable[T]) -> None:
|
|
310
|
+
self.root[index] = value # type: ignore
|
|
311
|
+
|
|
312
|
+
def __len__(self) -> int:
|
|
313
|
+
return len(self.root)
|
|
314
|
+
|
|
315
|
+
def __iter__(self): # type: ignore
|
|
316
|
+
return iter(self.root)
|
|
317
|
+
|
|
318
|
+
def __contains__(self, item: object) -> bool:
|
|
319
|
+
return item in self.root
|
|
320
|
+
|
|
321
|
+
def insert(self, index: int, value: T) -> None:
|
|
322
|
+
self.root.insert(index, value)
|
|
323
|
+
|
|
324
|
+
def __delitem__(self, index: int | slice) -> None:
|
|
325
|
+
del self.root[index]
|
|
326
|
+
|
|
327
|
+
|
|
328
|
+
class DictModel(RootModel[dict[K, T]], MutableMapping[K, T]):
|
|
329
|
+
root: dict[K, T] = Field(default_factory=dict) # type: ignore
|
|
330
|
+
|
|
331
|
+
def __contains__(self, key: object) -> bool:
|
|
332
|
+
return key in self.root
|
|
333
|
+
|
|
334
|
+
def items(self):
|
|
335
|
+
return self.root.items()
|
|
336
|
+
|
|
337
|
+
def keys(self):
|
|
338
|
+
return self.root.keys()
|
|
339
|
+
|
|
340
|
+
def values(self):
|
|
341
|
+
return self.root.values()
|
|
342
|
+
|
|
343
|
+
def __getitem__(self, key: K) -> T:
|
|
344
|
+
return self.root[key]
|
|
345
|
+
|
|
346
|
+
def __setitem__(self, key: K, value: T) -> None:
|
|
347
|
+
self.root[key] = value
|
|
348
|
+
|
|
349
|
+
def __delitem__(self, key: K) -> None:
|
|
350
|
+
del self.root[key]
|
|
351
|
+
|
|
352
|
+
def __len__(self) -> int:
|
|
353
|
+
return len(self.root)
|
|
354
|
+
|
|
355
|
+
def __iter__(self): # type: ignore
|
|
356
|
+
return iter(self.root)
|
|
357
|
+
|
|
358
|
+
|
|
359
|
+
class JsonStore(DictModel[str, T]):
|
|
360
|
+
_file_path: Optional[Path] = PrivateAttr(default=None)
|
|
361
|
+
|
|
362
|
+
def __setitem__(self, key: str, value: T) -> None:
|
|
363
|
+
self.root[key] = value
|
|
364
|
+
self.save_store()
|
|
365
|
+
|
|
366
|
+
def __init__(
|
|
367
|
+
self,
|
|
368
|
+
*args: Any,
|
|
369
|
+
file_path: Optional[Path] = None,
|
|
370
|
+
**kwargs: Any,
|
|
371
|
+
) -> None:
|
|
372
|
+
super().__init__(*args, **kwargs)
|
|
373
|
+
self._file_path = file_path
|
|
374
|
+
|
|
375
|
+
def save_store(self) -> None:
|
|
376
|
+
if self._file_path is None:
|
|
377
|
+
raise ValueError("File path is not set.")
|
|
378
|
+
self.to_file(self._file_path)
|
|
379
|
+
|
|
380
|
+
@classmethod
|
|
381
|
+
def connect(cls, file_path: Path) -> JsonStore[T]:
|
|
382
|
+
if not file_path.exists():
|
|
383
|
+
obj = cls(file_path=file_path)
|
|
384
|
+
obj.save_store()
|
|
385
|
+
obj = cls.from_file(file_path)
|
|
386
|
+
obj._file_path = file_path
|
|
387
|
+
return obj
|
|
File without changes
|