Architecture
Understanding numistalib’s internal architecture.
§ 1 Overview
numistalib follows a layered architecture:
┌─────────────────────────────────┐
│ CLI Layer │ Rich UI, Click commands
├─────────────────────────────────┤
│ Services Layer │ Business logic per API tag
├─────────────────────────────────┤
│ Client Layer │ HTTP, caching, rate limiting
├─────────────────────────────────┤
│ Models Layer │ Pydantic validation
├─────────────────────────────────┤
│ Transport Layer │ httpx + hishel + pyrate-limiter
└─────────────────────────────────┘
§ 2 Core Components
§ 2.1 Client (client.py)
Central HTTP abstraction providing:
Sync/Async Support: Both
httpx.Clientandhttpx.AsyncClientCaching: RFC 9111-compliant via hishel with SQLite backend
Rate Limiting: 45 requests/minute default via pyrate-limiter
Retry Logic: Exponential backoff with jitter via tenacity
Response Wrapping:
NumistaResponsewith cache indicator
Key Classes:
NumistaApiClient: Main client (sync)NumistaResponse: Wrapper exposingcachedbooleanCache strategy: 7-day TTL, persistent SQLite storage
Design Principles:
Single responsibility: HTTP concerns only
Stateful: Manages connection pool, cache, rate limiter
Injectable: Services receive client instance
Context manager: Proper resource cleanup
§ 2.2 Services (services/)
One service per Swagger tag, implementing business logic.
Structure:
services/
├── base/
│ ├── service.py # BaseService ABC
│ └── helpers.py # Shared utilities
├── types/
│ ├── base.py # TypeServiceBase ABC
│ └── service.py # TypeService implementation
├── catalogues/
│ ├── base.py
│ └── service.py
└── ...
Base Classes:
BaseService: Foundation for all servicesRequires
to_models()abstract methodProvides
clientandloggerinjection
SimpleListService: For endpoints returning{"key": [...]}Auto-extracts via
CLASS_ITEMS_KEYExample:
/catalogues→{"catalogues": [...]}
NestedResourceService: For paths like/types/{id}/issuesHandles resource nesting
EntityService: For single-entity endpoints like/types/{id}
Design Principles:
One service per API tag
Stateless except injected client/logger
No I/O beyond client calls
Immediate validation to Pydantic models
Abstract base defines contract
Sync and async methods paired
§ 2.3 Models (models/)
Pydantic models validating API payloads immediately upon receipt.
Structure:
models/
├── base/
│ └── base_model.py # BaseModel foundation
├── types.py # Type-related models
├── catalogues.py
├── issuer.py
└── ...
Key Models:
TypeBasic: Search result representationTypeFull: Complete type detailsIssue: Issue informationCatalogue: Reference catalogueIssuer: Country/entity issuing coins
Features:
Strict validation
Optional field handling
Rich rendering methods (
as_table(),format_fields())Serialization (
model_dump(),model_dump_json())
Design Principles:
Models own presentation logic
One file per entity domain
Inherit from
BaseModelFull type hints
NumPy-style docstrings for public APIs
§ 2.4 CLI (cli/)
Rich command-line interface with consistent UX.
Structure:
cli/
├── main.py # Entry point, command registration
├── theme.py # Rich v14 theming
├── types.py # Types commands
├── catalogues.py
└── ...
Patterns:
One file per command group
Registration function:
register_<group>_commands(cli)Services are headless; CLI owns all I/O
Consistent panel/table formatting
Cache indicators (💾/🌐)
Short/long option flags (
-q/--query)
Design Principles:
CLI contains all user interaction
Services provide data only
No prompts without flags
Consistent Rich theming
Error handling with rich.console
Model-driven rendering (call
Model.as_table())
§ 2.5 Configuration (config.py)
Pydantic Settings for environment-based configuration.
Features:
.envfile loadingEnvironment variable overrides
Validation
Type hints
Defaults
Settings:
API key (required)
Base URL
Cache directory & TTL
Rate limits
Retry settings
Log level
§ 3 Data Flow
§ 3.1 Typical Request
User/Python Code
│
├─> CLI Command / Service Method
│
├─> Service
│ │
│ ├─> Build parameters
│ ├─> Call client.get()
│ │
│ └─> Client
│ │
│ ├─> Check rate limit (pyrate-limiter)
│ ├─> Check cache (hishel SQLite)
│ ├─> Make HTTP request (httpx)
│ ├─> Retry on failure (tenacity)
│ └─> Return NumistaResponse
│
├─> Service validates to Pydantic models
│
└─> Return typed data
§ 3.2 Search with Pagination (Async)
User Code
│
├─> async for item in service.paginated_search()
│
├─> Service (async generator)
│ │
│ ├─> page = 1
│ │
│ └─> while True:
│ │
│ ├─> results = await client.get(page=page)
│ ├─> validate to models
│ ├─> yield each item
│ ├─> page += 1
│ └─> break if no more results
│
└─> Process items as they arrive
§ 4 Caching Strategy
§ 4.1 Implementation
Library: hishel (RFC 9111-compliant)
Storage: SQLite (persistent across runs)
Location:
.numista_cache/(configurable)TTL: 7 days default (configurable)
Scope: All GET requests
§ 4.2 Cache Key
Based on:
Full URL (including query parameters)
Request headers (API key hashed)
§ 4.3 Cache Indicators
Every NumistaResponse exposes:
cached(bool): True if served from cachecached_indicator(str): “💾” if cached, “🌐” if fresh
CLI displays indicator alongside results.
§ 4.4 Invalidation
Automatic: After TTL expires
Manual: Delete cache directory or specific files
§ 5 Rate Limiting
§ 5.1 Implementation
Library: pyrate-limiter
Strategy: Sliding window
Default: 45 requests per 60 seconds
Scope: Per-client instance
§ 5.2 Behavior
Blocks request if limit exceeded
Waits until window resets
Transparent to caller
§ 5.3 Configuration
settings = Settings(
rate_limit=30, # 30 requests
rate_limit_window=60 # per 60 seconds
)
§ 6 Retry Logic
§ 6.1 Implementation
Library: tenacity
Strategy: Exponential backoff with jitter
Max Attempts: 3 (configurable)
Backoff Factor: 2.0 (configurable)
§ 6.2 Retry Conditions
Network errors (connection timeout, etc.)
HTTP 5xx errors
HTTP 429 (rate limit) after wait
§ 6.3 Non-Retry Conditions
HTTP 4xx (except 429)
Validation errors
API key errors
§ 7 Error Handling
§ 7.1 Error Hierarchy
NumistaError (base)
├── AuthenticationError (401)
├── NotFoundError (404)
├── RateLimitError (429)
├── ValidationError (Pydantic)
└── NetworkError (connection issues)
§ 7.2 Error Flow
Client detects error
│
├─> HTTP error?
│ ├─> 401 → AuthenticationError
│ ├─> 404 → NotFoundError
│ ├─> 429 → RateLimitError (retry)
│ └─> 5xx → NetworkError (retry)
│
├─> Network error?
│ └─> NetworkError (retry)
│
├─> Validation error?
│ └─> ValidationError
│
└─> Propagate to caller
§ 7.3 Chaining
All exceptions use raise X from err to preserve causality.
§ 8 Dependency Injection
§ 8.1 Pattern
Services receive dependencies via constructor:
class TypeService:
def __init__(self, client: NumistaApiClient):
self.client = client
self.logger = logging.getLogger(__name__)
§ 8.2 Benefits
Testability: Mock client easily
Flexibility: Swap client implementations
Explicit dependencies: No hidden globals
§ 8.3 Usage
with NumistaApiClient(settings) as client:
service = TypeService(client)
results = service.search_types(query="dollar")
§ 9 Async/Sync Parity
§ 9.1 Implementation
Shared service class
Paired methods:
get_type()andget_type_async()Client supports both
httpx.Clientandhttpx.AsyncClient
§ 9.2 Method Naming
Sync:
get_type()Async:
get_type_async()
§ 9.3 Usage
Sync:
with NumistaApiClient(settings) as client:
service = TypeService(client)
result = service.get_type(95420)
Async:
async with NumistaApiClient(settings) as client:
service = TypeService(client)
result = await service.get_type_async(95420)
§ 10 Model-Driven Rendering
§ 10.1 Philosophy
Models own their presentation logic.
§ 10.2 Methods
Each model implements:
as_table(items, title): Class method returning Rich Tableformat_fields(fields): Class method formatting field values
§ 10.3 CLI Integration
CLI calls model methods:
# CLI layer
types = service.search_types(query="dollar")
# Model renders
from numistalib.models.types import TypeBasic
table = TypeBasic.as_table(types, "Search Results")
# CLI displays
console.print(table)
No rendering logic in CLI; it orchestrates only.
§ 11 Testing Strategy
§ 11.1 Approach
Mock network calls
Isolate cache directories
Test models, services, CLI separately
§ 11.2 Structure
tests/
├── test_models.py # Model validation
├── test_rich_renderables.py # Rendering tests
└── integration/ # Full workflow tests
├── test_cli_types_read.py
└── ...
§ 11.3 Patterns
Use
pytestfixtures for client/service setupMock HTTP responses at client layer
Validate Pydantic models with sample data
Integration tests use real cache (isolated)
§ 12 Design Principles Summary
§ 12.1 DRY (Don’t Repeat Yourself)
ABC patterns for service hierarchy
Shared base models
Centralized HTTP logic in client
§ 12.2 Single Responsibility
Client: HTTP concerns only
Services: Business logic per API tag
Models: Data validation and representation
CLI: User interaction only
§ 12.3 Dependency Injection
Services receive client
No ad-hoc client instantiation
Testable and flexible
§ 12.4 Type Safety
Full type hints everywhere
Pydantic validation
mypy/pyright strict mode
§ 12.5 Separation of Concerns
CLI owns I/O
Services provide data
Models validate and present
Client handles transport
§ 13 Extension Points
§ 13.1 Adding a Service
Create
services/<tag>/directoryDefine ABC in
base.pyImplement in
service.pyCreate models in
models/<tag>.pyRegister CLI in
cli/<tag>.py
§ 13.2 Custom Cache Backend
Extend hishel storage:
from hishel import Storage
class CustomStorage(Storage):
# Implement storage interface
pass
# Use in client
storage = CustomStorage()
client = NumistaApiClient(settings, storage=storage)
§ 13.3 Custom Rate Limiter
Replace pyrate-limiter:
from pyrate_limiter import Limiter
class CustomLimiter(Limiter):
# Implement limiter interface
pass
# Use in client
limiter = CustomLimiter()
client = NumistaApiClient(settings, limiter=limiter)
Next Steps
Contributing - Contribute to numistalib
API Reference - Full API documentation