ic-python-db 0.7.1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,88 @@
1
+ """System time management for the database."""
2
+
3
+ import time
4
+ from datetime import datetime
5
+ from typing import Optional
6
+
7
+
8
+ class SystemTime:
9
+ """Manages system time for the database.
10
+
11
+ This class allows setting a fixed time for testing or synchronizing
12
+ time across different systems. If no time is set, it uses the real
13
+ system time.
14
+ """
15
+
16
+ _instance = None
17
+ _current_time_ms: Optional[int] = None
18
+
19
+ def __init__(self):
20
+ if SystemTime._instance is not None:
21
+ raise RuntimeError("Use SystemTime.get_instance() instead")
22
+ SystemTime._instance = self
23
+
24
+ @classmethod
25
+ def get_instance(cls) -> "SystemTime":
26
+ """Get the singleton instance."""
27
+ if cls._instance is None:
28
+ cls._instance = cls()
29
+ return cls._instance
30
+
31
+ def get_time(self) -> int:
32
+ """Get the current time.
33
+
34
+ If a fixed time is set, returns the fixed time,
35
+ otherwise returns the real system time.
36
+
37
+ Returns:
38
+ int: Current time in milliseconds
39
+ """
40
+ if self._current_time_ms is not None:
41
+ return self._current_time_ms
42
+ return int(time.time() * 1000) # Convert to milliseconds
43
+
44
+ def set_time(self, timestamp: int) -> None:
45
+ """Set a fixed time.
46
+
47
+ Args:
48
+ timestamp: Time in milliseconds since epoch
49
+ """
50
+ self._current_time_ms = timestamp
51
+
52
+ def clear_time(self) -> None:
53
+ """Clear the fixed time and revert to using real system time."""
54
+ self._current_time_ms = None
55
+
56
+ def advance_time(self, milliseconds: int) -> None:
57
+ """Advance the current time by the specified number of milliseconds.
58
+
59
+ If no fixed time is set, this will set the time to current time + milliseconds.
60
+
61
+ Args:
62
+ milliseconds: Number of milliseconds to advance
63
+ """
64
+ current = self.get_time()
65
+ print(
66
+ f"Advancing time by {milliseconds} ms (current: {current}) = {current + milliseconds}"
67
+ )
68
+ self.set_time(current + milliseconds)
69
+
70
+ @staticmethod
71
+ def format_timestamp(timestamp: int) -> str:
72
+ """Format a timestamp as a human-readable string.
73
+
74
+ Args:
75
+ timestamp: Time in milliseconds since epoch
76
+
77
+ Returns:
78
+ Formatted string like "2025-02-09 15:26:27.123"
79
+ """
80
+ if not timestamp:
81
+ return "None"
82
+ dt = datetime.fromtimestamp(timestamp / 1000).strftime("%Y-%m-%d %H:%M:%S.%f")[
83
+ :-3
84
+ ]
85
+ return f"{dt}"
86
+
87
+ def print(self) -> str:
88
+ return self.format_timestamp(self.get_time())
@@ -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,17 @@
1
+ ic_python_db/__init__.py,sha256=7tiDQ0kSkzWyCnpk5zbBOIgJsrEkcsIhn30MD1yBoIw,814
2
+ ic_python_db/_cdk.py,sha256=zTj8HzXUcPz8R73nh5bo6JMkCorp-KSW1hDutzi5LZs,387
3
+ ic_python_db/constants.py,sha256=oLvKJev-65gXFd284DdYutWBX95sTF5Xkikc6IC4mhM,118
4
+ ic_python_db/context.py,sha256=97F4_EAJm3adOnUfh0SaDisX3nc7F2vFIZwZUsUAy58,1080
5
+ ic_python_db/db_engine.py,sha256=LOwFWvXw_BPMmJsRGe86GAKm1zzXP4gyG6U8toUGYEw,11624
6
+ ic_python_db/entity.py,sha256=p31QTI2TyaxKpse06f-P6pAow30wEr_zt9uxXkfcvkE,35863
7
+ ic_python_db/hooks.py,sha256=B1wV18dAMH-PootSripmMpG9Cr9M3W3lcNEF7QvPrL4,1620
8
+ ic_python_db/mixins.py,sha256=qZmb3ssXYf2NSjHJtgmdsOlkIJdouDjPHdQ9tZmrZi8,1937
9
+ ic_python_db/properties.py,sha256=NbDKKH7TBrV3lPP1zwdhMrWKVPDq2N3EViv1rL8gcjk,22433
10
+ ic_python_db/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
11
+ ic_python_db/storage.py,sha256=2F6m_01g2jwoF40s4BrdO2SffJNSO3gPf0QU3Co7T5Y,1873
12
+ ic_python_db/system_time.py,sha256=xrx8NfjLXCGsac3hUpuvsNwCH91tQ9jfH4N-XFo8iJk,2614
13
+ ic_python_db-0.7.1.dist-info/licenses/LICENSE,sha256=6q6XYNOGnJcVSus2bAezFn7bU_2Y5T6W4aGQHBb8X-c,1079
14
+ ic_python_db-0.7.1.dist-info/METADATA,sha256=pXPo2r6dtGWMru3EY5cBj6724SEan83xLtXcY97eHwA,12188
15
+ ic_python_db-0.7.1.dist-info/WHEEL,sha256=YCfwYGOYMi5Jhw2fU4yNgwErybb2IX5PEwBKV4ZbdBo,91
16
+ ic_python_db-0.7.1.dist-info/top_level.txt,sha256=ZaOTqhWKtJQEuXmqR920AqbDBAqv1mvsKj3TClWimDk,13
17
+ ic_python_db-0.7.1.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (82.0.0)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -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 @@
1
+ ic_python_db