openframe-adapters-db-oracle 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.
- openframe_adapters_db_oracle-0.1.0/.gitignore +222 -0
- openframe_adapters_db_oracle-0.1.0/PKG-INFO +260 -0
- openframe_adapters_db_oracle-0.1.0/README.md +238 -0
- openframe_adapters_db_oracle-0.1.0/openframe/adapters/db/oracle/__init__.py +54 -0
- openframe_adapters_db_oracle-0.1.0/openframe/adapters/db/oracle/config.py +70 -0
- openframe_adapters_db_oracle-0.1.0/openframe/adapters/db/oracle/connection.py +198 -0
- openframe_adapters_db_oracle-0.1.0/openframe/adapters/db/oracle/plugin.py +252 -0
- openframe_adapters_db_oracle-0.1.0/openframe/adapters/db/oracle/repository.py +592 -0
- openframe_adapters_db_oracle-0.1.0/pyproject.toml +45 -0
- openframe_adapters_db_oracle-0.1.0/tests/conftest.py +73 -0
- openframe_adapters_db_oracle-0.1.0/tests/test_config.py +62 -0
- openframe_adapters_db_oracle-0.1.0/tests/test_connection.py +180 -0
- openframe_adapters_db_oracle-0.1.0/tests/test_plugin.py +303 -0
- openframe_adapters_db_oracle-0.1.0/tests/test_repository.py +332 -0
|
@@ -0,0 +1,222 @@
|
|
|
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
|
+
# Redis
|
|
135
|
+
*.rdb
|
|
136
|
+
*.aof
|
|
137
|
+
*.pid
|
|
138
|
+
|
|
139
|
+
# RabbitMQ
|
|
140
|
+
mnesia/
|
|
141
|
+
rabbitmq/
|
|
142
|
+
rabbitmq-data/
|
|
143
|
+
|
|
144
|
+
# ActiveMQ
|
|
145
|
+
activemq-data/
|
|
146
|
+
|
|
147
|
+
# SageMath parsed files
|
|
148
|
+
*.sage.py
|
|
149
|
+
|
|
150
|
+
# Environments
|
|
151
|
+
.env
|
|
152
|
+
.envrc
|
|
153
|
+
.venv
|
|
154
|
+
env/
|
|
155
|
+
venv/
|
|
156
|
+
ENV/
|
|
157
|
+
env.bak/
|
|
158
|
+
venv.bak/
|
|
159
|
+
|
|
160
|
+
# Spyder project settings
|
|
161
|
+
.spyderproject
|
|
162
|
+
.spyproject
|
|
163
|
+
|
|
164
|
+
# Rope project settings
|
|
165
|
+
.ropeproject
|
|
166
|
+
|
|
167
|
+
# mkdocs documentation
|
|
168
|
+
/site
|
|
169
|
+
|
|
170
|
+
# mypy
|
|
171
|
+
.mypy_cache/
|
|
172
|
+
.dmypy.json
|
|
173
|
+
dmypy.json
|
|
174
|
+
|
|
175
|
+
# Pyre type checker
|
|
176
|
+
.pyre/
|
|
177
|
+
|
|
178
|
+
# pytype static type analyzer
|
|
179
|
+
.pytype/
|
|
180
|
+
|
|
181
|
+
# Cython debug symbols
|
|
182
|
+
cython_debug/
|
|
183
|
+
|
|
184
|
+
# PyCharm
|
|
185
|
+
# JetBrains specific template is maintained in a separate JetBrains.gitignore that can
|
|
186
|
+
# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
|
|
187
|
+
# and can be added to the global gitignore or merged into this file. For a more nuclear
|
|
188
|
+
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
|
|
189
|
+
# .idea/
|
|
190
|
+
|
|
191
|
+
# Abstra
|
|
192
|
+
# Abstra is an AI-powered process automation framework.
|
|
193
|
+
# Ignore directories containing user credentials, local state, and settings.
|
|
194
|
+
# Learn more at https://abstra.io/docs
|
|
195
|
+
.abstra/
|
|
196
|
+
|
|
197
|
+
# Visual Studio Code
|
|
198
|
+
# Visual Studio Code specific template is maintained in a separate VisualStudioCode.gitignore
|
|
199
|
+
# that can be found at https://github.com/github/gitignore/blob/main/Global/VisualStudioCode.gitignore
|
|
200
|
+
# and can be added to the global gitignore or merged into this file. However, if you prefer,
|
|
201
|
+
# you could uncomment the following to ignore the entire vscode folder
|
|
202
|
+
# .vscode/
|
|
203
|
+
# Temporary file for partial code execution
|
|
204
|
+
tempCodeRunnerFile.py
|
|
205
|
+
|
|
206
|
+
# Ruff stuff:
|
|
207
|
+
.ruff_cache/
|
|
208
|
+
|
|
209
|
+
# PyPI configuration file
|
|
210
|
+
.pypirc
|
|
211
|
+
|
|
212
|
+
# Marimo
|
|
213
|
+
marimo/_static/
|
|
214
|
+
marimo/_lsp/
|
|
215
|
+
__marimo__/
|
|
216
|
+
|
|
217
|
+
# Streamlit
|
|
218
|
+
.streamlit/secrets.toml
|
|
219
|
+
|
|
220
|
+
# Miscellaneous
|
|
221
|
+
.DS_Store
|
|
222
|
+
.claude/
|
|
@@ -0,0 +1,260 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: openframe-adapters-db-oracle
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: OpenFrame Microservice Suite — Oracle database adapter.
|
|
5
|
+
Project-URL: Homepage, https://github.com/Furious-Meteors/openframe-adapters
|
|
6
|
+
Project-URL: Documentation, https://furious-meteors.github.io/openframe-adapters/
|
|
7
|
+
Project-URL: Repository, https://github.com/Furious-Meteors/openframe-adapters
|
|
8
|
+
Project-URL: Changelog, https://github.com/Furious-Meteors/openframe-adapters/blob/production/.github/CHANGELOG.md
|
|
9
|
+
Project-URL: Bug Tracker, https://github.com/Furious-Meteors/openframe-adapters/issues
|
|
10
|
+
Author-email: Furious Meteors Engineering <engineering@furiousmeteors.dev>
|
|
11
|
+
Maintainer-email: Furious Meteors Engineering <engineering@furiousmeteors.dev>
|
|
12
|
+
License: MIT
|
|
13
|
+
Keywords: hexagonal,microservice,openframe,oracle,oracledb
|
|
14
|
+
Requires-Python: >=3.11
|
|
15
|
+
Requires-Dist: openframe-core<4,>=3.3
|
|
16
|
+
Requires-Dist: oracledb>=2.0
|
|
17
|
+
Provides-Extra: dev
|
|
18
|
+
Requires-Dist: pytest-asyncio>=0.23; extra == 'dev'
|
|
19
|
+
Requires-Dist: pytest-mock>=3.14; extra == 'dev'
|
|
20
|
+
Requires-Dist: pytest>=8.0; extra == 'dev'
|
|
21
|
+
Description-Content-Type: text/markdown
|
|
22
|
+
|
|
23
|
+
# openframe-adapters-db-oracle
|
|
24
|
+
|
|
25
|
+
Oracle database adapter for the **OpenFrame Microservice Suite**.
|
|
26
|
+
|
|
27
|
+
Part of the `openframe-adapters` monorepo. Implements `BaseRepository[T]`
|
|
28
|
+
from `openframe-core` using `python-oracledb`'s native async ("thin mode")
|
|
29
|
+
API.
|
|
30
|
+
|
|
31
|
+
---
|
|
32
|
+
|
|
33
|
+
## Async strategy — read this first
|
|
34
|
+
|
|
35
|
+
Unlike `cassandra-driver` (which has no async API at all and will need an
|
|
36
|
+
`asyncio.get_running_loop().run_in_executor(None, sync_fn)` wrapper when
|
|
37
|
+
built), `python-oracledb` **does** ship a real, documented native-async
|
|
38
|
+
surface in thin mode — no Oracle Client libraries required:
|
|
39
|
+
`oracledb.connect_async()`, `oracledb.create_pool_async()`,
|
|
40
|
+
`oracledb.AsyncConnection`, `oracledb.AsyncConnectionPool`,
|
|
41
|
+
`oracledb.AsyncCursor`.
|
|
42
|
+
|
|
43
|
+
This was verified directly against a freshly installed `oracledb==26.0.1`
|
|
44
|
+
(not assumed from documentation alone) before writing a single line of this
|
|
45
|
+
adapter — see `openframe/adapters/db/oracle/connection.py`'s module
|
|
46
|
+
docstring for the full investigation notes, including a real gotcha found
|
|
47
|
+
along the way: a DNS resolution failure while connecting escapes as a raw
|
|
48
|
+
`socket.gaierror` rather than an `oracledb.Error`, so this adapter catches
|
|
49
|
+
`OSError` explicitly at every connection boundary.
|
|
50
|
+
|
|
51
|
+
Because the native-async surface genuinely exists and works, this adapter
|
|
52
|
+
is built **native-async**, matching the shape of
|
|
53
|
+
`openframe-adapters-db-postgres`/`-mysql` — not the executor-wrapped shape
|
|
54
|
+
`-cassandra` will need. Per ADR-003, this decision is made per-adapter
|
|
55
|
+
based on actual driver support, not forced one way across the ecosystem.
|
|
56
|
+
|
|
57
|
+
---
|
|
58
|
+
|
|
59
|
+
## Installation
|
|
60
|
+
|
|
61
|
+
```bash
|
|
62
|
+
pip install openframe-adapters-db-oracle
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
Required env var:
|
|
66
|
+
|
|
67
|
+
```
|
|
68
|
+
ORACLE_DSN=app/secret@db.example.com:1521/orclpdb
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
`ORACLE_DSN` matches the format `oracledb.connect_async()`'s own `dsn`
|
|
72
|
+
parameter documents: `user/password@host:port/service_name`. A bare
|
|
73
|
+
connect descriptor without embedded credentials also works if you keep
|
|
74
|
+
credentials elsewhere.
|
|
75
|
+
|
|
76
|
+
---
|
|
77
|
+
|
|
78
|
+
## Quick start
|
|
79
|
+
|
|
80
|
+
### Raw dict mode
|
|
81
|
+
|
|
82
|
+
```python
|
|
83
|
+
from openframe.adapters.db.oracle import OracleSettings, OracleRepository
|
|
84
|
+
|
|
85
|
+
settings = OracleSettings() # reads ORACLE_DSN from env
|
|
86
|
+
repo = OracleRepository(settings, table="items", id_column="id")
|
|
87
|
+
|
|
88
|
+
item = await repo.get("abc-123") # dict | None
|
|
89
|
+
items, total = await repo.list(10, 0) # ([dict, ...], int)
|
|
90
|
+
created = await repo.create({"id": "1", "name": "x"})
|
|
91
|
+
updated = await repo.update({"id": "1", "name": "y"})
|
|
92
|
+
deleted = await repo.delete("1") # bool
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
### Typed domain mode
|
|
96
|
+
|
|
97
|
+
```python
|
|
98
|
+
from dataclasses import dataclass
|
|
99
|
+
from openframe.adapters.db.oracle import OracleSettings, OracleRepository
|
|
100
|
+
|
|
101
|
+
@dataclass
|
|
102
|
+
class Item:
|
|
103
|
+
id: str
|
|
104
|
+
name: str
|
|
105
|
+
|
|
106
|
+
class ItemRepository(OracleRepository[Item]):
|
|
107
|
+
_table = "items"
|
|
108
|
+
_id_column = "id"
|
|
109
|
+
|
|
110
|
+
def _row_to_entity(self, row: dict) -> Item:
|
|
111
|
+
return Item(**row)
|
|
112
|
+
|
|
113
|
+
def _entity_to_row(self, entity: Item) -> dict:
|
|
114
|
+
return {"id": entity.id, "name": entity.name}
|
|
115
|
+
|
|
116
|
+
settings = OracleSettings()
|
|
117
|
+
repo = ItemRepository(settings)
|
|
118
|
+
item: Item | None = await repo.get("abc-123")
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
---
|
|
122
|
+
|
|
123
|
+
## Wiring into an application
|
|
124
|
+
|
|
125
|
+
For a real service, wire `OraclePlugin` (the `BasePort`-satisfying plugin
|
|
126
|
+
class) through `ApplicationBootstrap.compose()` from `openframe-core`. This
|
|
127
|
+
gives you proper lifecycle management — `initialize()` / `health()` /
|
|
128
|
+
`shutdown()` — for free, instead of constructing `OracleRepository`
|
|
129
|
+
directly and managing the pool yourself:
|
|
130
|
+
|
|
131
|
+
```python
|
|
132
|
+
from openframe.core.runtime import ApplicationBootstrap
|
|
133
|
+
from openframe.core.ports import Capability
|
|
134
|
+
from openframe.adapters.db.oracle import OraclePlugin, OracleSettings
|
|
135
|
+
|
|
136
|
+
settings = OracleSettings() # reads ORACLE_DSN from env
|
|
137
|
+
plugin = OraclePlugin(settings, table="items", id_column="id")
|
|
138
|
+
|
|
139
|
+
async with ApplicationBootstrap.compose(plugin) as app:
|
|
140
|
+
repo = app.get(Capability.PERSISTENCE) # -> OracleRepository
|
|
141
|
+
item = await repo.get("abc-123")
|
|
142
|
+
# pool is closed automatically on exit (plugin.shutdown() ran)
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
`compose()` calls `plugin.initialize()` on entry and `plugin.shutdown()` on
|
|
146
|
+
exit, so the pool is created, health-checked, and torn down without any
|
|
147
|
+
manual lifecycle code. Requires `openframe-core>=3.3`.
|
|
148
|
+
|
|
149
|
+
Reach for a subclassed `ApplicationBootstrap` (with a `configure()` method)
|
|
150
|
+
only when you need per-port `config=`/`init_timeout=` or conditional
|
|
151
|
+
registration order; use `app.registry` as an escape hatch for anything
|
|
152
|
+
neither tier covers. The `OracleRepository(settings)` construction shown
|
|
153
|
+
above under "Quick start" remains valid for tests, scripts, or any context
|
|
154
|
+
that doesn't need plugin lifecycle management.
|
|
155
|
+
|
|
156
|
+
### Resilience — circuit breaking under sustained failure
|
|
157
|
+
|
|
158
|
+
`openframe-core>=3.4` ships `openframe.core.resilience.CircuitBreakerProxy`
|
|
159
|
+
— wrap a repository to short-circuit calls after repeated failures instead
|
|
160
|
+
of blocking every caller until `operation_timeout` during a sustained
|
|
161
|
+
outage:
|
|
162
|
+
|
|
163
|
+
```python
|
|
164
|
+
from openframe.core.resilience import CircuitBreakerProxy
|
|
165
|
+
from openframe.core.tracing import TracingProxy
|
|
166
|
+
|
|
167
|
+
repo = CircuitBreakerProxy(
|
|
168
|
+
TracingProxy(app.get(Capability.PERSISTENCE).get_repository(), prefix="repository.item"),
|
|
169
|
+
failure_threshold=5,
|
|
170
|
+
reset_timeout=30.0,
|
|
171
|
+
)
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
Wrap the traced repository, not the reverse — a short-circuited call never
|
|
175
|
+
reaches the adapter, so it shouldn't produce a misleading adapter span. No
|
|
176
|
+
adapter code changes to support this — `CircuitBreakerProxy` wraps from the
|
|
177
|
+
outside, exactly like `TracingProxy`.
|
|
178
|
+
|
|
179
|
+
---
|
|
180
|
+
|
|
181
|
+
## Configuration
|
|
182
|
+
|
|
183
|
+
All settings are read from environment variables.
|
|
184
|
+
|
|
185
|
+
| Env var | Type | Default | Description |
|
|
186
|
+
|---|---|---|---|
|
|
187
|
+
| `ORACLE_DSN` | `str` | **required** | `user/password@host:port/service_name` |
|
|
188
|
+
| `POOL_MIN` | `int` | `1` | Minimum pool size |
|
|
189
|
+
| `POOL_MAX` | `int` | `10` | Maximum pool size |
|
|
190
|
+
| `POOL_INCREMENT` | `int` | `1` | Connections opened per pool growth step |
|
|
191
|
+
| `POOL_TIMEOUT` | `int` | `60` | Seconds an idle pooled connection may sit before being closed |
|
|
192
|
+
| `CONNECTION_TIMEOUT` | `float` | `30.0` | Pool creation timeout (s) |
|
|
193
|
+
| `OPERATION_TIMEOUT` | `float` | `10.0` | Per-operation timeout (s) |
|
|
194
|
+
| `MAX_RETRIES` | `int` | `3` | Max retry attempts |
|
|
195
|
+
|
|
196
|
+
---
|
|
197
|
+
|
|
198
|
+
## Health checks
|
|
199
|
+
|
|
200
|
+
`OracleRepository` implements the unified `BasePort` lifecycle from
|
|
201
|
+
`openframe-core`.
|
|
202
|
+
|
|
203
|
+
```python
|
|
204
|
+
health = await repo.health() # PluginHealth — never raises
|
|
205
|
+
```
|
|
206
|
+
|
|
207
|
+
---
|
|
208
|
+
|
|
209
|
+
## Exception hierarchy
|
|
210
|
+
|
|
211
|
+
All exceptions are `AdapterError` subclasses from `openframe.core.exceptions`.
|
|
212
|
+
Raw `oracledb` exceptions (and the raw `OSError`/`socket.gaierror` a DNS
|
|
213
|
+
failure can raise — see "Async strategy" above) never escape the adapter.
|
|
214
|
+
|
|
215
|
+
| Situation | Exception |
|
|
216
|
+
|---|---|
|
|
217
|
+
| Cannot connect to Oracle (host unreachable, listener refused, DNS failure) | `AdapterConnectionError` |
|
|
218
|
+
| `ORACLE_DSN` is syntactically invalid | `AdapterConfigurationError` |
|
|
219
|
+
| Query failed (constraint, syntax, etc.) | `AdapterQueryError` |
|
|
220
|
+
| Connection lost mid-query (ORA-03113, ORA-03114, ORA-12541, ORA-12154, etc.) | `AdapterConnectionError` |
|
|
221
|
+
| Operation exceeded timeout | `AdapterTimeoutError` |
|
|
222
|
+
|
|
223
|
+
See `openframe/adapters/db/oracle/repository.py`'s module-level comment for
|
|
224
|
+
the exact ORA/DPY code classification, including which codes were verified
|
|
225
|
+
against the installed driver and which are documented-but-unverified
|
|
226
|
+
standard Oracle networking codes (no live Oracle server was available
|
|
227
|
+
during development — this is stated honestly rather than implied to be
|
|
228
|
+
fully verified).
|
|
229
|
+
|
|
230
|
+
---
|
|
231
|
+
|
|
232
|
+
## Development
|
|
233
|
+
|
|
234
|
+
```bash
|
|
235
|
+
# from the package directory
|
|
236
|
+
uv venv .venv && source .venv/bin/activate
|
|
237
|
+
uv pip install -e ".[dev]"
|
|
238
|
+
python -m pytest tests/ -q
|
|
239
|
+
```
|
|
240
|
+
|
|
241
|
+
All tests run with zero real network calls — `oracledb` is fully mocked.
|
|
242
|
+
|
|
243
|
+
---
|
|
244
|
+
|
|
245
|
+
## Protocol conformance
|
|
246
|
+
|
|
247
|
+
```python
|
|
248
|
+
from openframe.core.ports import BaseRepository
|
|
249
|
+
|
|
250
|
+
repo = OracleRepository(settings, table="items", id_column="id")
|
|
251
|
+
assert isinstance(repo, BaseRepository) # True — structural check
|
|
252
|
+
```
|
|
253
|
+
|
|
254
|
+
No inheritance from the Protocol is required or used.
|
|
255
|
+
|
|
256
|
+
---
|
|
257
|
+
|
|
258
|
+
## License
|
|
259
|
+
|
|
260
|
+
MIT
|