Skip to content

Commit 3571681

Browse files
committed
Add endpoint_path and payload_mapping to data_source_capabilities model
- Updated schema to include two new columns: `endpoint_path` and `payload_mapping`. - Created corresponding Alembic migrations. - Added documentation for `data_source_capabilities` in `data-sources.md`. - Updated related unit tests and example logic. - Linked new documentation in `README.md` and `DEVELOPMENT.md`.
1 parent 12f3321 commit 3571681

7 files changed

Lines changed: 178 additions & 3 deletions

File tree

DEVELOPMENT.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,3 +67,9 @@ Testing is highly recommended for validating model relationships and ensuring da
6767
```bash
6868
pytest
6969
```
70+
71+
## Technical Documentation
72+
73+
Detailed documentation on specific components:
74+
75+
- [Data Sources, Capabilities, Refresh Policies, Rate Limits, and Runs](docs/data-sources.md)

README.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,7 @@ OCI ships with two preconfigured sources:
5050
- **tmdb** — REST (`https://api.themoviedb.org/3`), seeded with a safe 40-requests-per-10-seconds limit (matching historical official limits).
5151
- **wikidata** — REST (`https://query.wikidata.org/sparql`), seeded with a 1-request-per-1-second limit (matching WDQS usage policy).
5252

53-
You can adjust limits, capabilities, refresh policies, credentials, or disable them by editing the corresponding rows after running migrations.
53+
You can adjust limits, [capabilities, refresh policies](docs/data-sources.md), credentials, or disable them by editing the corresponding rows after running migrations.
5454

5555
### Normalize
5656

@@ -72,7 +72,7 @@ Emits the indexed data in formats suitable for downstream systems.
7272

7373
Every piece of data stored by OCI is associated with:
7474

75-
- a source
75+
- a source (see [Data Sources](docs/data-sources.md))
7676
- a fetch timestamp
7777
- an optional confidence level
7878

docs/data-sources.md

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
# Data Sources and Ingestion
2+
3+
Open Cinema Index (OCI) uses a structured system to manage data ingestion from various external providers. This document explains the core components of the ingestion system: Data Sources, Capabilities, Refresh Policies, Rate Limits, and Runs.
4+
5+
## Data Sources
6+
7+
A `DataSource` represents an external entity that provides film-related data. Each source is uniquely identified by its name and has several configuration properties:
8+
9+
- **name**: A unique identifier for the source (e.g., `tmdb`, `wikidata`).
10+
- **kind**: The protocol used to communicate with the source (`rest`, `graphql`, `file`).
11+
- **base_url**: The root URL for API requests.
12+
- **user_agent**: A custom User-Agent string to be used for requests to this source.
13+
- **enabled**: A boolean flag to quickly enable or disable a source without deleting its configuration.
14+
15+
Data sources also track their execution history via `last_run_started_at`, `last_run_completed_at`, and `last_error`.
16+
17+
## Capabilities
18+
19+
`DataSourceCapability` defines what kind of data a specific `DataSource` is able to provide and how to access it. This allows the OCI pipeline to intelligently route requests to the most appropriate sources and construct correct URLs.
20+
21+
Properties:
22+
- **capability**: The identifier for the type of data (e.g., `films`, `people`, `assets`, `updates`).
23+
- **endpoint_path**: A relative path or query template that is appended to the `base_url` to fetch the data.
24+
- **payload_mapping**: A JSON-defined mapping that tells OCI how to translate the external response into its canonical schema.
25+
26+
Example paths:
27+
- `/movie/{id}` for TMDB films.
28+
- `?query={query}` for Wikidata SPARQL.
29+
30+
### Response Mapping
31+
32+
The `payload_mapping` field allows OCI to handle "unknown" sources by defining how to extract data from their responses. It typically maps JSON paths or keys from the source to OCI fields.
33+
34+
Example mapping for a `films` capability:
35+
```json
36+
{
37+
"title": "original_title",
38+
"runtime_minutes": "runtime",
39+
"original_language": "iso_639_1"
40+
}
41+
```
42+
43+
This mapping is used during the `normalize` phase of the ingestion pipeline.
44+
45+
Common capabilities include:
46+
- `films`: Can provide basic film metadata.
47+
- `people`: Can provide data about cast and crew.
48+
- `assets`: Can provide URLs for posters, backdrops, etc.
49+
- `updates`: Can provide a stream of recently changed records.
50+
51+
## Refresh Policies
52+
53+
The `DataSourceRefreshPolicy` determines how often data from a source should be updated and how to handle incremental fetches.
54+
55+
- **default_refresh_interval_minutes**: The standard time to wait before re-fetching a record from this source.
56+
- **max_record_age_days**: The maximum age a record can reach before it is considered stale, regardless of the refresh interval.
57+
- **incremental_cursor_field**: The field used to track progress during incremental ingestion (e.g., a timestamp or an ID).
58+
- **supports_webhook**: Indicates if the source can push updates to OCI via webhooks.
59+
60+
## Rate Limits
61+
62+
To be a good citizen of the web and avoid being blocked, OCI strictly adheres to rate limits defined in `DataSourceRateLimit`.
63+
64+
- **window_seconds**: The duration of the rate limit window (e.g., 60 seconds for a "per minute" limit).
65+
- **max_calls**: The maximum number of requests allowed within the window.
66+
- **burst**: The number of requests allowed in a single burst, even if it exceeds the average rate momentarily.
67+
- **retry_delay_seconds**: How long to wait before retrying if a rate limit is hit.
68+
69+
Multiple rate limits can be applied to a single source (e.g., 40 requests per 10 seconds AND 10,000 requests per day).
70+
71+
## Runs
72+
73+
A `DataSourceRun` represents a single execution of the ingestion process for a specific source. It provides observability and audit trails for data ingestion.
74+
75+
- **started_at** / **completed_at**: Timestamps for the duration of the run.
76+
- **status**: The outcome of the run (`started`, `success`, `failed`).
77+
- **error**: If the run failed, the error message or stack trace.
78+
- **items_fetched**: Total number of records retrieved from the source.
79+
- **items_processed**: Total number of records successfully integrated into OCI.
80+
81+
The `duration` of a run is calculated as the difference between `completed_at` and `started_at`.
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
"""add payload_mapping to data_source_capabilities
2+
3+
Revision ID: 980d5659e4cd
4+
Revises: d8a0021645a7
5+
Create Date: 2026-01-05 00:08:52.695139
6+
7+
"""
8+
from collections.abc import Sequence
9+
10+
import sqlalchemy as sa
11+
from alembic import op
12+
13+
# revision identifiers, used by Alembic.
14+
revision: str = '980d5659e4cd'
15+
down_revision: str | Sequence[str] | None = 'd8a0021645a7'
16+
branch_labels: str | Sequence[str] | None = None
17+
depends_on: str | Sequence[str] | None = None
18+
19+
20+
def upgrade() -> None:
21+
"""Upgrade schema."""
22+
# ### commands auto generated by Alembic - please adjust! ###
23+
op.add_column('data_source_capabilities', sa.Column('payload_mapping', sa.Text(), nullable=True))
24+
# ### end Alembic commands ###
25+
26+
27+
def downgrade() -> None:
28+
"""Downgrade schema."""
29+
# ### commands auto generated by Alembic - please adjust! ###
30+
op.drop_column('data_source_capabilities', 'payload_mapping')
31+
# ### end Alembic commands ###
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
"""add endpoint_path to data_source_capabilities
2+
3+
Revision ID: d8a0021645a7
4+
Revises: b92f2865ef96
5+
Create Date: 2026-01-05 00:04:52.004213
6+
7+
"""
8+
from collections.abc import Sequence
9+
10+
import sqlalchemy as sa
11+
from alembic import op
12+
13+
# revision identifiers, used by Alembic.
14+
revision: str = 'd8a0021645a7'
15+
down_revision: str | Sequence[str] | None = 'b92f2865ef96'
16+
branch_labels: str | Sequence[str] | None = None
17+
depends_on: str | Sequence[str] | None = None
18+
19+
20+
def upgrade() -> None:
21+
"""Upgrade schema."""
22+
# ### commands auto generated by Alembic - please adjust! ###
23+
op.add_column('data_source_capabilities', sa.Column('endpoint_path', sa.String(), nullable=True))
24+
# ### end Alembic commands ###
25+
26+
27+
def downgrade() -> None:
28+
"""Downgrade schema."""
29+
# ### commands auto generated by Alembic - please adjust! ###
30+
op.drop_column('data_source_capabilities', 'endpoint_path')
31+
# ### end Alembic commands ###

src/open_cinema_index/models.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -254,6 +254,8 @@ class DataSourceCapability(Base):
254254
id = Column(Integer, primary_key=True)
255255
data_source_id = Column(Integer, ForeignKey("data_sources.id", ondelete="CASCADE"), nullable=False)
256256
capability = Column(String, nullable=False) # films, people, assets, updates
257+
endpoint_path = Column(String, nullable=True) # e.g., /movie/{id} or ?query={query}
258+
payload_mapping = Column(Text, nullable=True) # JSON mapping of external fields to OCI fields
257259

258260
__table_args__ = (UniqueConstraint("data_source_id", "capability", name="uq_data_source_capability_unique"),)
259261

tests/test_models.py

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -219,12 +219,36 @@ def test_data_source_capability_unique(session):
219219
session.add(source)
220220
session.commit()
221221

222-
cap1 = DataSourceCapability(data_source_id=source.id, capability="films")
222+
cap1 = DataSourceCapability(
223+
data_source_id=source.id,
224+
capability="films",
225+
payload_mapping='{"title": "name"}'
226+
)
223227
cap2 = DataSourceCapability(data_source_id=source.id, capability="films")
224228
session.add_all([cap1, cap2])
225229
with pytest.raises(IntegrityError):
226230
session.commit()
227231

232+
session.rollback()
233+
234+
saved_cap = session.query(DataSourceCapability).filter_by(data_source_id=source.id, capability="films").first()
235+
if saved_cap:
236+
# This part depends on if cap1 was added before exception
237+
# But actually IntegrityError happens at commit.
238+
pass
239+
240+
# Test saving and reading payload_mapping
241+
cap3 = DataSourceCapability(
242+
data_source_id=source.id,
243+
capability="people",
244+
payload_mapping='{"name": "fullname"}'
245+
)
246+
session.add(cap3)
247+
session.commit()
248+
249+
fetched = session.query(DataSourceCapability).filter_by(data_source_id=source.id, capability="people").one()
250+
assert fetched.payload_mapping == '{"name": "fullname"}'
251+
228252

229253
def test_data_source_credential_expiry_and_uniqueness(session):
230254
source = DataSource(name="imdb")

0 commit comments

Comments
 (0)