pyfederation 1.0.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 (46) hide show
  1. pyfederation-1.0.0/.gitattributes +1 -0
  2. pyfederation-1.0.0/.github/workflows/ci.yml +91 -0
  3. pyfederation-1.0.0/.github/workflows/release.yml +65 -0
  4. pyfederation-1.0.0/.gitignore +11 -0
  5. pyfederation-1.0.0/CHANGELOG.md +18 -0
  6. pyfederation-1.0.0/LICENSE +15 -0
  7. pyfederation-1.0.0/Makefile +18 -0
  8. pyfederation-1.0.0/PKG-INFO +426 -0
  9. pyfederation-1.0.0/README.md +396 -0
  10. pyfederation-1.0.0/docker-compose.yml +74 -0
  11. pyfederation-1.0.0/pyproject.toml +74 -0
  12. pyfederation-1.0.0/scripts/unasync.py +64 -0
  13. pyfederation-1.0.0/src/pyfederation/__init__.py +65 -0
  14. pyfederation-1.0.0/src/pyfederation/_utils.py +105 -0
  15. pyfederation-1.0.0/src/pyfederation/async_client.py +1417 -0
  16. pyfederation-1.0.0/src/pyfederation/client.py +1406 -0
  17. pyfederation-1.0.0/src/pyfederation/enums.py +88 -0
  18. pyfederation-1.0.0/src/pyfederation/exceptions.py +23 -0
  19. pyfederation-1.0.0/src/pyfederation/py.typed +0 -0
  20. pyfederation-1.0.0/src/pyfederation/records.py +276 -0
  21. pyfederation-1.0.0/tests/__init__.py +0 -0
  22. pyfederation-1.0.0/tests/conftest.py +38 -0
  23. pyfederation-1.0.0/tests/fixtures/specification.json +9249 -0
  24. pyfederation-1.0.0/tests/integration/__init__.py +0 -0
  25. pyfederation-1.0.0/tests/integration/conftest.py +68 -0
  26. pyfederation-1.0.0/tests/integration/test_attachments.py +74 -0
  27. pyfederation-1.0.0/tests/integration/test_audit_logs.py +25 -0
  28. pyfederation-1.0.0/tests/integration/test_blacklist.py +76 -0
  29. pyfederation-1.0.0/tests/integration/test_entities.py +97 -0
  30. pyfederation-1.0.0/tests/integration/test_evidence.py +60 -0
  31. pyfederation-1.0.0/tests/integration/test_operators.py +76 -0
  32. pyfederation-1.0.0/tests/integration/test_reports.py +96 -0
  33. pyfederation-1.0.0/tests/integration/test_server.py +77 -0
  34. pyfederation-1.0.0/tests/payloads.py +148 -0
  35. pyfederation-1.0.0/tests/test_attachments.py +252 -0
  36. pyfederation-1.0.0/tests/test_audit_logs.py +92 -0
  37. pyfederation-1.0.0/tests/test_blacklist.py +131 -0
  38. pyfederation-1.0.0/tests/test_client.py +213 -0
  39. pyfederation-1.0.0/tests/test_entities.py +218 -0
  40. pyfederation-1.0.0/tests/test_evidence.py +165 -0
  41. pyfederation-1.0.0/tests/test_exceptions.py +29 -0
  42. pyfederation-1.0.0/tests/test_operators.py +196 -0
  43. pyfederation-1.0.0/tests/test_records.py +97 -0
  44. pyfederation-1.0.0/tests/test_reports.py +185 -0
  45. pyfederation-1.0.0/tests/test_specification.py +165 -0
  46. pyfederation-1.0.0/tests/utils.py +10 -0
@@ -0,0 +1 @@
1
+ * text=auto eol=lf
@@ -0,0 +1,91 @@
1
+ name: CI
2
+
3
+ on:
4
+ push:
5
+ branches:
6
+ - '**'
7
+ pull_request:
8
+ branches:
9
+ - '**'
10
+ workflow_dispatch:
11
+
12
+ jobs:
13
+ check:
14
+ runs-on: ubuntu-latest
15
+
16
+ steps:
17
+ - name: Checkout repository
18
+ uses: actions/checkout@v4
19
+
20
+ - name: Set up Python
21
+ uses: actions/setup-python@v5
22
+ with:
23
+ python-version: '3.11'
24
+
25
+ - name: Install dependencies
26
+ run: |
27
+ python -m pip install --upgrade pip
28
+ pip install -e . --group dev
29
+
30
+ - name: Check generated client, linting, formatting and types
31
+ run: make check
32
+
33
+ test:
34
+ runs-on: ubuntu-latest
35
+ strategy:
36
+ matrix:
37
+ python-version: ['3.11', '3.12', '3.13', '3.14']
38
+
39
+ steps:
40
+ - name: Checkout repository
41
+ uses: actions/checkout@v4
42
+
43
+ - name: Set up Python ${{ matrix.python-version }}
44
+ uses: actions/setup-python@v5
45
+ with:
46
+ python-version: ${{ matrix.python-version }}
47
+
48
+ - name: Install dependencies
49
+ run: |
50
+ python -m pip install --upgrade pip
51
+ pip install -e . --group dev
52
+
53
+ - name: Run unit tests
54
+ run: make test
55
+
56
+ integration:
57
+ needs: test
58
+ runs-on: ubuntu-latest
59
+
60
+ steps:
61
+ - name: Checkout repository
62
+ uses: actions/checkout@v4
63
+
64
+ - name: Set up Python
65
+ uses: actions/setup-python@v5
66
+ with:
67
+ python-version: '3.11'
68
+
69
+ - name: Install dependencies
70
+ run: |
71
+ python -m pip install --upgrade pip
72
+ pip install -e . --group dev
73
+
74
+ - name: Deploy Federation Server environment
75
+ run: |
76
+ docker compose up -d
77
+ echo "Waiting for server to be ready..."
78
+ for i in $(seq 1 60); do
79
+ if curl -s -o /dev/null -w "%{http_code}" http://localhost:7000/info | grep -q 200; then
80
+ echo "Server is ready!"
81
+ exit 0
82
+ fi
83
+ echo "Attempt $i: server not ready yet, waiting 5 seconds..."
84
+ sleep 5
85
+ done
86
+ echo "Timeout: server did not become ready within 5 minutes"
87
+ exit 1
88
+
89
+ - name: Run integration tests
90
+ run: make integration
91
+
@@ -0,0 +1,65 @@
1
+ name: Release
2
+
3
+ on:
4
+ release:
5
+ types: [published]
6
+ workflow_dispatch:
7
+
8
+ jobs:
9
+ build:
10
+ runs-on: ubuntu-latest
11
+
12
+ steps:
13
+ - name: Checkout repository
14
+ uses: actions/checkout@v4
15
+
16
+ - name: Set up Python
17
+ uses: actions/setup-python@v5
18
+ with:
19
+ python-version: '3.11'
20
+
21
+ - name: Install dependencies
22
+ run: |
23
+ python -m pip install --upgrade pip build twine
24
+ pip install -e . --group dev
25
+
26
+ - name: Run checks and unit tests
27
+ run: |
28
+ make check
29
+ make test
30
+
31
+ - name: Build distributions
32
+ run: |
33
+ python -m build
34
+ python -m twine check --strict dist/*
35
+
36
+ - name: Upload distributions
37
+ uses: actions/upload-artifact@v4
38
+ with:
39
+ name: dist
40
+ path: dist/
41
+
42
+ publish:
43
+ needs: build
44
+ runs-on: ubuntu-latest
45
+ # A published release goes to PyPI, a manual run to TestPyPI.
46
+ environment: ${{ github.event_name == 'release' && 'pypi' || 'testpypi' }}
47
+ permissions:
48
+ id-token: write
49
+
50
+ steps:
51
+ - name: Download distributions
52
+ uses: actions/download-artifact@v4
53
+ with:
54
+ name: dist
55
+ path: dist/
56
+
57
+ - name: Publish to TestPyPI
58
+ if: github.event_name != 'release'
59
+ uses: pypa/gh-action-pypi-publish@release/v1
60
+ with:
61
+ repository-url: https://test.pypi.org/legacy/
62
+
63
+ - name: Publish to PyPI
64
+ if: github.event_name == 'release'
65
+ uses: pypa/gh-action-pypi-publish@release/v1
@@ -0,0 +1,11 @@
1
+ __pycache__/
2
+ *.py[cod]
3
+ .venv/
4
+ build/
5
+ dist/
6
+ *.egg-info/
7
+ .mypy_cache/
8
+ .pytest_cache/
9
+ .ruff_cache/
10
+ .idea/
11
+ .vscode/
@@ -0,0 +1,18 @@
1
+ # Changelog
2
+
3
+ All notable changes to this project will be documented in this file.
4
+
5
+ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
6
+ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
+
8
+ ## [1.0.0] - 2026-10-13
9
+
10
+ The first release, a port of [JFederation](https://github.com/nosial/JFederation) 1.0.13. This release follows
11
+ version `v1.0-R4` of the OFD Specification.
12
+
13
+ ### Added
14
+ - `FederationClient` and `AsyncFederationClient`, covering every method of the Federation server API: server
15
+ information, search, content scanning, audit logs, operators, entities, evidence, reports, blacklist records and
16
+ file attachments.
17
+ - Records and enums for every object the server returns.
18
+ - `FederationClientException`, raised for every failed request with the HTTP status code and the server's message.
@@ -0,0 +1,15 @@
1
+ Copyright 2022-2026 Nosial
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated
4
+ documentation files (the "Software"), to deal in the Software without restriction, including without limitation
5
+ the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and
6
+ to permit persons to whom the Software is furnished to do so, subject to the following conditions:
7
+
8
+ The above copyright notice and this permission notice shall be included in all copies or substantial portions
9
+ of the Software.
10
+
11
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED
12
+ TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
13
+ THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF
14
+ CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
15
+ DEALINGS IN THE SOFTWARE.
@@ -0,0 +1,18 @@
1
+ PYTHON ?= python
2
+
3
+ .PHONY: generate check test integration
4
+
5
+ generate:
6
+ $(PYTHON) scripts/unasync.py
7
+
8
+ check:
9
+ $(PYTHON) scripts/unasync.py --check
10
+ $(PYTHON) -m ruff check .
11
+ $(PYTHON) -m ruff format --check .
12
+ $(PYTHON) -m mypy src tests scripts
13
+
14
+ test:
15
+ $(PYTHON) -m pytest
16
+
17
+ integration:
18
+ $(PYTHON) -m pytest -m integration
@@ -0,0 +1,426 @@
1
+ Metadata-Version: 2.5
2
+ Name: pyfederation
3
+ Version: 1.0.0
4
+ Summary: Python client for Open Federated Database (OFD) servers
5
+ Project-URL: Homepage, https://github.com/nosial/PyFederation
6
+ Project-URL: Source, https://github.com/nosial/PyFederation
7
+ Project-URL: Issues, https://github.com/nosial/PyFederation/issues
8
+ Project-URL: Changelog, https://github.com/nosial/PyFederation/blob/master/CHANGELOG.md
9
+ Project-URL: Specification, https://github.com/nosial/OFD-Specification
10
+ License-Expression: MIT
11
+ License-File: LICENSE
12
+ Keywords: client,federation,moderation,ofd,spam
13
+ Classifier: Development Status :: 5 - Production/Stable
14
+ Classifier: Framework :: AsyncIO
15
+ Classifier: Intended Audience :: Developers
16
+ Classifier: Operating System :: OS Independent
17
+ Classifier: Programming Language :: Python :: 3
18
+ Classifier: Programming Language :: Python :: 3 :: Only
19
+ Classifier: Programming Language :: Python :: 3.11
20
+ Classifier: Programming Language :: Python :: 3.12
21
+ Classifier: Programming Language :: Python :: 3.13
22
+ Classifier: Programming Language :: Python :: 3.14
23
+ Classifier: Topic :: Internet :: WWW/HTTP
24
+ Classifier: Topic :: Security
25
+ Classifier: Typing :: Typed
26
+ Requires-Python: >=3.11
27
+ Requires-Dist: httpx>=0.27
28
+ Requires-Dist: pydantic>=2.6
29
+ Description-Content-Type: text/markdown
30
+
31
+ # PyFederation
32
+
33
+ PyFederation is a Python client library for the Federated Database protocol. It implements the
34
+ [Open Federated Database](https://github.com/nosial/OFD-Specification) specification and follows the design of the
35
+ reference implementation, [FederationLib](https://github.com/nosial/FederationLib), and of its Java counterpart,
36
+ [JFederation](https://github.com/nosial/JFederation), from which it is ported.
37
+
38
+ ## Features
39
+
40
+ - Works with any OFD-compliant Federation server
41
+ - Content scanning with entity resolution, classification, and risk assessment
42
+ - Every method of the server API: operators, records, evidence, reports, blacklist records, file
43
+ attachments, audit logs, search, and server information
44
+ - Pagination, category filters, and sorting on every listing and search endpoint
45
+ - Synchronous and asynchronous clients, `FederationClient` and `AsyncFederationClient`, with the same methods
46
+ - All errors are mapped to a single exception carrying the HTTP status code and server message
47
+
48
+ ## Table of Contents
49
+
50
+ <!-- TOC -->
51
+ * [PyFederation](#pyfederation)
52
+ * [Features](#features)
53
+ * [Table of Contents](#table-of-contents)
54
+ * [Prerequisites](#prerequisites)
55
+ * [Installation](#installation)
56
+ * [Via PyPI](#via-pypi)
57
+ * [From source](#from-source)
58
+ * [Usage](#usage)
59
+ * [Connecting to a server](#connecting-to-a-server)
60
+ * [Asynchronous client](#asynchronous-client)
61
+ * [Content scanning](#content-scanning)
62
+ * [Searching records](#searching-records)
63
+ * [Operators](#operators)
64
+ * [Entities](#entities)
65
+ * [Evidence](#evidence)
66
+ * [Reports](#reports)
67
+ * [Blacklist records](#blacklist-records)
68
+ * [File attachments](#file-attachments)
69
+ * [Pagination and sorting](#pagination-and-sorting)
70
+ * [Error handling](#error-handling)
71
+ * [Testing](#testing)
72
+ * [License](#license)
73
+ <!-- TOC -->
74
+
75
+ ## Prerequisites
76
+
77
+ - **Python 3.11 or newer**
78
+ - **A running Federation server** for anything other than constructing a client, the reference
79
+ implementation is [FederationLib](https://github.com/nosial/FederationLib), which includes a
80
+ dockerized server deployment
81
+
82
+ Dependencies are HTTPX for HTTP transport and Pydantic for the records.
83
+
84
+ ## Installation
85
+
86
+ ### Via PyPI
87
+
88
+ The library is published on [PyPI](https://pypi.org/project/pyfederation/):
89
+
90
+ ```sh
91
+ pip install pyfederation
92
+ ```
93
+
94
+ ### From source
95
+
96
+ To use an unreleased version, install the library from a clone of the repository:
97
+
98
+ ```sh
99
+ git clone https://github.com/nosial/PyFederation.git
100
+ cd PyFederation
101
+ pip install .
102
+ ```
103
+
104
+ ## Usage
105
+
106
+ All functionality is exposed through the `FederationClient` class. Each method maps to a REST endpoint of the
107
+ Federation server and handles serialization, authentication, and error mapping.
108
+
109
+ ### Connecting to a server
110
+
111
+ A client is constructed with the base URL of the Federation server and an optional operator access
112
+ token. A client without a token operates anonymously against the server's public endpoints:
113
+
114
+ ```python
115
+ from pyfederation import FederationClient
116
+
117
+ # Anonymous client
118
+ client = FederationClient("https://federation.example.com")
119
+
120
+ # Authenticated client with an operator access token
121
+ client = FederationClient("https://federation.example.com", "access-token")
122
+ ```
123
+
124
+ The access token can be changed at any time during the lifetime of the client:
125
+
126
+ ```python
127
+ client.access_token = "new-token" # switch authentication
128
+ client.access_token = None # revert to anonymous access
129
+ ```
130
+
131
+ Clients should be closed when no longer needed to release the underlying HTTP connection pool, or
132
+ used as a context manager:
133
+
134
+ ```python
135
+ client.close()
136
+
137
+ with FederationClient("https://federation.example.com", "access-token") as client:
138
+ ...
139
+ ```
140
+
141
+ For custom HTTP settings (timeouts, proxies, TLS, etc.) an existing `httpx.Client` can be passed to
142
+ the constructor instead:
143
+
144
+ ```python
145
+ import httpx
146
+
147
+ http_client = httpx.Client(timeout=15)
148
+
149
+ client = FederationClient("https://federation.example.com", "access-token", http_client)
150
+ ```
151
+
152
+ Server information, including the server name, API version, feature flags (which record types are
153
+ publicly visible), and total record counts, is available through `get_server_information()`, the raw
154
+ OpenAPI specification of the server is available through `get_specification()`:
155
+
156
+ ```python
157
+ info = client.get_server_information()
158
+ print(f"Server {info.name}, API version {info.api_version}")
159
+ print(f"Known entities: {info.known_entities}, reports: {info.reports}")
160
+ ```
161
+
162
+ ### Asynchronous client
163
+
164
+ `AsyncFederationClient` has the same methods as `FederationClient`, as coroutines, and is closed with
165
+ `aclose()` or used as an async context manager:
166
+
167
+ ```python
168
+ from pyfederation import AsyncFederationClient
169
+
170
+ async with AsyncFederationClient("https://federation.example.com", "access-token") as client:
171
+ info = await client.get_server_information()
172
+ ```
173
+
174
+ ### Content scanning
175
+
176
+ Content scanning accepts text, a `ContentInput`, or a list of inputs. Each input can carry text, an
177
+ operator note, tag, confidentiality flag, and metadata; at least one input must contain text:
178
+
179
+ | Method | Return Type | Description |
180
+ |----------------------------------------------------|------------------|----------------------------------|
181
+ | `scan_content(evidence, author, top_k, threshold)` | `ScannedContent` | Scans one or more content inputs |
182
+
183
+ Scanning content and inspecting the classification result:
184
+
185
+ ```python
186
+ from pyfederation import ContentInput
187
+
188
+ scan = client.scan_content(ContentInput("Suspicious message to inspect"), "author@example.com")
189
+
190
+ classification = scan.classification
191
+ print(
192
+ f"Classification {classification.classification_flag} with "
193
+ f"{classification.confidence:.1%} confidence, detected language: {classification.detected_language}"
194
+ )
195
+ ```
196
+
197
+ ### Searching records
198
+
199
+ The global search queries all record types with an optional type filter. The search query must be at
200
+ least 2 characters:
201
+
202
+ ```python
203
+ from pyfederation import RecordType
204
+
205
+ # Search all record types
206
+ results = client.search("author@example.com")
207
+
208
+ # Search only entity and blacklist records, page 1, 25 per page
209
+ results = client.search("author@example.com", [RecordType.ENTITY, RecordType.BLACKLIST], 1, 25)
210
+ ```
211
+
212
+ `SearchResult` records carry the matched `type` and the `record`, resolved to the record class of
213
+ the matched type.
214
+
215
+ ### Operators
216
+
217
+ Operators are the accounts of the Federation server. Every change to the data is made by an
218
+ operator, and each operator holds permissions that decide what it may do.
219
+
220
+ | Method | Description |
221
+ |----------------------------------------------------------------------------------|--------------------------------------------------------------------------------------|
222
+ | `create_operator(name)` | Creates a new operator, returns the raw access token (only exposed at creation time) |
223
+ | `get_operator(uuid)` / `get_self()` | Returns an operator record / the currently authenticated operator |
224
+ | `list_operators(page, limit, category, by, order)` | Lists operators with optional filters |
225
+ | `search_operators(query, page, limit, category, by, order)` | Searches operators by name |
226
+ | `update_operator_name(uuid, name)` | Updates the display name of an operator |
227
+ | `delete_operator(uuid)` | Deletes an operator |
228
+ | `disable_operator(uuid)` / `enable_operator(uuid)` | Disables / enables an operator account |
229
+ | `set_operator_permissions(uuid, enabled)` | Sets whether the operator has operator-level permissions |
230
+ | `set_client_permissions(uuid, enabled)` | Sets whether the operator has client-level permissions |
231
+ | `set_management_permissions(uuid, enabled)` | Sets whether the operator has management-level permissions |
232
+ | `set_auto_assign(uuid, enabled)` | Sets whether reports are automatically assigned to the operator |
233
+ | `generate_access_token(update)` | Refreshes the current operator's token, optionally updating the client |
234
+ | `generate_operator_access_token(uuid)` | Refreshes another operator's token |
235
+ | `list_operator_audit_logs(uuid, ...)` | Lists the audit log entries of an operator |
236
+ | `list_operator_evidence(uuid, ...)` | Lists the evidence an operator submitted |
237
+ | `list_operator_blacklist(uuid, ...)` | Lists the blacklist records created by an operator |
238
+ | `list_operator_reports(uuid, ...)` / `list_assigned_operator_reports(uuid, ...)` | Lists the reports submitted by / assigned to an operator |
239
+
240
+ Creating an operator and generating a new token:
241
+
242
+ ```python
243
+ created = client.create_operator("my-application")
244
+ print(f"Operator UUID: {created.uuid}")
245
+
246
+ # The raw token is only returned once at creation time, store it securely
247
+ token = created.access_token
248
+
249
+ refreshed = client.generate_access_token(True) # refresh and update this client
250
+ ```
251
+
252
+ ### Entities
253
+
254
+ Entities are the subjects tracked by the federation: users, domains, IP addresses, or any other
255
+ identifier present in scanned content.
256
+
257
+ | Method | Description |
258
+ |------------------------------------------------------------|-------------------------------------------------------------------|
259
+ | `get_entity_record(identifier)` | Returns the record of an entity by identifier |
260
+ | `query_entity(identifier)` | Returns an entity's relationship group and active blacklist state |
261
+ | `search_entities(query, page, limit, category, by, order)` | Searches entities by identifier |
262
+ | `list_entities(page, limit, category, by, order)` | Lists entities |
263
+ | `get_top_threats(limit)` | Lists the highest-risk entities |
264
+ | `update_entity(identifier, metadata)` | Updates the metadata of an entity |
265
+ | `delete_entity(identifier)` | Deletes an entity |
266
+ | `set_entity_whitelist(identifier, whitelisted)` | (Un)whitelists an entity |
267
+ | `clear_entity_reputation(identifier)` | Clears the accumulated reputation of an entity |
268
+ | `set_entity_relationship(identifier, related_id, type)` | Sets the relationship between two entities |
269
+ | `clear_entity_relationship(identifier)` | Clears the relationship of an entity |
270
+ | `push_entity(host, id, metadata)` | Registers an entity with the server |
271
+ | `list_entity_audit_logs(identifier, ...)` | Lists the audit log entries involving an entity |
272
+ | `list_entity_blacklist_records(identifier, ...)` | Lists the blacklist records against an entity |
273
+ | `list_entity_evidence_records(identifier, ...)` | Lists the evidence submitted against an entity |
274
+ | `list_entity_reports(identifier, ...)` | Lists the reports submitted against an entity |
275
+
276
+ Entities are identified by their UUID, SHA-256 hash, or address, such as `example.com` or
277
+ `john@example.com`.
278
+
279
+ ### Evidence
280
+
281
+ Evidence records are the supporting data attached to entities, either when submitting a report or
282
+ manually by operators, and can hold text content, attachments, tags, notes, and confidentiality
283
+ flags:
284
+
285
+ | Method | Description |
286
+ |------------------------------------------------------------------------------------------------|----------------------------------------------|
287
+ | `submit_evidence(identifier, text_content, note, tag, confidential, metadata, classification)` | Submits evidence with all optional fields |
288
+ | `list_evidence(page, limit, include_confidential, category, by, order)` | Lists evidence records |
289
+ | `search_evidence(query, page, limit, category, by, order)` | Searches evidence records |
290
+ | `get_evidence_record(uuid)` | Returns a single evidence record |
291
+ | `get_evidence_attachments(uuid)` | Lists the attachments of an evidence record |
292
+ | `update_evidence_confidentiality(uuid, confidential)` | Updates the confidentiality flag of evidence |
293
+ | `update_evidence_tag(uuid, tag)` | Updates the tag of evidence |
294
+ | `classify_evidence(uuid, classification)` | Classifies evidence |
295
+ | `add_evidence_to_report(evidence_uuid, report_uuid)` | Attaches evidence to a report |
296
+ | `delete_evidence(uuid)` | Deletes an evidence record |
297
+
298
+ Confidential evidence is hidden from unauthenticated clients by default, listing endpoints
299
+ support an `include_confidential` parameter to include them when the operator has the appropriate
300
+ permissions.
301
+
302
+ ### Reports
303
+
304
+ Reports are the central mechanism of the federation server. A report is submitted against an entity,
305
+ groups one or more pieces of evidence, and can be classified, assigned to operators, and closed:
306
+
307
+ | Method | Description |
308
+ |----------------------------------------------------------------------------------|----------------------------------------------------------------------------------|
309
+ | `submit_report(entity, evidence, incident_type, report_message)` | Submits one or more evidence items with a report |
310
+ | `list_reports(page, limit, category, by, order)` | Lists reports |
311
+ | `list_opened_reports(page, limit, by, order)` | Lists the opened reports assigned to the current operator |
312
+ | `search_reports(query, page, limit, category, by, order)` | Searches reports by content |
313
+ | `get_report(uuid)` | Returns a single report |
314
+ | `list_report_evidence_records(report_uuid, ...)` | Lists evidence associated with a report |
315
+ | `close_report(uuid, classification, blacklist_incident_type, blacklist_expires)` | Closes a report, optionally classifying its evidence and blacklisting the entity |
316
+ | `assign_operator_to_report(report_uuid, operator_uuid)` | Assigns an operator to a report |
317
+ | `delete_report(uuid)` | Deletes a report |
318
+
319
+ `submit_report` returns a `ReportSubmission` object with the created `report` and its list of
320
+ `evidence` records. Upload attachments separately against an evidence record.
321
+
322
+ ### Blacklist records
323
+
324
+ Blacklist records are the server's mitigation mechanism. An operator blacklists an entity with an
325
+ incident type and, optionally, the report backing the decision:
326
+
327
+ | Method | Description |
328
+ |----------------------------------------------------------------------------|------------------------------------------------------------------------|
329
+ | `blacklist_entity(identifier, incident_type, report_uuid)` | Blacklists an entity permanently |
330
+ | `blacklist_entity(identifier, incident_type, report_uuid, expires)` | Blacklists an entity with an expiration timestamp (Unix epoch seconds) |
331
+ | `list_blacklist_records(page, limit, include_lifted, category, by, order)` | Lists blacklist entries |
332
+ | `search_blacklist(query, page, limit, category, by, order)` | Searches blacklist entries |
333
+ | `get_blacklist_record(uuid)` | Returns a single blacklist record |
334
+ | `lift_blacklist_record(uuid)` | Lifts the blacklist entry of an entity |
335
+ | `extend_blacklist_record(uuid, seconds)` | Extends the blacklist of an entity by the given seconds |
336
+ | `delete_blacklist_record(uuid)` | Deletes a blacklist record |
337
+
338
+ ### File attachments
339
+
340
+ Evidence records support file attachments with uploads from disk or URL, and note attachments (plain-text content):
341
+
342
+ | Method | Description |
343
+ |---------------------------------------------------------------------------|----------------------------------------------------|
344
+ | `upload_file_attachment(evidence_uuid, local_file_path)` | Uploads a local file attachment |
345
+ | `upload_file_attachment(evidence_uuid, local_file_path, file_name)` | Uploads a local file attachment with a custom name |
346
+ | `upload_file_attachment_from_url(evidence_uuid, file_url)` | Uploads an attachment from a reachable URL |
347
+ | `upload_file_attachment_from_url(evidence_uuid, file_url, max_file_size)` | Uploads an attachment from a URL with a size limit |
348
+ | `upload_note_attachment(evidence_uuid, file_name, content)` | Uploads a plain-text note attachment |
349
+ | `download_attachment(attachment_uuid, directory_path)` | Downloads an attachment into the given directory |
350
+ | `get_attachment_info(uuid)` | Returns the metadata of an attachment |
351
+ | `list_attachments(page, limit, category, by, order)` | Lists attachments |
352
+ | `search_attachments(query, page, limit, category, by, order)` | Searches attachments |
353
+ | `delete_attachment(uuid)` | Deletes an attachment |
354
+
355
+ URL uploads default to a maximum file size of 50 MB, matching the FederationLib client default, the
356
+ limit can be raised per request.
357
+
358
+ ### Pagination and sorting
359
+
360
+ Every listing and search endpoint accepts the same pagination and sorting arguments: `page` (1-based)
361
+ and `limit` (minimum 1), followed by an optional category filter, `by` (the field to sort by,
362
+ case-insensitive), and `order` (`"ASC"` or `"DESC"`), for example:
363
+
364
+ ```python
365
+ # Defaults: page 1, limit 100
366
+ entities = client.list_entities()
367
+
368
+ # Explicit pagination
369
+ entities = client.list_entities(2, 50)
370
+
371
+ # Pagination with a category filter, sorted by reputation ascending
372
+ entities = client.list_entities(2, 50, "WHITELISTED", "reputation", "ASC")
373
+ ```
374
+
375
+ The argument order is the same for every endpoint.
376
+
377
+ ## Error handling
378
+
379
+ All failures are raised as `FederationClientException`, carrying the HTTP status code of the
380
+ response and, when the server returns one, its error message:
381
+
382
+ ```python
383
+ from pyfederation import FederationClientException
384
+
385
+ try:
386
+ client.get_entity_record("non-existent-entity")
387
+ except FederationClientException as e:
388
+ print(f"Request failed with status {e.status_code}: {e}")
389
+ ```
390
+
391
+ Failures without a response, such as connection errors, have a status code of 0. Invalid arguments,
392
+ such as an empty UUID, a page below 1, a search query shorter than 2 characters, or an access token
393
+ that is empty or contains whitespace, are rejected by the client immediately with a `ValueError`.
394
+
395
+ ## Testing
396
+
397
+ The unit tests run without a server:
398
+
399
+ ```sh
400
+ pip install -e . --group dev
401
+ pytest
402
+ ```
403
+
404
+ The integration tests run against a live Federation server, and the repository includes a
405
+ [docker-compose.yml](docker-compose.yml) that brings up the reference
406
+ [FederationLib](https://github.com/nosial/FederationLib) server, MariaDB, and Redis on `http://localhost:7000`:
407
+
408
+ ```sh
409
+ docker compose up -d
410
+ pytest -m integration
411
+ ```
412
+
413
+ By default the tests expect the server at `http://localhost:7000` with access token
414
+ `abcdefghijklmnopqrstuvwxyz123456` (matching the compose defaults). To test against a different
415
+ server, set the `SERVER_ENDPOINT` and `SERVER_ACCESS_TOKEN` environment variables:
416
+
417
+ ```sh
418
+ SERVER_ENDPOINT=http://localhost:7000 SERVER_ACCESS_TOKEN=my-token pytest -m integration
419
+ ```
420
+
421
+ The synchronous client, `client.py`, is generated from the asynchronous client, `async_client.py`,
422
+ run `python scripts/unasync.py` after changing it.
423
+
424
+ # License
425
+
426
+ This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details.