ic-python-db 0.7.1__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 Smart Social Contracts
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,6 @@
1
+ include LICENSE
2
+ include README.md
3
+ include requirements-dev.txt
4
+ include pyproject.toml
5
+ include setup.cfg
6
+ include ic_python_db/py.typed
@@ -0,0 +1,356 @@
1
+ Metadata-Version: 2.4
2
+ Name: ic_python_db
3
+ Version: 0.7.1
4
+ Summary: A lightweight key-value database written in Python, intended for use on the Internet Computer (IC)
5
+ Home-page: https://github.com/smart-social-contracts/ic-python-db
6
+ Author: Smart Social Contracts
7
+ Author-email: Smart Social Contracts <contact@smartsocialcontracts.org>
8
+ License: MIT License
9
+
10
+ Copyright (c) 2025 Smart Social Contracts
11
+
12
+ Permission is hereby granted, free of charge, to any person obtaining a copy
13
+ of this software and associated documentation files (the "Software"), to deal
14
+ in the Software without restriction, including without limitation the rights
15
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
16
+ copies of the Software, and to permit persons to whom the Software is
17
+ furnished to do so, subject to the following conditions:
18
+
19
+ The above copyright notice and this permission notice shall be included in all
20
+ copies or substantial portions of the Software.
21
+
22
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
23
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
24
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
25
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
26
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
27
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
28
+ SOFTWARE.
29
+
30
+ Project-URL: Homepage, https://github.com/smart-social-contracts/ic-python-db
31
+ Project-URL: Repository, https://github.com/smart-social-contracts/ic-python-db.git
32
+ Project-URL: Issues, https://github.com/smart-social-contracts/ic-python-db/issues
33
+ Keywords: database,key-value,entity,relationship,audit
34
+ Classifier: Development Status :: 3 - Alpha
35
+ Classifier: Intended Audience :: Developers
36
+ Classifier: License :: OSI Approved :: MIT License
37
+ Classifier: Operating System :: OS Independent
38
+ Classifier: Programming Language :: Python :: 3
39
+ Classifier: Programming Language :: Python :: 3.10
40
+ Classifier: Topic :: Database
41
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
42
+ Requires-Python: >=3.7
43
+ Description-Content-Type: text/markdown
44
+ License-File: LICENSE
45
+ Dynamic: author
46
+ Dynamic: home-page
47
+ Dynamic: license-file
48
+ Dynamic: requires-python
49
+
50
+ # IC Python DB
51
+
52
+ A lightweight key-value database with entity relationships and audit logging capabilities, intended for small to medium-sized applications running on the Internet Computer. Forked from [kybra-simple-db](https://github.com/smart-social-contracts/kybra-simple-db).
53
+
54
+ [![Test on IC](https://github.com/smart-social-contracts/ic-python-db/actions/workflows/test_ic.yml/badge.svg)](https://github.com/smart-social-contracts/ic-python-db/actions)
55
+ [![Test](https://github.com/smart-social-contracts/ic-python-db/actions/workflows/test.yml/badge.svg)](https://github.com/smart-social-contracts/ic-python-db/actions)
56
+ [![PyPI version](https://badge.fury.io/py/ic-python-db.svg)](https://badge.fury.io/py/ic-python-db)
57
+ [![Python 3.10](https://img.shields.io/badge/python-3.10-blue.svg)](https://www.python.org/downloads/release/python-3107/)
58
+ [![License](https://img.shields.io/github/license/smart-social-contracts/ic-python-db.svg)](https://github.com/smart-social-contracts/ic-python-db/blob/main/LICENSE)
59
+
60
+ ## Features
61
+
62
+ - **Persistent Storage**: Works with StableBTreeMap stable structure for persistent storage on your canister's stable memory so your data persists automatically across canister upgrades.
63
+ - **Entity-Relational Database**: Create, read and write entities with OneToOne, OneToMany, ManyToOne, and ManyToMany relationships.
64
+ - **Entity Hooks**: Intercept and control entity lifecycle events (create, modify, delete) with `on_event` hooks.
65
+ - **Access Control**: Thread-safe context management for user identity tracking and ownership-based permissions.
66
+ - **Namespaces**: Organize entities into namespaces to avoid type conflicts when you have multiple entities with the same class name.
67
+ - **Audit Logging**: Track all changes to your data with created/updated timestamps and who created and updated each entity.
68
+
69
+
70
+ ## Installation
71
+
72
+ ```bash
73
+ pip install ic-python-db
74
+ ```
75
+
76
+ ## Quick Start
77
+
78
+ The database storage must be initialized before using IC Python DB. Here's an example of how to do it:
79
+
80
+ ```python
81
+ from basilisk import StableBTreeMap
82
+ from ic_python_db import Database
83
+
84
+ # Initialize storage and database
85
+ storage = StableBTreeMap[str, str](memory_id=1, max_key_size=100, max_value_size=1000) # Use a unique memory ID for each storage instance
86
+ Database.init(db_storage=storage)
87
+ ```
88
+
89
+ Read [Basilisk's documentation](https://github.com/smart-social-contracts/basilisk) for more information regarding StableBTreeMap and memory IDs.
90
+
91
+ Next, define your entities:
92
+
93
+ ```python
94
+ from ic_python_db import (
95
+ Database, Entity, String, Integer,
96
+ OneToOne, OneToMany, ManyToOne, ManyToMany, TimestampedMixin
97
+ )
98
+
99
+ class Person(Entity, TimestampedMixin):
100
+ __alias__ = "name" # Use `name` as the alias field for lookup
101
+ name = String(min_length=2, max_length=50)
102
+ age = Integer(min_value=0, max_value=120)
103
+ friends = ManyToMany("Person", "friends")
104
+ mother = ManyToOne("Person", "children")
105
+ children = OneToMany("Person", "mother")
106
+ spouse = OneToOne("Person", "spouse")
107
+ ```
108
+
109
+ ### Entity Lookup
110
+
111
+ Entities can be retrieved using `Entity[key]` syntax with three different lookup modes:
112
+
113
+ ```python
114
+ # Create an entity
115
+ john = Person(name="John", age=30)
116
+
117
+ # Lookup by ID
118
+ Person[1] # Returns john (by _id)
119
+ Person["1"] # Also works with string ID
120
+
121
+ # Lookup by alias (defined via __alias__)
122
+ Person["John"] # Tries ID first, then alias field "name"
123
+
124
+ # Lookup by specific field (tuple syntax)
125
+ Person["name", "John"] # Lookup by field "name" only
126
+ ```
127
+
128
+ | Syntax | Behavior |
129
+ |--------|----------|
130
+ | `Person[1]` | Lookup by `_id` |
131
+ | `Person["John"]` | Try `_id` first, then `__alias__` field |
132
+ | `Person["name", "John"]` | Lookup by specific field `name` only |
133
+
134
+ Then use the defined entities to store objects:
135
+
136
+ ```python
137
+ # Create and save an object
138
+ john = Person(name="John", age=30)
139
+
140
+ # Update an object's property
141
+ john.age = 33 # Type checking and validation happens automatically
142
+
143
+ # use the `_id` property to load an entity with the [] operator
144
+ Person(name="Peter")
145
+ peter = Person["Peter"]
146
+
147
+ # Delete an object
148
+ peter.delete()
149
+
150
+ # Create relationships
151
+ alice = Person(name="Alice")
152
+ eva = Person(name="Eva")
153
+ john.mother = alice
154
+ assert john in alice.children
155
+ eva.friends = [alice]
156
+ assert alice in eva.friends
157
+ assert eva in alice.friends
158
+
159
+ print(alice.serialize()) # Prints the dictionary representation of an object
160
+ # Prints: {'timestamp_created': '2025-09-12 22:15:35.882', 'timestamp_updated': '2025-09-12 22:15:35.883', 'creator': 'system', 'updater': 'system', 'owner': 'system', '_type': 'Person', '_id': '3', 'name': 'Alice', 'age': None, 'children': '1', 'friends': '4'}
161
+
162
+ assert Person.count() == 3
163
+ assert Person.max_id() == 4
164
+ assert Person.instances() == [john, alice, eva]
165
+
166
+ # Cursor-based pagination
167
+ assert Person.load_some(0, 2) == [john, alice]
168
+ assert Person.load_some(2, 2) == [eva]
169
+
170
+ # Retrieve database contents in JSON format
171
+ print(Database.get_instance().dump_json(pretty=True))
172
+
173
+ # Audit log
174
+ audit_records = Database.get_instance().get_audit(id_from=0, id_to=5)
175
+ pprint(audit_records['0'])
176
+ ''' Prints:
177
+
178
+ ['save',
179
+ 1744138342934,
180
+ 'Person@1',
181
+ {'_id': '1',
182
+ '_type': 'Person',
183
+ 'age': 30,
184
+ 'creator': 'system',
185
+ 'name': 'John',
186
+ 'owner': 'system',
187
+ 'timestamp_created': '2025-04-08 20:52:22.934',
188
+ 'timestamp_updated': '2025-04-08 20:52:22.934',
189
+ 'updater': 'system'}]
190
+
191
+ '''
192
+ ```
193
+
194
+ For more usage examples, see the [tests](tests/src/tests).
195
+
196
+ ## Namespaces
197
+
198
+ Organize entities with the `__namespace__` attribute to avoid type conflicts when you have the same class name in different modules:
199
+
200
+ ```python
201
+ # In app/models.py
202
+ class User(Entity):
203
+ __namespace__ = "app"
204
+ name = String()
205
+ role = String()
206
+ ```
207
+
208
+ ```python
209
+ # In admin/models.py
210
+ class User(Entity):
211
+ __namespace__ = "admin"
212
+ name = String()
213
+ permissions = String()
214
+ ```
215
+
216
+ ```python
217
+ from app.models import User as AppUser
218
+ from admin.models import User as AdminUser
219
+
220
+ app_user = AppUser(name="Alice", role="developer") # Stored as "app::User"
221
+ admin_user = AdminUser(name="Bob", permissions="all") # Stored as "admin::User"
222
+
223
+ # Each namespace has isolated ID sequences and storage
224
+ assert app_user._id == "1"
225
+ assert admin_user._id == "1"
226
+ ```
227
+
228
+ ## Entity Hooks
229
+
230
+ Intercept and control entity changes with the `on_event` hook:
231
+
232
+ ```python
233
+ from ic_python_db import Entity, String, ACTION_MODIFY
234
+
235
+ class User(Entity):
236
+ name = String()
237
+ email = String()
238
+
239
+ @staticmethod
240
+ def on_event(entity, field_name, old_value, new_value, action):
241
+ # Validate email format
242
+ if field_name == "email" and "@" not in new_value:
243
+ return False, None # Reject invalid email
244
+
245
+ # Auto-capitalize names
246
+ if field_name == "name":
247
+ return True, new_value.upper()
248
+
249
+ return True, new_value
250
+
251
+ user = User(name="alice", email="alice@example.com")
252
+ assert user.name == "ALICE" # Auto-capitalized
253
+ ```
254
+
255
+ See [docs/HOOKS.md](docs/HOOKS.md) for more patterns.
256
+
257
+ ## Access Control
258
+
259
+ Thread-safe user context management with `as_user()`:
260
+
261
+ ```python
262
+ from ic_python_db import Database, Entity, String, ACTION_MODIFY, ACTION_DELETE
263
+ from ic_python_db.mixins import TimestampedMixin
264
+ from ic_python_db.context import get_caller_id
265
+
266
+ class Document(Entity, TimestampedMixin):
267
+ title = String()
268
+
269
+ @staticmethod
270
+ def on_event(entity, field_name, old_value, new_value, action):
271
+ caller = get_caller_id()
272
+
273
+ # Only owner can modify or delete
274
+ if action in (ACTION_MODIFY, ACTION_DELETE):
275
+ if entity._owner != caller:
276
+ return False, None
277
+
278
+ return True, new_value
279
+
280
+ db = Database.get_instance()
281
+
282
+ # Alice creates a document
283
+ with db.as_user("alice"):
284
+ doc = Document(title="My Doc") # Owner: alice
285
+
286
+ # Bob cannot modify Alice's document
287
+ with db.as_user("bob"):
288
+ doc.title = "Hacked" # Raises ValueError
289
+ ```
290
+
291
+ See [docs/ACCESS_CONTROL.md](docs/ACCESS_CONTROL.md) and [examples/simple_access_control.py](examples/simple_access_control.py).
292
+
293
+ ## Type Hints
294
+
295
+ The library is fully typed (PEP 561 compliant). Type checkers and IDEs automatically infer property types:
296
+
297
+ ```python
298
+ class User(Entity):
299
+ name = String() # Inferred as str
300
+ age = Integer() # Inferred as int
301
+ active = Boolean() # Inferred as bool
302
+
303
+ user = User(name="Alice", age=30, active=True)
304
+ user.name # IDE knows this is str
305
+ user.age # IDE knows this is int
306
+ ```
307
+
308
+ For stricter typing, you can add explicit annotations:
309
+
310
+ ```python
311
+ from typing import Optional
312
+
313
+ class User(Entity):
314
+ name: str = String()
315
+ age: int = Integer()
316
+ profile: Optional["Profile"] = OneToOne("Profile", "user")
317
+ ```
318
+
319
+ ## API Reference
320
+
321
+ - **Core**: `Database`, `Entity`
322
+ - **Properties**: `String`, `Integer`, `Float`, `Boolean`
323
+ - **Relationships**: `OneToOne`, `OneToMany`, `ManyToOne`, `ManyToMany`
324
+ - **Mixins**: `TimestampedMixin` (timestamps and ownership tracking)
325
+ - **Hooks**: `ACTION_CREATE`, `ACTION_MODIFY`, `ACTION_DELETE`
326
+ - **Context**: `get_caller_id()`, `set_caller_id()`, `Database.as_user()`
327
+
328
+ ## Development
329
+
330
+ ### Setup Development Environment
331
+
332
+ ```bash
333
+ # Clone the repository
334
+ git clone https://github.com/smart-social-contracts/ic-python-db.git
335
+ cd ic-python-db
336
+
337
+ # Recommended setup
338
+ pyenv install 3.10.7
339
+ pyenv local 3.10.7
340
+ python -m venv venv
341
+ source venv/bin/activate
342
+
343
+ # Install development dependencies
344
+ pip install -r requirements-dev.txt
345
+
346
+ # Running tests
347
+ ./run_linters.sh && (cd tests && ./run_test.sh && ./run_test_ic.sh)
348
+ ```
349
+
350
+ ## Contributing
351
+
352
+ Contributions are welcome! Please feel free to submit a Pull Request.
353
+
354
+ ## License
355
+
356
+ [MIT](LICENSE).
@@ -0,0 +1,307 @@
1
+ # IC Python DB
2
+
3
+ A lightweight key-value database with entity relationships and audit logging capabilities, intended for small to medium-sized applications running on the Internet Computer. Forked from [kybra-simple-db](https://github.com/smart-social-contracts/kybra-simple-db).
4
+
5
+ [![Test on IC](https://github.com/smart-social-contracts/ic-python-db/actions/workflows/test_ic.yml/badge.svg)](https://github.com/smart-social-contracts/ic-python-db/actions)
6
+ [![Test](https://github.com/smart-social-contracts/ic-python-db/actions/workflows/test.yml/badge.svg)](https://github.com/smart-social-contracts/ic-python-db/actions)
7
+ [![PyPI version](https://badge.fury.io/py/ic-python-db.svg)](https://badge.fury.io/py/ic-python-db)
8
+ [![Python 3.10](https://img.shields.io/badge/python-3.10-blue.svg)](https://www.python.org/downloads/release/python-3107/)
9
+ [![License](https://img.shields.io/github/license/smart-social-contracts/ic-python-db.svg)](https://github.com/smart-social-contracts/ic-python-db/blob/main/LICENSE)
10
+
11
+ ## Features
12
+
13
+ - **Persistent Storage**: Works with StableBTreeMap stable structure for persistent storage on your canister's stable memory so your data persists automatically across canister upgrades.
14
+ - **Entity-Relational Database**: Create, read and write entities with OneToOne, OneToMany, ManyToOne, and ManyToMany relationships.
15
+ - **Entity Hooks**: Intercept and control entity lifecycle events (create, modify, delete) with `on_event` hooks.
16
+ - **Access Control**: Thread-safe context management for user identity tracking and ownership-based permissions.
17
+ - **Namespaces**: Organize entities into namespaces to avoid type conflicts when you have multiple entities with the same class name.
18
+ - **Audit Logging**: Track all changes to your data with created/updated timestamps and who created and updated each entity.
19
+
20
+
21
+ ## Installation
22
+
23
+ ```bash
24
+ pip install ic-python-db
25
+ ```
26
+
27
+ ## Quick Start
28
+
29
+ The database storage must be initialized before using IC Python DB. Here's an example of how to do it:
30
+
31
+ ```python
32
+ from basilisk import StableBTreeMap
33
+ from ic_python_db import Database
34
+
35
+ # Initialize storage and database
36
+ storage = StableBTreeMap[str, str](memory_id=1, max_key_size=100, max_value_size=1000) # Use a unique memory ID for each storage instance
37
+ Database.init(db_storage=storage)
38
+ ```
39
+
40
+ Read [Basilisk's documentation](https://github.com/smart-social-contracts/basilisk) for more information regarding StableBTreeMap and memory IDs.
41
+
42
+ Next, define your entities:
43
+
44
+ ```python
45
+ from ic_python_db import (
46
+ Database, Entity, String, Integer,
47
+ OneToOne, OneToMany, ManyToOne, ManyToMany, TimestampedMixin
48
+ )
49
+
50
+ class Person(Entity, TimestampedMixin):
51
+ __alias__ = "name" # Use `name` as the alias field for lookup
52
+ name = String(min_length=2, max_length=50)
53
+ age = Integer(min_value=0, max_value=120)
54
+ friends = ManyToMany("Person", "friends")
55
+ mother = ManyToOne("Person", "children")
56
+ children = OneToMany("Person", "mother")
57
+ spouse = OneToOne("Person", "spouse")
58
+ ```
59
+
60
+ ### Entity Lookup
61
+
62
+ Entities can be retrieved using `Entity[key]` syntax with three different lookup modes:
63
+
64
+ ```python
65
+ # Create an entity
66
+ john = Person(name="John", age=30)
67
+
68
+ # Lookup by ID
69
+ Person[1] # Returns john (by _id)
70
+ Person["1"] # Also works with string ID
71
+
72
+ # Lookup by alias (defined via __alias__)
73
+ Person["John"] # Tries ID first, then alias field "name"
74
+
75
+ # Lookup by specific field (tuple syntax)
76
+ Person["name", "John"] # Lookup by field "name" only
77
+ ```
78
+
79
+ | Syntax | Behavior |
80
+ |--------|----------|
81
+ | `Person[1]` | Lookup by `_id` |
82
+ | `Person["John"]` | Try `_id` first, then `__alias__` field |
83
+ | `Person["name", "John"]` | Lookup by specific field `name` only |
84
+
85
+ Then use the defined entities to store objects:
86
+
87
+ ```python
88
+ # Create and save an object
89
+ john = Person(name="John", age=30)
90
+
91
+ # Update an object's property
92
+ john.age = 33 # Type checking and validation happens automatically
93
+
94
+ # use the `_id` property to load an entity with the [] operator
95
+ Person(name="Peter")
96
+ peter = Person["Peter"]
97
+
98
+ # Delete an object
99
+ peter.delete()
100
+
101
+ # Create relationships
102
+ alice = Person(name="Alice")
103
+ eva = Person(name="Eva")
104
+ john.mother = alice
105
+ assert john in alice.children
106
+ eva.friends = [alice]
107
+ assert alice in eva.friends
108
+ assert eva in alice.friends
109
+
110
+ print(alice.serialize()) # Prints the dictionary representation of an object
111
+ # Prints: {'timestamp_created': '2025-09-12 22:15:35.882', 'timestamp_updated': '2025-09-12 22:15:35.883', 'creator': 'system', 'updater': 'system', 'owner': 'system', '_type': 'Person', '_id': '3', 'name': 'Alice', 'age': None, 'children': '1', 'friends': '4'}
112
+
113
+ assert Person.count() == 3
114
+ assert Person.max_id() == 4
115
+ assert Person.instances() == [john, alice, eva]
116
+
117
+ # Cursor-based pagination
118
+ assert Person.load_some(0, 2) == [john, alice]
119
+ assert Person.load_some(2, 2) == [eva]
120
+
121
+ # Retrieve database contents in JSON format
122
+ print(Database.get_instance().dump_json(pretty=True))
123
+
124
+ # Audit log
125
+ audit_records = Database.get_instance().get_audit(id_from=0, id_to=5)
126
+ pprint(audit_records['0'])
127
+ ''' Prints:
128
+
129
+ ['save',
130
+ 1744138342934,
131
+ 'Person@1',
132
+ {'_id': '1',
133
+ '_type': 'Person',
134
+ 'age': 30,
135
+ 'creator': 'system',
136
+ 'name': 'John',
137
+ 'owner': 'system',
138
+ 'timestamp_created': '2025-04-08 20:52:22.934',
139
+ 'timestamp_updated': '2025-04-08 20:52:22.934',
140
+ 'updater': 'system'}]
141
+
142
+ '''
143
+ ```
144
+
145
+ For more usage examples, see the [tests](tests/src/tests).
146
+
147
+ ## Namespaces
148
+
149
+ Organize entities with the `__namespace__` attribute to avoid type conflicts when you have the same class name in different modules:
150
+
151
+ ```python
152
+ # In app/models.py
153
+ class User(Entity):
154
+ __namespace__ = "app"
155
+ name = String()
156
+ role = String()
157
+ ```
158
+
159
+ ```python
160
+ # In admin/models.py
161
+ class User(Entity):
162
+ __namespace__ = "admin"
163
+ name = String()
164
+ permissions = String()
165
+ ```
166
+
167
+ ```python
168
+ from app.models import User as AppUser
169
+ from admin.models import User as AdminUser
170
+
171
+ app_user = AppUser(name="Alice", role="developer") # Stored as "app::User"
172
+ admin_user = AdminUser(name="Bob", permissions="all") # Stored as "admin::User"
173
+
174
+ # Each namespace has isolated ID sequences and storage
175
+ assert app_user._id == "1"
176
+ assert admin_user._id == "1"
177
+ ```
178
+
179
+ ## Entity Hooks
180
+
181
+ Intercept and control entity changes with the `on_event` hook:
182
+
183
+ ```python
184
+ from ic_python_db import Entity, String, ACTION_MODIFY
185
+
186
+ class User(Entity):
187
+ name = String()
188
+ email = String()
189
+
190
+ @staticmethod
191
+ def on_event(entity, field_name, old_value, new_value, action):
192
+ # Validate email format
193
+ if field_name == "email" and "@" not in new_value:
194
+ return False, None # Reject invalid email
195
+
196
+ # Auto-capitalize names
197
+ if field_name == "name":
198
+ return True, new_value.upper()
199
+
200
+ return True, new_value
201
+
202
+ user = User(name="alice", email="alice@example.com")
203
+ assert user.name == "ALICE" # Auto-capitalized
204
+ ```
205
+
206
+ See [docs/HOOKS.md](docs/HOOKS.md) for more patterns.
207
+
208
+ ## Access Control
209
+
210
+ Thread-safe user context management with `as_user()`:
211
+
212
+ ```python
213
+ from ic_python_db import Database, Entity, String, ACTION_MODIFY, ACTION_DELETE
214
+ from ic_python_db.mixins import TimestampedMixin
215
+ from ic_python_db.context import get_caller_id
216
+
217
+ class Document(Entity, TimestampedMixin):
218
+ title = String()
219
+
220
+ @staticmethod
221
+ def on_event(entity, field_name, old_value, new_value, action):
222
+ caller = get_caller_id()
223
+
224
+ # Only owner can modify or delete
225
+ if action in (ACTION_MODIFY, ACTION_DELETE):
226
+ if entity._owner != caller:
227
+ return False, None
228
+
229
+ return True, new_value
230
+
231
+ db = Database.get_instance()
232
+
233
+ # Alice creates a document
234
+ with db.as_user("alice"):
235
+ doc = Document(title="My Doc") # Owner: alice
236
+
237
+ # Bob cannot modify Alice's document
238
+ with db.as_user("bob"):
239
+ doc.title = "Hacked" # Raises ValueError
240
+ ```
241
+
242
+ See [docs/ACCESS_CONTROL.md](docs/ACCESS_CONTROL.md) and [examples/simple_access_control.py](examples/simple_access_control.py).
243
+
244
+ ## Type Hints
245
+
246
+ The library is fully typed (PEP 561 compliant). Type checkers and IDEs automatically infer property types:
247
+
248
+ ```python
249
+ class User(Entity):
250
+ name = String() # Inferred as str
251
+ age = Integer() # Inferred as int
252
+ active = Boolean() # Inferred as bool
253
+
254
+ user = User(name="Alice", age=30, active=True)
255
+ user.name # IDE knows this is str
256
+ user.age # IDE knows this is int
257
+ ```
258
+
259
+ For stricter typing, you can add explicit annotations:
260
+
261
+ ```python
262
+ from typing import Optional
263
+
264
+ class User(Entity):
265
+ name: str = String()
266
+ age: int = Integer()
267
+ profile: Optional["Profile"] = OneToOne("Profile", "user")
268
+ ```
269
+
270
+ ## API Reference
271
+
272
+ - **Core**: `Database`, `Entity`
273
+ - **Properties**: `String`, `Integer`, `Float`, `Boolean`
274
+ - **Relationships**: `OneToOne`, `OneToMany`, `ManyToOne`, `ManyToMany`
275
+ - **Mixins**: `TimestampedMixin` (timestamps and ownership tracking)
276
+ - **Hooks**: `ACTION_CREATE`, `ACTION_MODIFY`, `ACTION_DELETE`
277
+ - **Context**: `get_caller_id()`, `set_caller_id()`, `Database.as_user()`
278
+
279
+ ## Development
280
+
281
+ ### Setup Development Environment
282
+
283
+ ```bash
284
+ # Clone the repository
285
+ git clone https://github.com/smart-social-contracts/ic-python-db.git
286
+ cd ic-python-db
287
+
288
+ # Recommended setup
289
+ pyenv install 3.10.7
290
+ pyenv local 3.10.7
291
+ python -m venv venv
292
+ source venv/bin/activate
293
+
294
+ # Install development dependencies
295
+ pip install -r requirements-dev.txt
296
+
297
+ # Running tests
298
+ ./run_linters.sh && (cd tests && ./run_test.sh && ./run_test_ic.sh)
299
+ ```
300
+
301
+ ## Contributing
302
+
303
+ Contributions are welcome! Please feel free to submit a Pull Request.
304
+
305
+ ## License
306
+
307
+ [MIT](LICENSE).