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/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
+ [![PyPI version](https://badge.fury.io/py/tapper.svg)](https://badge.fury.io/py/tapper)
36
+ [![Python Versions](https://img.shields.io/pypi/pyversions/tapper.svg)](https://pypi.org/project/tapper/)
37
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](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,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.28.0
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ tapper = tapper.cli:main