Skip to content

Commit 40cfd59

Browse files
committed
Fixed formatting
1 parent 6b11892 commit 40cfd59

5 files changed

Lines changed: 67 additions & 21 deletions

File tree

README.md

Lines changed: 38 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
11
# Symbology Server
22

3-
A Python FastAPI Symbology Server that maintains persistent mappings between human-readable symbols (e.g. `NVDA`) and numeric identifiers via an HTTP API for querying/updating mappings over time.
3+
A Python FastAPI Symbology Server that maintains persistent mappings between human-readable symbols (e.g. `NVDA`) and numeric identifiers via an HTTP API for querying/updating symbol and identifier mappings over time.
44

55
## Features
66
- HTTP API to register, terminate, and query symbology mappings
7-
- Enforces:
7+
- Enforces the following domain constraints:
88
- One identifier per symbol per date
99
- One symbol per identifier per date
1010
- Persistent mappings until explicit termination
@@ -13,26 +13,26 @@ A Python FastAPI Symbology Server that maintains persistent mappings between hum
1313
- Symbol for an identifier on a specified date
1414
- All mappings overlapping a specific half-open date range `[begin, end)`
1515
- In-memory `MappingStorage` by default
16-
- Fully automated test suite with `pytest`
16+
- Fully automated test suite using `pytest`
1717

1818
## Requirements
1919
- Python ≥ 3.10
20-
- Dependencies listed in `requirements.txt`
20+
- Dependencies listed in `requirements.txt` and `pyproject.toml`
2121

2222
## Design Notes
2323
- **Temporal Semantics**: A symbol’s assignment to an identifier is **persistent**. A mapping becomes active
2424
on its `start_date` and remains active until it is explicitly terminated. All dates are ISO-8601 (`YYYY-MM-DD`) and independent of wall-clock time.
2525
- **Reassignment Behavior**: If a symbol is reassigned to a new identifier, the previously active mapping is
26-
then terminated at the new mapping’s start date. This behavior applies to same-day and future-dated reassignments. At any point in time, a symbol and an identifier may be in at most one active mapping.
27-
- **Conflict Handling**: The following invariants are enforced:
26+
then terminated at the new mapping’s start date. Each symbol and identifier can only appear in one active mapping at a time.
27+
- **Conflict Handling**: Violations of the following invariants raise explicit domain-level exceptions (surfaced as HTTP errors):
2828
- A symbol may have only one active identifier on a given date
2929
- An identifier may be assigned to only one symbol on a given date
30-
Violations raise explicit domain-level exceptions which are surfaced as HTTP errors at the API layer.
31-
- **Storage Model**: The storage layer is implemented as an in-memory collection, which is optimal for clarity and correctness rather than performance or persistence. This helps to keep the focus on symbology semantics and temporal reasoning.
32-
- **Scope and Limitations**: This symbology server implementation intentionally omits database persistence, concurrency control, and timezone normalization. These concerns are independent from the core problem and
33-
can be layered on without changing the domain model.
34-
- **Extensibility**: The domain model is intentionally decoupled from the storage layer, which allows alternative backends (e.g. database-backed, append-only log-based storage) to be introduced without changing any symbology semantics.
35-
- **Determinism**: All query results are deterministic functions of the input date parameters and stored mappings.
30+
- **Storage Model**: Uses an **in-memory storage layer** for clarity and correctness. Alternative storage backends (database, append-only log, etc.) can be introduced without changing the domain model.
31+
- **Scope and Limitations**: This implementation omits:
32+
- Database persistence
33+
- Concurrency control
34+
- Timezone normalization
35+
- **Determinism**: All query results are deterministic functions of input dates and stored mappings.
3636

3737
## Setup
3838

@@ -44,20 +44,43 @@ pip install -r requirements.txt
4444

4545
## Running the Server
4646

47+
```bash
4748
uvicorn src.main:app --reload --port 8000
49+
```
50+
51+
- The API will be available at http://localhost:8000.
4852

4953
## Running Tests
5054

5155
pytest tests/
5256

53-
## Example Usage of API
57+
## Example API Usage
58+
59+
### Add a mapping
5460

5561
curl -X POST http://localhost:8000/mapping \
5662
-H "Content-Type: application/json" \
5763
-d '{"symbol":"AAPL","identifier":1,"start_date":"2024-01-01"}'
5864

65+
66+
### Query identifier for a symbol
67+
68+
curl "http://localhost:8000/symbol/AAPL?date=2024-01-02"
69+
70+
71+
### Terminate a mapping
72+
73+
curl -X POST http://localhost:8000/mapping/terminate \
74+
-H "Content-Type: application/json" \
75+
-d '{"symbol":"AAPL","end_date":"2024-01-05"}'
76+
77+
78+
### Query mappings in a date range
79+
80+
curl "http://localhost:8000/mappings?begin=2024-01-01&end=2024-01-10"
81+
5982
## Notes
6083

6184
- Dependencies are defined in pyproject.toml.
62-
- requirements.txt is provided for convenience (pip install -r requirements.txt).
63-
- This project focuses on clarity and correctness of symbology semantics rather than concerns at a production scale.
85+
- requirements.txt is provided for convenience.
86+
- Focus is on clarity and correctness of symbology semantics rather than production-scale performance.

src/domain.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@
1212

1313

1414
class SymbologyServer:
15+
"""Domain layer for symbology server."""
16+
1517
def __init__(self, storage: MappingStorage) -> None:
1618
self.storage = storage
1719

@@ -44,6 +46,7 @@ def add_mapping(self, symbol: str, identifier: int, start_date: date) -> None:
4446
)
4547

4648
def terminate_mapping(self, symbol: str, end_date: date) -> None:
49+
"""Terminate an active mapping for the given symbol on end_date."""
4750
try:
4851
self.storage.terminate(symbol, end_date)
4952
except KeyError:
@@ -52,16 +55,19 @@ def terminate_mapping(self, symbol: str, end_date: date) -> None:
5255
)
5356

5457
def get_identifier(self, symbol: str, on: date) -> int:
58+
"""Get the identifier for a symbol on a given date."""
5559
m = self.storage.find_active_by_symbol(symbol, on)
5660
if not m:
5761
raise NotFoundError(f"No mapping for symbol '{symbol}' on {on}")
5862
return m.identifier
5963

6064
def get_symbol(self, identifier: int, on: date) -> str:
65+
"""Get the symbol for an identifier on a given date."""
6166
m = self.storage.find_active_by_identifier(identifier, on)
6267
if not m:
6368
raise NotFoundError(f"No mapping for identifier '{identifier}' on {on}")
6469
return m.symbol
6570

6671
def get_mappings_between(self, begin: date, end: date) -> List[Mapping]:
72+
"""Get all mappings overlapping the given date range [begin, end)."""
6773
return self.storage.find_range(begin, end)

src/routes.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ def create_router(domain: SymbologyServer) -> APIRouter:
1717

1818
@router.post("/mapping")
1919
def add_mapping(request: MappingCreate):
20+
"""Add a new symbology mapping."""
2021
try:
2122
domain.add_mapping(
2223
request.symbol,
@@ -34,6 +35,7 @@ def add_mapping(request: MappingCreate):
3435

3536
@router.post("/mapping/terminate")
3637
def terminate_mapping(request: MappingTerminate):
38+
"""Terminate an active symbology mapping."""
3739
try:
3840
domain.terminate_mapping(request.symbol, request.end_date)
3941
return {
@@ -46,20 +48,23 @@ def terminate_mapping(request: MappingTerminate):
4648

4749
@router.get("/symbol/{symbol}", response_model=int)
4850
def get_identifier(symbol: str, date: date):
51+
"""Get the identifier for a symbol on a given date."""
4952
try:
5053
return domain.get_identifier(symbol, date)
5154
except NotFoundError as exc:
5255
raise HTTPException(status_code=404, detail=str(exc))
5356

5457
@router.get("/identifier/{identifier}", response_model=str)
5558
def get_symbol(identifier: int, date: date):
59+
"""Get the symbol for an identifier on a given date."""
5660
try:
5761
return domain.get_symbol(identifier, date)
5862
except NotFoundError as exc:
5963
raise HTTPException(status_code=404, detail=str(exc))
6064

6165
@router.get("/mappings", response_model=List[MappingResponse])
6266
def get_mappings(begin: date, end: date):
67+
"""Get all mappings overlapping the given date range [begin, end)."""
6368
return domain.get_mappings_between(begin, end)
6469

6570
return router

src/storage.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,13 +11,17 @@
1111

1212

1313
class MappingStorage:
14+
"""In-memory storage backend for symbology mappings."""
15+
1416
def __init__(self) -> None:
1517
self._mappings: List[Mapping] = []
1618

1719
def insert(self, mapping: Mapping) -> None:
20+
"""Insert a new mapping."""
1821
self._mappings.append(mapping)
1922

2023
def all(self) -> List[Mapping]:
24+
"""Return all mappings."""
2125
return list(self._mappings)
2226

2327
def find_range(self, begin: date, end: date) -> List[Mapping]:
@@ -30,6 +34,7 @@ def find_range(self, begin: date, end: date) -> List[Mapping]:
3034
return results
3135

3236
def find_active_by_symbol(self, symbol: str, on: date) -> Optional[Mapping]:
37+
"""Find active mapping for symbol on given date."""
3338
for m in self._mappings:
3439
if (
3540
m.symbol == symbol
@@ -40,6 +45,7 @@ def find_active_by_symbol(self, symbol: str, on: date) -> Optional[Mapping]:
4045
return None
4146

4247
def find_active_by_identifier(self, identifier: int, on: date) -> Optional[Mapping]:
48+
"""Find active mapping for identifier on given date."""
4349
for m in self._mappings:
4450
if (
4551
m.identifier == identifier
@@ -50,6 +56,7 @@ def find_active_by_identifier(self, identifier: int, on: date) -> Optional[Mappi
5056
return None
5157

5258
def terminate(self, symbol: str, end_date: date) -> None:
59+
"""Terminate active mapping for symbol on given date."""
5360
m = self.find_active_by_symbol(symbol, end_date)
5461
if not m:
5562
raise KeyError

tests/test_domain.py

Lines changed: 11 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -10,21 +10,26 @@
1010
"""
1111

1212
from datetime import date
13+
import pytest
1314
from src.domain import SymbologyServer
1415
from src.storage import MappingStorage
15-
import pytest
16-
from src.exceptions import ConflictError
16+
from src.exceptions import ConflictError, NotFoundError
1717

1818

19-
def test_add_and_lookup_mapping() -> None:
20-
"""Verify a mapping can be added and queried."""
19+
def test_add_and_lookup_mapping():
2120
domain = SymbologyServer(MappingStorage())
2221
domain.add_mapping("AAPL", 1, date(2024, 1, 1))
2322
assert domain.get_identifier("AAPL", date(2024, 1, 2)) == 1
2423

25-
26-
def test_symbol_reassignment_same_date():
24+
def test_conflict_on_same_symbol():
2725
domain = SymbologyServer(MappingStorage())
2826
domain.add_mapping("AAPL", 1, date(2024, 1, 1))
2927
with pytest.raises(ConflictError):
3028
domain.add_mapping("AAPL", 2, date(2024, 1, 1))
29+
30+
def test_termination_and_notfound():
31+
domain = SymbologyServer(MappingStorage())
32+
domain.add_mapping("AAPL", 1, date(2024, 1, 1))
33+
domain.terminate_mapping("AAPL", date(2024, 1, 5))
34+
with pytest.raises(NotFoundError):
35+
domain.get_identifier("AAPL", date(2024, 1, 6))

0 commit comments

Comments
 (0)