watchdock-errors 0.3.1__tar.gz → 0.4.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 (27) hide show
  1. {watchdock_errors-0.3.1/src/watchdock_errors.egg-info → watchdock_errors-0.4.0}/PKG-INFO +25 -1
  2. {watchdock_errors-0.3.1 → watchdock_errors-0.4.0}/README.md +22 -0
  3. {watchdock_errors-0.3.1 → watchdock_errors-0.4.0}/pyproject.toml +1 -0
  4. {watchdock_errors-0.3.1 → watchdock_errors-0.4.0}/src/watchdock_errors/__init__.py +10 -1
  5. watchdock_errors-0.4.0/src/watchdock_errors/integrations/celery.py +280 -0
  6. {watchdock_errors-0.3.1 → watchdock_errors-0.4.0/src/watchdock_errors.egg-info}/PKG-INFO +25 -1
  7. {watchdock_errors-0.3.1 → watchdock_errors-0.4.0}/src/watchdock_errors.egg-info/SOURCES.txt +1 -0
  8. {watchdock_errors-0.3.1 → watchdock_errors-0.4.0}/src/watchdock_errors.egg-info/requires.txt +3 -0
  9. {watchdock_errors-0.3.1 → watchdock_errors-0.4.0}/src/watchdock_errors.egg-info/scm_file_list.json +12 -11
  10. watchdock_errors-0.4.0/src/watchdock_errors.egg-info/scm_version.json +8 -0
  11. watchdock_errors-0.3.1/src/watchdock_errors.egg-info/scm_version.json +0 -8
  12. {watchdock_errors-0.3.1 → watchdock_errors-0.4.0}/.github/workflows/ci.yml +0 -0
  13. {watchdock_errors-0.3.1 → watchdock_errors-0.4.0}/.github/workflows/release.yml +0 -0
  14. {watchdock_errors-0.3.1 → watchdock_errors-0.4.0}/.gitignore +0 -0
  15. {watchdock_errors-0.3.1 → watchdock_errors-0.4.0}/setup.cfg +0 -0
  16. {watchdock_errors-0.3.1 → watchdock_errors-0.4.0}/src/watchdock_errors/client.py +0 -0
  17. {watchdock_errors-0.3.1 → watchdock_errors-0.4.0}/src/watchdock_errors/config.py +0 -0
  18. {watchdock_errors-0.3.1 → watchdock_errors-0.4.0}/src/watchdock_errors/event.py +0 -0
  19. {watchdock_errors-0.3.1 → watchdock_errors-0.4.0}/src/watchdock_errors/integrations/__init__.py +0 -0
  20. {watchdock_errors-0.3.1 → watchdock_errors-0.4.0}/src/watchdock_errors/integrations/django.py +0 -0
  21. {watchdock_errors-0.3.1 → watchdock_errors-0.4.0}/src/watchdock_errors/integrations/fastapi.py +0 -0
  22. {watchdock_errors-0.3.1 → watchdock_errors-0.4.0}/src/watchdock_errors/utils.py +0 -0
  23. {watchdock_errors-0.3.1 → watchdock_errors-0.4.0}/src/watchdock_errors.egg-info/dependency_links.txt +0 -0
  24. {watchdock_errors-0.3.1 → watchdock_errors-0.4.0}/src/watchdock_errors.egg-info/top_level.txt +0 -0
  25. {watchdock_errors-0.3.1 → watchdock_errors-0.4.0}/tests/test_client.py +0 -0
  26. {watchdock_errors-0.3.1 → watchdock_errors-0.4.0}/tests/test_event.py +0 -0
  27. {watchdock_errors-0.3.1 → watchdock_errors-0.4.0}/tests/test_utils.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: watchdock-errors
3
- Version: 0.3.1
3
+ Version: 0.4.0
4
4
  Summary: Application error tracking SDK for the Watchdock platform
5
5
  License: MIT
6
6
  Keywords: error-tracking,observability,watchdock
@@ -12,6 +12,8 @@ Requires-Dist: django>=3.2; extra == "django"
12
12
  Provides-Extra: fastapi
13
13
  Requires-Dist: fastapi>=0.95; extra == "fastapi"
14
14
  Requires-Dist: starlette>=0.27; extra == "fastapi"
15
+ Provides-Extra: celery
16
+ Requires-Dist: celery>=5.0; extra == "celery"
15
17
  Provides-Extra: dev
16
18
  Requires-Dist: pytest>=7.0; extra == "dev"
17
19
  Requires-Dist: pytest-mock>=3.0; extra == "dev"
@@ -33,6 +35,7 @@ With framework extras:
33
35
  ```bash
34
36
  pip install "watchdock-errors[django]"
35
37
  pip install "watchdock-errors[fastapi]"
38
+ pip install "watchdock-errors[celery]"
36
39
  ```
37
40
 
38
41
  ## Quickstart
@@ -74,6 +77,27 @@ from watchdock_errors.integrations.fastapi import setup_watchdock
74
77
  setup_watchdock(app)
75
78
  ```
76
79
 
80
+ ### Celery
81
+
82
+ ```python
83
+ from celery import Celery
84
+ from watchdock_errors.integrations.celery import register
85
+
86
+ app = Celery("myproject")
87
+ app.config_from_object("django.conf:settings", namespace="CELERY")
88
+ app.autodiscover_tasks()
89
+
90
+ register(app)
91
+ ```
92
+
93
+ Call `register(app)` once, after `autodiscover_tasks()`. It reports, automatically:
94
+
95
+ - **Task registry** — every registered task (and, for periodic tasks, their beat schedule), once per worker on startup.
96
+ - **Failures, retries, and revocations** — via `task_failure`, `task_retry`, and `task_revoked`. Successful runs are not reported.
97
+ - **Heartbeats for periodic tasks** — a lightweight ping on every run of a beat-scheduled task, so Watchdock can flag one that's gone silent. Non-periodic tasks are never pinged.
98
+
99
+ `watchdock_errors.init()` must be called before these signals fire — if you're on Django, a `worker_process_init` handler that calls `init()` covers every forked worker process.
100
+
77
101
  ## Manual capture
78
102
 
79
103
  ```python
@@ -13,6 +13,7 @@ With framework extras:
13
13
  ```bash
14
14
  pip install "watchdock-errors[django]"
15
15
  pip install "watchdock-errors[fastapi]"
16
+ pip install "watchdock-errors[celery]"
16
17
  ```
17
18
 
18
19
  ## Quickstart
@@ -54,6 +55,27 @@ from watchdock_errors.integrations.fastapi import setup_watchdock
54
55
  setup_watchdock(app)
55
56
  ```
56
57
 
58
+ ### Celery
59
+
60
+ ```python
61
+ from celery import Celery
62
+ from watchdock_errors.integrations.celery import register
63
+
64
+ app = Celery("myproject")
65
+ app.config_from_object("django.conf:settings", namespace="CELERY")
66
+ app.autodiscover_tasks()
67
+
68
+ register(app)
69
+ ```
70
+
71
+ Call `register(app)` once, after `autodiscover_tasks()`. It reports, automatically:
72
+
73
+ - **Task registry** — every registered task (and, for periodic tasks, their beat schedule), once per worker on startup.
74
+ - **Failures, retries, and revocations** — via `task_failure`, `task_retry`, and `task_revoked`. Successful runs are not reported.
75
+ - **Heartbeats for periodic tasks** — a lightweight ping on every run of a beat-scheduled task, so Watchdock can flag one that's gone silent. Non-periodic tasks are never pinged.
76
+
77
+ `watchdock_errors.init()` must be called before these signals fire — if you're on Django, a `worker_process_init` handler that calls `init()` covers every forked worker process.
78
+
57
79
  ## Manual capture
58
80
 
59
81
  ```python
@@ -15,6 +15,7 @@ dependencies = ["requests>=2.28"]
15
15
  [project.optional-dependencies]
16
16
  django = ["django>=3.2"]
17
17
  fastapi = ["fastapi>=0.95", "starlette>=0.27"]
18
+ celery = ["celery>=5.0"]
18
19
  dev = [
19
20
  "pytest>=7.0",
20
21
  "pytest-mock>=3.0",
@@ -29,7 +29,7 @@ if TYPE_CHECKING:
29
29
  from .config import SDKConfig
30
30
  from .client import WatchdockClient
31
31
 
32
- __all__ = ["init", "capture_exception", "capture_message", "flush", "close", "__version__"]
32
+ __all__ = ["init", "capture_exception", "capture_message", "flush", "close", "get_config", "__version__"]
33
33
 
34
34
  _client: "WatchdockClient | None" = None
35
35
  _config: "SDKConfig | None" = None
@@ -139,6 +139,15 @@ def capture_message(
139
139
  _client.capture(event)
140
140
 
141
141
 
142
+ def get_config() -> "SDKConfig | None":
143
+ """Return the active SDKConfig, or None if init() has not been called.
144
+
145
+ Intended for integrations (e.g. celery.py) that need direct access to
146
+ api_key/endpoint/etc. rather than going through capture_exception/message.
147
+ """
148
+ return _config
149
+
150
+
142
151
  def flush(timeout: float = 2.0) -> None:
143
152
  """Block until all queued events have been sent or timeout expires."""
144
153
  if _client is not None:
@@ -0,0 +1,280 @@
1
+ """
2
+ Celery integration for watchdock-errors — task registry + failure/retry reporting.
3
+
4
+ Two things get reported automatically once wired up:
5
+
6
+ 1. Task registry: the customer's registered Celery tasks (and, for periodic
7
+ tasks, their beat schedule), reported once per worker right after Celery
8
+ finishes autodiscovering task modules. Tells Watchdock what tasks are
9
+ expected to exist — the prerequisite for beat-schedule health checks.
10
+
11
+ 2. Failures, retries, and revocations: every `task_failure`, `task_retry`,
12
+ and `task_revoked` is reported as it happens, with the exception,
13
+ traceback, task id/queue/worker hostname, and retry count vs.
14
+ max_retries. Successful runs are not reported — only what a dev
15
+ actually needs to see.
16
+
17
+ 3. Heartbeats for periodic (beat-scheduled) tasks only: a lightweight ping
18
+ on every `task_prerun` for a task name present in app.conf.beat_schedule,
19
+ so Watchdock can tell when a periodic task has gone silent. Non-periodic
20
+ tasks are never pinged — this stays cheap regardless of task volume.
21
+
22
+ Usage — call once from your Celery app module, after autodiscover_tasks():
23
+
24
+ from celery import Celery
25
+ from watchdock_errors.integrations.celery import register
26
+
27
+ app = Celery("myproject")
28
+ app.config_from_object("django.conf:settings", namespace="CELERY")
29
+ app.autodiscover_tasks()
30
+
31
+ register(app)
32
+
33
+ watchdock_errors.init() must have been called before these signals fire
34
+ (e.g. in a `worker_process_init` handler, or at import time) — if not,
35
+ the report is skipped.
36
+ """
37
+
38
+ from __future__ import annotations
39
+
40
+ import logging
41
+ import threading
42
+ from typing import TYPE_CHECKING
43
+
44
+ import requests
45
+
46
+ if TYPE_CHECKING:
47
+ from celery import Celery
48
+
49
+ logger = logging.getLogger("watchdock_errors")
50
+
51
+
52
+ def register(app: "Celery") -> None:
53
+ """Wire up the Watchdock Celery integration on the given Celery app."""
54
+ try:
55
+ from celery.signals import task_failure, task_prerun, task_retry, task_revoked, worker_ready
56
+ except ImportError:
57
+ logger.warning("watchdock_errors: celery is not installed — Celery integration disabled")
58
+ return
59
+
60
+ periodic_task_names = set(_periodic_schedules_by_task_name(app).keys())
61
+
62
+ @worker_ready.connect(weak=False)
63
+ def _on_worker_ready(sender=None, **kwargs) -> None:
64
+ _report_task_registry(app)
65
+
66
+ @task_prerun.connect(weak=False)
67
+ def _on_task_prerun(sender=None, task=None, **kwargs) -> None:
68
+ name = getattr(task, "name", None) or getattr(sender, "name", None)
69
+ if name in periodic_task_names:
70
+ _report_heartbeat(name)
71
+
72
+ @task_failure.connect(weak=False)
73
+ def _on_task_failure(sender=None, task_id=None, exception=None, einfo=None, **kwargs) -> None:
74
+ request = getattr(sender, "request", None)
75
+ _report_execution(
76
+ status="failure",
77
+ task_id=task_id,
78
+ task_name=getattr(sender, "name", None),
79
+ queue=getattr(sender, "queue", None),
80
+ worker_hostname=getattr(request, "hostname", None),
81
+ retries=getattr(request, "retries", None),
82
+ max_retries=getattr(sender, "max_retries", None),
83
+ exception_type=type(exception).__name__ if exception is not None else "",
84
+ exception_message=str(exception) if exception is not None else "",
85
+ traceback=str(einfo) if einfo is not None else "",
86
+ )
87
+
88
+ @task_retry.connect(weak=False)
89
+ def _on_task_retry(sender=None, request=None, reason=None, einfo=None, **kwargs) -> None:
90
+ _report_execution(
91
+ status="retry",
92
+ task_id=getattr(request, "id", None),
93
+ task_name=getattr(sender, "name", None),
94
+ queue=getattr(sender, "queue", None),
95
+ worker_hostname=getattr(request, "hostname", None),
96
+ retries=getattr(request, "retries", None),
97
+ max_retries=getattr(sender, "max_retries", None),
98
+ exception_type=type(reason).__name__ if reason is not None else "",
99
+ exception_message=str(reason) if reason is not None else "",
100
+ traceback=str(einfo) if einfo is not None else "",
101
+ )
102
+
103
+ @task_revoked.connect(weak=False)
104
+ def _on_task_revoked(sender=None, request=None, terminated=None, signum=None, expired=None, **kwargs) -> None:
105
+ if expired:
106
+ message = "Task expired before it could run"
107
+ elif terminated:
108
+ message = f"Task terminated mid-execution (signal {signum})"
109
+ else:
110
+ message = "Task revoked before it started running"
111
+
112
+ _report_execution(
113
+ status="revoked",
114
+ task_id=getattr(request, "id", None),
115
+ task_name=getattr(sender, "name", None),
116
+ queue=getattr(sender, "queue", None),
117
+ worker_hostname=getattr(request, "hostname", None),
118
+ retries=getattr(request, "retries", None),
119
+ max_retries=getattr(sender, "max_retries", None),
120
+ exception_type="TaskRevoked",
121
+ exception_message=message,
122
+ traceback="",
123
+ )
124
+
125
+
126
+ def _report_task_registry(app: "Celery") -> None:
127
+ config = _get_config_or_warn()
128
+ if config is None:
129
+ return
130
+
131
+ tasks = _collect_tasks(app)
132
+ threading.Thread(
133
+ target=_send_registry, args=(config, tasks), daemon=True, name="watchdock-celery-registry"
134
+ ).start()
135
+
136
+
137
+ def _report_execution(
138
+ status: str,
139
+ task_id: str | None,
140
+ task_name: str | None,
141
+ queue: str | None,
142
+ worker_hostname: str | None,
143
+ retries: int | None,
144
+ max_retries: int | None,
145
+ exception_type: str,
146
+ exception_message: str,
147
+ traceback: str,
148
+ ) -> None:
149
+ config = _get_config_or_warn()
150
+ if config is None:
151
+ return
152
+
153
+ event = {
154
+ "task_id": task_id or "",
155
+ "task_name": task_name or "unknown",
156
+ "status": status,
157
+ "queue": queue or "",
158
+ "worker_hostname": worker_hostname or "",
159
+ "retries": retries,
160
+ "max_retries": max_retries,
161
+ "exception_type": exception_type,
162
+ "exception_message": exception_message,
163
+ "traceback": traceback,
164
+ }
165
+ threading.Thread(
166
+ target=_send_execution, args=(config, event), daemon=True, name="watchdock-celery-execution"
167
+ ).start()
168
+
169
+
170
+ def _report_heartbeat(task_name: str) -> None:
171
+ config = _get_config_or_warn()
172
+ if config is None:
173
+ return
174
+ threading.Thread(
175
+ target=_send_heartbeat, args=(config, task_name), daemon=True, name="watchdock-celery-heartbeat"
176
+ ).start()
177
+
178
+
179
+ def _get_config_or_warn():
180
+ import watchdock_errors
181
+
182
+ config = watchdock_errors.get_config()
183
+ if config is None:
184
+ logger.warning("watchdock_errors: celery integration fired before init() — skipping report")
185
+ return config
186
+
187
+
188
+ def _periodic_schedules_by_task_name(app: "Celery") -> dict:
189
+ beat_schedule = app.conf.beat_schedule or {}
190
+ schedules_by_task_name = {}
191
+ for entry in beat_schedule.values():
192
+ task_name = entry.get("task") if isinstance(entry, dict) else getattr(entry, "task", None)
193
+ if task_name:
194
+ schedules_by_task_name[task_name] = _serialize_schedule(entry)
195
+ return schedules_by_task_name
196
+
197
+
198
+ def _collect_tasks(app: "Celery") -> list[dict]:
199
+ schedules_by_task_name = _periodic_schedules_by_task_name(app)
200
+
201
+ tasks = []
202
+ for name, task in app.tasks.items():
203
+ if name.startswith("celery."):
204
+ continue
205
+ tasks.append(
206
+ {
207
+ "name": name,
208
+ "module": getattr(task, "__module__", "") or "",
209
+ "queue": getattr(task, "queue", "") or "",
210
+ "max_retries": getattr(task, "max_retries", None),
211
+ "is_periodic": name in schedules_by_task_name,
212
+ "schedule": schedules_by_task_name.get(name),
213
+ }
214
+ )
215
+ return tasks
216
+
217
+
218
+ def _serialize_schedule(entry) -> dict | None:
219
+ schedule = entry.get("schedule") if isinstance(entry, dict) else getattr(entry, "schedule", None)
220
+ if schedule is None:
221
+ return None
222
+
223
+ if hasattr(schedule, "_orig_minute"): # celery.schedules.crontab
224
+ return {
225
+ "type": "crontab",
226
+ "minute": schedule._orig_minute,
227
+ "hour": schedule._orig_hour,
228
+ "day_of_week": schedule._orig_day_of_week,
229
+ "day_of_month": schedule._orig_day_of_month,
230
+ "month_of_year": schedule._orig_month_of_year,
231
+ }
232
+
233
+ seconds = getattr(schedule, "seconds", schedule) # celery.schedules.schedule or plain number
234
+ try:
235
+ return {"type": "interval", "every_seconds": float(seconds)}
236
+ except (TypeError, ValueError):
237
+ return {"type": "unknown"}
238
+
239
+
240
+ def _send_registry(config, tasks: list[dict]) -> None:
241
+ if not tasks:
242
+ return
243
+ _post(config, "/api/v1/celery/registry/", {"environment": config.environment, "tasks": tasks}, "task", len(tasks))
244
+
245
+
246
+ def _send_execution(config, event: dict) -> None:
247
+ payload = {**event, "environment": config.environment}
248
+ _post(config, "/api/v1/celery/executions/", payload, "execution", 1)
249
+
250
+
251
+ def _send_heartbeat(config, task_name: str) -> None:
252
+ payload = {"task_name": task_name, "environment": config.environment}
253
+ _post(config, "/api/v1/celery/heartbeat/", payload, "heartbeat", 1)
254
+
255
+
256
+ def _post(config, path: str, payload: dict, noun: str, count: int) -> None:
257
+ url = f"{config.endpoint.rstrip('/')}{path}"
258
+ try:
259
+ response = requests.post(
260
+ url,
261
+ json=payload,
262
+ headers={
263
+ "Authorization": f"Bearer {config.api_key}",
264
+ "Content-Type": "application/json",
265
+ "User-Agent": f"watchdock-errors/{config.sdk_version}",
266
+ },
267
+ timeout=config.timeout * 3,
268
+ )
269
+ if response.status_code in (200, 201, 202):
270
+ logger.info("watchdock_errors: reported %d celery %s(s) to %s", count, noun, path)
271
+ else:
272
+ logger.warning(
273
+ "watchdock_errors: celery %s rejected — status=%s body=%s", noun, response.status_code, response.text[:500]
274
+ )
275
+ except requests.exceptions.ConnectionError as e:
276
+ logger.error("watchdock_errors: connection failed reporting celery %s — %s", noun, e)
277
+ except requests.exceptions.Timeout:
278
+ logger.error("watchdock_errors: timed out reporting celery %s after %ss", noun, config.timeout * 3)
279
+ except Exception:
280
+ logger.error("watchdock_errors: unexpected error reporting celery %s", noun, exc_info=True)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: watchdock-errors
3
- Version: 0.3.1
3
+ Version: 0.4.0
4
4
  Summary: Application error tracking SDK for the Watchdock platform
5
5
  License: MIT
6
6
  Keywords: error-tracking,observability,watchdock
@@ -12,6 +12,8 @@ Requires-Dist: django>=3.2; extra == "django"
12
12
  Provides-Extra: fastapi
13
13
  Requires-Dist: fastapi>=0.95; extra == "fastapi"
14
14
  Requires-Dist: starlette>=0.27; extra == "fastapi"
15
+ Provides-Extra: celery
16
+ Requires-Dist: celery>=5.0; extra == "celery"
15
17
  Provides-Extra: dev
16
18
  Requires-Dist: pytest>=7.0; extra == "dev"
17
19
  Requires-Dist: pytest-mock>=3.0; extra == "dev"
@@ -33,6 +35,7 @@ With framework extras:
33
35
  ```bash
34
36
  pip install "watchdock-errors[django]"
35
37
  pip install "watchdock-errors[fastapi]"
38
+ pip install "watchdock-errors[celery]"
36
39
  ```
37
40
 
38
41
  ## Quickstart
@@ -74,6 +77,27 @@ from watchdock_errors.integrations.fastapi import setup_watchdock
74
77
  setup_watchdock(app)
75
78
  ```
76
79
 
80
+ ### Celery
81
+
82
+ ```python
83
+ from celery import Celery
84
+ from watchdock_errors.integrations.celery import register
85
+
86
+ app = Celery("myproject")
87
+ app.config_from_object("django.conf:settings", namespace="CELERY")
88
+ app.autodiscover_tasks()
89
+
90
+ register(app)
91
+ ```
92
+
93
+ Call `register(app)` once, after `autodiscover_tasks()`. It reports, automatically:
94
+
95
+ - **Task registry** — every registered task (and, for periodic tasks, their beat schedule), once per worker on startup.
96
+ - **Failures, retries, and revocations** — via `task_failure`, `task_retry`, and `task_revoked`. Successful runs are not reported.
97
+ - **Heartbeats for periodic tasks** — a lightweight ping on every run of a beat-scheduled task, so Watchdock can flag one that's gone silent. Non-periodic tasks are never pinged.
98
+
99
+ `watchdock_errors.init()` must be called before these signals fire — if you're on Django, a `worker_process_init` handler that calls `init()` covers every forked worker process.
100
+
77
101
  ## Manual capture
78
102
 
79
103
  ```python
@@ -16,6 +16,7 @@ src/watchdock_errors.egg-info/scm_file_list.json
16
16
  src/watchdock_errors.egg-info/scm_version.json
17
17
  src/watchdock_errors.egg-info/top_level.txt
18
18
  src/watchdock_errors/integrations/__init__.py
19
+ src/watchdock_errors/integrations/celery.py
19
20
  src/watchdock_errors/integrations/django.py
20
21
  src/watchdock_errors/integrations/fastapi.py
21
22
  tests/test_client.py
@@ -1,5 +1,8 @@
1
1
  requests>=2.28
2
2
 
3
+ [celery]
4
+ celery>=5.0
5
+
3
6
  [dev]
4
7
  pytest>=7.0
5
8
  pytest-mock>=3.0
@@ -1,20 +1,21 @@
1
1
  {
2
2
  "files": [
3
- "pyproject.toml",
4
- "README.md",
5
- ".gitignore",
6
3
  ".github/workflows/ci.yml",
7
4
  ".github/workflows/release.yml",
8
- "tests/test_event.py",
9
- "tests/test_client.py",
10
- "tests/test_utils.py",
11
- "src/watchdock_errors/config.py",
12
- "src/watchdock_errors/event.py",
5
+ ".gitignore",
6
+ "README.md",
7
+ "pyproject.toml",
13
8
  "src/watchdock_errors/__init__.py",
14
9
  "src/watchdock_errors/client.py",
15
- "src/watchdock_errors/utils.py",
16
- "src/watchdock_errors/integrations/django.py",
10
+ "src/watchdock_errors/config.py",
11
+ "src/watchdock_errors/event.py",
17
12
  "src/watchdock_errors/integrations/__init__.py",
18
- "src/watchdock_errors/integrations/fastapi.py"
13
+ "src/watchdock_errors/integrations/celery.py",
14
+ "src/watchdock_errors/integrations/django.py",
15
+ "src/watchdock_errors/integrations/fastapi.py",
16
+ "src/watchdock_errors/utils.py",
17
+ "tests/test_client.py",
18
+ "tests/test_event.py",
19
+ "tests/test_utils.py"
19
20
  ]
20
21
  }
@@ -0,0 +1,8 @@
1
+ {
2
+ "tag": "0.4.0",
3
+ "distance": 0,
4
+ "node": "gcbc7ed27dbd4e8399c6f64c46a993f99ab8f34ce",
5
+ "dirty": false,
6
+ "branch": "HEAD",
7
+ "node_date": "2026-08-13"
8
+ }
@@ -1,8 +0,0 @@
1
- {
2
- "tag": "0.3.1",
3
- "distance": 0,
4
- "node": "g1150220284b230c25b3e407265277e351f14b2f7",
5
- "dirty": false,
6
- "branch": "HEAD",
7
- "node_date": "2026-07-23"
8
- }