tyler-ag-tapper 0.1.0__py3-none-any.whl
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.
- tapper/__init__.py +66 -0
- tapper/cli.py +109 -0
- tapper/exceptions.py +35 -0
- tapper/gateway.py +294 -0
- tapper/load_balancer.py +148 -0
- tapper/models.py +53 -0
- tapper/registry/__init__.py +18 -0
- tapper/registry/backends/__init__.py +12 -0
- tapper/registry/backends/base.py +43 -0
- tapper/registry/backends/memory.py +93 -0
- tapper/registry/backends/redis.py +163 -0
- tapper/registry/client.py +190 -0
- tapper/registry/server.py +106 -0
- tapper/service.py +156 -0
- tyler_ag_tapper-0.1.0.dist-info/METADATA +194 -0
- tyler_ag_tapper-0.1.0.dist-info/RECORD +18 -0
- tyler_ag_tapper-0.1.0.dist-info/WHEEL +4 -0
- tyler_ag_tapper-0.1.0.dist-info/entry_points.txt +2 -0
tapper/service.py
ADDED
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
"""Service decorator for automatic service registration."""
|
|
2
|
+
|
|
3
|
+
import logging
|
|
4
|
+
import os
|
|
5
|
+
from contextlib import asynccontextmanager
|
|
6
|
+
from datetime import UTC, datetime
|
|
7
|
+
from typing import AsyncIterator, Callable
|
|
8
|
+
|
|
9
|
+
from fastapi import FastAPI
|
|
10
|
+
from fastapi.routing import APIRoute
|
|
11
|
+
|
|
12
|
+
from tapper.models import Route, ServiceInfo, ServiceInstance
|
|
13
|
+
from tapper.registry.client import RegistryClient
|
|
14
|
+
|
|
15
|
+
logger = logging.getLogger(__name__)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def _extract_routes(app: FastAPI, prefix: str | None = None) -> list[Route]:
|
|
19
|
+
"""Extract API routes from a FastAPI application."""
|
|
20
|
+
routes = []
|
|
21
|
+
for route in app.routes:
|
|
22
|
+
if isinstance(route, APIRoute):
|
|
23
|
+
path = route.path
|
|
24
|
+
if prefix and not path.startswith(prefix):
|
|
25
|
+
path = f"{prefix.rstrip('/')}/{path.lstrip('/')}"
|
|
26
|
+
routes.append(Route(path=path, methods=list(route.methods)))
|
|
27
|
+
return routes
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class Service:
|
|
31
|
+
"""Decorator to register a FastAPI application as a service.
|
|
32
|
+
|
|
33
|
+
Usage:
|
|
34
|
+
@Service(name="user-service", version="1.0.0")
|
|
35
|
+
app = FastAPI()
|
|
36
|
+
|
|
37
|
+
Or as a function call:
|
|
38
|
+
app = FastAPI()
|
|
39
|
+
app = Service(name="user-service", version="1.0.0")(app)
|
|
40
|
+
|
|
41
|
+
The decorator wraps the app's lifespan to:
|
|
42
|
+
- Register the service with the registry on startup
|
|
43
|
+
- Start a background heartbeat task
|
|
44
|
+
- Unregister the service on shutdown
|
|
45
|
+
"""
|
|
46
|
+
|
|
47
|
+
def __init__(
|
|
48
|
+
self,
|
|
49
|
+
name: str,
|
|
50
|
+
version: str = "1.0.0",
|
|
51
|
+
description: str | None = None,
|
|
52
|
+
prefix: str | None = None,
|
|
53
|
+
health_endpoint: str = "/health",
|
|
54
|
+
tags: list[str] | None = None,
|
|
55
|
+
registry_url: str | None = None,
|
|
56
|
+
url: str | None = None,
|
|
57
|
+
heartbeat_interval: float = 30.0,
|
|
58
|
+
) -> None:
|
|
59
|
+
"""Initialize the Service decorator.
|
|
60
|
+
|
|
61
|
+
Args:
|
|
62
|
+
name: Unique name for the service.
|
|
63
|
+
version: Service version.
|
|
64
|
+
description: Human-readable description.
|
|
65
|
+
prefix: URL prefix for routing.
|
|
66
|
+
health_endpoint: Path to health check endpoint.
|
|
67
|
+
tags: List of tags for categorization.
|
|
68
|
+
registry_url: URL of the registry server.
|
|
69
|
+
Defaults to TAPPER_REGISTRY_URL env var.
|
|
70
|
+
url: Explicit URL for this instance.
|
|
71
|
+
If not provided, attempts to auto-detect from uvicorn.
|
|
72
|
+
heartbeat_interval: Seconds between heartbeats.
|
|
73
|
+
"""
|
|
74
|
+
self.name = name
|
|
75
|
+
self.version = version
|
|
76
|
+
self.description = description
|
|
77
|
+
self.prefix = prefix
|
|
78
|
+
self.health_endpoint = health_endpoint
|
|
79
|
+
self.tags = tags or []
|
|
80
|
+
self.registry_url = registry_url or os.environ.get(
|
|
81
|
+
"TAPPER_REGISTRY_URL", "http://localhost:8001"
|
|
82
|
+
)
|
|
83
|
+
self.url = url
|
|
84
|
+
self.heartbeat_interval = heartbeat_interval
|
|
85
|
+
|
|
86
|
+
def __call__(self, app: FastAPI) -> FastAPI:
|
|
87
|
+
"""Apply the decorator to a FastAPI application."""
|
|
88
|
+
original_lifespan = app.router.lifespan_context
|
|
89
|
+
service_decorator = self
|
|
90
|
+
|
|
91
|
+
@asynccontextmanager
|
|
92
|
+
async def wrapped_lifespan(app: FastAPI) -> AsyncIterator[None]:
|
|
93
|
+
client = RegistryClient(service_decorator.registry_url)
|
|
94
|
+
instance_url = service_decorator._get_instance_url()
|
|
95
|
+
|
|
96
|
+
service_info = ServiceInfo(
|
|
97
|
+
name=service_decorator.name,
|
|
98
|
+
version=service_decorator.version,
|
|
99
|
+
description=service_decorator.description,
|
|
100
|
+
prefix=service_decorator.prefix,
|
|
101
|
+
routes=_extract_routes(app, service_decorator.prefix),
|
|
102
|
+
tags=service_decorator.tags,
|
|
103
|
+
)
|
|
104
|
+
|
|
105
|
+
instance = ServiceInstance(
|
|
106
|
+
url=instance_url,
|
|
107
|
+
health_endpoint=service_decorator.health_endpoint,
|
|
108
|
+
last_heartbeat=datetime.now(UTC),
|
|
109
|
+
)
|
|
110
|
+
|
|
111
|
+
try:
|
|
112
|
+
await client.register(service_info, instance)
|
|
113
|
+
logger.info(f"Registered service '{service_decorator.name}' at {instance_url}")
|
|
114
|
+
client.start_heartbeat(
|
|
115
|
+
service_decorator.name,
|
|
116
|
+
instance_url,
|
|
117
|
+
service_decorator.heartbeat_interval,
|
|
118
|
+
)
|
|
119
|
+
except Exception as e:
|
|
120
|
+
logger.error(f"Failed to register service: {e}")
|
|
121
|
+
|
|
122
|
+
try:
|
|
123
|
+
if original_lifespan is not None:
|
|
124
|
+
async with original_lifespan(app):
|
|
125
|
+
yield
|
|
126
|
+
else:
|
|
127
|
+
yield
|
|
128
|
+
finally:
|
|
129
|
+
try:
|
|
130
|
+
await client.stop_heartbeat()
|
|
131
|
+
await client.unregister(service_decorator.name, instance_url)
|
|
132
|
+
logger.info(f"Unregistered service '{service_decorator.name}'")
|
|
133
|
+
except Exception as e:
|
|
134
|
+
logger.error(f"Failed to unregister service: {e}")
|
|
135
|
+
finally:
|
|
136
|
+
await client.close()
|
|
137
|
+
|
|
138
|
+
app.router.lifespan_context = wrapped_lifespan
|
|
139
|
+
return app
|
|
140
|
+
|
|
141
|
+
def _get_instance_url(self) -> str:
|
|
142
|
+
"""Get the URL for this service instance.
|
|
143
|
+
|
|
144
|
+
Returns the explicitly configured URL, or attempts to detect
|
|
145
|
+
from environment variables commonly set by uvicorn.
|
|
146
|
+
"""
|
|
147
|
+
if self.url:
|
|
148
|
+
return self.url
|
|
149
|
+
|
|
150
|
+
host = os.environ.get("UVICORN_HOST", os.environ.get("HOST", "127.0.0.1"))
|
|
151
|
+
port = os.environ.get("UVICORN_PORT", os.environ.get("PORT", "8000"))
|
|
152
|
+
|
|
153
|
+
if host == "0.0.0.0":
|
|
154
|
+
host = "127.0.0.1"
|
|
155
|
+
|
|
156
|
+
return f"http://{host}:{port}"
|
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: tyler-ag-tapper
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: A Python microservices framework for FastAPI with service discovery and API gateway routing
|
|
5
|
+
Project-URL: Homepage, https://github.com/tapper/tapper
|
|
6
|
+
Project-URL: Documentation, https://tapper.readthedocs.io
|
|
7
|
+
Author: Tapper Team
|
|
8
|
+
License-Expression: MIT
|
|
9
|
+
Classifier: Development Status :: 3 - Alpha
|
|
10
|
+
Classifier: Framework :: FastAPI
|
|
11
|
+
Classifier: Intended Audience :: Developers
|
|
12
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
13
|
+
Classifier: Programming Language :: Python :: 3
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
16
|
+
Classifier: Topic :: Internet :: WWW/HTTP
|
|
17
|
+
Classifier: Topic :: Software Development :: Libraries :: Application Frameworks
|
|
18
|
+
Requires-Python: >=3.11
|
|
19
|
+
Requires-Dist: click>=8.0.0
|
|
20
|
+
Requires-Dist: fastapi>=0.100.0
|
|
21
|
+
Requires-Dist: httpx>=0.25.0
|
|
22
|
+
Requires-Dist: pydantic>=2.0.0
|
|
23
|
+
Requires-Dist: uvicorn>=0.23.0
|
|
24
|
+
Provides-Extra: dev
|
|
25
|
+
Requires-Dist: pytest-asyncio>=0.21.0; extra == 'dev'
|
|
26
|
+
Requires-Dist: pytest-cov>=4.0.0; extra == 'dev'
|
|
27
|
+
Requires-Dist: pytest>=7.0.0; extra == 'dev'
|
|
28
|
+
Provides-Extra: redis
|
|
29
|
+
Requires-Dist: redis>=5.0.0; extra == 'redis'
|
|
30
|
+
Description-Content-Type: text/markdown
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
# Tapper - Python Microservices Framework
|
|
34
|
+
|
|
35
|
+
[](https://badge.fury.io/py/tapper)
|
|
36
|
+
[](https://pypi.org/project/tapper/)
|
|
37
|
+
[](https://opensource.org/licenses/MIT)
|
|
38
|
+
|
|
39
|
+
**Tapper** is a lightweight, elegant Python framework that simplifies **service discovery** and **API gateway routing** for **FastAPI**-based microservices.
|
|
40
|
+
|
|
41
|
+
With a single `@Service` decorator placed on your service's `main.py`, Tapper automatically registers your service and provides a powerful **API gateway** that discovers all services and intelligently routes requests to the correct backend.
|
|
42
|
+
|
|
43
|
+
## Features
|
|
44
|
+
|
|
45
|
+
- **Zero-boilerplate registration** with `@tapper.Service`
|
|
46
|
+
- **Automatic service discovery** and registration
|
|
47
|
+
- **Built-in dynamic API gateway** with path-based routing
|
|
48
|
+
- **Transparent proxying** of requests (path params, query strings, headers, body)
|
|
49
|
+
- **Health checks** and service status monitoring
|
|
50
|
+
- **Load balancing** (round-robin by default)
|
|
51
|
+
- **Seamless integration** with existing FastAPI apps
|
|
52
|
+
- **Minimal configuration** — focus on your business logic
|
|
53
|
+
|
|
54
|
+
## Installation
|
|
55
|
+
|
|
56
|
+
```bash
|
|
57
|
+
pip install tapper
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
## Quick Start
|
|
61
|
+
|
|
62
|
+
### 1. Annotate Your Service (in `main.py` of each microservice)
|
|
63
|
+
|
|
64
|
+
```python
|
|
65
|
+
from fastapi import FastAPI
|
|
66
|
+
from tapper import Service
|
|
67
|
+
|
|
68
|
+
app = FastAPI()
|
|
69
|
+
|
|
70
|
+
@Service(name="user-service", version="1.0.0", description="User management service")
|
|
71
|
+
@app.get("/users/{user_id}")
|
|
72
|
+
async def get_user(user_id: int):
|
|
73
|
+
return {"user_id": user_id, "name": "Jane Doe"}
|
|
74
|
+
|
|
75
|
+
@app.post("/users/")
|
|
76
|
+
async def create_user(user: dict):
|
|
77
|
+
return {"message": "User created", "user": user}
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
That's all! The `@Service` decorator registers your service automatically.
|
|
81
|
+
|
|
82
|
+
### 2. Run the API Gateway
|
|
83
|
+
|
|
84
|
+
Create a separate gateway application (e.g., `gateway.py`):
|
|
85
|
+
|
|
86
|
+
```python
|
|
87
|
+
from fastapi import FastAPI
|
|
88
|
+
from tapper import TapperGateway
|
|
89
|
+
|
|
90
|
+
app = FastAPI(title="Tapper API Gateway")
|
|
91
|
+
|
|
92
|
+
# Initialize the gateway (you can pass registry_url or use env vars)
|
|
93
|
+
gateway = TapperGateway()
|
|
94
|
+
|
|
95
|
+
# Mount the gateway at the root
|
|
96
|
+
app.mount("/", gateway)
|
|
97
|
+
|
|
98
|
+
# Optional: expose gateway health
|
|
99
|
+
@app.get("/health")
|
|
100
|
+
async def health():
|
|
101
|
+
return {"status": "healthy", "services": gateway.get_service_status()}
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
Run it:
|
|
105
|
+
|
|
106
|
+
```bash
|
|
107
|
+
uvicorn gateway:app --port 8000
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
Now your gateway at `http://localhost:8000` will automatically discover and route to all `@Service`-decorated FastAPI apps.
|
|
111
|
+
|
|
112
|
+
## How It Works
|
|
113
|
+
|
|
114
|
+
1. Each microservice uses the `@Service` decorator on its `FastAPI` instance.
|
|
115
|
+
2. Tapper registers the service (name, version, endpoints) with a central registry.
|
|
116
|
+
3. The **TapperGateway** periodically discovers available services.
|
|
117
|
+
4. Incoming requests are matched against registered service routes.
|
|
118
|
+
5. Requests are proxied transparently to the correct service instance.
|
|
119
|
+
|
|
120
|
+
## Configuration Options
|
|
121
|
+
|
|
122
|
+
### Service Decorator
|
|
123
|
+
|
|
124
|
+
```python
|
|
125
|
+
@Service(
|
|
126
|
+
name="payment-service",
|
|
127
|
+
version="2.1.0",
|
|
128
|
+
description="Handles all payment operations",
|
|
129
|
+
prefix="/payments", # optional: prefix all routes
|
|
130
|
+
health_endpoint="/health", # optional: custom health check path
|
|
131
|
+
tags=["payments", "finance"] # optional: OpenAPI tags
|
|
132
|
+
)
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
### Gateway Configuration
|
|
136
|
+
|
|
137
|
+
```python
|
|
138
|
+
gateway = TapperGateway(
|
|
139
|
+
registry_url="http://registry:8001", # or via TAPER_REGISTRY_URL env var
|
|
140
|
+
discovery_interval=30, # seconds
|
|
141
|
+
timeout=5, # request timeout in seconds
|
|
142
|
+
load_balancer="round-robin", # or "random", "least-connections", custom
|
|
143
|
+
# auth_provider=YourAuthProvider(), # optional
|
|
144
|
+
)
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
## Advanced Usage
|
|
148
|
+
|
|
149
|
+
### Custom Load Balancer
|
|
150
|
+
|
|
151
|
+
```python
|
|
152
|
+
from tapper import LoadBalancer
|
|
153
|
+
|
|
154
|
+
class CustomBalancer(LoadBalancer):
|
|
155
|
+
def select_instance(self, instances):
|
|
156
|
+
# Your custom logic
|
|
157
|
+
return instances[0]
|
|
158
|
+
|
|
159
|
+
gateway = TapperGateway(load_balancer=CustomBalancer())
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
### Supported Registries
|
|
163
|
+
|
|
164
|
+
- In-memory (development)
|
|
165
|
+
- Redis
|
|
166
|
+
- Consul
|
|
167
|
+
- etcd
|
|
168
|
+
- Custom backend (extend `tapper.registry.RegistryBackend`)
|
|
169
|
+
|
|
170
|
+
## Contributing
|
|
171
|
+
|
|
172
|
+
Contributions are welcome!
|
|
173
|
+
|
|
174
|
+
1. Fork the repo
|
|
175
|
+
2. Create your feature branch (`git checkout -b feature/amazing-feature`)
|
|
176
|
+
3. Commit your changes (`git commit -m 'Add amazing feature'`)
|
|
177
|
+
4. Push to the branch (`git push origin feature/amazing-feature`)
|
|
178
|
+
5. Open a Pull Request
|
|
179
|
+
|
|
180
|
+
## License
|
|
181
|
+
|
|
182
|
+
Distributed under the **MIT License**. See [`LICENSE`](LICENSE) for more information.
|
|
183
|
+
|
|
184
|
+
## Acknowledgments
|
|
185
|
+
|
|
186
|
+
- Built on the amazing [FastAPI](https://fastapi.tiangolo.com/)
|
|
187
|
+
- Inspired by service discovery patterns from Kubernetes, Consul, and Spring Cloud
|
|
188
|
+
|
|
189
|
+
---
|
|
190
|
+
|
|
191
|
+
Happy microservicing with **Tapper**! 🚀
|
|
192
|
+
```
|
|
193
|
+
|
|
194
|
+
Feel free to customize badges, add a real logo, or adjust the configuration details as you implement the actual framework!
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
tapper/__init__.py,sha256=enQqpwbbHcIhUQ71Ew5ViL-unZFpTRcBjdUkf1Jz410,1481
|
|
2
|
+
tapper/cli.py,sha256=htnKiQZkVy-nC69TbEKiIYZN2Ik4OsC3NTaNvBaeia4,2755
|
|
3
|
+
tapper/exceptions.py,sha256=YWYkD5-viKcqyeJawrPlFjxGsoWvJWjhRsVDkvZqK2E,956
|
|
4
|
+
tapper/gateway.py,sha256=F4Hhly2Yr3QuOPhzB0VONj-xXpf5VegcFgFXFMndn2w,9826
|
|
5
|
+
tapper/load_balancer.py,sha256=AbIwhJLw9ZUbgO3qNhu2UtRBkCtok6Tncpv0bLWeYIw,4221
|
|
6
|
+
tapper/models.py,sha256=Y-rEySBLkSjO1aIQwW3ZxMiWtxz6Gwf0sOm3ZyERm1A,1674
|
|
7
|
+
tapper/service.py,sha256=XiC6H9VPCcvfB_lRwK0rRUhxoek1xFRhxejP6TOtyXs,5555
|
|
8
|
+
tapper/registry/__init__.py,sha256=WGZgbvCQKCkyeej4-qaaEwsru0dHM59S4Y-WDltzeJY,455
|
|
9
|
+
tapper/registry/client.py,sha256=JrMyi2rBzGA5yLGTuilZ9Qpv-ZmNL_kPSIm_7Y830dc,6124
|
|
10
|
+
tapper/registry/server.py,sha256=K_O_6hYm2Uxw7Kb4gZypGT0nb-S_wIY0PHUFRCOYL0g,3703
|
|
11
|
+
tapper/registry/backends/__init__.py,sha256=4Xo1Ug7ulH8rTm2dLJY6IjgR2vAxy3cvsRBLZvre7hU,339
|
|
12
|
+
tapper/registry/backends/base.py,sha256=ACzIOHWKI2VCtrdQzypb9HwXExRH3hqYNP9pQl8VSfY,1355
|
|
13
|
+
tapper/registry/backends/memory.py,sha256=TvTVll83gPqvdzI_8-6SYIvpurK0UekFzFxeBIcdKKo,3816
|
|
14
|
+
tapper/registry/backends/redis.py,sha256=2c0whwWeIkBJL7w_6mDUftEcb_MeRxVvUoHcMwYbFAE,5824
|
|
15
|
+
tyler_ag_tapper-0.1.0.dist-info/METADATA,sha256=iEB6ad4PAyeACX2nFNB5pbm3eWTL8TrF_hR9kaEFruU,6056
|
|
16
|
+
tyler_ag_tapper-0.1.0.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
|
|
17
|
+
tyler_ag_tapper-0.1.0.dist-info/entry_points.txt,sha256=HBib8RsAMXx7LEF6hl7KDIPeA8dOgmh7ds9WdtlAUq0,43
|
|
18
|
+
tyler_ag_tapper-0.1.0.dist-info/RECORD,,
|