pyfederation 1.0.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.
- pyfederation/__init__.py +65 -0
- pyfederation/_utils.py +105 -0
- pyfederation/async_client.py +1417 -0
- pyfederation/client.py +1406 -0
- pyfederation/enums.py +88 -0
- pyfederation/exceptions.py +23 -0
- pyfederation/py.typed +0 -0
- pyfederation/records.py +276 -0
- pyfederation-1.0.0.dist-info/METADATA +426 -0
- pyfederation-1.0.0.dist-info/RECORD +12 -0
- pyfederation-1.0.0.dist-info/WHEEL +4 -0
- pyfederation-1.0.0.dist-info/licenses/LICENSE +15 -0
pyfederation/__init__.py
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
"""Python client for Open Federated Database (OFD) servers."""
|
|
2
|
+
|
|
3
|
+
from .async_client import AsyncFederationClient
|
|
4
|
+
from .client import FederationClient
|
|
5
|
+
from .enums import (
|
|
6
|
+
AuditLogType,
|
|
7
|
+
ClassificationFlag,
|
|
8
|
+
EntityRelationshipType,
|
|
9
|
+
IncidentType,
|
|
10
|
+
NamedEntityType,
|
|
11
|
+
RecordType,
|
|
12
|
+
SuggestedAction,
|
|
13
|
+
)
|
|
14
|
+
from .exceptions import FederationClientException
|
|
15
|
+
from .records import (
|
|
16
|
+
AuditLog,
|
|
17
|
+
BlacklistRecord,
|
|
18
|
+
ContentClassification,
|
|
19
|
+
ContentInput,
|
|
20
|
+
EntityQueryResult,
|
|
21
|
+
EntityRecord,
|
|
22
|
+
EvidenceRecord,
|
|
23
|
+
FileAttachmentRecord,
|
|
24
|
+
OperatorCreated,
|
|
25
|
+
OperatorRecord,
|
|
26
|
+
ReportRecord,
|
|
27
|
+
ReportSubmission,
|
|
28
|
+
ResolvedEntity,
|
|
29
|
+
ResolvedEntityPosition,
|
|
30
|
+
ScannedContent,
|
|
31
|
+
SearchResult,
|
|
32
|
+
ServerInformation,
|
|
33
|
+
UploadResult,
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
__all__ = [
|
|
37
|
+
"AsyncFederationClient",
|
|
38
|
+
"AuditLog",
|
|
39
|
+
"AuditLogType",
|
|
40
|
+
"BlacklistRecord",
|
|
41
|
+
"ClassificationFlag",
|
|
42
|
+
"ContentClassification",
|
|
43
|
+
"ContentInput",
|
|
44
|
+
"EntityQueryResult",
|
|
45
|
+
"EntityRecord",
|
|
46
|
+
"EntityRelationshipType",
|
|
47
|
+
"EvidenceRecord",
|
|
48
|
+
"FederationClient",
|
|
49
|
+
"FederationClientException",
|
|
50
|
+
"FileAttachmentRecord",
|
|
51
|
+
"IncidentType",
|
|
52
|
+
"NamedEntityType",
|
|
53
|
+
"OperatorCreated",
|
|
54
|
+
"OperatorRecord",
|
|
55
|
+
"RecordType",
|
|
56
|
+
"ReportRecord",
|
|
57
|
+
"ReportSubmission",
|
|
58
|
+
"ResolvedEntity",
|
|
59
|
+
"ResolvedEntityPosition",
|
|
60
|
+
"ScannedContent",
|
|
61
|
+
"SearchResult",
|
|
62
|
+
"ServerInformation",
|
|
63
|
+
"SuggestedAction",
|
|
64
|
+
"UploadResult",
|
|
65
|
+
]
|
pyfederation/_utils.py
ADDED
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
from collections.abc import Sequence
|
|
2
|
+
from dataclasses import asdict
|
|
3
|
+
from email.message import Message
|
|
4
|
+
from typing import Any, TypeVar
|
|
5
|
+
from urllib.parse import quote
|
|
6
|
+
|
|
7
|
+
import httpx
|
|
8
|
+
from pydantic import BaseModel
|
|
9
|
+
|
|
10
|
+
from .records import ContentInput
|
|
11
|
+
|
|
12
|
+
M = TypeVar("M", bound=BaseModel)
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def compact(**values: Any) -> dict[str, Any]:
|
|
16
|
+
"""Returns the given values without the ones that are unset."""
|
|
17
|
+
return {key: value for key, value in values.items() if value is not None}
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def require(value: str | None, name: str) -> None:
|
|
21
|
+
"""Raises a ValueError if the value is empty."""
|
|
22
|
+
if not value:
|
|
23
|
+
raise ValueError(f"{name} cannot be empty")
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def page_params(
|
|
27
|
+
page: int, limit: int, by: str | None = None, order: str | None = None, **filters: Any
|
|
28
|
+
) -> dict[str, Any]:
|
|
29
|
+
"""Returns the query parameters of a paginated request.
|
|
30
|
+
|
|
31
|
+
The sort field is lowercased and the sort order uppercased, matching what the server expects.
|
|
32
|
+
"""
|
|
33
|
+
if page < 1:
|
|
34
|
+
raise ValueError("Page must be greater than 0")
|
|
35
|
+
if limit < 1:
|
|
36
|
+
raise ValueError("Limit must be greater than 0")
|
|
37
|
+
|
|
38
|
+
return compact(
|
|
39
|
+
page=page,
|
|
40
|
+
limit=limit,
|
|
41
|
+
**filters,
|
|
42
|
+
by=by.lower() if by is not None else None,
|
|
43
|
+
order=order.upper() if order is not None else None,
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def search_params(query: str, page: int, limit: int, **params: Any) -> dict[str, Any]:
|
|
48
|
+
"""Returns the query parameters of a paginated search."""
|
|
49
|
+
if len(query) < 2:
|
|
50
|
+
raise ValueError("Search query must be at least 2 characters")
|
|
51
|
+
|
|
52
|
+
return {"q": query, **page_params(page, limit, **params)}
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def path_segment(value: str) -> str:
|
|
56
|
+
"""Returns an identifier percent-encoded as one path segment, so it cannot address another
|
|
57
|
+
endpoint."""
|
|
58
|
+
if value in (".", ".."):
|
|
59
|
+
raise ValueError("Identifier cannot be '.' or '..'")
|
|
60
|
+
|
|
61
|
+
return quote(value, safe="")
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def entity_path(entity_identifier: str) -> str:
|
|
65
|
+
"""Returns the path of an entity.
|
|
66
|
+
|
|
67
|
+
Servers implementing OFD 1.x take the identifier unencoded, so an identifier that would not
|
|
68
|
+
stay one path segment is rejected.
|
|
69
|
+
"""
|
|
70
|
+
if entity_identifier in (".", "..") or any(c in entity_identifier for c in "/\\?#%"):
|
|
71
|
+
raise ValueError(f"Entity identifier cannot be used in a path: {entity_identifier}")
|
|
72
|
+
|
|
73
|
+
return f"entities/{entity_identifier}"
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def parse_list(model: type[M], data: list[Any]) -> list[M]:
|
|
77
|
+
return [model.model_validate(item) for item in data]
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def evidence_payload(
|
|
81
|
+
evidence: str | ContentInput | Sequence[ContentInput],
|
|
82
|
+
) -> dict[str, Any] | list[dict[str, Any]]:
|
|
83
|
+
if isinstance(evidence, str):
|
|
84
|
+
evidence = ContentInput(evidence)
|
|
85
|
+
|
|
86
|
+
if isinstance(evidence, ContentInput):
|
|
87
|
+
return compact(**asdict(evidence))
|
|
88
|
+
|
|
89
|
+
if not evidence:
|
|
90
|
+
raise ValueError("Evidence cannot be empty")
|
|
91
|
+
|
|
92
|
+
return [compact(**asdict(item)) for item in evidence]
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def attachment_file_name(response: httpx.Response, default: str) -> str:
|
|
96
|
+
"""Returns the file name given by the Content-Disposition header, without directories or a
|
|
97
|
+
drive letter.
|
|
98
|
+
|
|
99
|
+
Falls back to the default when the header names no usable file.
|
|
100
|
+
"""
|
|
101
|
+
message = Message()
|
|
102
|
+
message["Content-Disposition"] = response.headers.get("Content-Disposition", "")
|
|
103
|
+
file_name = message.get_filename(default)
|
|
104
|
+
file_name = file_name[max(map(file_name.rfind, "/\\:")) + 1 :]
|
|
105
|
+
return file_name if file_name not in ("", ".", "..") else default
|