ic-python-db 0.11.0__tar.gz → 0.13.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.
Files changed (29) hide show
  1. {ic_python_db-0.11.0/ic_python_db.egg-info → ic_python_db-0.13.0}/PKG-INFO +30 -6
  2. {ic_python_db-0.11.0 → ic_python_db-0.13.0}/README.md +29 -5
  3. {ic_python_db-0.11.0 → ic_python_db-0.13.0}/ic_python_db/__init__.py +5 -1
  4. ic_python_db-0.13.0/ic_python_db/constants.py +11 -0
  5. ic_python_db-0.13.0/ic_python_db/db_engine.py +927 -0
  6. {ic_python_db-0.11.0 → ic_python_db-0.13.0}/ic_python_db/entity.py +183 -66
  7. ic_python_db-0.13.0/ic_python_db/errors.py +10 -0
  8. {ic_python_db-0.11.0 → ic_python_db-0.13.0}/ic_python_db/properties.py +158 -59
  9. ic_python_db-0.13.0/ic_python_db/storage.py +118 -0
  10. {ic_python_db-0.11.0 → ic_python_db-0.13.0/ic_python_db.egg-info}/PKG-INFO +30 -6
  11. {ic_python_db-0.11.0 → ic_python_db-0.13.0}/ic_python_db.egg-info/SOURCES.txt +1 -0
  12. {ic_python_db-0.11.0 → ic_python_db-0.13.0}/pyproject.toml +1 -1
  13. {ic_python_db-0.11.0 → ic_python_db-0.13.0}/setup.py +1 -1
  14. ic_python_db-0.11.0/ic_python_db/constants.py +0 -6
  15. ic_python_db-0.11.0/ic_python_db/db_engine.py +0 -557
  16. ic_python_db-0.11.0/ic_python_db/storage.py +0 -69
  17. {ic_python_db-0.11.0 → ic_python_db-0.13.0}/LICENSE +0 -0
  18. {ic_python_db-0.11.0 → ic_python_db-0.13.0}/MANIFEST.in +0 -0
  19. {ic_python_db-0.11.0 → ic_python_db-0.13.0}/ic_python_db/_cdk.py +0 -0
  20. {ic_python_db-0.11.0 → ic_python_db-0.13.0}/ic_python_db/context.py +0 -0
  21. {ic_python_db-0.11.0 → ic_python_db-0.13.0}/ic_python_db/hooks.py +0 -0
  22. {ic_python_db-0.11.0 → ic_python_db-0.13.0}/ic_python_db/mixins.py +0 -0
  23. {ic_python_db-0.11.0 → ic_python_db-0.13.0}/ic_python_db/py.typed +0 -0
  24. {ic_python_db-0.11.0 → ic_python_db-0.13.0}/ic_python_db/schema.py +0 -0
  25. {ic_python_db-0.11.0 → ic_python_db-0.13.0}/ic_python_db/system_time.py +0 -0
  26. {ic_python_db-0.11.0 → ic_python_db-0.13.0}/ic_python_db.egg-info/dependency_links.txt +0 -0
  27. {ic_python_db-0.11.0 → ic_python_db-0.13.0}/ic_python_db.egg-info/top_level.txt +0 -0
  28. {ic_python_db-0.11.0 → ic_python_db-0.13.0}/requirements-dev.txt +0 -0
  29. {ic_python_db-0.11.0 → ic_python_db-0.13.0}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: ic-python-db
3
- Version: 0.11.0
3
+ Version: 0.13.0
4
4
  Summary: A lightweight key-value database written in Python, intended for use on the Internet Computer (IC)
5
5
  Home-page: https://github.com/smart-social-contracts/ic-python-db
6
6
  Author: Smart Social Contracts
@@ -60,6 +60,7 @@ A lightweight key-value database with entity relationships and audit logging cap
60
60
  ## Features
61
61
 
62
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
+ - **Range-based paging**: `page`, `load_some` and `rebuild_field_index` read a page of entities with a single ordered `range` call on the stable map (Basilisk `StableBTreeMap.range`), so cost tracks the rows returned rather than the highest id ever issued, and deleted ids are free. Backends without `range` (older CDK builds, plain dict test doubles) transparently fall back to one read per id. `instances()` and `find()` load a whole type: they are deprecated and raise `TooLargeError` past 1,000 entities (use `count()`, `find_by()` or `page()`).
63
64
  - **Entity-Relational Database**: Create, read and write entities with OneToOne, OneToMany, ManyToOne, and ManyToMany relationships.
64
65
  - **Schema Versioning & Upgrade Safety**: Automatic schema introspection, compatibility checking, and auto-migration for safe changes. Breaking changes without a `migrate()` method are rejected, preventing data corruption on canister upgrades.
65
66
  - **Entity Hooks**: Intercept and control entity lifecycle events (create, modify, delete) with `on_event` hooks.
@@ -162,11 +163,12 @@ Then use the defined entities to store objects:
162
163
 
163
164
  assert Person.count() == 3
164
165
  assert Person.max_id() == 4
165
- assert Person.instances() == [john, alice, eva]
166
166
 
167
- # Cursor-based pagination
168
- assert Person.load_some(0, 2) == [john, alice]
169
- assert Person.load_some(2, 2) == [eva]
167
+ # Cursor-based pagination: (page, next_from_id), None on the last page
168
+ page, cursor = Person.page(1, 2)
169
+ assert page == [john, alice]
170
+ page, cursor = Person.page(cursor, 2)
171
+ assert page == [eva] and cursor is None
170
172
 
171
173
  # Retrieve database contents in JSON format
172
174
  print(Database.get_instance().dump_json(pretty=True))
@@ -228,7 +230,29 @@ while cursor is not None:
228
230
 
229
231
  The rebuild is idempotent. The `indexed` flag is not part of the schema descriptor, so toggling it never triggers upgrade-compatibility errors.
230
232
 
231
- Note: each distinct value's ID list is stored in a single storage entry, so extremely hot values are bounded by the backing store's `max_value_size` — the same constraint as relationship reverse indexes.
233
+ Each matching id is its own storage entry, so a value can match any number of entities without reaching the backing store's `max_value_size`.
234
+
235
+ ## Large Relations
236
+
237
+ `OneToMany` and `ManyToMany` getters return a `RelationView`, not a list. Questions about the relation and single pages never load every member, so they stay cheap however large it grows:
238
+
239
+ ```python
240
+ if section.stands: ... # one read
241
+ len(section.stands) # one read
242
+ stand in section.stands # two reads
243
+ stands, cursor = section.stands.page(1, 50) # one range read + 50 loads
244
+ group.members.add(user) # index writes only
245
+ ```
246
+
247
+ Iterating, indexing and comparing still work, but they load every member, so past `constants.MAX_UNPAGED` (1,000) they raise `TooLargeError` instead of running into the IC instruction limit. Walk a large relation with `page()`, across calls if needed:
248
+
249
+ ```python
250
+ cursor = 1
251
+ while cursor is not None:
252
+ stands, cursor = section.stands.page(cursor, 50)
253
+ ```
254
+
255
+ `list(view)` gives a plain list. Deleting a parent is O(1): its index entries are purged in bounded steps by later writes, or by `Database.get_instance().purge_step()` from a timer.
232
256
 
233
257
  ## Namespaces
234
258
 
@@ -11,6 +11,7 @@ A lightweight key-value database with entity relationships and audit logging cap
11
11
  ## Features
12
12
 
13
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
+ - **Range-based paging**: `page`, `load_some` and `rebuild_field_index` read a page of entities with a single ordered `range` call on the stable map (Basilisk `StableBTreeMap.range`), so cost tracks the rows returned rather than the highest id ever issued, and deleted ids are free. Backends without `range` (older CDK builds, plain dict test doubles) transparently fall back to one read per id. `instances()` and `find()` load a whole type: they are deprecated and raise `TooLargeError` past 1,000 entities (use `count()`, `find_by()` or `page()`).
14
15
  - **Entity-Relational Database**: Create, read and write entities with OneToOne, OneToMany, ManyToOne, and ManyToMany relationships.
15
16
  - **Schema Versioning & Upgrade Safety**: Automatic schema introspection, compatibility checking, and auto-migration for safe changes. Breaking changes without a `migrate()` method are rejected, preventing data corruption on canister upgrades.
16
17
  - **Entity Hooks**: Intercept and control entity lifecycle events (create, modify, delete) with `on_event` hooks.
@@ -113,11 +114,12 @@ Then use the defined entities to store objects:
113
114
 
114
115
  assert Person.count() == 3
115
116
  assert Person.max_id() == 4
116
- assert Person.instances() == [john, alice, eva]
117
117
 
118
- # Cursor-based pagination
119
- assert Person.load_some(0, 2) == [john, alice]
120
- assert Person.load_some(2, 2) == [eva]
118
+ # Cursor-based pagination: (page, next_from_id), None on the last page
119
+ page, cursor = Person.page(1, 2)
120
+ assert page == [john, alice]
121
+ page, cursor = Person.page(cursor, 2)
122
+ assert page == [eva] and cursor is None
121
123
 
122
124
  # Retrieve database contents in JSON format
123
125
  print(Database.get_instance().dump_json(pretty=True))
@@ -179,7 +181,29 @@ while cursor is not None:
179
181
 
180
182
  The rebuild is idempotent. The `indexed` flag is not part of the schema descriptor, so toggling it never triggers upgrade-compatibility errors.
181
183
 
182
- Note: each distinct value's ID list is stored in a single storage entry, so extremely hot values are bounded by the backing store's `max_value_size` — the same constraint as relationship reverse indexes.
184
+ Each matching id is its own storage entry, so a value can match any number of entities without reaching the backing store's `max_value_size`.
185
+
186
+ ## Large Relations
187
+
188
+ `OneToMany` and `ManyToMany` getters return a `RelationView`, not a list. Questions about the relation and single pages never load every member, so they stay cheap however large it grows:
189
+
190
+ ```python
191
+ if section.stands: ... # one read
192
+ len(section.stands) # one read
193
+ stand in section.stands # two reads
194
+ stands, cursor = section.stands.page(1, 50) # one range read + 50 loads
195
+ group.members.add(user) # index writes only
196
+ ```
197
+
198
+ Iterating, indexing and comparing still work, but they load every member, so past `constants.MAX_UNPAGED` (1,000) they raise `TooLargeError` instead of running into the IC instruction limit. Walk a large relation with `page()`, across calls if needed:
199
+
200
+ ```python
201
+ cursor = 1
202
+ while cursor is not None:
203
+ stands, cursor = section.stands.page(cursor, 50)
204
+ ```
205
+
206
+ `list(view)` gives a plain list. Deleting a parent is O(1): its index entries are purged in bounded steps by later writes, or by `Database.get_instance().purge_step()` from a timer.
183
207
 
184
208
  ## Namespaces
185
209
 
@@ -5,6 +5,7 @@ IC Python DB - A lightweight key-value database with entity relationships and au
5
5
  from .constants import ACTION_CREATE, ACTION_DELETE, ACTION_MODIFY
6
6
  from .db_engine import Database
7
7
  from .entity import Entity
8
+ from .errors import TooLargeError
8
9
  from .mixins import TimestampedMixin
9
10
  from .properties import (
10
11
  Boolean,
@@ -14,13 +15,14 @@ from .properties import (
14
15
  ManyToOne,
15
16
  OneToMany,
16
17
  OneToOne,
18
+ RelationView,
17
19
  String,
18
20
  )
19
21
  from .schema import SchemaIncompatibleError, build_schema, diff_schemas, schema_hash
20
22
  from .storage import MemoryStorage, Storage
21
23
  from .system_time import SystemTime
22
24
 
23
- __version__ = "0.11.0"
25
+ __version__ = "0.13.0"
24
26
  __all__ = [
25
27
  "Database",
26
28
  "Entity",
@@ -40,6 +42,8 @@ __all__ = [
40
42
  "ACTION_MODIFY",
41
43
  "ACTION_DELETE",
42
44
  "SchemaIncompatibleError",
45
+ "TooLargeError",
46
+ "RelationView",
43
47
  "build_schema",
44
48
  "diff_schemas",
45
49
  "schema_hash",
@@ -0,0 +1,11 @@
1
+ LEVEL_MAX_DEFAULT = 3
2
+
3
+ # Hook action types
4
+ ACTION_CREATE = "create"
5
+ ACTION_MODIFY = "modify"
6
+ ACTION_DELETE = "delete"
7
+
8
+ # Largest collection an unpaged operation (iterating a relation, instances())
9
+ # loads in one call; past it they raise TooLargeError. At about 1M
10
+ # instructions per load this stays well inside a query's 5B budget.
11
+ MAX_UNPAGED = 1000