ozon-env is a runtime self-compiling domain engine.
It dynamically compiles schema definitions into executable domain models at runtime, without requiring application restarts.
It can run in two modes:
db: schema and data are loaded from MongoDBrest: schema is loaded fromcomponentsor from generated models inMODELS_FOLDER, while ORM operations are mapped to HTTPPOSTcalls
Designed to power:
- Web applications
- Distributed business logic workers
- Event-driven task processors
- AI-driven domain agents
Schema definitions can come from the database or from a local list of
FormIO-like components.
At runtime, ozon-env:
- Reads schema metadata
- Generates Python domain models
- Dynamically imports and loads them
- Executes business logic on top of them
Schema (DB or components)
↓
Runtime Model Compilation
↓
Domain Model (Pydantic)
↓
Worker / Web App / Agent Layer
It integrates with the Service App project.
For information about the Service App project, see https://github.com/INRIM/service-app
Models are regenerated automatically when their schema version changes.
No service restart is required.
- Generates Python models from stored schema
- Uses Pydantic for validation and typing
- Hot-reloads models when schema updates
- Isolated execution scope
- JWT-authenticated user lifecycle
- Supports concurrent environments
- Selectable backend interface:
dborrest
- Designed for distributed execution
- Compatible with task brokers (e.g. Redis streams)
- Idempotent task execution
- Suitable for BPMN-driven workflows
- Selection fields and dynamic options
- Nested models
- Datetime normalization
- Data transformation layer
ozon-env integrates with the
Service App project
Service App provides:
- Web UI
- Schema management
- Workflow integration
ozon-env provides:
- Domain runtime
- Model compilation
- Business logic execution
pip install ozon-envor
poetry add ozon-envgit clone https://github.com/archetipo/ozon-env.git
cd ozon-env
pip install poetry
poetry installOzonEnvCoreSettings.from_env() supports two configuration sources.
Create .ozonenv/config.yaml in the project root.
If the file exists it takes full precedence over environment variables.
mkdir .ozonenv
cp config.yaml.example .ozonenv/config.yaml
# edit .ozonenv/config.yaml.ozonenv/ is listed in .gitignore — the file is never committed.
config.yaml.example (committed) documents all available fields.
${VAR} interpolation — string values can reference environment variables:
app_code: ${APP_CODE}
mongo_url: ${MONGO_URL}
api_prefix: /v2 # literal value, no interpolation
require_auth: true # booleans and integers pass through as-isIf ${VAR} is the entire value and the variable is not set, the key is omitted
and the Pydantic field default is used instead.
If .ozonenv/config.yaml is absent, settings are read from environment
variables (original behaviour — see sections below).
db is the default backend.
export OZON_BACKEND_INTERFACE=db
export MONGO_USER=...
export MONGO_PASS=...
export MONGO_URL=...
export MONGO_DB=...
export OZON_KEYCLOAK_JWKS_URL=https://keycloak.example/realms/demo/protocol/openid-connect/certs
export OZON_KEYCLOAK_ISSUER=https://keycloak.example/realms/demo
export OZON_OAUTH_URL=https://keycloak.example/realms/demo/protocol/openid-connect/token
export OZON_CLIENT_ID=...
export OZON_CLIENT_SECRET=...
export OZON_TOKEN_AUDIENCE=...
export MODELS_FOLDER=/modelsIn this mode:
- schemas are discovered from MongoDB
- models are generated and cached in
MODELS_FOLDER - env activation requires a valid Keycloak JWT
- if the JWT is expired and a
refresh_tokenis available, ozon-env refreshes it through the OAuth token endpoint - the
usercollection storestokenas a dictionary with the current token data jobcontextis a DB model and must be managed in MongoDB like the other protected runtime models
export OZON_BACKEND_INTERFACE=rest
export OZON_REST_BASE_URL=http:/
export OZON_REST_API_PREFIX=/v2
export OZON_OAUTH_URL=https://keycloak.example/realms/demo/protocol/openid-connect/token
export OZON_CLIENT_ID=...
export OZON_CLIENT_SECRET=...
export OZON_TOKEN_AUDIENCE=...
export MODELS_FOLDER=/modelsIn this mode:
new()still creates local Python objects- ORM operations such as
find,load,insert,update,upsert,remove,count,distinctare mapped toPOSToperations - env activation requires a
job_token; the REST client does not resolveJobContextlocally and does not read the DB - the REST client can use a configured
rest_tokenor generate a dedicated M2M token with OAuthclient_credentials - every protected API call must also send a
job_tokenheader - the REST client never creates or updates
jobcontext; it only consumes thejob_tokenissued by the DB-side flow job_tokenis validated server-side against the persistedjobcontextrecord; theclient_idinJobContextmust match theclient_idclaim of the M2M tokensettingsandcomponentremain local bootstrap models;jobcontextremains authoritative in DB and is only consumed by the REST client
Expected REST path pattern:
POST {OZON_REST_BASE_URL}/v2/{operation_name}
REST API specification:
| Method | Path | Description |
|---|---|---|
POST |
{OZON_REST_API_PREFIX}/{operation_name} |
Executes an ORM operation on the REST backend. |
GET |
{OZON_REST_API_PREFIX}/collections_names |
Returns remote collection names used during bootstrap. |
GET |
{OZON_REST_API_PREFIX}/init_settings/{app_code} |
Returns app settings used during bootstrap. |
Headers:
Authorization: Bearer <token>
job_token: jctx_<generated-token>
Accept: application/json
Content-Type: application/jsonOAuth token generation:
When no token is already available, OzonDataApiClient can generate the M2M
token with OAuth client_credentials.
POST {OZON_OAUTH_URL}
Content-Type: application/x-www-form-urlencoded
Token request form data:
| Field | Source | Required |
|---|---|---|
grant_type |
fixed value client_credentials |
yes |
client_id |
OZON_CLIENT_ID or rest_client_id / client_id config |
yes |
client_secret |
OZON_CLIENT_SECRET or rest_client_secret / client_secret config |
yes |
audience |
OZON_TOKEN_AUDIENCE or rest_token_audience / token_audience config |
no |
The REST client uses access_token from the JSON response as the bearer token.
Standard POST payload:
{
"model": "user",
"data_model": "user",
"domain": {
"uid": "admin"
}
}job_token is not part of the JSON payload. It must be passed in the HTTP
header together with the bearer token.
When an env is created, the input token must be a valid Keycloak JWT. The
application entrypoint passes it to make_app_session() in
params["current_token"], and ozon-env resolves the authenticated principal in
the user collection.
Example:
await env.make_app_session(
params={
"current_token": {
"access_token": "<jwt>",
"refresh_token": "<refresh-token>",
}
}
)Rules:
access_tokenmust be a valid Keycloak JWT- when
access_tokenis expired andrefresh_tokenis present, ozon-env refreshes it usinggrant_type=refresh_token - after a successful login or refresh, the token dictionary is persisted on the
userrecord user_sessionis the resolvedUsermodel; there is no separate runtime authentication model in the new flow
JobContext is a persistent security model managed in db. It is created by a
user authenticated with a valid JWT and then consumed by rest clients.
Model fields:
| Field | Description |
|---|---|
job_token |
Generated token, for example jctx_123... |
client_id |
Mandatory client identifier requested by the user |
job_key |
Optional input; generated as UUID when omitted |
process_instance_key |
Optional input; generated as UUID when omitted |
resolved_user_id |
User id resolved from the validated JWT |
issued_at |
Creation timestamp |
expires_at |
Expiration timestamp |
DB responsibilities:
- full CRUD for the
jobcontextmodel - helper methods
create_job_context(),delete_job_context(),clean_job_contexts()andjob_done() - sidecar validation helpers
validate_job_context()/verify_job_context()andinit_api_job_context(m2m_token, job_token) - expiration cleanup for invalid or expired contexts
- A user authenticated with JWT creates a
JobContextindb. - ozon-env generates
job_token, timestamps and default UUID values whenjob_keyorprocess_instance_keyare missing. - The REST client calls the remote API with:
Authorization: Bearer <m2m-token>job_token: <jctx_...>
- The API executes only if:
- the
job_tokenexists and is active - the
job_tokenis not expired - the
client_idstored inJobContextmatches theclient_idcarried by the M2M token
- the
- When the job is completed,
job_done()ordelete_job_context(job_token)removes theJobContext.
| Operation | Required payload fields | Optional payload fields |
|---|---|---|
find |
model, data_model, domain |
sort, limit, skip, fields, batch_size |
load |
model, data_model, domain |
|
insert |
model, data_model, record |
is_many |
update |
model, data_model, record |
remove_mata, force_update_whole_record |
remove |
model, data_model, record |
|
remove_all |
model, data_model, domain |
|
count |
model, data_model, domain |
|
distinct |
model, data_model, field_name, query |
|
aggregate |
model, data_model, domain |
sort, limit, skip, pipeline_items, obfuscate_fields, fields, batch_size |
search_all_distinct |
model, data_model, distinct, query |
compute_label, sort, limit, skip, raw_result |
REST bootstrap endpoints used by OzonOrmRest.init_db_models():
GET {OZON_REST_BASE_URL}/v2/collections_names
GET {OZON_REST_BASE_URL}/v2/init_settings/{app_code}
from ozonenv.OzonEnv import OzonEnvRest
env = OzonEnvRest(
{
"app_code": "demo",
"rest_base_url": "http:/",
"rest_api_prefix": "/v2",
"rest_token": "<m2m-token>",
"rest_oauth_url": "https://keycloak.example/realms/demo/protocol/openid-connect/token",
"rest_client_id": "...",
"rest_client_secret": "...",
"rest_token_audience": "ozon-api",
"models_folder": "/tmp/models",
}
)
await env.init_env(
components=[...], # FormIO-like component schemas
settings={
"rec_name": "demo",
"upload_folder": "/uploads",
"tz": "Europe/Rome",
},
)
await env.make_app_session(
params={
"job_token": "jctx_123",
"current_user": {
"uid": "optional-local-user-metadata"
},
}
)If a generated model already exists in MODELS_FOLDER, ozon-env imports it.
If it does not exist, ozon-env generates it from the provided component schema.
If you pass a custom runtime model class to the env, it must inherit from
OzonModelBase and expose a coherent interface_type:
class MyRestModel(OzonModelRest):
interface_type = "rest"OzonEnvBase validates cls_model.interface_type against
backend_interface during init. It does not replace cls_model.
OzonWorkerEnvRest.make_app_session() uses the same worker API on REST:
from ozonenv.OzonEnv import OzonWorkerEnvRest
worker = OzonWorkerEnvRest(
{
"app_code": "demo",
"rest_base_url": "http:/",
"rest_api_prefix": "/v2",
"rest_token": "<m2m-token>",
}
)
await worker.make_app_session(
params={
"topic_name": "job",
"model": "user",
"job_token": "jctx_123",
},
local_model={"user": UserModel},
settings={"rec_name": "demo", "upload_folder": "/uploads"},
)For REST workers:
- a runtime
job_tokenis mandatory for protected operations - a configured
rest_tokenor generated M2M token is mandatory - the worker can use the same ORM API used in DB mode
- the actual persistence/query layer is delegated to the REST backend
./run_test.sh
All contributions, bug reports, bug fixes, documentation improvements, enhancements and ideas are welcome.