queuelens 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.
queuelens/__init__.py ADDED
@@ -0,0 +1,5 @@
1
+ from queuelens.client import QueueLens
2
+
3
+ __version__ = "0.1.0"
4
+
5
+ __all__ = ["QueueLens"]
queuelens/client.py ADDED
@@ -0,0 +1,91 @@
1
+
2
+ import logging
3
+ from typing import Any, Self
4
+
5
+ from queuelens.config import QueueLensConfig
6
+ from queuelens.exceptions import QueueLensConfigurationError
7
+
8
+ logger = logging.getLogger(__name__)
9
+
10
+
11
+ class QueueLens:
12
+ """Public client for integrating QueueLens with Celery applications."""
13
+
14
+ def __init__(
15
+ self,
16
+ api_key: str | None = None,
17
+ endpoint: str | None = None,
18
+ batch_size: int | None = None,
19
+ flush_interval: float | None = None,
20
+ timeout: float | None = None,
21
+ buffer_capacity: int | None = None,
22
+ max_batches_per_cycle: int | None = None,
23
+ shutdown_timeout: float | None = None,
24
+ ) -> None:
25
+ try:
26
+ self._config = QueueLensConfig.from_env(
27
+ api_key=api_key,
28
+ endpoint=endpoint,
29
+ batch_size=batch_size,
30
+ flush_interval=flush_interval,
31
+ timeout=timeout,
32
+ buffer_capacity=buffer_capacity,
33
+ max_batches_per_cycle=max_batches_per_cycle,
34
+ shutdown_timeout=shutdown_timeout,
35
+ )
36
+ except (TypeError, ValueError) as exc:
37
+ raise QueueLensConfigurationError(str(exc)) from exc
38
+
39
+ self._integrations: dict[int, Any] = {}
40
+ self._closed = False
41
+
42
+ def instrument(self, celery_app: Any) -> Any:
43
+ """Instrument a Celery application with QueueLens."""
44
+
45
+ if self._closed:
46
+ raise RuntimeError(
47
+ "QueueLens client is closed and cannot instrument applications"
48
+ )
49
+
50
+ from queuelens.instrumentation.celery import CeleryInstrumentation
51
+
52
+ app_id = id(celery_app)
53
+
54
+ if app_id in self._integrations:
55
+ return self._integrations[app_id]
56
+
57
+ integration = CeleryInstrumentation(self._config)
58
+ integration.instrument(celery_app)
59
+
60
+ self._integrations[app_id] = integration
61
+
62
+ logger.info("QueueLens instrumented Celery application")
63
+
64
+ return integration
65
+
66
+ def close(self) -> None:
67
+ """Close all registered integrations."""
68
+
69
+ self._closed = True
70
+
71
+ for app_id, integration in list(self._integrations.items()):
72
+ try:
73
+ integration.close()
74
+ except Exception:
75
+ logger.exception(
76
+ "QueueLens failed to close an integration for app %s",
77
+ app_id,
78
+ )
79
+ else:
80
+ self._integrations.pop(app_id, None)
81
+
82
+ logger.info("QueueLens client shutdown completed")
83
+
84
+ def __enter__(self) -> Self:
85
+ if self._closed:
86
+ raise RuntimeError("QueueLens client is already closed")
87
+
88
+ return self
89
+
90
+ def __exit__(self, exc_type, exc_value, traceback) -> None:
91
+ self.close()
queuelens/config.py ADDED
@@ -0,0 +1,100 @@
1
+ import os
2
+ from dataclasses import dataclass
3
+
4
+
5
+ @dataclass(frozen=True)
6
+ class QueueLensConfig:
7
+ api_key: str
8
+ endpoint: str
9
+ batch_size: int = 100
10
+ flush_interval: float = 5.0
11
+ timeout: float = 5.0
12
+ buffer_capacity: int = 1000
13
+ max_batches_per_cycle: int = 10
14
+ shutdown_timeout: float = 10.0
15
+
16
+ def __post_init__(self) -> None:
17
+ if not self.api_key.strip():
18
+ raise ValueError("API key cannot be empty")
19
+
20
+ if not self.endpoint.strip():
21
+ raise ValueError("Endpoint cannot be empty")
22
+
23
+ if self.batch_size < 1:
24
+ raise ValueError("batch_size must be at least 1")
25
+
26
+ if self.flush_interval <= 0:
27
+ raise ValueError("flush_interval must be greater than 0")
28
+
29
+ if self.timeout <= 0:
30
+ raise ValueError("timeout must be greater than 0")
31
+
32
+ if self.buffer_capacity < 1:
33
+ raise ValueError("buffer_capacity must be at least 1")
34
+
35
+ if self.max_batches_per_cycle < 1:
36
+ raise ValueError("max_batches_per_cycle must be at least 1")
37
+
38
+ if self.shutdown_timeout <= 0:
39
+ raise ValueError("shutdown_timeout must be greater than 0")
40
+
41
+ @classmethod
42
+ def from_env(
43
+ cls,
44
+ *,
45
+ api_key: str | None = None,
46
+ endpoint: str | None = None,
47
+ batch_size: int | None = None,
48
+ flush_interval: float | None = None,
49
+ timeout: float | None = None,
50
+ buffer_capacity: int | None = None,
51
+ max_batches_per_cycle: int | None = None,
52
+ shutdown_timeout: float | None = None,
53
+ ) -> "QueueLensConfig":
54
+ resolved_api_key = (
55
+ api_key if api_key is not None else os.getenv("QUEUELENS_API_KEY")
56
+ )
57
+ resolved_endpoint = (
58
+ endpoint if endpoint is not None else os.getenv("QUEUELENS_ENDPOINT")
59
+ )
60
+
61
+ if resolved_api_key is None:
62
+ raise ValueError("QUEUELENS_API_KEY is required")
63
+
64
+ if resolved_endpoint is None:
65
+ raise ValueError("QUEUELENS_ENDPOINT is required")
66
+
67
+ return cls(
68
+ api_key=resolved_api_key,
69
+ endpoint=resolved_endpoint,
70
+ batch_size=(
71
+ batch_size
72
+ if batch_size is not None
73
+ else int(os.getenv("QUEUELENS_BATCH_SIZE", "100"))
74
+ ),
75
+ flush_interval=(
76
+ flush_interval
77
+ if flush_interval is not None
78
+ else float(os.getenv("QUEUELENS_FLUSH_INTERVAL", "5"))
79
+ ),
80
+ timeout=(
81
+ timeout
82
+ if timeout is not None
83
+ else float(os.getenv("QUEUELENS_TIMEOUT", "5"))
84
+ ),
85
+ buffer_capacity=(
86
+ buffer_capacity
87
+ if buffer_capacity is not None
88
+ else int(os.getenv("QUEUELENS_BUFFER_CAPACITY", "1000"))
89
+ ),
90
+ max_batches_per_cycle=(
91
+ max_batches_per_cycle
92
+ if max_batches_per_cycle is not None
93
+ else int(os.getenv("QUEUELENS_MAX_BATCHES_PER_CYCLE", "10"))
94
+ ),
95
+ shutdown_timeout=(
96
+ shutdown_timeout
97
+ if shutdown_timeout is not None
98
+ else float(os.getenv("QUEUELENS_SHUTDOWN_TIMEOUT", "10"))
99
+ ),
100
+ )
queuelens/events.py ADDED
@@ -0,0 +1,46 @@
1
+ import uuid
2
+ from datetime import UTC, datetime
3
+ from enum import StrEnum
4
+
5
+ from pydantic import BaseModel, ConfigDict, Field, field_validator
6
+
7
+
8
+ class JobEventType(StrEnum):
9
+ RECEIVED = "received"
10
+ STARTED = "started"
11
+ SUCCEEDED = "succeeded"
12
+ FAILED = "failed"
13
+ RETRIED = "retried"
14
+
15
+
16
+ class JobEvent(BaseModel):
17
+ model_config = ConfigDict(extra="forbid")
18
+
19
+ event_id: uuid.UUID = Field(default_factory=uuid.uuid4)
20
+ task_id: str = Field(min_length=1)
21
+ task_name: str = Field(min_length=1)
22
+ event_type: JobEventType
23
+ occurred_at: datetime = Field(default_factory=lambda: datetime.now(UTC))
24
+
25
+ worker_name: str | None = None
26
+ queue_name: str | None = None
27
+ attempt: int = Field(default=1, ge=1)
28
+
29
+ exchange: str | None = Field(default=None, max_length=255)
30
+ routing_key: str | None = Field(default=None, max_length=255)
31
+
32
+ exception_type: str | None = Field(default=None, max_length=255)
33
+ exception_message: str | None = Field(default=None, max_length=4096)
34
+ traceback: str | None = Field(default=None, max_length=16000)
35
+
36
+ @field_validator("occurred_at")
37
+ @classmethod
38
+ def require_timezone(cls, value: datetime) -> datetime:
39
+ if value.tzinfo is None or value.utcoffset() is None:
40
+ raise ValueError("occurred_at must include timezone information")
41
+ return value
42
+
43
+
44
+ class IngestionResponse(BaseModel):
45
+ accepted_count: int = Field(ge=0)
46
+ duplicate_count: int = Field(ge=0)
@@ -0,0 +1,6 @@
1
+ class QueueLensError(Exception):
2
+ """Base exception for the QueueLens SDK."""
3
+
4
+
5
+ class QueueLensConfigurationError(QueueLensError):
6
+ """Raised when SDK configuration is invalid."""
@@ -0,0 +1,3 @@
1
+ from queuelens.instrumentation.celery import CeleryInstrumentation
2
+
3
+ __all__ = ["CeleryInstrumentation"]