servicekit 0.8.2__tar.gz → 0.9.0__tar.gz

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.
Files changed (38) hide show
  1. {servicekit-0.8.2 → servicekit-0.9.0}/PKG-INFO +1 -1
  2. {servicekit-0.8.2 → servicekit-0.9.0}/pyproject.toml +1 -1
  3. {servicekit-0.8.2 → servicekit-0.9.0}/src/servicekit/api/registration.py +89 -3
  4. {servicekit-0.8.2 → servicekit-0.9.0}/src/servicekit/api/service_builder.py +22 -0
  5. {servicekit-0.8.2 → servicekit-0.9.0}/README.md +0 -0
  6. {servicekit-0.8.2 → servicekit-0.9.0}/src/servicekit/.DS_Store +0 -0
  7. {servicekit-0.8.2 → servicekit-0.9.0}/src/servicekit/__init__.py +0 -0
  8. {servicekit-0.8.2 → servicekit-0.9.0}/src/servicekit/api/.DS_Store +0 -0
  9. {servicekit-0.8.2 → servicekit-0.9.0}/src/servicekit/api/__init__.py +0 -0
  10. {servicekit-0.8.2 → servicekit-0.9.0}/src/servicekit/api/app.py +0 -0
  11. {servicekit-0.8.2 → servicekit-0.9.0}/src/servicekit/api/apps/.DS_Store +0 -0
  12. {servicekit-0.8.2 → servicekit-0.9.0}/src/servicekit/api/apps/landing/index.html +0 -0
  13. {servicekit-0.8.2 → servicekit-0.9.0}/src/servicekit/api/apps/landing/manifest.json +0 -0
  14. {servicekit-0.8.2 → servicekit-0.9.0}/src/servicekit/api/auth.py +0 -0
  15. {servicekit-0.8.2 → servicekit-0.9.0}/src/servicekit/api/crud.py +0 -0
  16. {servicekit-0.8.2 → servicekit-0.9.0}/src/servicekit/api/dependencies.py +0 -0
  17. {servicekit-0.8.2 → servicekit-0.9.0}/src/servicekit/api/middleware.py +0 -0
  18. {servicekit-0.8.2 → servicekit-0.9.0}/src/servicekit/api/monitoring.py +0 -0
  19. {servicekit-0.8.2 → servicekit-0.9.0}/src/servicekit/api/pagination.py +0 -0
  20. {servicekit-0.8.2 → servicekit-0.9.0}/src/servicekit/api/router.py +0 -0
  21. {servicekit-0.8.2 → servicekit-0.9.0}/src/servicekit/api/routers/__init__.py +0 -0
  22. {servicekit-0.8.2 → servicekit-0.9.0}/src/servicekit/api/routers/health.py +0 -0
  23. {servicekit-0.8.2 → servicekit-0.9.0}/src/servicekit/api/routers/job.py +0 -0
  24. {servicekit-0.8.2 → servicekit-0.9.0}/src/servicekit/api/routers/metrics.py +0 -0
  25. {servicekit-0.8.2 → servicekit-0.9.0}/src/servicekit/api/routers/system.py +0 -0
  26. {servicekit-0.8.2 → servicekit-0.9.0}/src/servicekit/api/sse.py +0 -0
  27. {servicekit-0.8.2 → servicekit-0.9.0}/src/servicekit/api/utilities.py +0 -0
  28. {servicekit-0.8.2 → servicekit-0.9.0}/src/servicekit/database.py +0 -0
  29. {servicekit-0.8.2 → servicekit-0.9.0}/src/servicekit/exceptions.py +0 -0
  30. {servicekit-0.8.2 → servicekit-0.9.0}/src/servicekit/gunicorn.conf.py +0 -0
  31. {servicekit-0.8.2 → servicekit-0.9.0}/src/servicekit/logging.py +0 -0
  32. {servicekit-0.8.2 → servicekit-0.9.0}/src/servicekit/manager.py +0 -0
  33. {servicekit-0.8.2 → servicekit-0.9.0}/src/servicekit/models.py +0 -0
  34. {servicekit-0.8.2 → servicekit-0.9.0}/src/servicekit/py.typed +0 -0
  35. {servicekit-0.8.2 → servicekit-0.9.0}/src/servicekit/repository.py +0 -0
  36. {servicekit-0.8.2 → servicekit-0.9.0}/src/servicekit/scheduler.py +0 -0
  37. {servicekit-0.8.2 → servicekit-0.9.0}/src/servicekit/schemas.py +0 -0
  38. {servicekit-0.8.2 → servicekit-0.9.0}/src/servicekit/types.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: servicekit
3
- Version: 0.8.2
3
+ Version: 0.9.0
4
4
  Summary: Async SQLAlchemy framework with FastAPI integration - reusable foundation for building data services
5
5
  Keywords: fastapi,sqlalchemy,async,database,rest-api,crud,framework
6
6
  Author: Morten Hansen
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "servicekit"
3
- version = "0.8.2"
3
+ version = "0.9.0"
4
4
  description = "Async SQLAlchemy framework with FastAPI integration - reusable foundation for building data services"
5
5
  readme = "README.md"
6
6
  authors = [{ name = "Morten Hansen", email = "morten@winterop.com" }]
@@ -6,7 +6,7 @@ import socket
6
6
  from typing import Any
7
7
 
8
8
  import httpx
9
- from pydantic import BaseModel
9
+ from pydantic import BaseModel, ConfigDict
10
10
 
11
11
  from servicekit.logging import get_logger
12
12
 
@@ -19,6 +19,26 @@ _ping_url: str | None = None
19
19
  _service_key: str | None = None
20
20
 
21
21
 
22
+ class RegistrationConfig(BaseModel):
23
+ """Parameters for service registration, used for re-registration in keepalive."""
24
+
25
+ model_config = ConfigDict(arbitrary_types_allowed=True)
26
+
27
+ orchestrator_url: str | None = None
28
+ host: str | None = None
29
+ port: int | None = None
30
+ info: BaseModel
31
+ orchestrator_url_env: str = "SERVICEKIT_ORCHESTRATOR_URL"
32
+ host_env: str = "SERVICEKIT_HOST"
33
+ port_env: str = "SERVICEKIT_PORT"
34
+ max_retries: int = 5
35
+ retry_delay: float = 2.0
36
+ fail_on_error: bool = False
37
+ timeout: float = 10.0
38
+ service_key: str | None = None
39
+ service_key_env: str = "SERVICEKIT_REGISTRATION_KEY"
40
+
41
+
22
42
  def _resolve_service_key(service_key: str | None, service_key_env: str) -> str | None:
23
43
  """Resolve service key from parameter or environment variable."""
24
44
  if service_key:
@@ -218,7 +238,14 @@ async def register_service(
218
238
  return None
219
239
 
220
240
 
221
- async def _keepalive_loop(ping_url: str, interval: float, timeout: float, service_key: str | None) -> None:
241
+ async def _keepalive_loop(
242
+ ping_url: str,
243
+ interval: float,
244
+ timeout: float,
245
+ service_key: str | None,
246
+ registration_config: RegistrationConfig | None = None,
247
+ re_register_grace_period: float = 30.0,
248
+ ) -> None:
222
249
  """Background task to periodically ping the orchestrator."""
223
250
  logger.info("keepalive.started", ping_url=ping_url, interval_seconds=interval)
224
251
 
@@ -247,6 +274,54 @@ async def _keepalive_loop(ping_url: str, interval: float, timeout: float, servic
247
274
  logger.info("keepalive.cancelled", service_id=_service_id)
248
275
  raise
249
276
 
277
+ except httpx.HTTPStatusError as e:
278
+ if e.response.status_code == 404 and registration_config is not None:
279
+ logger.warning(
280
+ "keepalive.service_not_found",
281
+ service_id=_service_id,
282
+ ping_url=ping_url,
283
+ grace_period=re_register_grace_period,
284
+ )
285
+
286
+ # Grace period to avoid thundering herd during orchestrator flap
287
+ await asyncio.sleep(re_register_grace_period)
288
+
289
+ # Attempt re-registration with original parameters
290
+ try:
291
+ result = await register_service(**registration_config.model_dump())
292
+ if result and result.get("ping_url"):
293
+ ping_url = result["ping_url"]
294
+ # Rebuild headers in case service_key changed
295
+ headers = {}
296
+ if _service_key:
297
+ headers["X-Service-Key"] = _service_key
298
+ logger.info(
299
+ "keepalive.re_registered",
300
+ service_id=_service_id,
301
+ new_ping_url=ping_url,
302
+ )
303
+ else:
304
+ logger.warning(
305
+ "keepalive.re_registration_failed",
306
+ service_id=_service_id,
307
+ reason="register_service returned None",
308
+ )
309
+ except Exception as re_reg_error:
310
+ logger.warning(
311
+ "keepalive.re_registration_error",
312
+ service_id=_service_id,
313
+ error=str(re_reg_error),
314
+ error_type=type(re_reg_error).__name__,
315
+ )
316
+ else:
317
+ logger.warning(
318
+ "keepalive.ping_failed",
319
+ service_id=_service_id,
320
+ ping_url=ping_url,
321
+ error=str(e),
322
+ error_type=type(e).__name__,
323
+ )
324
+
250
325
  except Exception as e:
251
326
  logger.warning(
252
327
  "keepalive.ping_failed",
@@ -264,6 +339,8 @@ async def start_keepalive(
264
339
  timeout: float = 10.0,
265
340
  service_key: str | None = None,
266
341
  service_key_env: str = "SERVICEKIT_REGISTRATION_KEY",
342
+ registration_config: RegistrationConfig | None = None,
343
+ re_register_grace_period: float = 30.0,
267
344
  ) -> None:
268
345
  """Start background keepalive task to ping orchestrator."""
269
346
  global _keepalive_task
@@ -275,7 +352,16 @@ async def start_keepalive(
275
352
  # Resolve service key (use global from registration if not provided)
276
353
  resolved_service_key = _resolve_service_key(service_key, service_key_env) or _service_key
277
354
 
278
- _keepalive_task = asyncio.create_task(_keepalive_loop(ping_url, interval, timeout, resolved_service_key))
355
+ _keepalive_task = asyncio.create_task(
356
+ _keepalive_loop(
357
+ ping_url,
358
+ interval,
359
+ timeout,
360
+ resolved_service_key,
361
+ registration_config=registration_config,
362
+ re_register_grace_period=re_register_grace_period,
363
+ )
364
+ )
279
365
  logger.info("keepalive.task_started", ping_url=ping_url, interval_seconds=interval)
280
366
 
281
367
 
@@ -95,6 +95,7 @@ class _RegistrationOptions:
95
95
  auto_deregister: bool
96
96
  service_key: str | None
97
97
  service_key_env: str
98
+ re_register_grace_period: float
98
99
 
99
100
 
100
101
  class ServiceInfo(BaseModel):
@@ -330,6 +331,7 @@ class BaseServiceBuilder:
330
331
  auto_deregister: bool = True,
331
332
  service_key: str | None = None,
332
333
  service_key_env: str = "SERVICEKIT_REGISTRATION_KEY",
334
+ re_register_grace_period: float = 30.0,
333
335
  ) -> Self:
334
336
  """Enable service registration with orchestrator for service discovery."""
335
337
  self._registration_options = _RegistrationOptions(
@@ -348,6 +350,7 @@ class BaseServiceBuilder:
348
350
  auto_deregister=auto_deregister,
349
351
  service_key=service_key,
350
352
  service_key_env=service_key_env,
353
+ re_register_grace_period=re_register_grace_period,
351
354
  )
352
355
  return self
353
356
 
@@ -685,12 +688,31 @@ class BaseServiceBuilder:
685
688
  if registration_info and registration_options.enable_keepalive:
686
689
  ping_url = registration_info.get("ping_url")
687
690
  if ping_url:
691
+ from .registration import RegistrationConfig
692
+
693
+ registration_config = RegistrationConfig(
694
+ orchestrator_url=registration_options.orchestrator_url,
695
+ host=registration_options.host,
696
+ port=registration_options.port,
697
+ info=info,
698
+ orchestrator_url_env=registration_options.orchestrator_url_env,
699
+ host_env=registration_options.host_env,
700
+ port_env=registration_options.port_env,
701
+ max_retries=registration_options.max_retries,
702
+ retry_delay=registration_options.retry_delay,
703
+ fail_on_error=False,
704
+ timeout=registration_options.timeout,
705
+ service_key=registration_options.service_key,
706
+ service_key_env=registration_options.service_key_env,
707
+ )
688
708
  await start_keepalive(
689
709
  ping_url=ping_url,
690
710
  interval=registration_options.keepalive_interval,
691
711
  timeout=registration_options.timeout,
692
712
  service_key=registration_options.service_key,
693
713
  service_key_env=registration_options.service_key_env,
714
+ registration_config=registration_config,
715
+ re_register_grace_period=registration_options.re_register_grace_period,
694
716
  )
695
717
 
696
718
  try:
File without changes