road24-artifacthub 0.1.0__tar.gz → 0.1.2__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 (64) hide show
  1. {road24_artifacthub-0.1.0 → road24_artifacthub-0.1.2}/Makefile +5 -0
  2. {road24_artifacthub-0.1.0 → road24_artifacthub-0.1.2}/PKG-INFO +15 -26
  3. {road24_artifacthub-0.1.0 → road24_artifacthub-0.1.2}/README.md +14 -25
  4. {road24_artifacthub-0.1.0 → road24_artifacthub-0.1.2}/pyproject.toml +1 -1
  5. {road24_artifacthub-0.1.0 → road24_artifacthub-0.1.2}/road24_sdk/integrations/fastapi.py +23 -4
  6. {road24_artifacthub-0.1.0 → road24_artifacthub-0.1.2}/road24_sdk/integrations/httpx.py +27 -2
  7. {road24_artifacthub-0.1.0 → road24_artifacthub-0.1.2}/tests/test_integrations/test_fastapi.py +23 -3
  8. {road24_artifacthub-0.1.0 → road24_artifacthub-0.1.2}/.claude/CLAUDE.md +0 -0
  9. {road24_artifacthub-0.1.0 → road24_artifacthub-0.1.2}/.claude/agents/engineer.md +0 -0
  10. {road24_artifacthub-0.1.0 → road24_artifacthub-0.1.2}/.claude/agents/tester.md +0 -0
  11. {road24_artifacthub-0.1.0 → road24_artifacthub-0.1.2}/.gitignore +0 -0
  12. {road24_artifacthub-0.1.0 → road24_artifacthub-0.1.2}/.hooks/check-commit-msg.sh +0 -0
  13. {road24_artifacthub-0.1.0 → road24_artifacthub-0.1.2}/.pre-commit-config.yaml +0 -0
  14. {road24_artifacthub-0.1.0 → road24_artifacthub-0.1.2}/commands/__init__.py +0 -0
  15. {road24_artifacthub-0.1.0 → road24_artifacthub-0.1.2}/commands/test_logs_db.py +0 -0
  16. {road24_artifacthub-0.1.0 → road24_artifacthub-0.1.2}/commands/test_logs_exceptions.py +0 -0
  17. {road24_artifacthub-0.1.0 → road24_artifacthub-0.1.2}/commands/test_logs_http_input.py +0 -0
  18. {road24_artifacthub-0.1.0 → road24_artifacthub-0.1.2}/commands/test_logs_http_output.py +0 -0
  19. {road24_artifacthub-0.1.0 → road24_artifacthub-0.1.2}/commands/test_logs_redis.py +0 -0
  20. {road24_artifacthub-0.1.0 → road24_artifacthub-0.1.2}/commands/test_metrics_http.py +0 -0
  21. {road24_artifacthub-0.1.0 → road24_artifacthub-0.1.2}/examples/__init__.py +0 -0
  22. {road24_artifacthub-0.1.0 → road24_artifacthub-0.1.2}/examples/core/__init__.py +0 -0
  23. {road24_artifacthub-0.1.0 → road24_artifacthub-0.1.2}/examples/core/database/__init__.py +0 -0
  24. {road24_artifacthub-0.1.0 → road24_artifacthub-0.1.2}/examples/core/database/config.py +0 -0
  25. {road24_artifacthub-0.1.0 → road24_artifacthub-0.1.2}/examples/core/database/mixins.py +0 -0
  26. {road24_artifacthub-0.1.0 → road24_artifacthub-0.1.2}/examples/core/http.py +0 -0
  27. {road24_artifacthub-0.1.0 → road24_artifacthub-0.1.2}/examples/core/redis.py +0 -0
  28. {road24_artifacthub-0.1.0 → road24_artifacthub-0.1.2}/examples/core/road24.py +0 -0
  29. {road24_artifacthub-0.1.0 → road24_artifacthub-0.1.2}/examples/core/settings.py +0 -0
  30. {road24_artifacthub-0.1.0 → road24_artifacthub-0.1.2}/examples/core/utils.py +0 -0
  31. {road24_artifacthub-0.1.0 → road24_artifacthub-0.1.2}/examples/src/__init__.py +0 -0
  32. {road24_artifacthub-0.1.0 → road24_artifacthub-0.1.2}/examples/src/main.py +0 -0
  33. {road24_artifacthub-0.1.0 → road24_artifacthub-0.1.2}/main.py +0 -0
  34. {road24_artifacthub-0.1.0 → road24_artifacthub-0.1.2}/road24_sdk/__init__.py +0 -0
  35. {road24_artifacthub-0.1.0 → road24_artifacthub-0.1.2}/road24_sdk/_formatter.py +0 -0
  36. {road24_artifacthub-0.1.0 → road24_artifacthub-0.1.2}/road24_sdk/_sanitizer.py +0 -0
  37. {road24_artifacthub-0.1.0 → road24_artifacthub-0.1.2}/road24_sdk/_schemas.py +0 -0
  38. {road24_artifacthub-0.1.0 → road24_artifacthub-0.1.2}/road24_sdk/_types.py +0 -0
  39. {road24_artifacthub-0.1.0 → road24_artifacthub-0.1.2}/road24_sdk/integrations/__init__.py +0 -0
  40. {road24_artifacthub-0.1.0 → road24_artifacthub-0.1.2}/road24_sdk/integrations/_base.py +0 -0
  41. {road24_artifacthub-0.1.0 → road24_artifacthub-0.1.2}/road24_sdk/integrations/redis.py +0 -0
  42. {road24_artifacthub-0.1.0 → road24_artifacthub-0.1.2}/road24_sdk/integrations/sqlalchemy.py +0 -0
  43. {road24_artifacthub-0.1.0 → road24_artifacthub-0.1.2}/road24_sdk/metrics/__init__.py +0 -0
  44. {road24_artifacthub-0.1.0 → road24_artifacthub-0.1.2}/road24_sdk/metrics/db.py +0 -0
  45. {road24_artifacthub-0.1.0 → road24_artifacthub-0.1.2}/road24_sdk/metrics/http.py +0 -0
  46. {road24_artifacthub-0.1.0 → road24_artifacthub-0.1.2}/road24_sdk/metrics/redis.py +0 -0
  47. {road24_artifacthub-0.1.0 → road24_artifacthub-0.1.2}/tests/__init__.py +0 -0
  48. {road24_artifacthub-0.1.0 → road24_artifacthub-0.1.2}/tests/conftest.py +0 -0
  49. {road24_artifacthub-0.1.0 → road24_artifacthub-0.1.2}/tests/test_data_classes.py +0 -0
  50. {road24_artifacthub-0.1.0 → road24_artifacthub-0.1.2}/tests/test_formatter.py +0 -0
  51. {road24_artifacthub-0.1.0 → road24_artifacthub-0.1.2}/tests/test_init.py +0 -0
  52. {road24_artifacthub-0.1.0 → road24_artifacthub-0.1.2}/tests/test_integrations/__init__.py +0 -0
  53. {road24_artifacthub-0.1.0 → road24_artifacthub-0.1.2}/tests/test_integrations/test_base.py +0 -0
  54. {road24_artifacthub-0.1.0 → road24_artifacthub-0.1.2}/tests/test_integrations/test_httpx.py +0 -0
  55. {road24_artifacthub-0.1.0 → road24_artifacthub-0.1.2}/tests/test_integrations/test_redis.py +0 -0
  56. {road24_artifacthub-0.1.0 → road24_artifacthub-0.1.2}/tests/test_integrations/test_sqlalchemy.py +0 -0
  57. {road24_artifacthub-0.1.0 → road24_artifacthub-0.1.2}/tests/test_metrics/__init__.py +0 -0
  58. {road24_artifacthub-0.1.0 → road24_artifacthub-0.1.2}/tests/test_metrics/test_db.py +0 -0
  59. {road24_artifacthub-0.1.0 → road24_artifacthub-0.1.2}/tests/test_metrics/test_http.py +0 -0
  60. {road24_artifacthub-0.1.0 → road24_artifacthub-0.1.2}/tests/test_metrics/test_redis.py +0 -0
  61. {road24_artifacthub-0.1.0 → road24_artifacthub-0.1.2}/tests/test_sanitizer.py +0 -0
  62. {road24_artifacthub-0.1.0 → road24_artifacthub-0.1.2}/tests/test_schemas.py +0 -0
  63. {road24_artifacthub-0.1.0 → road24_artifacthub-0.1.2}/tests/test_types.py +0 -0
  64. {road24_artifacthub-0.1.0 → road24_artifacthub-0.1.2}/uv.lock +0 -0
@@ -16,6 +16,11 @@ format:
16
16
 
17
17
  check: lint test
18
18
 
19
+ push_to_pypi:
20
+ rm -rf dist/
21
+ python -m build
22
+ python -m twine upload dist/*
23
+
19
24
  # --- Manual test scripts (no server needed) ---
20
25
 
21
26
  test-logs-http-input:
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: road24-artifacthub
3
- Version: 0.1.0
3
+ Version: 0.1.2
4
4
  Summary: Shared logging and metrics library for Road24 FastAPI microservices
5
5
  Requires-Python: >=3.12
6
6
  Requires-Dist: build>=1.4.0
@@ -56,28 +56,27 @@ pip install road24-artifacthub[dev]
56
56
  ```python
57
57
  import road24_sdk
58
58
  from fastapi import FastAPI
59
+ from httpx import AsyncClient, Timeout
59
60
  from road24_sdk.integrations.fastapi import FastApiLoggingIntegration
60
61
  from road24_sdk.integrations.httpx import HttpxLoggingIntegration
61
62
  from road24_sdk.integrations.redis import RedisLoggingIntegration
62
63
  from road24_sdk.integrations.sqlalchemy import SqlalchemyLoggingIntegration
63
64
 
64
- road24_sdk.init(
65
- service_name="my-service",
66
- log_level="INFO",
67
- integrations=[
68
- FastApiLoggingIntegration(),
69
- HttpxLoggingIntegration(),
70
- SqlalchemyLoggingIntegration(),
71
- RedisLoggingIntegration(),
72
- ],
73
- )
74
-
75
- # FastAPI still requires .setup(app) since it needs the app instance
65
+ def setup_road24_sdk():
66
+ road24_sdk.init(
67
+ service_name="my-service",
68
+ log_level="INFO",
69
+ integrations=[
70
+ HttpxLoggingIntegration(),
71
+ SqlalchemyLoggingIntegration(),
72
+ RedisLoggingIntegration(),
73
+ ],
74
+ )
75
+
76
+ # FastAPI requires .setup(app) since it needs the app instance
76
77
  app = FastAPI()
78
+ setup_road24_sdk()
77
79
  FastApiLoggingIntegration().setup(app)
78
-
79
- # All other clients are auto-instrumented — no .setup() needed
80
- client = AsyncClient(timeout=Timeout(25.0)) # already instrumented
81
80
  ```
82
81
 
83
82
  ## Available Integrations
@@ -89,16 +88,6 @@ client = AsyncClient(timeout=Timeout(25.0)) # already instrumented
89
88
  | `SqlalchemyLoggingIntegration` | `sqlalchemy` | Database query logging and metrics |
90
89
  | `RedisLoggingIntegration` | `redis` | Redis command logging and metrics |
91
90
 
92
- Pass integrations to `init()` for automatic class-level patching. All new client instances are auto-instrumented.
93
-
94
- The `.setup(client)` method is still available for per-instance patching (backwards compatible):
95
-
96
- ```python
97
- # Per-instance patching (still works)
98
- client = AsyncClient(timeout=Timeout(25.0))
99
- HttpxLoggingIntegration().setup(client)
100
- ```
101
-
102
91
  ## Manual Testing
103
92
 
104
93
  Standalone scripts to inspect structured JSON logs and metrics (no server needed):
@@ -20,28 +20,27 @@ pip install road24-artifacthub[dev]
20
20
  ```python
21
21
  import road24_sdk
22
22
  from fastapi import FastAPI
23
+ from httpx import AsyncClient, Timeout
23
24
  from road24_sdk.integrations.fastapi import FastApiLoggingIntegration
24
25
  from road24_sdk.integrations.httpx import HttpxLoggingIntegration
25
26
  from road24_sdk.integrations.redis import RedisLoggingIntegration
26
27
  from road24_sdk.integrations.sqlalchemy import SqlalchemyLoggingIntegration
27
28
 
28
- road24_sdk.init(
29
- service_name="my-service",
30
- log_level="INFO",
31
- integrations=[
32
- FastApiLoggingIntegration(),
33
- HttpxLoggingIntegration(),
34
- SqlalchemyLoggingIntegration(),
35
- RedisLoggingIntegration(),
36
- ],
37
- )
38
-
39
- # FastAPI still requires .setup(app) since it needs the app instance
29
+ def setup_road24_sdk():
30
+ road24_sdk.init(
31
+ service_name="my-service",
32
+ log_level="INFO",
33
+ integrations=[
34
+ HttpxLoggingIntegration(),
35
+ SqlalchemyLoggingIntegration(),
36
+ RedisLoggingIntegration(),
37
+ ],
38
+ )
39
+
40
+ # FastAPI requires .setup(app) since it needs the app instance
40
41
  app = FastAPI()
42
+ setup_road24_sdk()
41
43
  FastApiLoggingIntegration().setup(app)
42
-
43
- # All other clients are auto-instrumented — no .setup() needed
44
- client = AsyncClient(timeout=Timeout(25.0)) # already instrumented
45
44
  ```
46
45
 
47
46
  ## Available Integrations
@@ -53,16 +52,6 @@ client = AsyncClient(timeout=Timeout(25.0)) # already instrumented
53
52
  | `SqlalchemyLoggingIntegration` | `sqlalchemy` | Database query logging and metrics |
54
53
  | `RedisLoggingIntegration` | `redis` | Redis command logging and metrics |
55
54
 
56
- Pass integrations to `init()` for automatic class-level patching. All new client instances are auto-instrumented.
57
-
58
- The `.setup(client)` method is still available for per-instance patching (backwards compatible):
59
-
60
- ```python
61
- # Per-instance patching (still works)
62
- client = AsyncClient(timeout=Timeout(25.0))
63
- HttpxLoggingIntegration().setup(client)
64
- ```
65
-
66
55
  ## Manual Testing
67
56
 
68
57
  Standalone scripts to inspect structured JSON logs and metrics (no server needed):
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "road24-artifacthub"
3
- version = "0.1.0"
3
+ version = "0.1.2"
4
4
  description = "Shared logging and metrics library for Road24 FastAPI microservices"
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.12"
@@ -21,6 +21,17 @@ _request_exception: ContextVar[Exception | None] = ContextVar("request_exception
21
21
 
22
22
 
23
23
  class HttpInputLogger:
24
+ _BINARY_CONTENT_TYPES = (
25
+ "multipart/form-data",
26
+ "application/octet-stream",
27
+ "image/",
28
+ "audio/",
29
+ "video/",
30
+ "application/pdf",
31
+ "application/zip",
32
+ "application/gzip",
33
+ )
34
+
24
35
  def log(
25
36
  self,
26
37
  scope: "Scope",
@@ -37,6 +48,7 @@ class HttpInputLogger:
37
48
  headers = dict(scope.get("headers", []))
38
49
  host = headers.get(b"host", b"").decode() or ""
39
50
  url = f"{scheme}://{host}{path}" if host else path
51
+ content_type = headers.get(b"content-type", b"").decode() or ""
40
52
 
41
53
  if record_metrics:
42
54
  from road24_sdk.metrics.http import record_http_request
@@ -56,8 +68,8 @@ class HttpInputLogger:
56
68
  target=path,
57
69
  status_code=status_code,
58
70
  duration_seconds=duration_seconds,
59
- request_body=self._decode_body(request_body),
60
- response_body=self._decode_body(response_body),
71
+ request_body=self._decode_body(request_body, content_type),
72
+ response_body=self._decode_body(response_body, None),
61
73
  error_class=type(exc).__name__ if exc else "",
62
74
  error_message=str(exc) if exc else "",
63
75
  )
@@ -71,14 +83,21 @@ class HttpInputLogger:
71
83
  headers = dict(scope.get("headers", []))
72
84
  return headers.get(b"x-trace-id", b"").decode() or secrets.token_hex(16)
73
85
 
74
- def _decode_body(self, body: bytes) -> str:
86
+ def _decode_body(self, body: bytes, content_type: str | None) -> str:
75
87
  if not body:
76
88
  return ""
89
+ if self._is_binary_content(content_type):
90
+ return f"<binary data: {len(body)} bytes>"
77
91
  try:
78
92
  decoded = body.decode("utf-8")
79
93
  return sanitize_body(decoded)
80
94
  except UnicodeDecodeError:
81
- return "<binary data>"
95
+ return f"<binary data: {len(body)} bytes>"
96
+
97
+ def _is_binary_content(self, content_type: str | None) -> bool:
98
+ if not content_type or not isinstance(content_type, str):
99
+ return False
100
+ return any(bt in content_type.lower() for bt in self._BINARY_CONTENT_TYPES)
82
101
 
83
102
  def _get_log_level(self, status_code: int) -> str:
84
103
  if status_code < HTTPStatus.BAD_REQUEST:
@@ -46,8 +46,8 @@ class HttpOutputLogger:
46
46
  direction=HttpDirection.OUTPUT,
47
47
  )
48
48
 
49
- request_body = request.content.decode("utf-8") if request.content else ""
50
- response_body = response.text if response.text else ""
49
+ request_body = self._decode_body(request.content, request.headers.get("content-type"))
50
+ response_body = self._decode_body(response.content, response.headers.get("content-type"))
51
51
 
52
52
  http_attrs = HttpAttributes(
53
53
  direction=HttpDirection.OUTPUT,
@@ -63,6 +63,31 @@ class HttpOutputLogger:
63
63
  log_level = self._get_log_level(response.status_code)
64
64
  getattr(logger, log_level)(LogType.HTTP_REQUEST, extra=http_attrs.as_dict())
65
65
 
66
+ def _decode_body(self, content: bytes, content_type: str | None) -> str:
67
+ if not content:
68
+ return ""
69
+ if self._is_binary_content(content_type):
70
+ return f"<binary data: {len(content)} bytes>"
71
+ try:
72
+ return content.decode("utf-8")
73
+ except UnicodeDecodeError:
74
+ return f"<binary data: {len(content)} bytes>"
75
+
76
+ def _is_binary_content(self, content_type: str | None) -> bool:
77
+ if not content_type or not isinstance(content_type, str):
78
+ return False
79
+ binary_types = (
80
+ "multipart/form-data",
81
+ "application/octet-stream",
82
+ "image/",
83
+ "audio/",
84
+ "video/",
85
+ "application/pdf",
86
+ "application/zip",
87
+ "application/gzip",
88
+ )
89
+ return any(bt in content_type.lower() for bt in binary_types)
90
+
66
91
  def _get_log_level(self, status_code: int) -> str:
67
92
  if status_code < HTTPStatus.BAD_REQUEST:
68
93
  return "info"
@@ -282,7 +282,7 @@ class TestHttpInputLogger:
282
282
  input_logger = HttpInputLogger()
283
283
 
284
284
  # Act
285
- result = input_logger._decode_body(b"")
285
+ result = input_logger._decode_body(b"", None)
286
286
 
287
287
  # Assert
288
288
  assert result == "", "Empty bytes should return empty string"
@@ -292,10 +292,30 @@ class TestHttpInputLogger:
292
292
  input_logger = HttpInputLogger()
293
293
 
294
294
  # Act
295
- result = input_logger._decode_body(b"\x80\x81\x82")
295
+ result = input_logger._decode_body(b"\x80\x81\x82", None)
296
296
 
297
297
  # Assert
298
- assert result == "<binary data>", "Non-UTF8 data should return binary data marker"
298
+ assert "<binary data:" in result, "Non-UTF8 data should return binary data marker"
299
+
300
+ async def test_decode_body_multipart_form_data(self) -> None:
301
+ # Arrange
302
+ input_logger = HttpInputLogger()
303
+
304
+ # Act
305
+ result = input_logger._decode_body(b"file content", "multipart/form-data; boundary=xxx")
306
+
307
+ # Assert
308
+ assert "<binary data:" in result, "Multipart form data should return binary marker"
309
+
310
+ async def test_decode_body_image_content(self) -> None:
311
+ # Arrange
312
+ input_logger = HttpInputLogger()
313
+
314
+ # Act
315
+ result = input_logger._decode_body(b"\x89PNG\r\n", "image/png")
316
+
317
+ # Assert
318
+ assert "<binary data:" in result, "Image content should return binary marker"
299
319
 
300
320
  async def test_generate_trace_id_from_header(
301
321
  self, mock_scope: dict[str, Any]