law4devs 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.
- law4devs/__init__.py +48 -0
- law4devs/_http.py +79 -0
- law4devs/_pagination.py +30 -0
- law4devs/_version.py +1 -0
- law4devs/client.py +46 -0
- law4devs/exceptions.py +19 -0
- law4devs/models/__init__.py +19 -0
- law4devs/models/annex.py +34 -0
- law4devs/models/article.py +62 -0
- law4devs/models/compliance.py +25 -0
- law4devs/models/framework.py +72 -0
- law4devs/models/recital.py +21 -0
- law4devs/models/requirement.py +37 -0
- law4devs/models/search.py +32 -0
- law4devs/models/tag.py +26 -0
- law4devs/py.typed +0 -0
- law4devs/resources/__init__.py +21 -0
- law4devs/resources/_base.py +49 -0
- law4devs/resources/annexes.py +19 -0
- law4devs/resources/articles.py +23 -0
- law4devs/resources/compliance.py +20 -0
- law4devs/resources/frameworks.py +25 -0
- law4devs/resources/recitals.py +19 -0
- law4devs/resources/requirements.py +20 -0
- law4devs/resources/search.py +16 -0
- law4devs/resources/tags.py +19 -0
- law4devs-0.1.0.dist-info/METADATA +230 -0
- law4devs-0.1.0.dist-info/RECORD +30 -0
- law4devs-0.1.0.dist-info/WHEEL +4 -0
- law4devs-0.1.0.dist-info/licenses/LICENSE +21 -0
law4devs/__init__.py
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
from ._version import __version__
|
|
2
|
+
from .client import Law4DevsClient
|
|
3
|
+
from .exceptions import (
|
|
4
|
+
Law4DevsError,
|
|
5
|
+
NotFoundError,
|
|
6
|
+
ValidationError,
|
|
7
|
+
RateLimitError,
|
|
8
|
+
ServerError,
|
|
9
|
+
)
|
|
10
|
+
from .models import (
|
|
11
|
+
Framework,
|
|
12
|
+
FrameworkDetail,
|
|
13
|
+
Article,
|
|
14
|
+
ArticleSummary,
|
|
15
|
+
ArticleParagraph,
|
|
16
|
+
Recital,
|
|
17
|
+
Requirement,
|
|
18
|
+
Tag,
|
|
19
|
+
ComplianceDeadline,
|
|
20
|
+
Annex,
|
|
21
|
+
AnnexSummary,
|
|
22
|
+
SearchResult,
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
__all__ = [
|
|
27
|
+
"Law4DevsClient",
|
|
28
|
+
"__version__",
|
|
29
|
+
# Exceptions
|
|
30
|
+
"Law4DevsError",
|
|
31
|
+
"NotFoundError",
|
|
32
|
+
"ValidationError",
|
|
33
|
+
"RateLimitError",
|
|
34
|
+
"ServerError",
|
|
35
|
+
# Models
|
|
36
|
+
"Framework",
|
|
37
|
+
"FrameworkDetail",
|
|
38
|
+
"Article",
|
|
39
|
+
"ArticleSummary",
|
|
40
|
+
"ArticleParagraph",
|
|
41
|
+
"Recital",
|
|
42
|
+
"Requirement",
|
|
43
|
+
"Tag",
|
|
44
|
+
"ComplianceDeadline",
|
|
45
|
+
"Annex",
|
|
46
|
+
"AnnexSummary",
|
|
47
|
+
"SearchResult",
|
|
48
|
+
]
|
law4devs/_http.py
ADDED
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
import json
|
|
3
|
+
import time
|
|
4
|
+
import urllib.error
|
|
5
|
+
import urllib.parse
|
|
6
|
+
import urllib.request
|
|
7
|
+
from typing import Any
|
|
8
|
+
|
|
9
|
+
from ._version import __version__
|
|
10
|
+
from .exceptions import Law4DevsError, NotFoundError, RateLimitError, ServerError, ValidationError
|
|
11
|
+
|
|
12
|
+
DEFAULT_BASE_URL = "https://api.law4devs.eu/api/v1"
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class HTTPClient:
|
|
16
|
+
def __init__(
|
|
17
|
+
self,
|
|
18
|
+
base_url: str = DEFAULT_BASE_URL,
|
|
19
|
+
api_key: str | None = None,
|
|
20
|
+
timeout: int = 30,
|
|
21
|
+
max_retries: int = 3,
|
|
22
|
+
):
|
|
23
|
+
self.base_url = base_url.rstrip("/")
|
|
24
|
+
self.timeout = timeout
|
|
25
|
+
self.max_retries = max_retries
|
|
26
|
+
self._headers: dict[str, str] = {
|
|
27
|
+
"Accept": "application/json",
|
|
28
|
+
"User-Agent": f"law4devs-python/{__version__}",
|
|
29
|
+
}
|
|
30
|
+
if api_key:
|
|
31
|
+
self._headers["X-API-Key"] = api_key
|
|
32
|
+
|
|
33
|
+
def get(self, path: str, params: dict[str, Any] | None = None) -> Any:
|
|
34
|
+
"""Make a GET request with automatic retry on 429/5xx."""
|
|
35
|
+
url = self.base_url + path
|
|
36
|
+
if params:
|
|
37
|
+
clean = {k: str(v) for k, v in params.items() if v is not None}
|
|
38
|
+
if clean:
|
|
39
|
+
url += "?" + urllib.parse.urlencode(clean)
|
|
40
|
+
|
|
41
|
+
last_exc: Exception | None = None
|
|
42
|
+
for attempt in range(self.max_retries):
|
|
43
|
+
try:
|
|
44
|
+
req = urllib.request.Request(url, headers=self._headers)
|
|
45
|
+
with urllib.request.urlopen(req, timeout=self.timeout) as resp:
|
|
46
|
+
return json.loads(resp.read())
|
|
47
|
+
except urllib.error.HTTPError as exc:
|
|
48
|
+
body: dict = {}
|
|
49
|
+
try:
|
|
50
|
+
body = json.loads(exc.read())
|
|
51
|
+
except Exception:
|
|
52
|
+
pass
|
|
53
|
+
msg = body.get("error", {}).get("message", str(exc))
|
|
54
|
+
status = exc.code
|
|
55
|
+
if status == 404:
|
|
56
|
+
raise NotFoundError(msg, status) from exc
|
|
57
|
+
if status == 400:
|
|
58
|
+
raise ValidationError(msg, status) from exc
|
|
59
|
+
if status == 429:
|
|
60
|
+
if attempt < self.max_retries - 1:
|
|
61
|
+
time.sleep(2 ** attempt)
|
|
62
|
+
last_exc = exc
|
|
63
|
+
continue
|
|
64
|
+
raise RateLimitError(msg, status) from exc
|
|
65
|
+
if status >= 500:
|
|
66
|
+
if attempt < self.max_retries - 1:
|
|
67
|
+
time.sleep(2 ** attempt)
|
|
68
|
+
last_exc = exc
|
|
69
|
+
continue
|
|
70
|
+
raise ServerError(msg, status) from exc
|
|
71
|
+
raise Law4DevsError(msg, status) from exc
|
|
72
|
+
except urllib.error.URLError as exc:
|
|
73
|
+
if attempt < self.max_retries - 1:
|
|
74
|
+
time.sleep(2 ** attempt)
|
|
75
|
+
last_exc = exc
|
|
76
|
+
continue
|
|
77
|
+
raise Law4DevsError(f"Request failed: {exc}") from exc
|
|
78
|
+
|
|
79
|
+
raise Law4DevsError("Max retries exceeded") from last_exc
|
law4devs/_pagination.py
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
from dataclasses import dataclass
|
|
3
|
+
from typing import Generic, Iterator, List, TypeVar
|
|
4
|
+
|
|
5
|
+
T = TypeVar("T")
|
|
6
|
+
|
|
7
|
+
@dataclass
|
|
8
|
+
class PageMeta:
|
|
9
|
+
api_version: str
|
|
10
|
+
total: int
|
|
11
|
+
page: int
|
|
12
|
+
per_page: int
|
|
13
|
+
pages: int
|
|
14
|
+
|
|
15
|
+
@dataclass
|
|
16
|
+
class PageLinks:
|
|
17
|
+
next: str | None
|
|
18
|
+
prev: str | None
|
|
19
|
+
|
|
20
|
+
@dataclass
|
|
21
|
+
class Page(Generic[T]):
|
|
22
|
+
data: List[T]
|
|
23
|
+
meta: PageMeta
|
|
24
|
+
links: PageLinks
|
|
25
|
+
|
|
26
|
+
def __iter__(self) -> Iterator[T]:
|
|
27
|
+
return iter(self.data)
|
|
28
|
+
|
|
29
|
+
def __len__(self) -> int:
|
|
30
|
+
return len(self.data)
|
law4devs/_version.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "0.1.0"
|
law4devs/client.py
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
from ._http import HTTPClient, DEFAULT_BASE_URL
|
|
3
|
+
from .resources.frameworks import FrameworksResource
|
|
4
|
+
from .resources.articles import ArticlesResource
|
|
5
|
+
from .resources.recitals import RecitalsResource
|
|
6
|
+
from .resources.requirements import RequirementsResource
|
|
7
|
+
from .resources.tags import TagsResource
|
|
8
|
+
from .resources.compliance import ComplianceResource
|
|
9
|
+
from .resources.annexes import AnnexesResource
|
|
10
|
+
from .resources.search import SearchResource
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class Law4DevsClient:
|
|
14
|
+
"""Official Python client for the Law4Devs EU Regulatory Compliance API.
|
|
15
|
+
|
|
16
|
+
Usage::
|
|
17
|
+
|
|
18
|
+
from law4devs import Law4DevsClient
|
|
19
|
+
|
|
20
|
+
client = Law4DevsClient()
|
|
21
|
+
frameworks = client.frameworks.list()
|
|
22
|
+
cra = client.frameworks.get("cra")
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
def __init__(
|
|
26
|
+
self,
|
|
27
|
+
*,
|
|
28
|
+
base_url: str = DEFAULT_BASE_URL,
|
|
29
|
+
api_key: str | None = None,
|
|
30
|
+
timeout: int = 30,
|
|
31
|
+
max_retries: int = 3,
|
|
32
|
+
) -> None:
|
|
33
|
+
self._http = HTTPClient(
|
|
34
|
+
base_url=base_url,
|
|
35
|
+
api_key=api_key,
|
|
36
|
+
timeout=timeout,
|
|
37
|
+
max_retries=max_retries,
|
|
38
|
+
)
|
|
39
|
+
self.frameworks = FrameworksResource(self._http)
|
|
40
|
+
self.articles = ArticlesResource(self._http)
|
|
41
|
+
self.recitals = RecitalsResource(self._http)
|
|
42
|
+
self.requirements = RequirementsResource(self._http)
|
|
43
|
+
self.tags = TagsResource(self._http)
|
|
44
|
+
self.compliance = ComplianceResource(self._http)
|
|
45
|
+
self.annexes = AnnexesResource(self._http)
|
|
46
|
+
self.search = SearchResource(self._http)
|
law4devs/exceptions.py
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
class Law4DevsError(Exception):
|
|
4
|
+
"""Base exception for all SDK errors."""
|
|
5
|
+
def __init__(self, message: str, status_code: int | None = None):
|
|
6
|
+
super().__init__(message)
|
|
7
|
+
self.status_code = status_code
|
|
8
|
+
|
|
9
|
+
class NotFoundError(Law4DevsError):
|
|
10
|
+
"""Raised when a resource is not found (404)."""
|
|
11
|
+
|
|
12
|
+
class ValidationError(Law4DevsError):
|
|
13
|
+
"""Raised for invalid request parameters (400)."""
|
|
14
|
+
|
|
15
|
+
class RateLimitError(Law4DevsError):
|
|
16
|
+
"""Raised when rate limit is exceeded (429)."""
|
|
17
|
+
|
|
18
|
+
class ServerError(Law4DevsError):
|
|
19
|
+
"""Raised for server-side errors (5xx)."""
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
from .framework import Framework, FrameworkDetail
|
|
2
|
+
from .article import Article, ArticleSummary, ArticleParagraph
|
|
3
|
+
from .recital import Recital
|
|
4
|
+
from .requirement import Requirement
|
|
5
|
+
from .tag import Tag
|
|
6
|
+
from .compliance import ComplianceDeadline
|
|
7
|
+
from .annex import Annex, AnnexSummary
|
|
8
|
+
from .search import SearchResult
|
|
9
|
+
|
|
10
|
+
__all__ = [
|
|
11
|
+
"Framework", "FrameworkDetail",
|
|
12
|
+
"Article", "ArticleSummary", "ArticleParagraph",
|
|
13
|
+
"Recital",
|
|
14
|
+
"Requirement",
|
|
15
|
+
"Tag",
|
|
16
|
+
"ComplianceDeadline",
|
|
17
|
+
"Annex", "AnnexSummary",
|
|
18
|
+
"SearchResult",
|
|
19
|
+
]
|
law4devs/models/annex.py
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
from dataclasses import dataclass
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
@dataclass
|
|
6
|
+
class AnnexSummary:
|
|
7
|
+
id: int
|
|
8
|
+
framework_slug: str
|
|
9
|
+
annex_number: str
|
|
10
|
+
title: str
|
|
11
|
+
|
|
12
|
+
@classmethod
|
|
13
|
+
def _from_dict(cls, d: dict) -> AnnexSummary:
|
|
14
|
+
return cls(
|
|
15
|
+
id=d["id"],
|
|
16
|
+
framework_slug=d.get("framework_slug", d.get("framework", {}).get("slug", "")),
|
|
17
|
+
annex_number=str(d.get("annex_number", "")),
|
|
18
|
+
title=d.get("title", ""),
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
@dataclass
|
|
23
|
+
class Annex(AnnexSummary):
|
|
24
|
+
content: str = ""
|
|
25
|
+
|
|
26
|
+
@classmethod
|
|
27
|
+
def _from_dict(cls, d: dict) -> Annex:
|
|
28
|
+
return cls(
|
|
29
|
+
id=d["id"],
|
|
30
|
+
framework_slug=d.get("framework_slug", d.get("framework", {}).get("slug", "")),
|
|
31
|
+
annex_number=str(d.get("annex_number", "")),
|
|
32
|
+
title=d.get("title", ""),
|
|
33
|
+
content=d.get("content", ""),
|
|
34
|
+
)
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
from dataclasses import dataclass, field
|
|
3
|
+
from typing import List
|
|
4
|
+
from .tag import Tag
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
@dataclass
|
|
8
|
+
class ArticleParagraph:
|
|
9
|
+
paragraph_ref: str
|
|
10
|
+
content: str
|
|
11
|
+
position: int
|
|
12
|
+
|
|
13
|
+
@classmethod
|
|
14
|
+
def _from_dict(cls, d: dict) -> ArticleParagraph:
|
|
15
|
+
return cls(
|
|
16
|
+
paragraph_ref=d.get("paragraph_ref", ""),
|
|
17
|
+
content=d.get("content", ""),
|
|
18
|
+
position=d.get("position", 0),
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
@dataclass
|
|
23
|
+
class ArticleSummary:
|
|
24
|
+
id: int
|
|
25
|
+
framework_slug: str
|
|
26
|
+
article_number: int
|
|
27
|
+
title: str
|
|
28
|
+
position: int
|
|
29
|
+
paragraph_count: int
|
|
30
|
+
tags: List[Tag]
|
|
31
|
+
|
|
32
|
+
@classmethod
|
|
33
|
+
def _from_dict(cls, d: dict) -> ArticleSummary:
|
|
34
|
+
return cls(
|
|
35
|
+
id=d["id"],
|
|
36
|
+
framework_slug=d.get("framework_slug", d.get("framework", {}).get("slug", "")),
|
|
37
|
+
article_number=int(d.get("article_number", 0)),
|
|
38
|
+
title=d.get("title", ""),
|
|
39
|
+
position=d.get("position", 0),
|
|
40
|
+
paragraph_count=d.get("paragraph_count", 0),
|
|
41
|
+
tags=[Tag._from_dict(t) if isinstance(t, dict) else t for t in d.get("tags", [])],
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
@dataclass
|
|
46
|
+
class Article(ArticleSummary):
|
|
47
|
+
content: str = ""
|
|
48
|
+
paragraphs: List[ArticleParagraph] = field(default_factory=list)
|
|
49
|
+
|
|
50
|
+
@classmethod
|
|
51
|
+
def _from_dict(cls, d: dict) -> Article:
|
|
52
|
+
return cls(
|
|
53
|
+
id=d["id"],
|
|
54
|
+
framework_slug=d.get("framework_slug", d.get("framework", {}).get("slug", "")),
|
|
55
|
+
article_number=int(d.get("article_number", 0)),
|
|
56
|
+
title=d.get("title", ""),
|
|
57
|
+
position=d.get("position", 0),
|
|
58
|
+
paragraph_count=d.get("paragraph_count", 0),
|
|
59
|
+
tags=[Tag._from_dict(t) if isinstance(t, dict) else t for t in d.get("tags", [])],
|
|
60
|
+
content=d.get("content", ""),
|
|
61
|
+
paragraphs=[ArticleParagraph._from_dict(p) for p in d.get("paragraphs", [])],
|
|
62
|
+
)
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
from dataclasses import dataclass
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
@dataclass
|
|
6
|
+
class ComplianceDeadline:
|
|
7
|
+
id: int
|
|
8
|
+
framework_slug: str
|
|
9
|
+
article_number: str | None
|
|
10
|
+
paragraph_ref: str | None
|
|
11
|
+
deadline_date: str
|
|
12
|
+
deadline_type: str
|
|
13
|
+
description: str | None
|
|
14
|
+
|
|
15
|
+
@classmethod
|
|
16
|
+
def _from_dict(cls, d: dict) -> ComplianceDeadline:
|
|
17
|
+
return cls(
|
|
18
|
+
id=d["id"],
|
|
19
|
+
framework_slug=d.get("framework_slug", d.get("framework", {}).get("slug", "")),
|
|
20
|
+
article_number=d.get("article_number"),
|
|
21
|
+
paragraph_ref=d.get("paragraph_ref"),
|
|
22
|
+
deadline_date=d.get("deadline_date", ""),
|
|
23
|
+
deadline_type=d.get("deadline_type", ""),
|
|
24
|
+
description=d.get("description"),
|
|
25
|
+
)
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
from dataclasses import dataclass
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
@dataclass
|
|
6
|
+
class Framework:
|
|
7
|
+
id: int
|
|
8
|
+
slug: str
|
|
9
|
+
name: str
|
|
10
|
+
short_name: str
|
|
11
|
+
celex_number: str
|
|
12
|
+
description: str | None
|
|
13
|
+
is_active: bool
|
|
14
|
+
status: str
|
|
15
|
+
expected_articles: int
|
|
16
|
+
expected_recitals: int
|
|
17
|
+
last_synced_at: str | None
|
|
18
|
+
created_at: str
|
|
19
|
+
article_count: int
|
|
20
|
+
recital_count: int
|
|
21
|
+
|
|
22
|
+
@classmethod
|
|
23
|
+
def _from_dict(cls, d: dict) -> Framework:
|
|
24
|
+
return cls(
|
|
25
|
+
id=d["id"],
|
|
26
|
+
slug=d["slug"],
|
|
27
|
+
name=d["name"],
|
|
28
|
+
short_name=d.get("short_name", ""),
|
|
29
|
+
celex_number=d.get("celex_number", ""),
|
|
30
|
+
description=d.get("description"),
|
|
31
|
+
is_active=d.get("is_active", True),
|
|
32
|
+
status=d.get("status", "active"),
|
|
33
|
+
expected_articles=d.get("expected_articles", 0),
|
|
34
|
+
expected_recitals=d.get("expected_recitals", 0),
|
|
35
|
+
last_synced_at=d.get("last_synced_at"),
|
|
36
|
+
created_at=d.get("created_at", ""),
|
|
37
|
+
article_count=d.get("article_count", 0),
|
|
38
|
+
recital_count=d.get("recital_count", 0),
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
@dataclass
|
|
43
|
+
class FrameworkDetail(Framework):
|
|
44
|
+
eurlex_url: str = ""
|
|
45
|
+
requirement_count: int = 0
|
|
46
|
+
annex_count: int = 0
|
|
47
|
+
tag_count: int = 0
|
|
48
|
+
coverage: dict | None = None
|
|
49
|
+
|
|
50
|
+
@classmethod
|
|
51
|
+
def _from_dict(cls, d: dict) -> FrameworkDetail:
|
|
52
|
+
return cls(
|
|
53
|
+
id=d["id"],
|
|
54
|
+
slug=d["slug"],
|
|
55
|
+
name=d["name"],
|
|
56
|
+
short_name=d.get("short_name", ""),
|
|
57
|
+
celex_number=d.get("celex_number", ""),
|
|
58
|
+
description=d.get("description"),
|
|
59
|
+
is_active=d.get("is_active", True),
|
|
60
|
+
status=d.get("status", "active"),
|
|
61
|
+
expected_articles=d.get("expected_articles", 0),
|
|
62
|
+
expected_recitals=d.get("expected_recitals", 0),
|
|
63
|
+
last_synced_at=d.get("last_synced_at"),
|
|
64
|
+
created_at=d.get("created_at", ""),
|
|
65
|
+
article_count=d.get("article_count", 0),
|
|
66
|
+
recital_count=d.get("recital_count", 0),
|
|
67
|
+
eurlex_url=d.get("eurlex_url", ""),
|
|
68
|
+
requirement_count=d.get("requirement_count", 0),
|
|
69
|
+
annex_count=d.get("annex_count", 0),
|
|
70
|
+
tag_count=d.get("tag_count", 0),
|
|
71
|
+
coverage=d.get("coverage"),
|
|
72
|
+
)
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
from dataclasses import dataclass
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
@dataclass
|
|
6
|
+
class Recital:
|
|
7
|
+
id: int
|
|
8
|
+
framework_slug: str
|
|
9
|
+
recital_number: int
|
|
10
|
+
content: str
|
|
11
|
+
position: int
|
|
12
|
+
|
|
13
|
+
@classmethod
|
|
14
|
+
def _from_dict(cls, d: dict) -> Recital:
|
|
15
|
+
return cls(
|
|
16
|
+
id=d["id"],
|
|
17
|
+
framework_slug=d.get("framework_slug", d.get("framework", {}).get("slug", "")),
|
|
18
|
+
recital_number=int(d.get("recital_number", 0)),
|
|
19
|
+
content=d.get("content", ""),
|
|
20
|
+
position=d.get("position", 0),
|
|
21
|
+
)
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
from dataclasses import dataclass
|
|
3
|
+
from typing import List
|
|
4
|
+
from .tag import Tag
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
@dataclass
|
|
8
|
+
class Requirement:
|
|
9
|
+
id: int
|
|
10
|
+
framework_slug: str
|
|
11
|
+
article_number: int | None
|
|
12
|
+
paragraph_ref: str | None
|
|
13
|
+
paragraph_content: str | None
|
|
14
|
+
requirement_text: str
|
|
15
|
+
requirement_type: str
|
|
16
|
+
compliance_deadline: str | None
|
|
17
|
+
linked_article_numbers: List[int]
|
|
18
|
+
stakeholder_roles: List[str]
|
|
19
|
+
tags: List[Tag]
|
|
20
|
+
created_at: str
|
|
21
|
+
|
|
22
|
+
@classmethod
|
|
23
|
+
def _from_dict(cls, d: dict) -> Requirement:
|
|
24
|
+
return cls(
|
|
25
|
+
id=d["id"],
|
|
26
|
+
framework_slug=d.get("framework_slug", d.get("framework", {}).get("slug", "")),
|
|
27
|
+
article_number=d.get("article_number"),
|
|
28
|
+
paragraph_ref=d.get("paragraph_ref"),
|
|
29
|
+
paragraph_content=d.get("paragraph_content"),
|
|
30
|
+
requirement_text=d.get("requirement_text", ""),
|
|
31
|
+
requirement_type=d.get("requirement_type", "general"),
|
|
32
|
+
compliance_deadline=d.get("compliance_deadline"),
|
|
33
|
+
linked_article_numbers=d.get("linked_article_numbers", []),
|
|
34
|
+
stakeholder_roles=d.get("stakeholder_roles", []),
|
|
35
|
+
tags=[Tag._from_dict(t) if isinstance(t, dict) else t for t in d.get("tags", [])],
|
|
36
|
+
created_at=d.get("created_at", ""),
|
|
37
|
+
)
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
from dataclasses import dataclass
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
@dataclass
|
|
6
|
+
class SearchResult:
|
|
7
|
+
type: str
|
|
8
|
+
framework_slug: str
|
|
9
|
+
framework_name: str
|
|
10
|
+
article_number: int | None
|
|
11
|
+
recital_number: int | None
|
|
12
|
+
paragraph_ref: str | None
|
|
13
|
+
title: str | None
|
|
14
|
+
requirement_type: str | None
|
|
15
|
+
match_context: str
|
|
16
|
+
url: str
|
|
17
|
+
|
|
18
|
+
@classmethod
|
|
19
|
+
def _from_dict(cls, d: dict) -> SearchResult:
|
|
20
|
+
fw = d.get("framework", {})
|
|
21
|
+
return cls(
|
|
22
|
+
type=d.get("type", ""),
|
|
23
|
+
framework_slug=d.get("framework_slug", fw.get("slug", "")),
|
|
24
|
+
framework_name=d.get("framework_name", fw.get("name", "")),
|
|
25
|
+
article_number=d.get("article_number"),
|
|
26
|
+
recital_number=d.get("recital_number"),
|
|
27
|
+
paragraph_ref=d.get("paragraph_ref"),
|
|
28
|
+
title=d.get("title"),
|
|
29
|
+
requirement_type=d.get("requirement_type"),
|
|
30
|
+
match_context=d.get("match_context", ""),
|
|
31
|
+
url=d.get("url", ""),
|
|
32
|
+
)
|
law4devs/models/tag.py
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
from dataclasses import dataclass
|
|
3
|
+
from typing import List
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
@dataclass
|
|
7
|
+
class Tag:
|
|
8
|
+
id: int
|
|
9
|
+
slug: str
|
|
10
|
+
name: str
|
|
11
|
+
description: str | None
|
|
12
|
+
keywords: List[str]
|
|
13
|
+
color: str
|
|
14
|
+
created_at: str
|
|
15
|
+
|
|
16
|
+
@classmethod
|
|
17
|
+
def _from_dict(cls, d: dict) -> Tag:
|
|
18
|
+
return cls(
|
|
19
|
+
id=d["id"],
|
|
20
|
+
slug=d["slug"],
|
|
21
|
+
name=d["name"],
|
|
22
|
+
description=d.get("description"),
|
|
23
|
+
keywords=d.get("keywords", []),
|
|
24
|
+
color=d.get("color", ""),
|
|
25
|
+
created_at=d.get("created_at", ""),
|
|
26
|
+
)
|
law4devs/py.typed
ADDED
|
File without changes
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
from ._base import BaseResource
|
|
2
|
+
from .frameworks import FrameworksResource
|
|
3
|
+
from .articles import ArticlesResource
|
|
4
|
+
from .recitals import RecitalsResource
|
|
5
|
+
from .requirements import RequirementsResource
|
|
6
|
+
from .tags import TagsResource
|
|
7
|
+
from .compliance import ComplianceResource
|
|
8
|
+
from .annexes import AnnexesResource
|
|
9
|
+
from .search import SearchResource
|
|
10
|
+
|
|
11
|
+
__all__ = [
|
|
12
|
+
"BaseResource",
|
|
13
|
+
"FrameworksResource",
|
|
14
|
+
"ArticlesResource",
|
|
15
|
+
"RecitalsResource",
|
|
16
|
+
"RequirementsResource",
|
|
17
|
+
"TagsResource",
|
|
18
|
+
"ComplianceResource",
|
|
19
|
+
"AnnexesResource",
|
|
20
|
+
"SearchResource",
|
|
21
|
+
]
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
from typing import TYPE_CHECKING, Any, Iterator, TypeVar
|
|
3
|
+
|
|
4
|
+
if TYPE_CHECKING:
|
|
5
|
+
from law4devs._http import HTTPClient
|
|
6
|
+
|
|
7
|
+
T = TypeVar("T")
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class BaseResource:
|
|
11
|
+
def __init__(self, http: HTTPClient) -> None:
|
|
12
|
+
self._http = http
|
|
13
|
+
|
|
14
|
+
def _page_params(self, page: int | None, per_page: int | None) -> dict[str, Any]:
|
|
15
|
+
params: dict[str, Any] = {}
|
|
16
|
+
if page is not None:
|
|
17
|
+
params["page"] = page
|
|
18
|
+
if per_page is not None:
|
|
19
|
+
params["per_page"] = per_page
|
|
20
|
+
return params
|
|
21
|
+
|
|
22
|
+
def _parse_page(self, raw: dict, item_factory) -> Any:
|
|
23
|
+
from law4devs._pagination import Page, PageMeta, PageLinks
|
|
24
|
+
meta_raw = raw.get("meta", {})
|
|
25
|
+
links_raw = raw.get("links", {})
|
|
26
|
+
return Page(
|
|
27
|
+
data=[item_factory(item) for item in raw.get("data", [])],
|
|
28
|
+
meta=PageMeta(
|
|
29
|
+
api_version=meta_raw.get("api_version", "1.0"),
|
|
30
|
+
total=meta_raw.get("total", 0),
|
|
31
|
+
page=meta_raw.get("page", 1),
|
|
32
|
+
per_page=meta_raw.get("per_page", 20),
|
|
33
|
+
pages=meta_raw.get("pages", 0),
|
|
34
|
+
),
|
|
35
|
+
links=PageLinks(
|
|
36
|
+
next=links_raw.get("next"),
|
|
37
|
+
prev=links_raw.get("prev"),
|
|
38
|
+
),
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
def _iter_pages(self, list_fn, *args, per_page: int = 20, **kwargs) -> Iterator:
|
|
42
|
+
"""Auto-paginate through all pages and yield individual items."""
|
|
43
|
+
page = 1
|
|
44
|
+
while True:
|
|
45
|
+
result = list_fn(*args, page=page, per_page=per_page, **kwargs)
|
|
46
|
+
yield from result.data
|
|
47
|
+
if not result.links.next:
|
|
48
|
+
break
|
|
49
|
+
page += 1
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
from typing import Iterator
|
|
3
|
+
from ._base import BaseResource
|
|
4
|
+
from ..models.annex import Annex, AnnexSummary
|
|
5
|
+
from .._pagination import Page
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class AnnexesResource(BaseResource):
|
|
9
|
+
def list(self, framework_slug: str, *, page: int | None = None, per_page: int | None = None) -> Page[AnnexSummary]:
|
|
10
|
+
raw = self._http.get(f"/frameworks/{framework_slug}/annexes", params=self._page_params(page, per_page))
|
|
11
|
+
return self._parse_page(raw, AnnexSummary._from_dict)
|
|
12
|
+
|
|
13
|
+
def iter(self, framework_slug: str, *, per_page: int = 20) -> Iterator[AnnexSummary]:
|
|
14
|
+
"""Iterate over all annexes for a framework across all pages."""
|
|
15
|
+
return self._iter_pages(self.list, framework_slug, per_page=per_page)
|
|
16
|
+
|
|
17
|
+
def get(self, framework_slug: str, annex_number: int | str) -> Annex:
|
|
18
|
+
raw = self._http.get(f"/frameworks/{framework_slug}/annexes/{annex_number}")
|
|
19
|
+
return Annex._from_dict(raw.get("data", raw))
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
from typing import Iterator
|
|
3
|
+
from ._base import BaseResource
|
|
4
|
+
from ..models.article import Article, ArticleSummary
|
|
5
|
+
from .._pagination import Page
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class ArticlesResource(BaseResource):
|
|
9
|
+
def list(self, framework_slug: str, *, page: int | None = None, per_page: int | None = None) -> Page[ArticleSummary]:
|
|
10
|
+
raw = self._http.get(f"/frameworks/{framework_slug}/articles", params=self._page_params(page, per_page))
|
|
11
|
+
return self._parse_page(raw, ArticleSummary._from_dict)
|
|
12
|
+
|
|
13
|
+
def iter(self, framework_slug: str, *, per_page: int = 20) -> Iterator[ArticleSummary]:
|
|
14
|
+
"""Iterate over all articles for a framework across all pages."""
|
|
15
|
+
return self._iter_pages(self.list, framework_slug, per_page=per_page)
|
|
16
|
+
|
|
17
|
+
def get(self, framework_slug: str, article_number: int | str) -> Article:
|
|
18
|
+
raw = self._http.get(f"/frameworks/{framework_slug}/articles/{article_number}")
|
|
19
|
+
return Article._from_dict(raw.get("data", raw))
|
|
20
|
+
|
|
21
|
+
def related(self, framework_slug: str, article_number: int | str, *, page: int | None = None, per_page: int | None = None) -> Page[ArticleSummary]:
|
|
22
|
+
raw = self._http.get(f"/frameworks/{framework_slug}/articles/{article_number}/related", params=self._page_params(page, per_page))
|
|
23
|
+
return self._parse_page(raw, ArticleSummary._from_dict)
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
from typing import Iterator
|
|
3
|
+
from ._base import BaseResource
|
|
4
|
+
from ..models.compliance import ComplianceDeadline
|
|
5
|
+
from .._pagination import Page
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class ComplianceResource(BaseResource):
|
|
9
|
+
def deadlines(self, *, framework_slug: str | None = None, page: int | None = None, per_page: int | None = None) -> Page[ComplianceDeadline]:
|
|
10
|
+
if framework_slug:
|
|
11
|
+
path = f"/frameworks/{framework_slug}/compliance/deadlines"
|
|
12
|
+
else:
|
|
13
|
+
path = "/compliance/deadlines"
|
|
14
|
+
params = self._page_params(page, per_page)
|
|
15
|
+
raw = self._http.get(path, params=params)
|
|
16
|
+
return self._parse_page(raw, ComplianceDeadline._from_dict)
|
|
17
|
+
|
|
18
|
+
def iter_deadlines(self, *, framework_slug: str | None = None, per_page: int = 20) -> Iterator[ComplianceDeadline]:
|
|
19
|
+
"""Iterate over all compliance deadlines across all pages."""
|
|
20
|
+
return self._iter_pages(self.deadlines, framework_slug=framework_slug, per_page=per_page)
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
from typing import Any, Iterator
|
|
3
|
+
from ._base import BaseResource
|
|
4
|
+
from ..models.framework import Framework, FrameworkDetail
|
|
5
|
+
from .._pagination import Page
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class FrameworksResource(BaseResource):
|
|
9
|
+
def list(self, *, page: int | None = None, per_page: int | None = None) -> Page[Framework]:
|
|
10
|
+
raw = self._http.get("/frameworks", params=self._page_params(page, per_page))
|
|
11
|
+
return self._parse_page(raw, Framework._from_dict)
|
|
12
|
+
|
|
13
|
+
def iter(self, *, per_page: int = 20) -> Iterator[Framework]:
|
|
14
|
+
"""Iterate over all frameworks across all pages."""
|
|
15
|
+
return self._iter_pages(self.list, per_page=per_page)
|
|
16
|
+
|
|
17
|
+
def get(self, slug: str) -> FrameworkDetail:
|
|
18
|
+
raw = self._http.get(f"/frameworks/{slug}")
|
|
19
|
+
return FrameworkDetail._from_dict(raw.get("data", raw))
|
|
20
|
+
|
|
21
|
+
def stats(self, slug: str) -> dict:
|
|
22
|
+
return self._http.get(f"/frameworks/{slug}/stats")
|
|
23
|
+
|
|
24
|
+
def changelog(self, slug: str, *, page: int | None = None, per_page: int | None = None) -> dict:
|
|
25
|
+
return self._http.get(f"/frameworks/{slug}/changelog", params=self._page_params(page, per_page))
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
from typing import Iterator
|
|
3
|
+
from ._base import BaseResource
|
|
4
|
+
from ..models.recital import Recital
|
|
5
|
+
from .._pagination import Page
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class RecitalsResource(BaseResource):
|
|
9
|
+
def list(self, framework_slug: str, *, page: int | None = None, per_page: int | None = None) -> Page[Recital]:
|
|
10
|
+
raw = self._http.get(f"/frameworks/{framework_slug}/recitals", params=self._page_params(page, per_page))
|
|
11
|
+
return self._parse_page(raw, Recital._from_dict)
|
|
12
|
+
|
|
13
|
+
def iter(self, framework_slug: str, *, per_page: int = 20) -> Iterator[Recital]:
|
|
14
|
+
"""Iterate over all recitals for a framework across all pages."""
|
|
15
|
+
return self._iter_pages(self.list, framework_slug, per_page=per_page)
|
|
16
|
+
|
|
17
|
+
def get(self, framework_slug: str, recital_number: int | str) -> Recital:
|
|
18
|
+
raw = self._http.get(f"/frameworks/{framework_slug}/recitals/{recital_number}")
|
|
19
|
+
return Recital._from_dict(raw.get("data", raw))
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
from typing import Iterator
|
|
3
|
+
from ._base import BaseResource
|
|
4
|
+
from ..models.requirement import Requirement
|
|
5
|
+
from .._pagination import Page
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class RequirementsResource(BaseResource):
|
|
9
|
+
def list(self, *, framework_slug: str | None = None, page: int | None = None, per_page: int | None = None) -> Page[Requirement]:
|
|
10
|
+
if framework_slug:
|
|
11
|
+
path = f"/frameworks/{framework_slug}/requirements"
|
|
12
|
+
else:
|
|
13
|
+
path = "/requirements"
|
|
14
|
+
params = self._page_params(page, per_page)
|
|
15
|
+
raw = self._http.get(path, params=params)
|
|
16
|
+
return self._parse_page(raw, Requirement._from_dict)
|
|
17
|
+
|
|
18
|
+
def iter(self, *, framework_slug: str | None = None, per_page: int = 20) -> Iterator[Requirement]:
|
|
19
|
+
"""Iterate over all requirements across all pages."""
|
|
20
|
+
return self._iter_pages(self.list, framework_slug=framework_slug, per_page=per_page)
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
from ._base import BaseResource
|
|
3
|
+
from ..models.search import SearchResult
|
|
4
|
+
from .._pagination import Page
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class SearchResource(BaseResource):
|
|
8
|
+
def query(self, q: str, *, framework: str | None = None, result_type: str | None = None, page: int | None = None, per_page: int | None = None) -> Page[SearchResult]:
|
|
9
|
+
params: dict = {"q": q}
|
|
10
|
+
if framework:
|
|
11
|
+
params["framework"] = framework
|
|
12
|
+
if result_type:
|
|
13
|
+
params["type"] = result_type
|
|
14
|
+
params.update(self._page_params(page, per_page))
|
|
15
|
+
raw = self._http.get("/search", params=params)
|
|
16
|
+
return self._parse_page(raw, SearchResult._from_dict)
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
from typing import Iterator
|
|
3
|
+
from ._base import BaseResource
|
|
4
|
+
from ..models.tag import Tag
|
|
5
|
+
from .._pagination import Page
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class TagsResource(BaseResource):
|
|
9
|
+
def list(self, *, page: int | None = None, per_page: int | None = None) -> Page[Tag]:
|
|
10
|
+
raw = self._http.get("/tags", params=self._page_params(page, per_page))
|
|
11
|
+
return self._parse_page(raw, Tag._from_dict)
|
|
12
|
+
|
|
13
|
+
def iter(self, *, per_page: int = 20) -> Iterator[Tag]:
|
|
14
|
+
"""Iterate over all tags across all pages."""
|
|
15
|
+
return self._iter_pages(self.list, per_page=per_page)
|
|
16
|
+
|
|
17
|
+
def get(self, slug: str) -> Tag:
|
|
18
|
+
raw = self._http.get(f"/tags/{slug}")
|
|
19
|
+
return Tag._from_dict(raw.get("data", raw))
|
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: law4devs
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Official Python SDK for the Law4Devs EU Regulatory Compliance API
|
|
5
|
+
Project-URL: Homepage, https://law4devs.eu
|
|
6
|
+
Project-URL: Documentation, https://docs.law4devs.eu/sdks/python
|
|
7
|
+
Author-email: law4devs <sdk@law4devs.eu>
|
|
8
|
+
Maintainer-email: Sofiane Hamlaoui <contact@law4devs.eu>
|
|
9
|
+
License: MIT
|
|
10
|
+
License-File: LICENSE
|
|
11
|
+
Keywords: ai-act,compliance,cra,eu,gdpr,law4devs,nis2,regulatory
|
|
12
|
+
Classifier: Development Status :: 3 - Alpha
|
|
13
|
+
Classifier: Intended Audience :: Developers
|
|
14
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
15
|
+
Classifier: Programming Language :: Python :: 3
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
20
|
+
Classifier: Topic :: Software Development :: Libraries
|
|
21
|
+
Requires-Python: >=3.9
|
|
22
|
+
Description-Content-Type: text/markdown
|
|
23
|
+
|
|
24
|
+
# law4devs
|
|
25
|
+
|
|
26
|
+
Official Python SDK for the [Law4Devs](https://law4devs.eu) EU Regulatory Compliance API.
|
|
27
|
+
|
|
28
|
+
Access structured, developer-friendly data for EU regulations — GDPR, Cyber Resilience Act, NIS2, AI Act, and more.
|
|
29
|
+
|
|
30
|
+
## Installation
|
|
31
|
+
|
|
32
|
+
```bash
|
|
33
|
+
pip install law4devs
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
Python 3.9+ required. Zero runtime dependencies (stdlib urllib only).
|
|
37
|
+
|
|
38
|
+
## Quick Start
|
|
39
|
+
|
|
40
|
+
```python
|
|
41
|
+
from law4devs import Law4DevsClient
|
|
42
|
+
|
|
43
|
+
client = Law4DevsClient()
|
|
44
|
+
|
|
45
|
+
# List all frameworks
|
|
46
|
+
page = client.frameworks.list()
|
|
47
|
+
for fw in page:
|
|
48
|
+
print(fw.slug, fw.name)
|
|
49
|
+
|
|
50
|
+
# Get framework detail
|
|
51
|
+
cra = client.frameworks.get("cra")
|
|
52
|
+
print(cra.name, cra.requirement_count)
|
|
53
|
+
|
|
54
|
+
# Fetch a specific article
|
|
55
|
+
article = client.articles.get("cra", 13)
|
|
56
|
+
print(article.title, article.content)
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
## Authentication
|
|
60
|
+
|
|
61
|
+
The public API tier does not require authentication. Authenticated tiers provide higher rate limits:
|
|
62
|
+
|
|
63
|
+
```python
|
|
64
|
+
client = Law4DevsClient(api_key="your-api-key")
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
## Resources
|
|
68
|
+
|
|
69
|
+
### Frameworks
|
|
70
|
+
|
|
71
|
+
```python
|
|
72
|
+
page = client.frameworks.list(page=1, per_page=10)
|
|
73
|
+
print(page.meta.total, page.meta.pages)
|
|
74
|
+
|
|
75
|
+
cra = client.frameworks.get("cra")
|
|
76
|
+
|
|
77
|
+
for fw in client.frameworks.iter():
|
|
78
|
+
print(fw.slug)
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
### Articles
|
|
82
|
+
|
|
83
|
+
```python
|
|
84
|
+
page = client.articles.list("cra", per_page=20)
|
|
85
|
+
art = client.articles.get("cra", 13)
|
|
86
|
+
related = client.articles.related("cra", 13)
|
|
87
|
+
|
|
88
|
+
for art in client.articles.iter("cra"):
|
|
89
|
+
print(art.article_number, art.title)
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
### Recitals
|
|
93
|
+
|
|
94
|
+
```python
|
|
95
|
+
page = client.recitals.list("cra")
|
|
96
|
+
recital = client.recitals.get("cra", 10)
|
|
97
|
+
|
|
98
|
+
for recital in client.recitals.iter("cra"):
|
|
99
|
+
print(recital.recital_number, recital.content[:80])
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
### Requirements
|
|
103
|
+
|
|
104
|
+
```python
|
|
105
|
+
page = client.requirements.list()
|
|
106
|
+
page = client.requirements.list(framework_slug="cra")
|
|
107
|
+
|
|
108
|
+
for req in client.requirements.iter(framework_slug="cra"):
|
|
109
|
+
print(req.requirement_type, req.requirement_text[:60])
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
### Compliance Deadlines
|
|
113
|
+
|
|
114
|
+
```python
|
|
115
|
+
page = client.compliance.deadlines()
|
|
116
|
+
page = client.compliance.deadlines(framework_slug="nis2")
|
|
117
|
+
|
|
118
|
+
for d in client.compliance.iter_deadlines(framework_slug="nis2"):
|
|
119
|
+
print(d.deadline_date, d.description)
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
### Tags
|
|
123
|
+
|
|
124
|
+
```python
|
|
125
|
+
page = client.tags.list()
|
|
126
|
+
tag = client.tags.get("security")
|
|
127
|
+
|
|
128
|
+
for tag in client.tags.iter():
|
|
129
|
+
print(tag.slug, tag.name)
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
### Annexes
|
|
133
|
+
|
|
134
|
+
```python
|
|
135
|
+
page = client.annexes.list("cra")
|
|
136
|
+
annex = client.annexes.get("cra", "I")
|
|
137
|
+
|
|
138
|
+
for annex in client.annexes.iter("cra"):
|
|
139
|
+
print(annex.annex_number, annex.title)
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
### Search
|
|
143
|
+
|
|
144
|
+
```python
|
|
145
|
+
results = client.search.query("vulnerability reporting")
|
|
146
|
+
results = client.search.query("audit", framework="gdpr")
|
|
147
|
+
results = client.search.query("encryption", result_type="requirement")
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
## Auto-Pagination
|
|
151
|
+
|
|
152
|
+
All list resources support `iter()` which automatically fetches all pages:
|
|
153
|
+
|
|
154
|
+
```python
|
|
155
|
+
all_articles = list(client.articles.iter("cra"))
|
|
156
|
+
|
|
157
|
+
for art in client.articles.iter("cra", per_page=50):
|
|
158
|
+
process(art)
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
## Error Handling
|
|
162
|
+
|
|
163
|
+
```python
|
|
164
|
+
from law4devs import Law4DevsClient, NotFoundError, RateLimitError, Law4DevsError
|
|
165
|
+
|
|
166
|
+
client = Law4DevsClient()
|
|
167
|
+
|
|
168
|
+
try:
|
|
169
|
+
fw = client.frameworks.get("nonexistent")
|
|
170
|
+
except NotFoundError as e:
|
|
171
|
+
print(f"Not found: {e} (status {e.status_code})")
|
|
172
|
+
except RateLimitError:
|
|
173
|
+
print("Rate limited — slow down requests")
|
|
174
|
+
except Law4DevsError as e:
|
|
175
|
+
print(f"API error: {e}")
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
### Exception Hierarchy
|
|
179
|
+
|
|
180
|
+
| Exception | HTTP Status | Description |
|
|
181
|
+
|-----------|-------------|-------------|
|
|
182
|
+
| `Law4DevsError` | any | Base exception |
|
|
183
|
+
| `NotFoundError` | 404 | Resource not found |
|
|
184
|
+
| `ValidationError` | 400 | Invalid request parameters |
|
|
185
|
+
| `RateLimitError` | 429 | Rate limit exceeded |
|
|
186
|
+
| `ServerError` | 5xx | Server-side error |
|
|
187
|
+
|
|
188
|
+
## Configuration
|
|
189
|
+
|
|
190
|
+
| Parameter | Default | Description |
|
|
191
|
+
|-----------|---------|-------------|
|
|
192
|
+
| `base_url` | `https://api.law4devs.eu/api/v1` | API base URL |
|
|
193
|
+
| `api_key` | `None` | API key for authenticated tiers |
|
|
194
|
+
| `timeout` | `30` | Request timeout in seconds |
|
|
195
|
+
| `max_retries` | `3` | Retries on 429/5xx with exponential backoff |
|
|
196
|
+
|
|
197
|
+
## Available Frameworks
|
|
198
|
+
|
|
199
|
+
| Slug | Name | CELEX | Status |
|
|
200
|
+
|------|------|-------|--------|
|
|
201
|
+
| `cra` | Cyber Resilience Act | 32024R2847 | active |
|
|
202
|
+
| `nis2` | NIS2 Directive | 32022L2555 | active |
|
|
203
|
+
| `dora` | Digital Operational Resilience Act | 32022R2554 | active |
|
|
204
|
+
| `gdpr` | General Data Protection Regulation | 32016R0679 | active |
|
|
205
|
+
| `ai_act` | AI Act | 32024R1689 | active |
|
|
206
|
+
| `eidas` | eIDAS Regulation | 32014R0910 | active |
|
|
207
|
+
| `dsa` | Digital Services Act | 32022R2065 | active |
|
|
208
|
+
| `dma` | Digital Markets Act | 32022R1925 | active |
|
|
209
|
+
| `data_act` | Data Act | 32023R2854 | active |
|
|
210
|
+
| `dga` | Data Governance Act | 32022R0868 | active |
|
|
211
|
+
| `eidas2` | European Digital Identity Regulation | 32024R1183 | active |
|
|
212
|
+
| `cer` | Critical Entities Resilience Directive | 32022L2557 | active |
|
|
213
|
+
| `psd2` | Payment Services Directive 2 | 32015L2366 | active |
|
|
214
|
+
| `mica` | Markets in Crypto-Assets Regulation | 32023R1114 | active |
|
|
215
|
+
| `cybersecurity_act` | Cybersecurity Act | 32019R0881 | active |
|
|
216
|
+
| `eprivacy` | ePrivacy Directive | 32002L0058 | active |
|
|
217
|
+
| `red` | Radio Equipment Directive | 32014L0053 | active |
|
|
218
|
+
| `csrd` | Corporate Sustainability Reporting Dir. | 32022L2464 | active |
|
|
219
|
+
| `nis1` | NIS Directive (Original) | 32016L1148 | superseded |
|
|
220
|
+
|
|
221
|
+
|
|
222
|
+
## License
|
|
223
|
+
|
|
224
|
+
MIT License. See [LICENSE](LICENSE) for details.
|
|
225
|
+
|
|
226
|
+
## Links
|
|
227
|
+
|
|
228
|
+
- [Law4Devs API](https://law4devs.eu)
|
|
229
|
+
- [API Reference](https://docs.law4devs.eu)
|
|
230
|
+
- [PyPI](https://pypi.org/project/law4devs/)
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
law4devs/__init__.py,sha256=PBIDVDEHgF3osGzmDwHNLlnFIi96ElT5OPUEFVJwC7w,836
|
|
2
|
+
law4devs/_http.py,sha256=V4EwXVJeP_igZIjiljZDBcue8qAP55_mwlVT5-t6_kE,2985
|
|
3
|
+
law4devs/_pagination.py,sha256=OAjssCGmvQxVJbFZVksJwZ2fqB_DCWViOV0p1whQauE,547
|
|
4
|
+
law4devs/_version.py,sha256=QTYqXqSTHFRkM9TEgpDFcHvwLbvqHDqvqfQ9EiXkcAM,23
|
|
5
|
+
law4devs/client.py,sha256=tvVgfVFyTOYcyMM26jtCfINKqk81SqON39313qHnkHg,1557
|
|
6
|
+
law4devs/exceptions.py,sha256=f02RtKJokKVICCN_yJ6YwHX_sMJ5aA2AFMUzZQemyMg,612
|
|
7
|
+
law4devs/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
8
|
+
law4devs/models/__init__.py,sha256=_xdC3j4pzYeKbuRWsiDM1pCWRPaXdrC2SIktHUL8OwU,539
|
|
9
|
+
law4devs/models/annex.py,sha256=PBYaEvdpgis7glfncKvP0P-Y_P0waMExPgiQXvrRjvg,890
|
|
10
|
+
law4devs/models/article.py,sha256=4mL99Mi2ExNn_Me6cCRottVK5UXsXs5xeTjkf5zFgRE,1908
|
|
11
|
+
law4devs/models/compliance.py,sha256=a5w0ly10JNnyQxTs317D4GLT-k8XlYIzylZL1XCJ0GY,755
|
|
12
|
+
law4devs/models/framework.py,sha256=QKMA1vHxxVQrTBPI1ov2JKvFDHPy4rahQGa-Ml1Bywg,2315
|
|
13
|
+
law4devs/models/recital.py,sha256=nGqNQXFrQORFLHe2dC5CPYB6PV94_MCgrw8ln_Dl8iA,546
|
|
14
|
+
law4devs/models/requirement.py,sha256=uc9qscBf1UbA6JPg9LcmzJFCMLUa_q2S4TV4DbRvnPs,1315
|
|
15
|
+
law4devs/models/search.py,sha256=I9zWSiNReN3Uwq8Frgx6wBCIuhKons2aiXNE4d1dY0E,993
|
|
16
|
+
law4devs/models/tag.py,sha256=7SJ1xKIg_a25lk8NIcFuFXdF8OB3ddkUMbChxlBOrHs,589
|
|
17
|
+
law4devs/resources/__init__.py,sha256=nuiyaPvQGu0lYa7kxhtwVyvxiKbZ-mSmKH-DpdiVxKs,574
|
|
18
|
+
law4devs/resources/_base.py,sha256=1CVy4WXptVZ4CGdld03SUrN5mkHsQXLAQ0amfAM6pQ4,1686
|
|
19
|
+
law4devs/resources/annexes.py,sha256=6jOZRZ6_Cnl7j5hh5un_XQN8qaD__vM-REgMxHfPo8U,962
|
|
20
|
+
law4devs/resources/articles.py,sha256=mARXWBAFNJ5-v66P42OICm6-ngNeRUnt5AKHA5iZ6Oo,1341
|
|
21
|
+
law4devs/resources/compliance.py,sha256=O0Rn-IsTVtahfuimoP27T_8Uc5ECdhBAssCh6S-e4gw,978
|
|
22
|
+
law4devs/resources/frameworks.py,sha256=POaJvYVF-j56ypYEn8-ql_kkAb2knyqT7j7KW2WOr1o,1134
|
|
23
|
+
law4devs/resources/recitals.py,sha256=AOQDmmTgOiNjK2PunIzDbZ6PynHm0fVo0nSefe_bwkc,949
|
|
24
|
+
law4devs/resources/requirements.py,sha256=PSCaBK85KkIPswYw9NdB6KKdxuvCz4O4uLoYGWZvZqE,909
|
|
25
|
+
law4devs/resources/search.py,sha256=3Y1rbCbIxeKNKLgU84AnIO3hAtLAe4haVa515Byiq14,684
|
|
26
|
+
law4devs/resources/tags.py,sha256=XlfAHFyxtZi2GLf_0CBRDGQPolvlGIQD53blGCHztNA,727
|
|
27
|
+
law4devs-0.1.0.dist-info/METADATA,sha256=5BlmWYwYX0DiJv8z3EqmfV4pW4cFN7wv3PvCnJFoHzY,6345
|
|
28
|
+
law4devs-0.1.0.dist-info/WHEEL,sha256=QccIxa26bgl1E6uMy58deGWi-0aeIkkangHcxk2kWfw,87
|
|
29
|
+
law4devs-0.1.0.dist-info/licenses/LICENSE,sha256=p8gNxtKvHogeIY8HF6FZY28sIeFIDnxkYlI_NRNDQnk,1086
|
|
30
|
+
law4devs-0.1.0.dist-info/RECORD,,
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 law4devs
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|