chrono-temporal 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.
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Daniel
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.
@@ -0,0 +1,3 @@
1
+ include README.md
2
+ include LICENSE
3
+ recursive-include chrono_temporal *.py
@@ -0,0 +1,33 @@
1
+ Metadata-Version: 2.4
2
+ Name: chrono-temporal
3
+ Version: 0.1.0
4
+ Summary: Time-travel queries for any data entity. Query what your data looked like at any point in history.
5
+ Author-email: Daniel <daniel@example.com>
6
+ License: MIT
7
+ Project-URL: Homepage, https://github.com/Daniel7303/chrono-temporal-api-framework
8
+ Project-URL: Repository, https://github.com/Daniel7303/chrono-temporal-api-framework
9
+ Project-URL: Issues, https://github.com/Daniel7303/chrono-temporal-api-framework/issues
10
+ Keywords: temporal,time-travel,database,audit,history,fastapi,sqlalchemy
11
+ Classifier: Development Status :: 3 - Alpha
12
+ Classifier: Intended Audience :: Developers
13
+ Classifier: License :: OSI Approved :: MIT License
14
+ Classifier: Programming Language :: Python :: 3
15
+ Classifier: Programming Language :: Python :: 3.11
16
+ Classifier: Programming Language :: Python :: 3.12
17
+ Classifier: Framework :: FastAPI
18
+ Classifier: Topic :: Database
19
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
20
+ Requires-Python: >=3.11
21
+ Description-Content-Type: text/markdown
22
+ License-File: LICENSE
23
+ Requires-Dist: sqlalchemy>=2.0.0
24
+ Requires-Dist: asyncpg>=0.29.0
25
+ Requires-Dist: pydantic>=2.0.0
26
+ Provides-Extra: fastapi
27
+ Requires-Dist: fastapi>=0.111.0; extra == "fastapi"
28
+ Requires-Dist: uvicorn[standard]>=0.29.0; extra == "fastapi"
29
+ Provides-Extra: dev
30
+ Requires-Dist: pytest>=8.0.0; extra == "dev"
31
+ Requires-Dist: pytest-asyncio>=0.23.0; extra == "dev"
32
+ Requires-Dist: httpx>=0.27.0; extra == "dev"
33
+ Dynamic: license-file
@@ -0,0 +1,22 @@
1
+ """
2
+ Chrono Temporal — Time-travel queries for any data entity.
3
+
4
+ Usage:
5
+ from chrono_temporal import TemporalService, TemporalRecord, TemporalRecordCreate
6
+ """
7
+
8
+ from chrono_temporal.service import TemporalService
9
+ from chrono_temporal.models import TemporalRecord
10
+ from chrono_temporal.schemas import TemporalRecordCreate, TemporalRecordRead
11
+ from chrono_temporal.base import ChronoBase, get_engine, get_session
12
+
13
+ __version__ = "0.1.0"
14
+ __all__ = [
15
+ "TemporalService",
16
+ "TemporalRecord",
17
+ "TemporalRecordCreate",
18
+ "TemporalRecordRead",
19
+ "ChronoBase",
20
+ "get_engine",
21
+ "get_session",
22
+ ]
@@ -0,0 +1,61 @@
1
+ """
2
+ Database engine and session utilities for Chrono Temporal.
3
+ """
4
+ from typing import AsyncGenerator
5
+ from sqlalchemy.ext.asyncio import (
6
+ AsyncSession,
7
+ create_async_engine,
8
+ async_sessionmaker,
9
+ AsyncEngine,
10
+ )
11
+ from sqlalchemy.orm import DeclarativeBase
12
+
13
+
14
+ class ChronoBase(DeclarativeBase):
15
+ """Base class for all Chrono Temporal models."""
16
+ pass
17
+
18
+
19
+ def get_engine(database_url: str, echo: bool = False, **kwargs) -> AsyncEngine:
20
+ """
21
+ Create an async SQLAlchemy engine.
22
+
23
+ Args:
24
+ database_url: PostgreSQL connection string (must use asyncpg driver)
25
+ e.g. postgresql+asyncpg://user:pass@localhost/dbname
26
+ echo: Log SQL statements (default False)
27
+ **kwargs: Additional engine options
28
+
29
+ Returns:
30
+ AsyncEngine instance
31
+ """
32
+ return create_async_engine(
33
+ database_url,
34
+ echo=echo,
35
+ pool_pre_ping=True,
36
+ **kwargs,
37
+ )
38
+
39
+
40
+ def get_session(engine: AsyncEngine) -> async_sessionmaker[AsyncSession]:
41
+ """
42
+ Create an async session factory.
43
+
44
+ Args:
45
+ engine: AsyncEngine instance from get_engine()
46
+
47
+ Returns:
48
+ async_sessionmaker configured for the given engine
49
+ """
50
+ return async_sessionmaker(
51
+ engine,
52
+ class_=AsyncSession,
53
+ expire_on_commit=False,
54
+ )
55
+
56
+
57
+ async def create_tables(engine: AsyncEngine) -> None:
58
+ """Create all Chrono Temporal tables in the database."""
59
+ from chrono_temporal.models import TemporalRecord # noqa
60
+ async with engine.begin() as conn:
61
+ await conn.run_sync(ChronoBase.metadata.create_all)
@@ -0,0 +1,34 @@
1
+ from sqlalchemy import Column, Integer, String, DateTime, JSON, Text, func
2
+ from chrono_temporal.base import ChronoBase
3
+
4
+
5
+ class TemporalRecord(ChronoBase):
6
+ """
7
+ Core temporal record model.
8
+
9
+ Stores any entity with valid_from/valid_to time bounds,
10
+ enabling time-travel queries across the full history of any entity.
11
+
12
+ Columns:
13
+ entity_type: Category of entity e.g. "user", "order", "product"
14
+ entity_id: Unique identifier within the entity type
15
+ valid_from: When this version of the entity became true
16
+ valid_to: When this version expired (NULL = still valid)
17
+ created_at: When this record was inserted (transaction time)
18
+ data: JSON payload — any fields you want to track
19
+ notes: Optional human-readable description of the change
20
+ """
21
+ __tablename__ = "temporal_records"
22
+
23
+ id = Column(Integer, primary_key=True, index=True)
24
+ entity_type = Column(String(100), nullable=False, index=True)
25
+ entity_id = Column(String(255), nullable=False, index=True)
26
+
27
+ valid_from = Column(DateTime(timezone=True), nullable=False)
28
+ valid_to = Column(DateTime(timezone=True), nullable=True)
29
+
30
+ created_at = Column(DateTime(timezone=True), server_default=func.now(), nullable=False)
31
+ updated_at = Column(DateTime(timezone=True), server_default=func.now(), onupdate=func.now())
32
+
33
+ data = Column(JSON, nullable=False)
34
+ notes = Column(Text, nullable=True)
@@ -0,0 +1,42 @@
1
+ from pydantic import BaseModel
2
+ from datetime import datetime
3
+ from typing import Optional, Any
4
+
5
+
6
+ class TemporalRecordCreate(BaseModel):
7
+ """Schema for creating a new temporal record."""
8
+ entity_type: str
9
+ entity_id: str
10
+ valid_from: datetime
11
+ valid_to: Optional[datetime] = None
12
+ data: dict[str, Any]
13
+ notes: Optional[str] = None
14
+
15
+
16
+ class TemporalRecordRead(BaseModel):
17
+ """Schema for reading a temporal record."""
18
+ id: int
19
+ entity_type: str
20
+ entity_id: str
21
+ valid_from: datetime
22
+ valid_to: Optional[datetime] = None
23
+ created_at: datetime
24
+ updated_at: Optional[datetime] = None
25
+ data: dict[str, Any]
26
+ notes: Optional[str] = None
27
+
28
+ class Config:
29
+ from_attributes = True
30
+
31
+
32
+ class TemporalDiff(BaseModel):
33
+ """Result of a diff between two points in time."""
34
+ entity_type: str
35
+ entity_id: str
36
+ from_dt: str
37
+ to_dt: str
38
+ changed: dict[str, Any]
39
+ added: dict[str, Any]
40
+ removed: dict[str, Any]
41
+ unchanged: list[str]
42
+ has_changes: bool
@@ -0,0 +1,171 @@
1
+ from sqlalchemy.ext.asyncio import AsyncSession
2
+ from sqlalchemy import select, and_, or_
3
+ from datetime import datetime, timezone
4
+ from typing import Optional
5
+ from chrono_temporal.models import TemporalRecord
6
+ from chrono_temporal.schemas import TemporalRecordCreate
7
+
8
+
9
+ class TemporalService:
10
+ """
11
+ Core service for time-travel queries on any entity.
12
+
13
+ Usage:
14
+ engine = get_engine("postgresql+asyncpg://user:pass@localhost/db")
15
+ session_factory = get_session(engine)
16
+
17
+ async with session_factory() as session:
18
+ svc = TemporalService(session)
19
+ record = await svc.create(TemporalRecordCreate(...))
20
+ """
21
+
22
+ def __init__(self, db: AsyncSession):
23
+ self.db = db
24
+
25
+ async def create(self, payload: TemporalRecordCreate) -> TemporalRecord:
26
+ """Create a new temporal record."""
27
+ record = TemporalRecord(**payload.model_dump())
28
+ self.db.add(record)
29
+ await self.db.flush()
30
+ await self.db.refresh(record)
31
+ return record
32
+
33
+ async def get_by_id(self, record_id: int) -> Optional[TemporalRecord]:
34
+ """Get a record by its primary key."""
35
+ result = await self.db.execute(
36
+ select(TemporalRecord).where(TemporalRecord.id == record_id)
37
+ )
38
+ return result.scalar_one_or_none()
39
+
40
+ async def get_current(self, entity_type: str, entity_id: str) -> list[TemporalRecord]:
41
+ """Get records valid right now."""
42
+ now = datetime.now(timezone.utc)
43
+ result = await self.db.execute(
44
+ select(TemporalRecord).where(
45
+ and_(
46
+ TemporalRecord.entity_type == entity_type,
47
+ TemporalRecord.entity_id == entity_id,
48
+ TemporalRecord.valid_from <= now,
49
+ or_(
50
+ TemporalRecord.valid_to.is_(None),
51
+ TemporalRecord.valid_to > now,
52
+ ),
53
+ )
54
+ ).order_by(TemporalRecord.valid_from.desc())
55
+ )
56
+ return result.scalars().all()
57
+
58
+ async def get_at_point_in_time(
59
+ self, entity_type: str, entity_id: str, as_of: datetime
60
+ ) -> list[TemporalRecord]:
61
+ """
62
+ Time-travel query — get entity state at a specific point in time.
63
+
64
+ Example:
65
+ records = await svc.get_at_point_in_time("user", "user_001", datetime(2024, 3, 1))
66
+ """
67
+ result = await self.db.execute(
68
+ select(TemporalRecord).where(
69
+ and_(
70
+ TemporalRecord.entity_type == entity_type,
71
+ TemporalRecord.entity_id == entity_id,
72
+ TemporalRecord.valid_from <= as_of,
73
+ or_(
74
+ TemporalRecord.valid_to.is_(None),
75
+ TemporalRecord.valid_to > as_of,
76
+ ),
77
+ )
78
+ ).order_by(TemporalRecord.valid_from.desc())
79
+ )
80
+ return result.scalars().all()
81
+
82
+ async def get_history(self, entity_type: str, entity_id: str) -> list[TemporalRecord]:
83
+ """Get full timeline of changes for an entity."""
84
+ result = await self.db.execute(
85
+ select(TemporalRecord).where(
86
+ and_(
87
+ TemporalRecord.entity_type == entity_type,
88
+ TemporalRecord.entity_id == entity_id,
89
+ )
90
+ ).order_by(TemporalRecord.valid_from.asc())
91
+ )
92
+ return result.scalars().all()
93
+
94
+ async def get_diff(
95
+ self, entity_type: str, entity_id: str, from_dt: datetime, to_dt: datetime
96
+ ) -> dict:
97
+ """
98
+ Diff an entity's state between two points in time.
99
+
100
+ Returns what fields changed, were added, or were removed.
101
+
102
+ Example:
103
+ diff = await svc.get_diff("user", "user_001", datetime(2024, 1, 1), datetime(2025, 1, 1))
104
+ # diff["changed"] = {"plan": {"from": "free", "to": "pro"}}
105
+ """
106
+ async def get_snapshot(at: datetime):
107
+ result = await self.db.execute(
108
+ select(TemporalRecord).where(
109
+ and_(
110
+ TemporalRecord.entity_type == entity_type,
111
+ TemporalRecord.entity_id == entity_id,
112
+ TemporalRecord.valid_from <= at,
113
+ or_(
114
+ TemporalRecord.valid_to.is_(None),
115
+ TemporalRecord.valid_to > at,
116
+ ),
117
+ )
118
+ ).order_by(TemporalRecord.valid_from.desc())
119
+ )
120
+ records = result.scalars().all()
121
+ return records[0].data if records else None
122
+
123
+ from_data = await get_snapshot(from_dt)
124
+ to_data = await get_snapshot(to_dt)
125
+
126
+ if from_data is None and to_data is None:
127
+ return {"error": "No records found for this entity at either point in time"}
128
+ if from_data is None:
129
+ return {"error": f"No records found at {from_dt}", "to": to_data}
130
+ if to_data is None:
131
+ return {"error": f"No records found at {to_dt}", "from": from_data}
132
+
133
+ all_keys = set(from_data.keys()) | set(to_data.keys())
134
+ changed, unchanged, added, removed = {}, [], {}, {}
135
+
136
+ for key in all_keys:
137
+ if key not in from_data:
138
+ added[key] = to_data[key]
139
+ elif key not in to_data:
140
+ removed[key] = from_data[key]
141
+ elif from_data[key] != to_data[key]:
142
+ changed[key] = {"from": from_data[key], "to": to_data[key]}
143
+ else:
144
+ unchanged.append(key)
145
+
146
+ return {
147
+ "entity_type": entity_type,
148
+ "entity_id": entity_id,
149
+ "from": str(from_dt),
150
+ "to": str(to_dt),
151
+ "changed": changed,
152
+ "added": added,
153
+ "removed": removed,
154
+ "unchanged": unchanged,
155
+ "has_changes": bool(changed or added or removed),
156
+ }
157
+
158
+ async def close_record(
159
+ self, record_id: int, closed_at: Optional[datetime] = None
160
+ ) -> Optional[TemporalRecord]:
161
+ """
162
+ Close a record by setting its valid_to date.
163
+ Use this when a new version of the entity becomes valid.
164
+ """
165
+ record = await self.get_by_id(record_id)
166
+ if not record:
167
+ return None
168
+ record.valid_to = closed_at or datetime.now(timezone.utc)
169
+ await self.db.flush()
170
+ await self.db.refresh(record)
171
+ return record
@@ -0,0 +1,33 @@
1
+ Metadata-Version: 2.4
2
+ Name: chrono-temporal
3
+ Version: 0.1.0
4
+ Summary: Time-travel queries for any data entity. Query what your data looked like at any point in history.
5
+ Author-email: Daniel <daniel@example.com>
6
+ License: MIT
7
+ Project-URL: Homepage, https://github.com/Daniel7303/chrono-temporal-api-framework
8
+ Project-URL: Repository, https://github.com/Daniel7303/chrono-temporal-api-framework
9
+ Project-URL: Issues, https://github.com/Daniel7303/chrono-temporal-api-framework/issues
10
+ Keywords: temporal,time-travel,database,audit,history,fastapi,sqlalchemy
11
+ Classifier: Development Status :: 3 - Alpha
12
+ Classifier: Intended Audience :: Developers
13
+ Classifier: License :: OSI Approved :: MIT License
14
+ Classifier: Programming Language :: Python :: 3
15
+ Classifier: Programming Language :: Python :: 3.11
16
+ Classifier: Programming Language :: Python :: 3.12
17
+ Classifier: Framework :: FastAPI
18
+ Classifier: Topic :: Database
19
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
20
+ Requires-Python: >=3.11
21
+ Description-Content-Type: text/markdown
22
+ License-File: LICENSE
23
+ Requires-Dist: sqlalchemy>=2.0.0
24
+ Requires-Dist: asyncpg>=0.29.0
25
+ Requires-Dist: pydantic>=2.0.0
26
+ Provides-Extra: fastapi
27
+ Requires-Dist: fastapi>=0.111.0; extra == "fastapi"
28
+ Requires-Dist: uvicorn[standard]>=0.29.0; extra == "fastapi"
29
+ Provides-Extra: dev
30
+ Requires-Dist: pytest>=8.0.0; extra == "dev"
31
+ Requires-Dist: pytest-asyncio>=0.23.0; extra == "dev"
32
+ Requires-Dist: httpx>=0.27.0; extra == "dev"
33
+ Dynamic: license-file
@@ -0,0 +1,13 @@
1
+ LICENSE
2
+ MANIFEST.in
3
+ pyproject.toml
4
+ chrono_temporal/__init__.py
5
+ chrono_temporal/base.py
6
+ chrono_temporal/models.py
7
+ chrono_temporal/schemas.py
8
+ chrono_temporal/service.py
9
+ chrono_temporal.egg-info/PKG-INFO
10
+ chrono_temporal.egg-info/SOURCES.txt
11
+ chrono_temporal.egg-info/dependency_links.txt
12
+ chrono_temporal.egg-info/requires.txt
13
+ chrono_temporal.egg-info/top_level.txt
@@ -0,0 +1,12 @@
1
+ sqlalchemy>=2.0.0
2
+ asyncpg>=0.29.0
3
+ pydantic>=2.0.0
4
+
5
+ [dev]
6
+ pytest>=8.0.0
7
+ pytest-asyncio>=0.23.0
8
+ httpx>=0.27.0
9
+
10
+ [fastapi]
11
+ fastapi>=0.111.0
12
+ uvicorn[standard]>=0.29.0
@@ -0,0 +1 @@
1
+ chrono_temporal
@@ -0,0 +1,51 @@
1
+ [build-system]
2
+ requires = ["setuptools>=68.0", "wheel"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "chrono-temporal"
7
+ version = "0.1.0"
8
+ description = "Time-travel queries for any data entity. Query what your data looked like at any point in history."
9
+ readme = "README.md"
10
+ license = { text = "MIT" }
11
+ authors = [
12
+ { name = "Daniel", email = "daniel@example.com" }
13
+ ]
14
+ keywords = ["temporal", "time-travel", "database", "audit", "history", "fastapi", "sqlalchemy"]
15
+ classifiers = [
16
+ "Development Status :: 3 - Alpha",
17
+ "Intended Audience :: Developers",
18
+ "License :: OSI Approved :: MIT License",
19
+ "Programming Language :: Python :: 3",
20
+ "Programming Language :: Python :: 3.11",
21
+ "Programming Language :: Python :: 3.12",
22
+ "Framework :: FastAPI",
23
+ "Topic :: Database",
24
+ "Topic :: Software Development :: Libraries :: Python Modules",
25
+ ]
26
+ requires-python = ">=3.11"
27
+ dependencies = [
28
+ "sqlalchemy>=2.0.0",
29
+ "asyncpg>=0.29.0",
30
+ "pydantic>=2.0.0",
31
+ ]
32
+
33
+ [project.optional-dependencies]
34
+ fastapi = [
35
+ "fastapi>=0.111.0",
36
+ "uvicorn[standard]>=0.29.0",
37
+ ]
38
+ dev = [
39
+ "pytest>=8.0.0",
40
+ "pytest-asyncio>=0.23.0",
41
+ "httpx>=0.27.0",
42
+ ]
43
+
44
+ [project.urls]
45
+ Homepage = "https://github.com/Daniel7303/chrono-temporal-api-framework"
46
+ Repository = "https://github.com/Daniel7303/chrono-temporal-api-framework"
47
+ Issues = "https://github.com/Daniel7303/chrono-temporal-api-framework/issues"
48
+
49
+ [tool.setuptools.packages.find]
50
+ where = ["."]
51
+ include = ["chrono_temporal*"]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+