fairagro-middleware-shared 12.2.2.dev37__tar.gz → 12.2.2.dev40__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 (40) hide show
  1. {fairagro_middleware_shared-12.2.2.dev37 → fairagro_middleware_shared-12.2.2.dev40}/PKG-INFO +1 -1
  2. {fairagro_middleware_shared-12.2.2.dev37 → fairagro_middleware_shared-12.2.2.dev40}/src/middleware/shared/security/__init__.py +2 -1
  3. fairagro_middleware_shared-12.2.2.dev40/src/middleware/shared/security/url_str.py +83 -0
  4. fairagro_middleware_shared-12.2.2.dev40/tests/unit/test_url_str.py +51 -0
  5. {fairagro_middleware_shared-12.2.2.dev37 → fairagro_middleware_shared-12.2.2.dev40}/.gitignore +0 -0
  6. {fairagro_middleware_shared-12.2.2.dev37 → fairagro_middleware_shared-12.2.2.dev40}/README.md +0 -0
  7. {fairagro_middleware_shared-12.2.2.dev37 → fairagro_middleware_shared-12.2.2.dev40}/pyproject.toml +0 -0
  8. {fairagro_middleware_shared-12.2.2.dev37 → fairagro_middleware_shared-12.2.2.dev40}/src/middleware/shared/__init__.py +0 -0
  9. {fairagro_middleware_shared-12.2.2.dev37 → fairagro_middleware_shared-12.2.2.dev40}/src/middleware/shared/api_models/__init__.py +0 -0
  10. {fairagro_middleware_shared-12.2.2.dev37 → fairagro_middleware_shared-12.2.2.dev40}/src/middleware/shared/api_models/common/__init__.py +0 -0
  11. {fairagro_middleware_shared-12.2.2.dev37 → fairagro_middleware_shared-12.2.2.dev40}/src/middleware/shared/api_models/common/models.py +0 -0
  12. {fairagro_middleware_shared-12.2.2.dev37 → fairagro_middleware_shared-12.2.2.dev40}/src/middleware/shared/api_models/common/rocrate.py +0 -0
  13. {fairagro_middleware_shared-12.2.2.dev37 → fairagro_middleware_shared-12.2.2.dev40}/src/middleware/shared/api_models/py.typed +0 -0
  14. {fairagro_middleware_shared-12.2.2.dev37 → fairagro_middleware_shared-12.2.2.dev40}/src/middleware/shared/api_models/v1/__init__.py +0 -0
  15. {fairagro_middleware_shared-12.2.2.dev37 → fairagro_middleware_shared-12.2.2.dev40}/src/middleware/shared/api_models/v1/models.py +0 -0
  16. {fairagro_middleware_shared-12.2.2.dev37 → fairagro_middleware_shared-12.2.2.dev40}/src/middleware/shared/api_models/v2/__init__.py +0 -0
  17. {fairagro_middleware_shared-12.2.2.dev37 → fairagro_middleware_shared-12.2.2.dev40}/src/middleware/shared/api_models/v2/models.py +0 -0
  18. {fairagro_middleware_shared-12.2.2.dev37 → fairagro_middleware_shared-12.2.2.dev40}/src/middleware/shared/api_models/v3/__init__.py +0 -0
  19. {fairagro_middleware_shared-12.2.2.dev37 → fairagro_middleware_shared-12.2.2.dev40}/src/middleware/shared/api_models/v3/models.py +0 -0
  20. {fairagro_middleware_shared-12.2.2.dev37 → fairagro_middleware_shared-12.2.2.dev40}/src/middleware/shared/config/__init__.py +0 -0
  21. {fairagro_middleware_shared-12.2.2.dev37 → fairagro_middleware_shared-12.2.2.dev40}/src/middleware/shared/config/config_base.py +0 -0
  22. {fairagro_middleware_shared-12.2.2.dev37 → fairagro_middleware_shared-12.2.2.dev40}/src/middleware/shared/config/config_wrapper.py +0 -0
  23. {fairagro_middleware_shared-12.2.2.dev37 → fairagro_middleware_shared-12.2.2.dev40}/src/middleware/shared/config/logging.py +0 -0
  24. {fairagro_middleware_shared-12.2.2.dev37 → fairagro_middleware_shared-12.2.2.dev40}/src/middleware/shared/json_types.py +0 -0
  25. {fairagro_middleware_shared-12.2.2.dev37 → fairagro_middleware_shared-12.2.2.dev40}/src/middleware/shared/py.typed +0 -0
  26. {fairagro_middleware_shared-12.2.2.dev37 → fairagro_middleware_shared-12.2.2.dev40}/src/middleware/shared/report/__init__.py +0 -0
  27. {fairagro_middleware_shared-12.2.2.dev37 → fairagro_middleware_shared-12.2.2.dev40}/src/middleware/shared/report/formats/__init__.py +0 -0
  28. {fairagro_middleware_shared-12.2.2.dev37 → fairagro_middleware_shared-12.2.2.dev40}/src/middleware/shared/report/formats/base.py +0 -0
  29. {fairagro_middleware_shared-12.2.2.dev37 → fairagro_middleware_shared-12.2.2.dev40}/src/middleware/shared/report/formats/jsonld.py +0 -0
  30. {fairagro_middleware_shared-12.2.2.dev37 → fairagro_middleware_shared-12.2.2.dev40}/src/middleware/shared/report/model.py +0 -0
  31. {fairagro_middleware_shared-12.2.2.dev37 → fairagro_middleware_shared-12.2.2.dev40}/src/middleware/shared/security/url_redact.py +0 -0
  32. {fairagro_middleware_shared-12.2.2.dev37 → fairagro_middleware_shared-12.2.2.dev40}/src/middleware/shared/tracing.py +0 -0
  33. {fairagro_middleware_shared-12.2.2.dev37 → fairagro_middleware_shared-12.2.2.dev40}/tests/unit/test_api_models.py +0 -0
  34. {fairagro_middleware_shared-12.2.2.dev37 → fairagro_middleware_shared-12.2.2.dev40}/tests/unit/test_config_base.py +0 -0
  35. {fairagro_middleware_shared-12.2.2.dev37 → fairagro_middleware_shared-12.2.2.dev40}/tests/unit/test_config_wrapper.py +0 -0
  36. {fairagro_middleware_shared-12.2.2.dev37 → fairagro_middleware_shared-12.2.2.dev40}/tests/unit/test_harvest_report.py +0 -0
  37. {fairagro_middleware_shared-12.2.2.dev37 → fairagro_middleware_shared-12.2.2.dev40}/tests/unit/test_logging.py +0 -0
  38. {fairagro_middleware_shared-12.2.2.dev37 → fairagro_middleware_shared-12.2.2.dev40}/tests/unit/test_rocrate.py +0 -0
  39. {fairagro_middleware_shared-12.2.2.dev37 → fairagro_middleware_shared-12.2.2.dev40}/tests/unit/test_tracing.py +0 -0
  40. {fairagro_middleware_shared-12.2.2.dev37 → fairagro_middleware_shared-12.2.2.dev40}/tests/unit/test_url_redact.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: fairagro-middleware-shared
3
- Version: 12.2.2.dev37
3
+ Version: 12.2.2.dev40
4
4
  Summary: The FAIRagro advanced middleware shared components
5
5
  Requires-Python: >=3.12
6
6
  Requires-Dist: opentelemetry-api>=1.26.0
@@ -1,5 +1,6 @@
1
1
  """Security helpers shared across middleware components."""
2
2
 
3
3
  from middleware.shared.security.url_redact import redact_url_userinfo
4
+ from middleware.shared.security.url_str import UrlStr
4
5
 
5
- __all__ = ["redact_url_userinfo"]
6
+ __all__ = ["UrlStr", "redact_url_userinfo"]
@@ -0,0 +1,83 @@
1
+ """URL value type that redacts HTTP(S) userinfo on ordinary stringification."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Any
6
+
7
+ from pydantic import GetCoreSchemaHandler
8
+ from pydantic_core import PydanticCustomError, core_schema
9
+
10
+ from middleware.shared.security.url_redact import redact_url_userinfo
11
+
12
+
13
+ class UrlStr:
14
+ """Credential-bearing URL: ``str()`` redacts userinfo; ``unredacted()`` for Git.
15
+
16
+ Use this instead of plain ``str`` (or fully opaque ``SecretStr``) for remotes
17
+ that may embed oauth2 tokens in the URL. Host and path remain visible after
18
+ redaction for ops diagnostics.
19
+ """
20
+
21
+ __slots__ = ("_url",)
22
+
23
+ def __init__(self, url: str) -> None:
24
+ """Wrap *url*; may contain userinfo credentials."""
25
+ self._url = url
26
+
27
+ def unredacted(self) -> str:
28
+ """Return the full URL including credentials (Git CLI / GitPython only)."""
29
+ return self._url
30
+
31
+ def __str__(self) -> str:
32
+ """Redacted form safe for logs, traces, and diagnostics."""
33
+ return redact_url_userinfo(self._url)
34
+
35
+ def __repr__(self) -> str:
36
+ """Repr uses the redacted form so tokens never appear in debug output."""
37
+ return f"UrlStr({str(self)!r})"
38
+
39
+ def __eq__(self, other: object) -> bool:
40
+ """Compare on the raw URL; also accepts an equal plain ``str``."""
41
+ if isinstance(other, UrlStr):
42
+ return self._url == other._url
43
+ if isinstance(other, str):
44
+ return self._url == other
45
+ return NotImplemented
46
+
47
+ def __hash__(self) -> int:
48
+ """Hash the raw URL so equal ``UrlStr`` values collide correctly."""
49
+ return hash(self._url)
50
+
51
+ @classmethod
52
+ def __get_pydantic_core_schema__(
53
+ cls,
54
+ _source: type[Any],
55
+ _handler: GetCoreSchemaHandler,
56
+ ) -> core_schema.CoreSchema:
57
+ """Accept ``str`` or ``UrlStr``; serialize as the redacted string."""
58
+
59
+ def _validate(value: object) -> UrlStr:
60
+ if isinstance(value, UrlStr):
61
+ return value
62
+ if isinstance(value, str):
63
+ return UrlStr(value)
64
+ raise PydanticCustomError(
65
+ "url_str_type",
66
+ "UrlStr input should be a str or UrlStr",
67
+ )
68
+
69
+ python_schema = core_schema.no_info_plain_validator_function(_validate)
70
+ return core_schema.json_or_python_schema(
71
+ python_schema=core_schema.union_schema([
72
+ core_schema.is_instance_schema(cls),
73
+ python_schema,
74
+ ]),
75
+ json_schema=core_schema.no_info_after_validator_function(
76
+ _validate,
77
+ core_schema.str_schema(),
78
+ ),
79
+ serialization=core_schema.plain_serializer_function_ser_schema(
80
+ str,
81
+ when_used="always",
82
+ ),
83
+ )
@@ -0,0 +1,51 @@
1
+ """Tests for UrlStr redacting URL type."""
2
+
3
+ import pytest
4
+ from pydantic import BaseModel, ValidationError
5
+
6
+ from middleware.shared.security import UrlStr
7
+
8
+
9
+ def test_url_str_str_and_repr_redact_userinfo() -> None:
10
+ """Ordinary string conversion hides oauth2 userinfo; host/path remain."""
11
+ url = UrlStr("https://oauth2:secret-token@gitlab.example.com/group/repo.git")
12
+ assert str(url) == "https://***@gitlab.example.com/group/repo.git"
13
+ assert "secret-token" not in str(url)
14
+ assert "secret-token" not in repr(url)
15
+ assert "gitlab.example.com/group/repo.git" in str(url)
16
+
17
+
18
+ def test_url_str_unredacted_returns_raw() -> None:
19
+ """Explicit accessor returns the full credential-bearing URL."""
20
+ raw = "https://oauth2:secret-token@gitlab.example.com/group/repo.git"
21
+ assert UrlStr(raw).unredacted() == raw
22
+
23
+
24
+ def test_url_str_equality_and_hash() -> None:
25
+ """Equality and hashing use the raw URL (including vs plain str)."""
26
+ raw = "https://oauth2:tok@host/r.git"
27
+ a = UrlStr(raw)
28
+ b = UrlStr(raw)
29
+ assert a == b
30
+ assert a == raw
31
+ assert hash(a) == hash(b)
32
+ assert a != UrlStr("https://other.example/r.git")
33
+
34
+
35
+ def test_url_str_pydantic_field_accepts_str_and_url_str() -> None:
36
+ """Model fields typed as UrlStr accept str or UrlStr; dump is redacted."""
37
+
38
+ class Model(BaseModel):
39
+ repo: UrlStr
40
+
41
+ # model_validate exercises str→UrlStr coercion (constructor is typed as UrlStr only).
42
+ from_str = Model.model_validate({"repo": "https://oauth2:secret@host/r.git"})
43
+ assert from_str.repo.unredacted() == "https://oauth2:secret@host/r.git"
44
+ assert from_str.model_dump()["repo"] == "https://***@host/r.git"
45
+
46
+ wrapped = UrlStr("https://oauth2:secret@host/r.git")
47
+ from_url = Model(repo=wrapped)
48
+ assert from_url.repo is wrapped or from_url.repo == wrapped
49
+
50
+ with pytest.raises(ValidationError):
51
+ Model.model_validate({"repo": 123})