3tears 0.14.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.
- 3tears-0.14.0/.gitignore +216 -0
- 3tears-0.14.0/LICENSE +21 -0
- 3tears-0.14.0/PKG-INFO +260 -0
- 3tears-0.14.0/README.md +224 -0
- 3tears-0.14.0/pyproject.toml +59 -0
- 3tears-0.14.0/src/threetears/core/__init__.py +170 -0
- 3tears-0.14.0/src/threetears/core/_bridge.py +102 -0
- 3tears-0.14.0/src/threetears/core/backends/__init__.py +16 -0
- 3tears-0.14.0/src/threetears/core/backends/nats_proxy.py +1125 -0
- 3tears-0.14.0/src/threetears/core/backends/protocol.py +186 -0
- 3tears-0.14.0/src/threetears/core/backends/schema_sql.py +705 -0
- 3tears-0.14.0/src/threetears/core/backends/sql.py +446 -0
- 3tears-0.14.0/src/threetears/core/cache/__init__.py +6 -0
- 3tears-0.14.0/src/threetears/core/cache/base.py +131 -0
- 3tears-0.14.0/src/threetears/core/cache/duckdb.py +419 -0
- 3tears-0.14.0/src/threetears/core/cache/kv.py +436 -0
- 3tears-0.14.0/src/threetears/core/cache/sqlite.py +578 -0
- 3tears-0.14.0/src/threetears/core/collections/__init__.py +58 -0
- 3tears-0.14.0/src/threetears/core/collections/asyncpg_init.py +90 -0
- 3tears-0.14.0/src/threetears/core/collections/base.py +1169 -0
- 3tears-0.14.0/src/threetears/core/collections/durable_store.py +92 -0
- 3tears-0.14.0/src/threetears/core/collections/flush.py +460 -0
- 3tears-0.14.0/src/threetears/core/collections/merge.py +79 -0
- 3tears-0.14.0/src/threetears/core/collections/registry.py +364 -0
- 3tears-0.14.0/src/threetears/core/collections/schema_backed.py +1575 -0
- 3tears-0.14.0/src/threetears/core/config.py +38 -0
- 3tears-0.14.0/src/threetears/core/coordination/__init__.py +29 -0
- 3tears-0.14.0/src/threetears/core/coordination/lease.py +563 -0
- 3tears-0.14.0/src/threetears/core/coordination/replay_guard.py +125 -0
- 3tears-0.14.0/src/threetears/core/data/__init__.py +21 -0
- 3tears-0.14.0/src/threetears/core/data/collection_factory.py +369 -0
- 3tears-0.14.0/src/threetears/core/data/migrations/README.md +191 -0
- 3tears-0.14.0/src/threetears/core/data/migrations/__init__.py +83 -0
- 3tears-0.14.0/src/threetears/core/data/migrations/drift.py +513 -0
- 3tears-0.14.0/src/threetears/core/data/migrations/enforcement.py +850 -0
- 3tears-0.14.0/src/threetears/core/data/migrations/errors.py +68 -0
- 3tears-0.14.0/src/threetears/core/data/migrations/helpers.py +888 -0
- 3tears-0.14.0/src/threetears/core/data/migrations/preview.py +154 -0
- 3tears-0.14.0/src/threetears/core/data/migrations/registry.py +200 -0
- 3tears-0.14.0/src/threetears/core/data/migrations/runner.py +686 -0
- 3tears-0.14.0/src/threetears/core/data/migrations/scope.py +33 -0
- 3tears-0.14.0/src/threetears/core/data/migrations/template.py +135 -0
- 3tears-0.14.0/src/threetears/core/data/schema.py +327 -0
- 3tears-0.14.0/src/threetears/core/data/sql_builder.py +127 -0
- 3tears-0.14.0/src/threetears/core/data/store.py +167 -0
- 3tears-0.14.0/src/threetears/core/entities/__init__.py +3 -0
- 3tears-0.14.0/src/threetears/core/entities/base.py +243 -0
- 3tears-0.14.0/src/threetears/core/exceptions.py +29 -0
- 3tears-0.14.0/src/threetears/core/models.py +111 -0
- 3tears-0.14.0/src/threetears/core/namespaces.py +168 -0
- 3tears-0.14.0/src/threetears/core/pagination.py +188 -0
- 3tears-0.14.0/src/threetears/core/py.typed +0 -0
- 3tears-0.14.0/src/threetears/core/security/__init__.py +94 -0
- 3tears-0.14.0/src/threetears/core/security/encryption.py +97 -0
- 3tears-0.14.0/src/threetears/core/security/identity_token.py +328 -0
- 3tears-0.14.0/src/threetears/core/security/jwks_provider.py +239 -0
- 3tears-0.14.0/src/threetears/core/security/pop.py +174 -0
- 3tears-0.14.0/src/threetears/core/security/proxy_assertion.py +196 -0
- 3tears-0.14.0/src/threetears/core/security/proxy_signer.py +131 -0
- 3tears-0.14.0/src/threetears/core/security/sandbox.py +436 -0
- 3tears-0.14.0/src/threetears/core/security/secret_refs.py +226 -0
- 3tears-0.14.0/src/threetears/core/serialization.py +234 -0
- 3tears-0.14.0/src/threetears/core/testing/__init__.py +57 -0
- 3tears-0.14.0/src/threetears/core/testing/containers.py +157 -0
- 3tears-0.14.0/src/threetears/core/testing/fixtures.py +149 -0
- 3tears-0.14.0/src/threetears/core/testing/sqla_parity.py +339 -0
- 3tears-0.14.0/src/threetears/core/utils/__init__.py +29 -0
- 3tears-0.14.0/src/threetears/core/utils/atomic_write.py +85 -0
- 3tears-0.14.0/src/threetears/core/utils/pg_pool_kwargs.py +366 -0
- 3tears-0.14.0/src/threetears/knowledge/__init__.py +78 -0
- 3tears-0.14.0/src/threetears/knowledge/chains.py +215 -0
- 3tears-0.14.0/src/threetears/knowledge/concept_merge.py +277 -0
- 3tears-0.14.0/src/threetears/knowledge/events.py +224 -0
- 3tears-0.14.0/src/threetears/knowledge/merge.py +331 -0
- 3tears-0.14.0/src/threetears/knowledge/scope.py +88 -0
- 3tears-0.14.0/tests/conftest.py +15 -0
- 3tears-0.14.0/tests/enforcement/__init__.py +0 -0
- 3tears-0.14.0/tests/enforcement/_jsonb_native_binding_exemptions.txt +15 -0
- 3tears-0.14.0/tests/enforcement/_migration_exemptions.txt +19 -0
- 3tears-0.14.0/tests/enforcement/test_codebase_conventions.py +146 -0
- 3tears-0.14.0/tests/enforcement/test_column_type_alignment.py +717 -0
- 3tears-0.14.0/tests/enforcement/test_dict_state_detection.py +533 -0
- 3tears-0.14.0/tests/enforcement/test_docstring_conventions.py +166 -0
- 3tears-0.14.0/tests/enforcement/test_identity_token_alg_pinning.py +121 -0
- 3tears-0.14.0/tests/enforcement/test_jsonb_native_binding.py +219 -0
- 3tears-0.14.0/tests/enforcement/test_l1_cache_roundtrip.py +207 -0
- 3tears-0.14.0/tests/enforcement/test_migration_yugabyte_safety.py +148 -0
- 3tears-0.14.0/tests/enforcement/test_migration_yugabyte_safety_self.py +239 -0
- 3tears-0.14.0/tests/enforcement/test_no_datetime_type_columns.py +136 -0
- 3tears-0.14.0/tests/enforcement/test_no_llm_deps.py +74 -0
- 3tears-0.14.0/tests/enforcement/test_no_metallm_imports.py +51 -0
- 3tears-0.14.0/tests/enforcement/test_partition_column_enforcement.py +388 -0
- 3tears-0.14.0/tests/enforcement/test_partition_column_enforcement_self.py +156 -0
- 3tears-0.14.0/tests/integration/__init__.py +0 -0
- 3tears-0.14.0/tests/integration/migrations/__init__.py +1 -0
- 3tears-0.14.0/tests/integration/migrations/test_helpers_against_postgres.py +441 -0
- 3tears-0.14.0/tests/integration/test_composite_pk_three_tier.py +375 -0
- 3tears-0.14.0/tests/integration/test_datastore_asyncpg_roundtrip.py +182 -0
- 3tears-0.14.0/tests/integration/test_migration_rollback.py +435 -0
- 3tears-0.14.0/tests/integration/test_nats_proxy.py +0 -0
- 3tears-0.14.0/tests/integration/test_schema_backed_vector_fetch.py +406 -0
- 3tears-0.14.0/tests/test_base_collection.py +1264 -0
- 3tears-0.14.0/tests/test_base_entity.py +266 -0
- 3tears-0.14.0/tests/test_cache_coherence.py +1001 -0
- 3tears-0.14.0/tests/test_composite_pk.py +683 -0
- 3tears-0.14.0/tests/test_config.py +34 -0
- 3tears-0.14.0/tests/test_duckdb_backend.py +292 -0
- 3tears-0.14.0/tests/test_durable_store_collection.py +178 -0
- 3tears-0.14.0/tests/test_flush.py +632 -0
- 3tears-0.14.0/tests/test_integration.py +406 -0
- 3tears-0.14.0/tests/test_kv_client.py +326 -0
- 3tears-0.14.0/tests/test_l1_backend.py +172 -0
- 3tears-0.14.0/tests/test_l3_backends.py +537 -0
- 3tears-0.14.0/tests/test_lazy_init.py +86 -0
- 3tears-0.14.0/tests/test_logging.py +154 -0
- 3tears-0.14.0/tests/test_registry.py +206 -0
- 3tears-0.14.0/tests/test_serialization.py +155 -0
- 3tears-0.14.0/tests/test_smoke.py +14 -0
- 3tears-0.14.0/tests/test_sqlite_backend.py +486 -0
- 3tears-0.14.0/tests/test_tracing.py +122 -0
- 3tears-0.14.0/tests/unit/__init__.py +0 -0
- 3tears-0.14.0/tests/unit/backends/__init__.py +0 -0
- 3tears-0.14.0/tests/unit/backends/test_nats_proxy.py +1094 -0
- 3tears-0.14.0/tests/unit/backends/test_nats_proxy_tx_namespace.py +275 -0
- 3tears-0.14.0/tests/unit/collections/__init__.py +0 -0
- 3tears-0.14.0/tests/unit/collections/test_merge.py +114 -0
- 3tears-0.14.0/tests/unit/collections/test_schema_backed.py +2074 -0
- 3tears-0.14.0/tests/unit/collections/test_to_sqlalchemy_table.py +798 -0
- 3tears-0.14.0/tests/unit/coordination/__init__.py +1 -0
- 3tears-0.14.0/tests/unit/coordination/_fake_kv.py +203 -0
- 3tears-0.14.0/tests/unit/coordination/test_lease.py +340 -0
- 3tears-0.14.0/tests/unit/coordination/test_replay_guard.py +114 -0
- 3tears-0.14.0/tests/unit/data/__init__.py +0 -0
- 3tears-0.14.0/tests/unit/data/migrations/__init__.py +0 -0
- 3tears-0.14.0/tests/unit/data/migrations/_fake_store.py +122 -0
- 3tears-0.14.0/tests/unit/data/migrations/test_drift.py +215 -0
- 3tears-0.14.0/tests/unit/data/migrations/test_helpers.py +526 -0
- 3tears-0.14.0/tests/unit/data/migrations/test_preview.py +283 -0
- 3tears-0.14.0/tests/unit/data/migrations/test_runner.py +362 -0
- 3tears-0.14.0/tests/unit/data/test_collection_factory.py +310 -0
- 3tears-0.14.0/tests/unit/data/test_schema.py +33 -0
- 3tears-0.14.0/tests/unit/data/test_sql_builder.py +37 -0
- 3tears-0.14.0/tests/unit/security/__init__.py +1 -0
- 3tears-0.14.0/tests/unit/security/test_encryption.py +97 -0
- 3tears-0.14.0/tests/unit/security/test_identity_token.py +437 -0
- 3tears-0.14.0/tests/unit/security/test_jwks_provider.py +242 -0
- 3tears-0.14.0/tests/unit/security/test_path_sandbox.py +360 -0
- 3tears-0.14.0/tests/unit/security/test_pop.py +130 -0
- 3tears-0.14.0/tests/unit/security/test_proxy_assertion.py +103 -0
- 3tears-0.14.0/tests/unit/security/test_proxy_signer.py +66 -0
- 3tears-0.14.0/tests/unit/security/test_sandbox.py +160 -0
- 3tears-0.14.0/tests/unit/security/test_secret_refs.py +89 -0
- 3tears-0.14.0/tests/unit/test_format_handler_registry.py +256 -0
- 3tears-0.14.0/tests/unit/test_knowledge_concept_merge.py +357 -0
- 3tears-0.14.0/tests/unit/test_knowledge_events.py +203 -0
- 3tears-0.14.0/tests/unit/test_knowledge_merge.py +392 -0
- 3tears-0.14.0/tests/unit/test_namespaces.py +101 -0
- 3tears-0.14.0/tests/unit/test_pagination.py +114 -0
- 3tears-0.14.0/tests/unit/utils/__init__.py +1 -0
- 3tears-0.14.0/tests/unit/utils/test_atomic_write.py +126 -0
- 3tears-0.14.0/tests/unit/utils/test_pg_pool_kwargs.py +123 -0
3tears-0.14.0/.gitignore
ADDED
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
# Byte-compiled / optimized / DLL files
|
|
2
|
+
__pycache__/
|
|
3
|
+
*.py[codz]
|
|
4
|
+
*$py.class
|
|
5
|
+
|
|
6
|
+
# C extensions
|
|
7
|
+
*.so
|
|
8
|
+
|
|
9
|
+
# Distribution / packaging
|
|
10
|
+
.Python
|
|
11
|
+
build/
|
|
12
|
+
develop-eggs/
|
|
13
|
+
dist/
|
|
14
|
+
downloads/
|
|
15
|
+
eggs/
|
|
16
|
+
.eggs/
|
|
17
|
+
lib/
|
|
18
|
+
lib64/
|
|
19
|
+
parts/
|
|
20
|
+
sdist/
|
|
21
|
+
var/
|
|
22
|
+
wheels/
|
|
23
|
+
share/python-wheels/
|
|
24
|
+
*.egg-info/
|
|
25
|
+
.installed.cfg
|
|
26
|
+
*.egg
|
|
27
|
+
MANIFEST
|
|
28
|
+
|
|
29
|
+
# PyInstaller
|
|
30
|
+
# Usually these files are written by a python script from a template
|
|
31
|
+
# before PyInstaller builds the exe, so as to inject date/other infos into it.
|
|
32
|
+
*.manifest
|
|
33
|
+
*.spec
|
|
34
|
+
|
|
35
|
+
# Installer logs
|
|
36
|
+
pip-log.txt
|
|
37
|
+
pip-delete-this-directory.txt
|
|
38
|
+
|
|
39
|
+
# Unit test / coverage reports
|
|
40
|
+
htmlcov/
|
|
41
|
+
.tox/
|
|
42
|
+
.nox/
|
|
43
|
+
.coverage
|
|
44
|
+
.coverage.*
|
|
45
|
+
.cache
|
|
46
|
+
nosetests.xml
|
|
47
|
+
coverage.xml
|
|
48
|
+
*.cover
|
|
49
|
+
*.py.cover
|
|
50
|
+
.hypothesis/
|
|
51
|
+
.pytest_cache/
|
|
52
|
+
cover/
|
|
53
|
+
|
|
54
|
+
# Translations
|
|
55
|
+
*.mo
|
|
56
|
+
*.pot
|
|
57
|
+
|
|
58
|
+
# Django stuff:
|
|
59
|
+
*.log
|
|
60
|
+
local_settings.py
|
|
61
|
+
db.sqlite3
|
|
62
|
+
db.sqlite3-journal
|
|
63
|
+
|
|
64
|
+
# Flask stuff:
|
|
65
|
+
instance/
|
|
66
|
+
.webassets-cache
|
|
67
|
+
|
|
68
|
+
# Scrapy stuff:
|
|
69
|
+
.scrapy
|
|
70
|
+
|
|
71
|
+
# Sphinx documentation
|
|
72
|
+
docs/_build/
|
|
73
|
+
|
|
74
|
+
# PyBuilder
|
|
75
|
+
.pybuilder/
|
|
76
|
+
target/
|
|
77
|
+
|
|
78
|
+
# Jupyter Notebook
|
|
79
|
+
.ipynb_checkpoints
|
|
80
|
+
|
|
81
|
+
# IPython
|
|
82
|
+
profile_default/
|
|
83
|
+
ipython_config.py
|
|
84
|
+
|
|
85
|
+
# pyenv
|
|
86
|
+
# For a library or package, you might want to ignore these files since the code is
|
|
87
|
+
# intended to run in multiple environments; otherwise, check them in:
|
|
88
|
+
# .python-version
|
|
89
|
+
|
|
90
|
+
# pipenv
|
|
91
|
+
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
|
|
92
|
+
# However, in case of collaboration, if having platform-specific dependencies or dependencies
|
|
93
|
+
# having no cross-platform support, pipenv may install dependencies that don't work, or not
|
|
94
|
+
# install all needed dependencies.
|
|
95
|
+
#Pipfile.lock
|
|
96
|
+
|
|
97
|
+
# UV
|
|
98
|
+
# Similar to Pipfile.lock, it is generally recommended to include uv.lock in version control.
|
|
99
|
+
# This is especially recommended for binary packages to ensure reproducibility, and is more
|
|
100
|
+
# commonly ignored for libraries.
|
|
101
|
+
#uv.lock
|
|
102
|
+
|
|
103
|
+
# poetry
|
|
104
|
+
# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
|
|
105
|
+
# This is especially recommended for binary packages to ensure reproducibility, and is more
|
|
106
|
+
# commonly ignored for libraries.
|
|
107
|
+
# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
|
|
108
|
+
#poetry.lock
|
|
109
|
+
#poetry.toml
|
|
110
|
+
|
|
111
|
+
# pdm
|
|
112
|
+
# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
|
|
113
|
+
# pdm recommends including project-wide configuration in pdm.toml, but excluding .pdm-python.
|
|
114
|
+
# https://pdm-project.org/en/latest/usage/project/#working-with-version-control
|
|
115
|
+
#pdm.lock
|
|
116
|
+
#pdm.toml
|
|
117
|
+
.pdm-python
|
|
118
|
+
.pdm-build/
|
|
119
|
+
|
|
120
|
+
# pixi
|
|
121
|
+
# Similar to Pipfile.lock, it is generally recommended to include pixi.lock in version control.
|
|
122
|
+
#pixi.lock
|
|
123
|
+
# Pixi creates a virtual environment in the .pixi directory, just like venv module creates one
|
|
124
|
+
# in the .venv directory. It is recommended not to include this directory in version control.
|
|
125
|
+
.pixi
|
|
126
|
+
|
|
127
|
+
# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
|
|
128
|
+
__pypackages__/
|
|
129
|
+
|
|
130
|
+
# Celery stuff
|
|
131
|
+
celerybeat-schedule
|
|
132
|
+
celerybeat.pid
|
|
133
|
+
|
|
134
|
+
# SageMath parsed files
|
|
135
|
+
*.sage.py
|
|
136
|
+
|
|
137
|
+
# Environments
|
|
138
|
+
.env
|
|
139
|
+
.envrc
|
|
140
|
+
.venv
|
|
141
|
+
env/
|
|
142
|
+
venv/
|
|
143
|
+
ENV/
|
|
144
|
+
env.bak/
|
|
145
|
+
venv.bak/
|
|
146
|
+
|
|
147
|
+
# Spyder project settings
|
|
148
|
+
.spyderproject
|
|
149
|
+
.spyproject
|
|
150
|
+
|
|
151
|
+
# Rope project settings
|
|
152
|
+
.ropeproject
|
|
153
|
+
|
|
154
|
+
# mkdocs documentation
|
|
155
|
+
/site
|
|
156
|
+
|
|
157
|
+
# mypy
|
|
158
|
+
.mypy_cache/
|
|
159
|
+
.dmypy.json
|
|
160
|
+
dmypy.json
|
|
161
|
+
|
|
162
|
+
# Pyre type checker
|
|
163
|
+
.pyre/
|
|
164
|
+
|
|
165
|
+
# pytype static type analyzer
|
|
166
|
+
.pytype/
|
|
167
|
+
|
|
168
|
+
# Cython debug symbols
|
|
169
|
+
cython_debug/
|
|
170
|
+
|
|
171
|
+
# PyCharm
|
|
172
|
+
# JetBrains specific template is maintained in a separate JetBrains.gitignore that can
|
|
173
|
+
# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
|
|
174
|
+
# and can be added to the global gitignore or merged into this file. For a more nuclear
|
|
175
|
+
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
|
|
176
|
+
#.idea/
|
|
177
|
+
|
|
178
|
+
# Abstra
|
|
179
|
+
# Abstra is an AI-powered process automation framework.
|
|
180
|
+
# Ignore directories containing user credentials, local state, and settings.
|
|
181
|
+
# Learn more at https://abstra.io/docs
|
|
182
|
+
.abstra/
|
|
183
|
+
|
|
184
|
+
# Visual Studio Code
|
|
185
|
+
# Visual Studio Code specific template is maintained in a separate VisualStudioCode.gitignore
|
|
186
|
+
# that can be found at https://github.com/github/gitignore/blob/main/Global/VisualStudioCode.gitignore
|
|
187
|
+
# and can be added to the global gitignore or merged into this file. However, if you prefer,
|
|
188
|
+
# you could uncomment the following to ignore the entire vscode folder
|
|
189
|
+
# .vscode/
|
|
190
|
+
|
|
191
|
+
# Ruff stuff:
|
|
192
|
+
.ruff_cache/
|
|
193
|
+
|
|
194
|
+
# PyPI configuration file
|
|
195
|
+
.pypirc
|
|
196
|
+
|
|
197
|
+
# Cursor
|
|
198
|
+
# Cursor is an AI-powered code editor. `.cursorignore` specifies files/directories to
|
|
199
|
+
# exclude from AI features like autocomplete and code analysis. Recommended for sensitive data
|
|
200
|
+
# refer to https://docs.cursor.com/context/ignore-files
|
|
201
|
+
.cursorignore
|
|
202
|
+
.cursorindexingignore
|
|
203
|
+
|
|
204
|
+
# Marimo
|
|
205
|
+
marimo/_static/
|
|
206
|
+
marimo/_lsp/
|
|
207
|
+
__marimo__/
|
|
208
|
+
|
|
209
|
+
# Claude Code local state
|
|
210
|
+
.claude/
|
|
211
|
+
|
|
212
|
+
# prawduct session evidence (local governance artifacts, never shipped)
|
|
213
|
+
.prawduct/
|
|
214
|
+
|
|
215
|
+
# macOS folder metadata
|
|
216
|
+
.DS_Store
|
3tears-0.14.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Mark Pace
|
|
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.
|
3tears-0.14.0/PKG-INFO
ADDED
|
@@ -0,0 +1,260 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: 3tears
|
|
3
|
+
Version: 0.14.0
|
|
4
|
+
Summary: Three-tier caching framework for Python -- L1 SQLite, L2 NATS KV, L3 PostgreSQL
|
|
5
|
+
Project-URL: Repository, https://github.com/pacepace/3tears
|
|
6
|
+
Author: pace
|
|
7
|
+
License-Expression: MIT
|
|
8
|
+
License-File: LICENSE
|
|
9
|
+
Classifier: Development Status :: 3 - Alpha
|
|
10
|
+
Classifier: Framework :: AsyncIO
|
|
11
|
+
Classifier: Intended Audience :: Developers
|
|
12
|
+
Classifier: Programming Language :: Python :: 3
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
14
|
+
Classifier: Topic :: Database
|
|
15
|
+
Classifier: Topic :: Software Development :: Libraries
|
|
16
|
+
Classifier: Typing :: Typed
|
|
17
|
+
Requires-Python: >=3.14
|
|
18
|
+
Requires-Dist: 3tears-nats
|
|
19
|
+
Requires-Dist: 3tears-observe
|
|
20
|
+
Requires-Dist: aiosqlite
|
|
21
|
+
Requires-Dist: asyncpg
|
|
22
|
+
Requires-Dist: cryptography>=42
|
|
23
|
+
Requires-Dist: nats-py
|
|
24
|
+
Requires-Dist: pydantic
|
|
25
|
+
Requires-Dist: pyjwt[crypto]>=2.8
|
|
26
|
+
Requires-Dist: sqlalchemy
|
|
27
|
+
Provides-Extra: duckdb
|
|
28
|
+
Requires-Dist: duckdb>=1.0; extra == 'duckdb'
|
|
29
|
+
Provides-Extra: testing
|
|
30
|
+
Requires-Dist: docker>=7.0; extra == 'testing'
|
|
31
|
+
Requires-Dist: pytest>=8.0; extra == 'testing'
|
|
32
|
+
Requires-Dist: testcontainers[nats,postgres]>=4.0; extra == 'testing'
|
|
33
|
+
Provides-Extra: tracing
|
|
34
|
+
Requires-Dist: opentelemetry-api>=1.20; extra == 'tracing'
|
|
35
|
+
Description-Content-Type: text/markdown
|
|
36
|
+
|
|
37
|
+
# 3tears Core
|
|
38
|
+
|
|
39
|
+
Three-tier caching library for Python applications. Provides collections (L1 SQLite -> L2 NATS KV -> L3 PostgreSQL) with subscript access, entity proxy objects, and configurable flush strategies.
|
|
40
|
+
|
|
41
|
+
## Architecture
|
|
42
|
+
|
|
43
|
+
```
|
|
44
|
+
L1 (SQLite, in-process, sync) -> L2 (NATS KV, shared, async) -> L3 (PostgreSQL, persistent, async)
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
- **L1**: In-memory SQLite via WAL mode. Sync access. Used by entity attribute reads and writes.
|
|
48
|
+
- **L2**: NATS KV shared cache. Async. Cross-pod consistency for multi-instance deployments.
|
|
49
|
+
- **L3**: PostgreSQL (or PostGIS, YugabyteDB, etc.). Async. Source of truth.
|
|
50
|
+
|
|
51
|
+
Reads promote up the stack (L3 miss -> L2 miss -> L1 hit on next access). Writes flow down (L1 -> L2 -> L3, with optional deferred flush).
|
|
52
|
+
|
|
53
|
+
## Quick Start
|
|
54
|
+
|
|
55
|
+
### 1. Configure the Registry
|
|
56
|
+
|
|
57
|
+
```python
|
|
58
|
+
from threetears.core.collections.registry import CollectionRegistry
|
|
59
|
+
from threetears.core.cache.sqlite import SQLiteBackend
|
|
60
|
+
|
|
61
|
+
# Create and configure
|
|
62
|
+
l1 = SQLiteBackend("my_app_cache")
|
|
63
|
+
l1.initialize(sa_metadata) # SQLAlchemy metadata with your table definitions
|
|
64
|
+
|
|
65
|
+
registry = CollectionRegistry()
|
|
66
|
+
registry.configure(
|
|
67
|
+
l1_backend=l1, # SQLiteBackend instance
|
|
68
|
+
l2_client=nats_client, # NATS client (optional, None to skip L2)
|
|
69
|
+
l3_pool=postgres_pool, # asyncpg pool
|
|
70
|
+
)
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
### 2. Per-Collection Pool Overrides
|
|
74
|
+
|
|
75
|
+
Different collections can use different databases:
|
|
76
|
+
|
|
77
|
+
```python
|
|
78
|
+
# Default: all collections use YugabyteDB
|
|
79
|
+
registry.configure(l3_pool=yugabyte_pool)
|
|
80
|
+
|
|
81
|
+
# Override: geo collection uses PostGIS
|
|
82
|
+
registry.configure() # keep defaults
|
|
83
|
+
# When creating the collection, register with override:
|
|
84
|
+
geo_collection = GeoCollection(registry, config, nats_client, write_buffer)
|
|
85
|
+
registry.register(geo_collection, l3_pool=postgis_pool)
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
### 3. Define a Collection
|
|
89
|
+
|
|
90
|
+
```python
|
|
91
|
+
from threetears.core.collections.base import BaseCollection
|
|
92
|
+
from threetears.core.entities.base import BaseEntity
|
|
93
|
+
|
|
94
|
+
class UserEntity(BaseEntity):
|
|
95
|
+
primary_key_field = "user_id"
|
|
96
|
+
|
|
97
|
+
class UsersCollection(BaseCollection[UserEntity]):
|
|
98
|
+
primary_key_column = "user_id"
|
|
99
|
+
|
|
100
|
+
@property
|
|
101
|
+
def table_name(self) -> str:
|
|
102
|
+
return "users"
|
|
103
|
+
|
|
104
|
+
@property
|
|
105
|
+
def entity_class(self) -> type[UserEntity]:
|
|
106
|
+
return UserEntity
|
|
107
|
+
|
|
108
|
+
async def fetch_from_store(self, entity_id):
|
|
109
|
+
row = await self.l3_pool.fetchrow(
|
|
110
|
+
"SELECT * FROM users WHERE user_id = $1", entity_id
|
|
111
|
+
)
|
|
112
|
+
return dict(row) if row else None
|
|
113
|
+
|
|
114
|
+
async def save_to_store(self, data, original_timestamp=None):
|
|
115
|
+
# INSERT or UPDATE with optimistic locking
|
|
116
|
+
...
|
|
117
|
+
|
|
118
|
+
async def delete_from_store(self, entity_id):
|
|
119
|
+
await self.l3_pool.execute(
|
|
120
|
+
"DELETE FROM users WHERE user_id = $1", entity_id
|
|
121
|
+
)
|
|
122
|
+
|
|
123
|
+
def serialize(self, data):
|
|
124
|
+
return json.dumps(data, default=str).encode()
|
|
125
|
+
|
|
126
|
+
def deserialize(self, data):
|
|
127
|
+
return json.loads(data)
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
### 4. Create Collection Instances
|
|
131
|
+
|
|
132
|
+
```python
|
|
133
|
+
from threetears.core.collections.flush import WriteBuffer
|
|
134
|
+
|
|
135
|
+
write_buffer = WriteBuffer()
|
|
136
|
+
users = UsersCollection(registry, config, nats_client, write_buffer)
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
The `config` parameter must satisfy the `CoreConfig` protocol:
|
|
140
|
+
|
|
141
|
+
```python
|
|
142
|
+
class CoreConfig(Protocol):
|
|
143
|
+
collection_flush: str # "ALWAYS", "ON_CHECKPOINT", "ON_SCHEDULE", "ON_SHUTDOWN"
|
|
144
|
+
collection_flush_interval: int # seconds between scheduled flushes
|
|
145
|
+
collection_flush_tables: str # comma-separated table names eligible for deferred flush
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
## Access Patterns
|
|
149
|
+
|
|
150
|
+
### Subscript Access (sync, transparent pull-through)
|
|
151
|
+
|
|
152
|
+
Subscript access is the primary API. On L1 miss, data is transparently pulled through L2/L3 via a background event loop. No `await` needed, no `ensure()` required:
|
|
153
|
+
|
|
154
|
+
```python
|
|
155
|
+
# Read entity -- pulls through L2/L3 automatically on L1 miss
|
|
156
|
+
entity = users[user_id]
|
|
157
|
+
|
|
158
|
+
# Read single field
|
|
159
|
+
name = users[user_id, "name_display"]
|
|
160
|
+
|
|
161
|
+
# Write single field (writes to L1, tracks for flush)
|
|
162
|
+
users[user_id, "name_display"] = "New Name"
|
|
163
|
+
|
|
164
|
+
# Write full entity data (writes dict to L1)
|
|
165
|
+
users[user_id] = {"user_id": user_id, "name_display": "New Name", ...}
|
|
166
|
+
|
|
167
|
+
# Check if entity is in L1 (does NOT pull through -- L1 only)
|
|
168
|
+
if user_id in users:
|
|
169
|
+
entity = users[user_id]
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
`__getitem__` raises `KeyError` only if the entity doesn't exist in any tier. The L1 fast path is ~microseconds; an L1 miss with pull-through adds ~50-200us bridge overhead plus the actual L2/L3 I/O time.
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
For hot-path code where you want to avoid the sync-async bridge overhead on first access, you can pre-warm L1:
|
|
176
|
+
|
|
177
|
+
```python
|
|
178
|
+
await users.ensure(user_id) # async: pre-warms L1
|
|
179
|
+
entity = users[user_id] # guaranteed L1 hit, no bridge needed
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
### Async Operations
|
|
183
|
+
|
|
184
|
+
```python
|
|
185
|
+
# Three-tier read: L1 -> L2 -> L3, promotes on miss. Returns None if not found.
|
|
186
|
+
entity = await users.get(user_id)
|
|
187
|
+
|
|
188
|
+
# Create a new entity (not persisted until save)
|
|
189
|
+
entity = users.create({"user_id": uuid7(), "name_display": "Alice", ...})
|
|
190
|
+
|
|
191
|
+
# Save through three-tier write path (L3 -> L1 -> L2)
|
|
192
|
+
await users.save_entity(entity)
|
|
193
|
+
# Or via entity directly:
|
|
194
|
+
await entity.save()
|
|
195
|
+
|
|
196
|
+
# Reload from L3 (discards local changes)
|
|
197
|
+
await entity.reload()
|
|
198
|
+
|
|
199
|
+
# Delete from all tiers
|
|
200
|
+
await users.delete(user_id)
|
|
201
|
+
|
|
202
|
+
# Invalidate L1 + L2 (force next read to hit L3)
|
|
203
|
+
await users.invalidate_cache(user_id)
|
|
204
|
+
```
|
|
205
|
+
|
|
206
|
+
### Entity Attribute Access
|
|
207
|
+
|
|
208
|
+
Entities are thin cache proxies. Field data lives in L1, not in the entity object.
|
|
209
|
+
|
|
210
|
+
```python
|
|
211
|
+
entity = await users.get(user_id)
|
|
212
|
+
|
|
213
|
+
# Read (checks entity._changes first, then L1 cache)
|
|
214
|
+
print(entity.name_display)
|
|
215
|
+
|
|
216
|
+
# Write (writes to L1 + tracks change)
|
|
217
|
+
entity.name_display = "Updated Name"
|
|
218
|
+
|
|
219
|
+
# Check dirty state
|
|
220
|
+
entity.is_dirty # True after modification
|
|
221
|
+
entity.is_new # True if created via collection.create()
|
|
222
|
+
|
|
223
|
+
# Get all changes
|
|
224
|
+
entity.get_changes() # {"name_display": "Updated Name"}
|
|
225
|
+
|
|
226
|
+
# Export full entity data from L1
|
|
227
|
+
entity.to_dict()
|
|
228
|
+
|
|
229
|
+
# Persist
|
|
230
|
+
await entity.save()
|
|
231
|
+
```
|
|
232
|
+
|
|
233
|
+
## Flush Strategies
|
|
234
|
+
|
|
235
|
+
Controls when deferred writes reach L3 (PostgreSQL):
|
|
236
|
+
|
|
237
|
+
| Strategy | Behavior |
|
|
238
|
+
|---|---|
|
|
239
|
+
| `ALWAYS` | Every `save_entity()` writes to L3 immediately |
|
|
240
|
+
| `ON_CHECKPOINT` | Writes buffer to L1 + L2; flushes to L3 on explicit `flush_pending()` call |
|
|
241
|
+
| `ON_SCHEDULE` | Same as ON_CHECKPOINT but with timer-based auto-flush |
|
|
242
|
+
| `ON_SHUTDOWN` | Writes buffer; flushes to L3 on application shutdown |
|
|
243
|
+
|
|
244
|
+
Only tables listed in `collection_flush_tables` are eligible for deferred writes. All other tables always write immediately regardless of strategy.
|
|
245
|
+
|
|
246
|
+
## Optimistic Locking
|
|
247
|
+
|
|
248
|
+
Collections use `date_updated` for optimistic locking. When saving an existing entity, the `save_to_store` implementation should check:
|
|
249
|
+
|
|
250
|
+
```sql
|
|
251
|
+
UPDATE users SET ... WHERE user_id = $1 AND date_updated = $2
|
|
252
|
+
```
|
|
253
|
+
|
|
254
|
+
If `rows_affected == 0` for an UPDATE, `BaseCollection.save_entity()` raises `ConcurrentModificationError`.
|
|
255
|
+
|
|
256
|
+
## Subclassing Guide
|
|
257
|
+
|
|
258
|
+
**BaseEntity**: Set `primary_key_field` to your PK column name. Add computed properties as needed. Do NOT store data in instance attributes. All data lives in L1.
|
|
259
|
+
|
|
260
|
+
**BaseCollection**: Set `primary_key_column`. Implement the 5 abstract methods: `fetch_from_store`, `save_to_store`, `delete_from_store`, `serialize`, `deserialize`. Use `self.l3_pool` for database access. Add domain-specific query methods (e.g., `find_by_email`).
|