Observed since 21 Oct, 09:17 UTC
No superseded metadata states collected. Unchanged observations do not create new revisions.
Lightweight Universal API Router
⚠️ Alpha Release - This is the debut release of agnomux2. While functional, the API may change in future versions.
Lightweight Universal API Router - Route requests to any API through configuration instead of hardcoded clients. Works with any data type and transport protocol. Plug in your own transports and parsers as needed.
Part of the Agnosweaver™ project suite.
Tired of maintaining dozens of API client libraries? Frustrated with hardcoded HTTP clients, GraphQL clients, gRPC clients, and custom API wrappers? agnomux2 eliminates this complexity by providing a single, configurable router for all your API needs.
# Configure once, route anywhere
router = Router()
router.add_endpoint('weather', {
'transport': 'http',
'parser': 'json',
'request_template': {'url': 'https://api.weather.com/{city}'}
})
router.add_endpoint('payment', {
'transport': 'grpc',
'parser': 'protobuf',
'request_template': {'service': 'PaymentService'}
})
# Same interface for different APIs and protocols
weather = router.route('weather', city="London")
payment = router.route('payment', amount=100, currency="USD")
# PyPI
pip install agnomux2
# GitHub
pip install git+https://github.com/agnosweaver/agnomux2.git
Requirements: Python 3.11 or higher
from agnomux2 import Router, TransportRegistry, ParserRegistry
# Create router
router = Router()
Since agnomux2 provides the core routing logic, you implement transports and parsers for your specific needs:
import requests
import json
from agnomux2 import BaseTransport, BaseParser, RawResponse
class HTTPTransport(BaseTransport):
def send(self, data, **kwargs):
response = requests.post(data['url'], json=data.get('payload'))
return RawResponse(response.text, response.headers, response.status_code)
async def asend(self, data, **kwargs):
# Your async HTTP implementation
pass
class JSONParser(BaseParser):
def parse_request(self, data, template):
# Build request from template
return {
'url': template['url'].format(**data),
'payload': data
}
def parse_response(self, raw_response):
return json.loads(raw_response.data)
def get_content_type(self, raw_response):
return raw_response.headers.get('content-type', 'application/json')
# Register your components
TransportRegistry.register('http', HTTPTransport)
ParserRegistry.register('json', JSONParser)
# Add endpoints
router.add_endpoint('weather', {
'transport': 'http',
'parser': 'json',
'request_template': {
'url': 'https://api.openweathermap.org/data/2.5/weather?q={city}&appid={api_key}'
}
})
router.add_endpoint('users', {
'transport': 'http',
'parser': 'json',
'request_template': {
'url': 'https://jsonplaceholder.typicode.com/users/{user_id}'
}
})
# Route requests
weather_response = router.route('weather', city="London", api_key="your-key")
user_response = router.route('users', user_id=1)
print(f"Weather: {weather_response.content}")
print(f"User: {user_response.content}")
# Route to different backends based on endpoint
router.add_endpoint('v1/payments', {
'transport': 'http',
'parser': 'json',
'request_template': {'url': 'https://payments-v1.internal.com/{endpoint}'}
})
router.add_endpoint('v2/payments', {
'transport': 'grpc',
'parser': 'protobuf',
'request_template': {'service': 'PaymentsV2Service'}
})
# Same interface, different backend implementations
old_payment = router.route('v1/payments', endpoint='charge', amount=100)
new_payment = router.route('v2/payments', amount=100, currency='USD')
# HTTP REST API
router.add_endpoint('rest_users', {
'transport': 'http',
'parser': 'json'
})
# GraphQL API
router.add_endpoint('graphql_users', {
'transport': 'http',
'parser': 'graphql'
})
# gRPC Service
router.add_endpoint('grpc_users', {
'transport': 'grpc',
'parser': 'protobuf'
})
# Same application logic, different protocols
for endpoint in ['rest_users', 'graphql_users', 'grpc_users']:
users = router.route(endpoint, query="active_users")
process_users(users.content) # Same processing logic
# Route to different databases with same interface
router.add_endpoint('postgres_users', {
'transport': 'sql',
'parser': 'json',
'request_template': {'query': 'SELECT * FROM users WHERE id = {user_id}'}
})
router.add_endpoint('mongodb_users', {
'transport': 'mongo',
'parser': 'bson',
'request_template': {'collection': 'users', 'filter': {'_id': '{user_id}'}}
})
# Same interface for different databases
pg_user = router.route('postgres_users', user_id=123)
mongo_user = router.route('mongodb_users', user_id=123)
# Stream from any endpoint that supports it
for chunk in router.route_stream('live_metrics', metric_type='cpu'):
print(f"CPU: {chunk.content['value']}%")
if chunk.is_final:
break
# Handle different streaming data types
for chunk in router.route_stream('video_feed', camera_id=1):
if chunk.content_type == 'video/mp4':
process_video_frame(chunk.content)
RouterMain interface for configuring endpoints and routing requests.
Methods:
add_endpoint(name, config) - Configure a new endpointroute(endpoint, **data) - Route request synchronouslyaroute(endpoint, **data) - Route request asynchronouslyroute_stream(endpoint, **data) - Stream from endpoint synchronouslyaroute_stream(endpoint, **data) - Stream from endpoint asynchronouslyBaseTransportAbstract base class for transport implementations.
Must Implement:
send(data, **kwargs) - Send data synchronouslyasend(data, **kwargs) - Send data asynchronouslyOptional Override:
send_stream(data, **kwargs) - Send with streaming responseasend_stream(data, **kwargs) - Async send with streaming responseBaseParserAbstract base class for parser implementations.
Must Implement:
parse_request(data, template) - Build request from data and templateparse_response(raw_response) - Parse transport responseOptional Override:
get_content_type(raw_response) - Extract content typeparse_stream_chunk(raw_chunk) - Parse streaming chunkResponse@dataclass
class Response:
content: Any # Can be any data type
endpoint_name: str
content_type: str
timestamp: str
metadata: Optional[Dict[str, Any]] = None
StreamChunk@dataclass
class StreamChunk:
content: Any # Can be any data type
endpoint_name: str
content_type: str
timestamp: str
metadata: Optional[Dict[str, Any]] = None
is_final: bool = False
RawResponseclass RawResponse:
def __init__(self, data: Any, headers: Optional[Dict] = None, status: Optional[Any] = None):
self.data = data
self.headers = headers or {}
self.status = status
agnomux2 follows a clean, modular architecture with clear separation of concerns:
┌─────────────────┐ ┌─────────────────┐ ┌──────────────────┐
│ Router │────│ Registry │────│ BaseTransport │
│ (Orchestrator) │ │ (Discovery) │ │ (Communication) │
└─────────────────┘ └─────────────────┘ └──────────────────┘
│ │ │
│ ┌────────┴────────┐ │
│ │ │ │
▼ ▼ ▼ ▼
┌─────────────────┐ ┌──────────┐ ┌──────────────┐ ┌──────────────┐
│ Your App Code │ │ HTTP │ │ gRPC │ │ WebSocket │
│ │ │Transport │ │ Transport │ │ Transport │
└─────────────────┘ └──────────┘ └──────────────┘ └──────────────┘
│
┌──────────────────────┼──────────────────────┐
│ │ │
▼ ▼ ▼
┌──────────────┐ ┌──────────────┐ ┌──────────────┐
│ JSON │ │ Protobuf │ │ XML │
│ Parser │ │ Parser │ │ Parser │
└──────────────┘ └──────────────┘ └──────────────┘
Key Principles:
from agnomux2 import BaseTemplateEngine, TemplateRegistry
class JinjaTemplateEngine(BaseTemplateEngine):
def render_template(self, template, variables):
from jinja2 import Template
return Template(template).render(**variables)
TemplateRegistry.register('jinja', JinjaTemplateEngine)
# Use in endpoint config
router.add_endpoint('dynamic_api', {
'transport': 'http',
'parser': 'json',
'template_engine': 'jinja',
'request_template': {
'url': 'https://api.service.com/{{ endpoint }}/{{ resource_id }}',
'headers': {'Authorization': 'Bearer {{ token }}'}
}
})
import websocket
from agnomux2 import BaseTransport
class WebSocketTransport(BaseTransport):
def send(self, data, **kwargs):
ws = websocket.create_connection(data['url'])
ws.send(json.dumps(data['payload']))
result = ws.recv()
ws.close()
return RawResponse(result)
class MessageQueueTransport(BaseTransport):
def send(self, data, **kwargs):
# Your message queue implementation
pass
class ProtobufParser(BaseParser):
def parse_request(self, data, template):
# Serialize to protobuf
message = MyProtoMessage(**data)
return {'payload': message.SerializeToString()}
def parse_response(self, raw_response):
# Deserialize protobuf response
message = MyProtoMessage()
message.ParseFromString(raw_response.data)
return message
def get_content_type(self, raw_response):
return "application/x-protobuf"
Coming Soon: A dedicated repository for community contributions and ideas is being set up. Stay tuned!
For now, if you have suggestions or find issues, please reach out via email.
Licensed under the PolyForm Noncommercial License 1.0.0.
⚠️ Important: This license prohibits commercial use. If you need to use agnomux2 in a commercial project, please contact agnosweaver@gmail.com for a commercial license.
One interface. Many APIs.
agnomux2 keeps it clean and flexible.
Download counts and popularity will appear here as the collector receives them.
| Version | Published | Direct dependencies | Open version |
|---|---|---|---|
| 0.0.1a0 | 12 Sept 2025 | 3 |
This describes our collected data; it is not a security assessment.
Observed since 21 Oct, 09:17 UTC
No superseded metadata states collected. Unchanged observations do not create new revisions.
package:79b54c41b465d7352501effb9e6aaf7d71e849c9c37dda10cd446b2853f55009