hammad-python 0.0.13__py3-none-any.whl → 0.0.14__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.
Files changed (78) hide show
  1. hammad/__init__.py +1 -180
  2. hammad/ai/__init__.py +0 -58
  3. hammad/ai/completions/__init__.py +3 -2
  4. hammad/ai/completions/client.py +84 -129
  5. hammad/ai/completions/create.py +33 -9
  6. hammad/ai/completions/settings.py +100 -0
  7. hammad/ai/completions/types.py +86 -5
  8. hammad/ai/completions/utils.py +112 -0
  9. hammad/ai/embeddings/__init__.py +2 -2
  10. hammad/ai/embeddings/client/fastembed_text_embeddings_client.py +1 -1
  11. hammad/ai/embeddings/client/litellm_embeddings_client.py +1 -1
  12. hammad/ai/embeddings/types.py +4 -4
  13. hammad/cache/__init__.py +13 -21
  14. hammad/cli/__init__.py +2 -2
  15. hammad/cli/animations.py +8 -39
  16. hammad/cli/styles/__init__.py +2 -2
  17. hammad/data/__init__.py +19 -2
  18. hammad/data/collections/__init__.py +2 -2
  19. hammad/data/collections/vector_collection.py +0 -7
  20. hammad/{configuration → data/configurations}/__init__.py +2 -2
  21. hammad/{configuration → data/configurations}/configuration.py +1 -1
  22. hammad/data/databases/__init__.py +2 -2
  23. hammad/data/models/__init__.py +44 -0
  24. hammad/{base → data/models/base}/__init__.py +3 -3
  25. hammad/{pydantic → data/models/pydantic}/__init__.py +28 -16
  26. hammad/{pydantic → data/models/pydantic}/converters.py +11 -2
  27. hammad/{pydantic → data/models/pydantic}/models/__init__.py +3 -3
  28. hammad/{pydantic → data/models/pydantic}/models/arbitrary_model.py +1 -1
  29. hammad/{pydantic → data/models/pydantic}/models/cacheable_model.py +1 -1
  30. hammad/{pydantic → data/models/pydantic}/models/fast_model.py +1 -1
  31. hammad/{pydantic → data/models/pydantic}/models/function_model.py +1 -1
  32. hammad/{pydantic → data/models/pydantic}/models/subscriptable_model.py +1 -1
  33. hammad/data/types/__init__.py +41 -0
  34. hammad/{types → data/types}/file.py +2 -2
  35. hammad/{multimodal → data/types/multimodal}/__init__.py +2 -2
  36. hammad/{multimodal → data/types/multimodal}/audio.py +2 -2
  37. hammad/{multimodal → data/types/multimodal}/image.py +2 -2
  38. hammad/{text → data/types}/text.py +4 -4
  39. hammad/formatting/__init__.py +38 -0
  40. hammad/{json → formatting/json}/__init__.py +3 -3
  41. hammad/{json → formatting/json}/converters.py +2 -2
  42. hammad/{text → formatting/text}/__init__.py +5 -24
  43. hammad/{text → formatting/text}/converters.py +2 -2
  44. hammad/{text → formatting/text}/markdown.py +1 -1
  45. hammad/{yaml → formatting/yaml}/__init__.py +3 -7
  46. hammad/formatting/yaml/converters.py +5 -0
  47. hammad/logging/__init__.py +2 -2
  48. hammad/mcp/__init__.py +50 -0
  49. hammad/mcp/client/__init__.py +1 -0
  50. hammad/mcp/client/client.py +523 -0
  51. hammad/mcp/client/client_service.py +393 -0
  52. hammad/mcp/client/settings.py +178 -0
  53. hammad/mcp/servers/__init__.py +1 -0
  54. hammad/mcp/servers/launcher.py +1161 -0
  55. hammad/performance/__init__.py +36 -0
  56. hammad/{_core/_utils/_import_utils.py → performance/imports.py} +125 -76
  57. hammad/performance/runtime/__init__.py +32 -0
  58. hammad/performance/runtime/decorators.py +142 -0
  59. hammad/performance/runtime/run.py +299 -0
  60. hammad/service/__init__.py +49 -0
  61. hammad/service/create.py +532 -0
  62. hammad/service/decorators.py +285 -0
  63. hammad/web/__init__.py +2 -2
  64. hammad/web/http/client.py +1 -1
  65. hammad/web/openapi/__init__.py +1 -0
  66. {hammad_python-0.0.13.dist-info → hammad_python-0.0.14.dist-info}/METADATA +35 -3
  67. hammad_python-0.0.14.dist-info/RECORD +99 -0
  68. hammad/_core/__init__.py +0 -1
  69. hammad/_core/_utils/__init__.py +0 -4
  70. hammad/multithreading/__init__.py +0 -304
  71. hammad/types/__init__.py +0 -11
  72. hammad/yaml/converters.py +0 -19
  73. hammad_python-0.0.13.dist-info/RECORD +0 -85
  74. /hammad/{base → data/models/base}/fields.py +0 -0
  75. /hammad/{base → data/models/base}/model.py +0 -0
  76. /hammad/{base → data/models/base}/utils.py +0 -0
  77. {hammad_python-0.0.13.dist-info → hammad_python-0.0.14.dist-info}/WHEEL +0 -0
  78. {hammad_python-0.0.13.dist-info → hammad_python-0.0.14.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,532 @@
1
+ """hammad.service.create
2
+
3
+ Service creation utilities for launching FastAPI servers from various Python objects.
4
+ """
5
+
6
+ import inspect
7
+ import signal
8
+ import atexit
9
+ from typing import (
10
+ Any,
11
+ Callable,
12
+ Dict,
13
+ List,
14
+ Literal,
15
+ Optional,
16
+ Type,
17
+ Union,
18
+ get_type_hints,
19
+ )
20
+ from dataclasses import dataclass, fields, is_dataclass, MISSING
21
+ from enum import Enum
22
+
23
+ try:
24
+ from fastapi import FastAPI, HTTPException
25
+ from pydantic import BaseModel, create_model
26
+ from uvicorn import Config, Server
27
+ import uvloop
28
+ except ImportError as e:
29
+ raise ImportError(
30
+ "Service dependencies not installed. Install with: pip install hammad-python[serve]"
31
+ ) from e
32
+
33
+ import logging
34
+
35
+ logger = logging.getLogger(__name__)
36
+
37
+
38
+ class ServiceStatus(str, Enum):
39
+ """Service status enumeration."""
40
+
41
+ STARTING = "starting"
42
+ RUNNING = "running"
43
+ STOPPING = "stopping"
44
+ STOPPED = "stopped"
45
+ ERROR = "error"
46
+
47
+
48
+ @dataclass
49
+ class ServiceConfig:
50
+ """Configuration for service creation."""
51
+
52
+ host: str = "0.0.0.0"
53
+ port: int = 8000
54
+ log_level: str = "info"
55
+ reload: bool = False
56
+ workers: int = 1
57
+ timeout_keep_alive: int = 5
58
+ timeout_graceful_shutdown: int = 30
59
+ access_log: bool = True
60
+ use_colors: bool = True
61
+ loop: str = "uvloop"
62
+
63
+
64
+ class ServiceManager:
65
+ """Manages service lifecycle including graceful shutdown."""
66
+
67
+ def __init__(self):
68
+ self.servers: List[Server] = []
69
+ self.status = ServiceStatus.STOPPED
70
+ self._shutdown_handlers_registered = False
71
+
72
+ def register_shutdown_handlers(self):
73
+ """Register signal handlers for graceful shutdown."""
74
+ if self._shutdown_handlers_registered:
75
+ return
76
+
77
+ def signal_handler(signum, _):
78
+ logger.info(f"Received signal {signum}. Shutting down services...")
79
+ self.shutdown_all()
80
+
81
+ signal.signal(signal.SIGTERM, signal_handler)
82
+ signal.signal(signal.SIGINT, signal_handler)
83
+ atexit.register(self.shutdown_all)
84
+ self._shutdown_handlers_registered = True
85
+
86
+ def add_server(self, server: Server):
87
+ """Add a server to be managed."""
88
+ self.servers.append(server)
89
+ self.register_shutdown_handlers()
90
+
91
+ def shutdown_all(self):
92
+ """Shutdown all managed servers."""
93
+ if self.status == ServiceStatus.STOPPING:
94
+ return
95
+
96
+ self.status = ServiceStatus.STOPPING
97
+ logger.info(f"Shutting down {len(self.servers)} service(s)...")
98
+
99
+ for server in self.servers:
100
+ try:
101
+ if server.should_exit:
102
+ continue
103
+ server.should_exit = True
104
+ logger.info("Service shutdown initiated")
105
+ except Exception as e:
106
+ logger.error(f"Error shutting down server: {e}")
107
+
108
+ self.status = ServiceStatus.STOPPED
109
+ logger.info("All services shut down")
110
+
111
+
112
+ # Global service manager
113
+ _service_manager = ServiceManager()
114
+
115
+
116
+ def _python_type_to_openapi_type(python_type: Type) -> str:
117
+ """Convert Python type to OpenAPI type string."""
118
+ type_mapping = {
119
+ str: "string",
120
+ int: "integer",
121
+ float: "number",
122
+ bool: "boolean",
123
+ list: "array",
124
+ dict: "object",
125
+ }
126
+
127
+ # Handle Union types (Optional)
128
+ if hasattr(python_type, "__origin__"):
129
+ if python_type.__origin__ is Union:
130
+ # For Optional[T], use the non-None type
131
+ non_none_types = [t for t in python_type.__args__ if t != type(None)]
132
+ if non_none_types:
133
+ return _python_type_to_openapi_type(non_none_types[0])
134
+ elif python_type.__origin__ is list:
135
+ return "array"
136
+ elif python_type.__origin__ is dict:
137
+ return "object"
138
+
139
+ return type_mapping.get(python_type, "string")
140
+
141
+
142
+ def _create_pydantic_model_from_function(func: Callable) -> Type[BaseModel]:
143
+ """Create a Pydantic model from function signature."""
144
+ sig = inspect.signature(func)
145
+ type_hints = get_type_hints(func)
146
+
147
+ fields_dict = {}
148
+ for param_name, param in sig.parameters.items():
149
+ if param_name == "self":
150
+ continue
151
+
152
+ param_type = type_hints.get(param_name, str)
153
+ default_value = (
154
+ ... if param.default == inspect.Parameter.empty else param.default
155
+ )
156
+
157
+ fields_dict[param_name] = (param_type, default_value)
158
+
159
+ return create_model(f"{func.__name__}Model", **fields_dict)
160
+
161
+
162
+ def _create_fastapi_from_function(
163
+ func: Callable,
164
+ *,
165
+ name: Optional[str] = None,
166
+ method: Literal[
167
+ "GET", "POST", "PUT", "DELETE", "PATCH", "OPTIONS", "HEAD"
168
+ ] = "POST",
169
+ path: str = "/",
170
+ include_in_schema: bool = True,
171
+ dependencies: Optional[List[Callable[..., Any]]] = None,
172
+ tags: Optional[List[str]] = None,
173
+ description: Optional[str] = None,
174
+ ) -> FastAPI:
175
+ """Create a FastAPI app from a function."""
176
+ app_name = name or func.__name__
177
+ app = FastAPI(
178
+ title=app_name,
179
+ description=description or f"Auto-generated API for {func.__name__}",
180
+ )
181
+
182
+ # Create request model for POST/PUT/PATCH methods
183
+ if method in ["POST", "PUT", "PATCH"]:
184
+ request_model = _create_pydantic_model_from_function(func)
185
+
186
+ async def endpoint(request: request_model): # type: ignore
187
+ try:
188
+ # Convert request to dict and call function
189
+ kwargs = request.model_dump()
190
+ result = func(**kwargs)
191
+ return {"result": result}
192
+ except Exception as e:
193
+ logger.error(f"Error in {func.__name__}: {e}")
194
+ raise HTTPException(status_code=500, detail=str(e))
195
+
196
+ app.add_api_route(
197
+ path,
198
+ endpoint,
199
+ methods=[method],
200
+ include_in_schema=include_in_schema,
201
+ dependencies=dependencies,
202
+ tags=tags,
203
+ )
204
+ else:
205
+ # For GET and other methods, use query parameters
206
+ sig = inspect.signature(func)
207
+
208
+ async def endpoint(**kwargs):
209
+ try:
210
+ # Filter kwargs to only include function parameters
211
+ func_kwargs = {
212
+ key: value for key, value in kwargs.items() if key in sig.parameters
213
+ }
214
+ result = func(**func_kwargs)
215
+ return {"result": result}
216
+ except Exception as e:
217
+ logger.error(f"Error in {func.__name__}: {e}")
218
+ raise HTTPException(status_code=500, detail=str(e))
219
+
220
+ # Dynamically set the endpoint signature to match the function
221
+ endpoint.__signature__ = sig
222
+
223
+ app.add_api_route(
224
+ path,
225
+ endpoint,
226
+ methods=[method],
227
+ include_in_schema=include_in_schema,
228
+ dependencies=dependencies,
229
+ tags=tags,
230
+ )
231
+
232
+ return app
233
+
234
+
235
+ def _create_fastapi_from_model(
236
+ model: Union[Type[BaseModel], Type, Any],
237
+ *,
238
+ name: Optional[str] = None,
239
+ methods: List[Literal["GET", "POST", "PUT", "DELETE"]] = None,
240
+ path: str = "/",
241
+ include_in_schema: bool = True,
242
+ dependencies: Optional[List[Callable[..., Any]]] = None,
243
+ tags: Optional[List[str]] = None,
244
+ description: Optional[str] = None,
245
+ ) -> FastAPI:
246
+ """Create a FastAPI app from a model (Pydantic, dataclass, etc.)."""
247
+ if methods is None:
248
+ methods = ["GET", "POST"]
249
+
250
+ app_name = name or getattr(model, "__name__", "ModelService")
251
+ app = FastAPI(
252
+ title=app_name,
253
+ description=description or f"Auto-generated API for {app_name}",
254
+ )
255
+
256
+ # Convert model to Pydantic if needed
257
+ if is_dataclass(model) and not issubclass(model, BaseModel):
258
+ # Convert dataclass to Pydantic model
259
+ field_definitions = {}
260
+ for field in fields(model):
261
+ field_definitions[field.name] = (
262
+ field.type,
263
+ field.default if field.default != MISSING else ...,
264
+ )
265
+ pydantic_model = create_model(f"{model.__name__}Model", **field_definitions)
266
+ elif inspect.isclass(model) and issubclass(model, BaseModel):
267
+ pydantic_model = model
268
+ else:
269
+ # For other types, create a simple wrapper
270
+ pydantic_model = create_model(f"{app_name}Model", value=(str, ...))
271
+
272
+ # Store for the service
273
+ items: Dict[str, Any] = {}
274
+
275
+ if "GET" in methods:
276
+
277
+ @app.get(path, response_model=Dict[str, Any])
278
+ async def get_items():
279
+ return {"items": list(items.values())}
280
+
281
+ @app.get(f"{path}/{{item_id}}", response_model=pydantic_model)
282
+ async def get_item(item_id: str):
283
+ if item_id not in items:
284
+ raise HTTPException(status_code=404, detail="Item not found")
285
+ return items[item_id]
286
+
287
+ if "POST" in methods:
288
+
289
+ @app.post(path, response_model=Dict[str, Any])
290
+ async def create_item(item: pydantic_model): # type: ignore
291
+ item_id = str(len(items))
292
+ items[item_id] = item
293
+ return {"id": item_id, "item": item}
294
+
295
+ if "PUT" in methods:
296
+
297
+ @app.put(f"{path}/{{item_id}}", response_model=pydantic_model)
298
+ async def update_item(item_id: str, item: pydantic_model): # type: ignore
299
+ if item_id not in items:
300
+ raise HTTPException(status_code=404, detail="Item not found")
301
+ items[item_id] = item
302
+ return item
303
+
304
+ if "DELETE" in methods:
305
+
306
+ @app.delete(f"{path}/{{item_id}}")
307
+ async def delete_item(item_id: str):
308
+ if item_id not in items:
309
+ raise HTTPException(status_code=404, detail="Item not found")
310
+ del items[item_id]
311
+ return {"message": "Item deleted"}
312
+
313
+ return app
314
+
315
+
316
+ def create_service(
317
+ target: Union[Callable, Type[BaseModel], Type, Any],
318
+ *,
319
+ # Service configuration
320
+ config: Optional[ServiceConfig] = None,
321
+ host: str = "0.0.0.0",
322
+ port: int = 8000,
323
+ # Function-specific parameters
324
+ name: Optional[str] = None,
325
+ method: Literal[
326
+ "GET", "POST", "PUT", "DELETE", "PATCH", "OPTIONS", "HEAD"
327
+ ] = "POST",
328
+ path: str = "/",
329
+ # Model-specific parameters
330
+ methods: List[Literal["GET", "POST", "PUT", "DELETE"]] = None,
331
+ # FastAPI parameters
332
+ include_in_schema: bool = True,
333
+ dependencies: Optional[List[Callable[..., Any]]] = None,
334
+ tags: Optional[List[str]] = None,
335
+ description: Optional[str] = None,
336
+ # Server parameters
337
+ log_level: str = "info",
338
+ reload: bool = False,
339
+ workers: int = 1,
340
+ timeout_keep_alive: int = 5,
341
+ access_log: bool = True,
342
+ use_colors: bool = True,
343
+ auto_start: bool = True,
344
+ ) -> Union[FastAPI, Server]:
345
+ """
346
+ Create a service from a function, Pydantic model, dataclass, or other object.
347
+
348
+ Args:
349
+ target: The function or model to create a service from
350
+ config: ServiceConfig object (overrides individual parameters)
351
+ host: Host to bind to
352
+ port: Port to bind to
353
+ name: Service name (defaults to function/class name)
354
+ method: HTTP method for functions (GET, POST, etc.)
355
+ path: API path
356
+ methods: HTTP methods for models (list of methods)
357
+ include_in_schema: Include in OpenAPI schema
358
+ dependencies: FastAPI dependencies
359
+ tags: API tags
360
+ description: API description
361
+ log_level: Uvicorn log level
362
+ reload: Enable auto-reload
363
+ workers: Number of worker processes
364
+ timeout_keep_alive: Keep-alive timeout
365
+ access_log: Enable access logging
366
+ use_colors: Use colored logs
367
+ auto_start: Automatically start the server
368
+
369
+ Returns:
370
+ FastAPI app if auto_start=False, Server instance if auto_start=True
371
+ """
372
+ # Use config if provided, otherwise use individual parameters
373
+ if config:
374
+ host = config.host
375
+ port = config.port
376
+ log_level = config.log_level
377
+ reload = config.reload
378
+ workers = config.workers
379
+ timeout_keep_alive = config.timeout_keep_alive
380
+ access_log = config.access_log
381
+ use_colors = config.use_colors
382
+
383
+ # Determine if target is a function or model-like object
384
+ if callable(target) and not inspect.isclass(target):
385
+ # It's a function
386
+ app = _create_fastapi_from_function(
387
+ target,
388
+ name=name,
389
+ method=method,
390
+ path=path,
391
+ include_in_schema=include_in_schema,
392
+ dependencies=dependencies,
393
+ tags=tags,
394
+ description=description,
395
+ )
396
+ else:
397
+ # It's a model-like object (class, Pydantic model, dataclass, etc.)
398
+ app = _create_fastapi_from_model(
399
+ target,
400
+ name=name,
401
+ methods=methods,
402
+ path=path,
403
+ include_in_schema=include_in_schema,
404
+ dependencies=dependencies,
405
+ tags=tags,
406
+ description=description,
407
+ )
408
+
409
+ if not auto_start:
410
+ return app
411
+
412
+ # Set up uvloop if available
413
+ if hasattr(uvloop, "install"):
414
+ uvloop.install()
415
+
416
+ # Create and configure server
417
+ config_obj = Config(
418
+ app=app,
419
+ host=host,
420
+ port=port,
421
+ log_level=log_level,
422
+ reload=reload,
423
+ workers=workers,
424
+ timeout_keep_alive=timeout_keep_alive,
425
+ access_log=access_log,
426
+ use_colors=use_colors,
427
+ loop="uvloop" if hasattr(uvloop, "install") else "asyncio",
428
+ )
429
+
430
+ server = Server(config_obj)
431
+ _service_manager.add_server(server)
432
+
433
+ logger.info(f"Starting service on {host}:{port}")
434
+ _service_manager.status = ServiceStatus.STARTING
435
+
436
+ try:
437
+ server.run()
438
+ _service_manager.status = ServiceStatus.RUNNING
439
+ except Exception as e:
440
+ _service_manager.status = ServiceStatus.ERROR
441
+ logger.error(f"Service failed to start: {e}")
442
+ raise
443
+
444
+ return server
445
+
446
+
447
+ async def async_create_service(
448
+ target: Union[Callable, Type[BaseModel], Type, Any],
449
+ *,
450
+ config: Optional[ServiceConfig] = None,
451
+ **kwargs,
452
+ ) -> Union[FastAPI, Server]:
453
+ """
454
+ Async version of create_service.
455
+
456
+ Args:
457
+ target: The function or model to create a service from
458
+ config: ServiceConfig object
459
+ **kwargs: Same as create_service
460
+
461
+ Returns:
462
+ FastAPI app if auto_start=False, Server instance if auto_start=True
463
+ """
464
+ # Check if auto_start is provided and respect it
465
+ auto_start = kwargs.get("auto_start", True)
466
+
467
+ # Force auto_start=False to get the app first
468
+ kwargs["auto_start"] = False
469
+ app = create_service(target, config=config, **kwargs)
470
+
471
+ # If auto_start was False, just return the app
472
+ if not auto_start:
473
+ return app
474
+
475
+ # Use config if provided
476
+ if config:
477
+ host = config.host
478
+ port = config.port
479
+ log_level = config.log_level
480
+ reload = config.reload
481
+ workers = config.workers
482
+ timeout_keep_alive = config.timeout_keep_alive
483
+ access_log = config.access_log
484
+ use_colors = config.use_colors
485
+ else:
486
+ host = kwargs.get("host", "0.0.0.0")
487
+ port = kwargs.get("port", 8000)
488
+ log_level = kwargs.get("log_level", "info")
489
+ reload = kwargs.get("reload", False)
490
+ workers = kwargs.get("workers", 1)
491
+ timeout_keep_alive = kwargs.get("timeout_keep_alive", 5)
492
+ access_log = kwargs.get("access_log", True)
493
+ use_colors = kwargs.get("use_colors", True)
494
+
495
+ config_obj = Config(
496
+ app=app,
497
+ host=host,
498
+ port=port,
499
+ log_level=log_level,
500
+ reload=reload,
501
+ workers=workers,
502
+ timeout_keep_alive=timeout_keep_alive,
503
+ access_log=access_log,
504
+ use_colors=use_colors,
505
+ loop="uvloop" if hasattr(uvloop, "install") else "asyncio",
506
+ )
507
+
508
+ server = Server(config_obj)
509
+ _service_manager.add_server(server)
510
+
511
+ logger.info(f"Starting async service on {host}:{port}")
512
+ _service_manager.status = ServiceStatus.STARTING
513
+
514
+ try:
515
+ await server.serve()
516
+ _service_manager.status = ServiceStatus.RUNNING
517
+ except Exception as e:
518
+ _service_manager.status = ServiceStatus.ERROR
519
+ logger.error(f"Async service failed to start: {e}")
520
+ raise
521
+
522
+ return server
523
+
524
+
525
+ def shutdown_all_services():
526
+ """Shutdown all managed services."""
527
+ _service_manager.shutdown_all()
528
+
529
+
530
+ def get_service_status() -> ServiceStatus:
531
+ """Get the current service status."""
532
+ return _service_manager.status