immich-api 0.2.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.
Files changed (63) hide show
  1. immich_api/__init__.py +54 -0
  2. immich_api/__main__.py +4 -0
  3. immich_api/album_metadata.py +119 -0
  4. immich_api/api/__init__.py +3 -0
  5. immich_api/api/api_client_system.py +7 -0
  6. immich_api/auth.py +94 -0
  7. immich_api/connector_manifest.yml +212 -0
  8. immich_api/connectors/__init__.py +0 -0
  9. immich_api/connectors/mcp_source_presets.json +3 -0
  10. immich_api/connectors/tool_schema_fingerprints.json +1 -0
  11. immich_api/error_authority.py +248 -0
  12. immich_api/error_rendering.py +105 -0
  13. immich_api/immich_input_models.py +12 -0
  14. immich_api/immich_response_models.py +15 -0
  15. immich_api/kg_ingest.py +75 -0
  16. immich_api/mcp/__init__.py +21 -0
  17. immich_api/mcp/album_sample.py +38 -0
  18. immich_api/mcp_config.json +3 -0
  19. immich_api/mcp_server.py +50 -0
  20. immich_api/ontology/__init__.py +6 -0
  21. immich_api/ontology/immich.ttl +119 -0
  22. immich_api/ontology/schema-provenance.json +483 -0
  23. immich_api/ontology/shapes/immich.ttl +69 -0
  24. immich_api/openapi_auth_scheme.py +77 -0
  25. immich_api/openapi_contract.json +80122 -0
  26. immich_api/openapi_execution.py +30 -0
  27. immich_api/openapi_inventory.json +16154 -0
  28. immich_api/openapi_request.py +119 -0
  29. immich_api/openapi_response.py +76 -0
  30. immich_api/openapi_runtime.py +203 -0
  31. immich_api/parity/__init__.py +17 -0
  32. immich_api/parity/check_openapi_parity.py +173 -0
  33. immich_api/parity/openapi_discovery.py +95 -0
  34. immich_api/parity/openapi_parity.py +239 -0
  35. immich_api/parity/openapi_provider_evidence.py +143 -0
  36. immich_api/parity/openapi_provider_overlay.py +239 -0
  37. immich_api/parity/openapi_provider_responses.py +132 -0
  38. immich_api/parity/openapi_schema.py +206 -0
  39. immich_api/parity/openapi_schema_budget.py +104 -0
  40. immich_api/parity/openapi_schema_inputs.py +89 -0
  41. immich_api/parity/openapi_schema_types.py +163 -0
  42. immich_api/parity/openapi_security.py +157 -0
  43. immich_api/parity/openapi_source_parity.py +98 -0
  44. immich_api/parity/openapi_transport_parity.py +169 -0
  45. immich_api/parity/unicode_patterns.py +105 -0
  46. immich_api/parity/unicode_properties.json +4719 -0
  47. immich_api/prompts/__init__.py +0 -0
  48. immich_api/prompts/immich_album_review.json +18 -0
  49. immich_api/prompts/immich_asset_metadata.json +18 -0
  50. immich_api/prompts/immich_person_record.json +18 -0
  51. immich_api/prompts/immich_specialist.json +17 -0
  52. immich_api/prompts/main_agent.json +24 -0
  53. immich_api/skills/__init__.py +0 -0
  54. immich_api/skills/immich-album-review/SKILL.md +17 -0
  55. immich_api/skills/immich-asset-metadata/SKILL.md +17 -0
  56. immich_api/skills/immich-health/SKILL.md +17 -0
  57. immich_api/skills/immich-person-record/SKILL.md +17 -0
  58. immich_api-0.2.0.dist-info/METADATA +242 -0
  59. immich_api-0.2.0.dist-info/RECORD +63 -0
  60. immich_api-0.2.0.dist-info/WHEEL +5 -0
  61. immich_api-0.2.0.dist-info/entry_points.txt +14 -0
  62. immich_api-0.2.0.dist-info/licenses/LICENSE +21 -0
  63. immich_api-0.2.0.dist-info/top_level.txt +1 -0
immich_api/__init__.py ADDED
@@ -0,0 +1,54 @@
1
+ #!/usr/bin/env python
2
+
3
+ import importlib
4
+ import inspect
5
+ import warnings
6
+
7
+ warnings.filterwarnings("ignore", message=".*urllib3.*or chardet.*")
8
+
9
+ __all__: list[str] = []
10
+
11
+ CORE_MODULES = [
12
+ "immich_api.api",
13
+ "immich_api.error_authority",
14
+ ]
15
+
16
+ OPTIONAL_MODULES = {
17
+ "immich_api.mcp_server": "mcp",
18
+ }
19
+
20
+
21
+ def _import_module_safely(module_name: str):
22
+ """Try to import a module and return it, or None if not available."""
23
+ try:
24
+ return importlib.import_module(module_name)
25
+ except ImportError:
26
+ return None
27
+
28
+
29
+ def _expose_members(module):
30
+ """Expose public classes and functions from a module into globals and __all__."""
31
+ for name, obj in inspect.getmembers(module):
32
+ if (inspect.isclass(obj) or inspect.isfunction(obj)) and not name.startswith(
33
+ "_"
34
+ ):
35
+ globals()[name] = obj
36
+ __all__.append(name)
37
+
38
+
39
+ for module_name in CORE_MODULES:
40
+ try:
41
+ module = importlib.import_module(module_name)
42
+ _expose_members(module)
43
+ except ImportError:
44
+ pass
45
+
46
+ for module_name, extra_name in OPTIONAL_MODULES.items():
47
+ module = _import_module_safely(module_name)
48
+ if module is not None:
49
+ _expose_members(module)
50
+ globals()[f"_{extra_name.upper()}_AVAILABLE"] = True
51
+ else:
52
+ globals()[f"_{extra_name.upper()}_AVAILABLE"] = False
53
+
54
+ __all__.extend(["_MCP_AVAILABLE"])
immich_api/__main__.py ADDED
@@ -0,0 +1,4 @@
1
+ from .mcp_server import mcp_server
2
+
3
+ if __name__ == "__main__":
4
+ mcp_server()
@@ -0,0 +1,119 @@
1
+ """Bounded, explicit projection of album metadata; no network or graph work."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ from datetime import datetime
7
+ from pathlib import Path
8
+ from typing import TypedDict
9
+ from uuid import UUID
10
+
11
+
12
+ class AlbumMetadata(TypedDict):
13
+ id: str
14
+ node_type: str
15
+ albumName: str
16
+ assetCount: int
17
+ shared: bool
18
+ hasSharedLink: bool
19
+ createdAt: str
20
+ updatedAt: str
21
+
22
+
23
+ class AlbumSample(TypedDict):
24
+ records: list[AlbumMetadata]
25
+ complete: bool
26
+ authoritative: bool
27
+ source_action: str
28
+ source_schema_sha256: str
29
+
30
+
31
+ SOURCE_SHA256 = "7db8383741eeb5c5c8c87a6221cd1cdd1206c8b1190b83363c64223a00bb020b"
32
+
33
+
34
+ def album_graph_properties(record: dict) -> dict[str, str | int | bool]:
35
+ """Map selected provider fields to the actual ontology predicate keys."""
36
+ projected = project_album(record)
37
+ provenance = json.loads(
38
+ (Path(__file__).parent / "ontology/schema-provenance.json").read_text()
39
+ )
40
+ targets = {
41
+ item["source_pointer"].rsplit("/", 1)[1]: item["property"]
42
+ for item in provenance["properties"]
43
+ if item["class"] == "ImmichAlbum" and item["range"].startswith("xsd:")
44
+ }
45
+ return {
46
+ targets[field]: value
47
+ for field, value in projected.items()
48
+ if field != "node_type"
49
+ }
50
+
51
+
52
+ def _validate_timestamp(value):
53
+ try:
54
+ valid = (
55
+ isinstance(value, str)
56
+ and datetime.fromisoformat(value.replace("Z", "+00:00")).tzinfo is not None
57
+ )
58
+ except ValueError:
59
+ valid = False
60
+ if not valid:
61
+ raise ValueError("album metadata requires timestamps with time zones")
62
+
63
+
64
+ def _validate_album_fields(record):
65
+ name, count = record.get("albumName"), record.get("assetCount")
66
+ if not isinstance(name, str) or type(count) is not int:
67
+ raise ValueError("album metadata does not satisfy its source schema")
68
+ if not 0 <= count <= 9007199254740991:
69
+ raise ValueError("album metadata does not satisfy its source schema")
70
+ for key in ("shared", "hasSharedLink"):
71
+ if type(record.get(key)) is not bool:
72
+ raise ValueError("album metadata does not satisfy its source schema")
73
+ for key in ("createdAt", "updatedAt"):
74
+ _validate_timestamp(record.get(key))
75
+
76
+
77
+ def project_album(record: dict) -> AlbumMetadata:
78
+ """Retain only declared fields; private nested values never leave this function."""
79
+ record_id = record.get("id")
80
+ if not isinstance(record_id, str) or UUID(record_id).version != 4:
81
+ raise ValueError("album requires a stable UUID v4 identity")
82
+ _validate_album_fields(record)
83
+ return AlbumMetadata(
84
+ id=record_id,
85
+ node_type="ImmichAlbum",
86
+ albumName=record["albumName"],
87
+ assetCount=record["assetCount"],
88
+ shared=record["shared"],
89
+ hasSharedLink=record["hasSharedLink"],
90
+ createdAt=record["createdAt"],
91
+ updatedAt=record["updatedAt"],
92
+ )
93
+
94
+
95
+ def _validate_source(records):
96
+ if not isinstance(records, list) or any(
97
+ not isinstance(record, dict) for record in records
98
+ ):
99
+ raise ValueError("album source must return a list of objects")
100
+ identities = [record.get("id") for record in records]
101
+ if any(not isinstance(value, str) for value in identities):
102
+ raise ValueError("album source identities must be unique strings")
103
+ if len(set(identities)) != len(identities):
104
+ raise ValueError("album source identities must be unique strings")
105
+
106
+
107
+ def album_sample(records: list[dict], limit: int = 1) -> AlbumSample:
108
+ """Return a deterministic bounded sample, never an authoritative live set."""
109
+ if type(limit) is not int or not 1 <= limit <= 5:
110
+ raise ValueError("album sample limit must be between one and five")
111
+ _validate_source(records)
112
+ selected = sorted(records, key=lambda record: record["id"])[:limit]
113
+ return AlbumSample(
114
+ records=[project_album(record) for record in selected],
115
+ complete=len(records) <= limit,
116
+ authoritative=False,
117
+ source_action="getAllAlbums",
118
+ source_schema_sha256=SOURCE_SHA256,
119
+ )
@@ -0,0 +1,3 @@
1
+ from .api_client_system import ApiClientSystem
2
+
3
+ __all__ = ["ApiClientSystem"]
@@ -0,0 +1,7 @@
1
+ """Provider client with operation-bound OpenAPI dispatch."""
2
+
3
+ from ..openapi_runtime import OpenApiClient
4
+
5
+
6
+ class ApiClientSystem(OpenApiClient):
7
+ """Use the canonical client factory with validated domain actions."""
immich_api/auth.py ADDED
@@ -0,0 +1,94 @@
1
+ """Resolve provider credentials and delegated caller auth through canonical SDK APIs."""
2
+
3
+ from contextlib import ExitStack
4
+
5
+ import httpx
6
+ from agent_connector_sdk.auth.delegation import DelegatedTokenAuth, DelegationSettings
7
+ from agent_connector_sdk.auth.static import api_key_auth, bearer_auth
8
+ from agent_connector_sdk.config import load_config, setting
9
+ from agent_connector_sdk.http.client import create_http_client
10
+ from agent_connector_sdk.http.options import HttpClientOptions
11
+ from agent_connector_sdk.tls.resolve import resolve_tls_profile
12
+
13
+ from .api import ApiClientSystem
14
+ from .openapi_auth_scheme import AuthKind, declared_scheme
15
+ from .openapi_runtime import CONTRACT
16
+
17
+ _client = None
18
+ _resources = ExitStack()
19
+
20
+
21
+ def _bearer_scheme(definition):
22
+ return definition is not None and definition.kind is AuthKind.BEARER
23
+
24
+
25
+ def _delegated_auth(delegation, resources):
26
+ token_tls = resolve_tls_profile("oidc")
27
+ resources.callback(token_tls.cleanup)
28
+ token_client = create_http_client(
29
+ HttpClientOptions(base_url=delegation.token_endpoint, tls=token_tls)
30
+ )
31
+ resources.callback(token_client.close)
32
+ return DelegatedTokenAuth(delegation, http_client=token_client)
33
+
34
+
35
+ def _static_auth(reference, definition):
36
+ if definition is None:
37
+ raise ValueError("A credential requires a declared authentication scheme")
38
+ if definition.kind is AuthKind.HEADER:
39
+ return api_key_auth(reference, header=definition.header)
40
+ return bearer_auth(reference)
41
+
42
+
43
+ def _resolve_auth(resources):
44
+ scheme = setting("IMMICH_API_AUTH_SCHEME", "api_key")
45
+ definition = declared_scheme(scheme, CONTRACT["security_schemes"])
46
+ delegation = DelegationSettings.from_settings()
47
+ if delegation.enabled:
48
+ if not _bearer_scheme(definition):
49
+ raise ValueError("Delegation requires a declared HTTP bearer auth scheme")
50
+ return _delegated_auth(delegation, resources), scheme
51
+ reference = setting("IMMICH_API_TOKEN_REF", "")
52
+ if not reference:
53
+ return httpx.Auth(), None
54
+ return _static_auth(reference, definition), scheme
55
+
56
+
57
+ def get_client():
58
+ """Build one client; delegated auth resolves the verified caller per request."""
59
+ global _client, _resources
60
+ if _client is not None:
61
+ return _client
62
+ load_config()
63
+ base_url = setting("IMMICH_API_BASE_URL", "")
64
+ if not base_url:
65
+ raise ValueError("Provider base URL is required")
66
+ with ExitStack() as resources:
67
+ tls = resolve_tls_profile("immich-api")
68
+ resources.callback(tls.cleanup)
69
+ auth, credential_scheme = _resolve_auth(resources)
70
+ client = ApiClientSystem(
71
+ base_url=base_url,
72
+ auth=auth,
73
+ tls_profile=tls,
74
+ credential_scheme=credential_scheme,
75
+ write_mode=(
76
+ "read-write"
77
+ if setting("IMMICH_API_ALLOW_WRITES", False)
78
+ else "read-only"
79
+ ),
80
+ allow_plaintext=setting("IMMICH_API_ALLOW_PLAINTEXT", False),
81
+ )
82
+ resources.callback(client.close)
83
+ _resources = resources.pop_all()
84
+ _client = client
85
+ return client
86
+
87
+
88
+ def close_client():
89
+ """Close the cached client, token client and materialized TLS resources."""
90
+ global _client
91
+ try:
92
+ _resources.close()
93
+ finally:
94
+ _client = None
@@ -0,0 +1,212 @@
1
+ {
2
+ "connector": "immich-api",
3
+ "ontology_source": "immich",
4
+ "schema_version": "1",
5
+ "resources": [
6
+ {
7
+ "name": "ImmichAsset",
8
+ "relations": [
9
+ {
10
+ "name": "immichAssetOwner",
11
+ "target": "ImmichUser",
12
+ "label": "owner"
13
+ },
14
+ {
15
+ "name": "immichAssetPeople",
16
+ "target": "ImmichPersonRecord",
17
+ "label": "people"
18
+ },
19
+ {
20
+ "name": "immichAssetTags",
21
+ "target": "ImmichTag",
22
+ "label": "tags"
23
+ },
24
+ {
25
+ "name": "immichAssetStack",
26
+ "target": "ImmichAssetStack",
27
+ "label": "stack"
28
+ }
29
+ ]
30
+ },
31
+ {
32
+ "name": "ImmichAlbum",
33
+ "relations": [
34
+ {
35
+ "name": "immichAlbumAlbumUsers",
36
+ "target": "ImmichAlbumMembership",
37
+ "label": "albumUsers"
38
+ }
39
+ ]
40
+ },
41
+ {
42
+ "name": "ImmichPersonRecord",
43
+ "relations": []
44
+ },
45
+ {
46
+ "name": "ImmichUser",
47
+ "relations": []
48
+ },
49
+ {
50
+ "name": "ImmichLibrary",
51
+ "relations": []
52
+ },
53
+ {
54
+ "name": "ImmichAlbumMembership",
55
+ "relations": [
56
+ {
57
+ "name": "immichAlbumMembershipUser",
58
+ "target": "ImmichUser",
59
+ "label": "user"
60
+ }
61
+ ]
62
+ },
63
+ {
64
+ "name": "ImmichTag",
65
+ "relations": []
66
+ },
67
+ {
68
+ "name": "ImmichAssetStack",
69
+ "relations": []
70
+ }
71
+ ],
72
+ "identity": {
73
+ "id_field": {
74
+ "ImmichAsset": "immichAssetId",
75
+ "ImmichAlbum": "immichAlbumId",
76
+ "ImmichPersonRecord": "immichPersonRecordId",
77
+ "ImmichUser": "immichUserId",
78
+ "ImmichLibrary": "immichLibraryId",
79
+ "ImmichTag": "immichTagId",
80
+ "ImmichAssetStack": "immichAssetStackId"
81
+ },
82
+ "title_field": {
83
+ "ImmichAlbum": "immichAlbumAlbumName",
84
+ "ImmichPersonRecord": "immichPersonRecordName",
85
+ "ImmichUser": "immichUserName",
86
+ "ImmichLibrary": "immichLibraryName",
87
+ "ImmichTag": "immichTagName"
88
+ },
89
+ "updated_field": {
90
+ "ImmichAlbum": "immichAlbumUpdatedAt",
91
+ "ImmichAsset": "immichAssetUpdatedAt"
92
+ }
93
+ },
94
+ "schema_mappings": {
95
+ "ImmichAsset": {
96
+ "ontology_class": "ImmichAsset",
97
+ "fields": {
98
+ "immichAssetId": "xsd:string",
99
+ "immichAssetType": "xsd:string",
100
+ "immichAssetOriginalMimeType": "xsd:string",
101
+ "immichAssetDuration": "xsd:integer",
102
+ "immichAssetWidth": "xsd:integer",
103
+ "immichAssetHeight": "xsd:integer",
104
+ "immichAssetFileCreatedAt": "xsd:dateTime",
105
+ "immichAssetUpdatedAt": "xsd:dateTime",
106
+ "immichAssetIsFavorite": "xsd:boolean",
107
+ "immichAssetIsArchived": "xsd:boolean",
108
+ "immichAssetIsTrashed": "xsd:boolean",
109
+ "immichAssetVisibility": "xsd:string"
110
+ }
111
+ },
112
+ "ImmichAlbum": {
113
+ "ontology_class": "ImmichAlbum",
114
+ "fields": {
115
+ "immichAlbumId": "xsd:string",
116
+ "immichAlbumAlbumName": "xsd:string",
117
+ "immichAlbumAssetCount": "xsd:integer",
118
+ "immichAlbumShared": "xsd:boolean",
119
+ "immichAlbumHasSharedLink": "xsd:boolean",
120
+ "immichAlbumCreatedAt": "xsd:dateTime",
121
+ "immichAlbumUpdatedAt": "xsd:dateTime",
122
+ "immichAlbumAlbumThumbnailAssetId": "xsd:string"
123
+ }
124
+ },
125
+ "ImmichPersonRecord": {
126
+ "ontology_class": "ImmichPersonRecord",
127
+ "fields": {
128
+ "immichPersonRecordId": "xsd:string",
129
+ "immichPersonRecordName": "xsd:string",
130
+ "immichPersonRecordIsHidden": "xsd:boolean",
131
+ "immichPersonRecordIsFavorite": "xsd:boolean",
132
+ "immichPersonRecordUpdatedAt": "xsd:dateTime"
133
+ }
134
+ },
135
+ "ImmichUser": {
136
+ "ontology_class": "ImmichUser",
137
+ "fields": {
138
+ "immichUserId": "xsd:string",
139
+ "immichUserName": "xsd:string"
140
+ }
141
+ },
142
+ "ImmichLibrary": {
143
+ "ontology_class": "ImmichLibrary",
144
+ "fields": {
145
+ "immichLibraryId": "xsd:string",
146
+ "immichLibraryName": "xsd:string",
147
+ "immichLibraryAssetCount": "xsd:integer",
148
+ "immichLibraryCreatedAt": "xsd:dateTime",
149
+ "immichLibraryUpdatedAt": "xsd:dateTime"
150
+ }
151
+ },
152
+ "ImmichAlbumMembership": {
153
+ "ontology_class": "ImmichAlbumMembership",
154
+ "fields": {
155
+ "immichAlbumMembershipRole": "xsd:string"
156
+ }
157
+ },
158
+ "ImmichTag": {
159
+ "ontology_class": "ImmichTag",
160
+ "fields": {
161
+ "immichTagId": "xsd:string",
162
+ "immichTagName": "xsd:string",
163
+ "immichTagCreatedAt": "xsd:dateTime",
164
+ "immichTagUpdatedAt": "xsd:dateTime"
165
+ }
166
+ },
167
+ "ImmichAssetStack": {
168
+ "ontology_class": "ImmichAssetStack",
169
+ "fields": {
170
+ "immichAssetStackId": "xsd:string",
171
+ "immichAssetStackAssetCount": "xsd:integer",
172
+ "immichAssetStackPrimaryAssetId": "xsd:string"
173
+ }
174
+ }
175
+ },
176
+ "sync": [],
177
+ "provenance": {
178
+ "generated_by": "scripts/generate_immich_content.py",
179
+ "source_artifacts": [
180
+ "openapi_contract.json",
181
+ "ontology/schema-provenance.json",
182
+ "ontology/immich.ttl",
183
+ "ontology/shapes/immich.ttl"
184
+ ],
185
+ "integrity": {
186
+ "algorithm": "sha256",
187
+ "hash": "c68d2270fcfbcacb40369011d3f65fdf625e6ea1eabba10bf4309f7c007e391d"
188
+ }
189
+ },
190
+ "review_todos": [
191
+ "Structural schema declarations are not evidence of live EG capture or semantic identity alignment.",
192
+ "Only the bounded album metadata projection is implemented for ingestion; remaining DTO classes are structural declarations.",
193
+ "Require explicit source/account binding, existing scoped credentials and a verified EG receipt/readback before enabling ingestion.",
194
+ "Ontology integrity is a byte SHA256, not canonical RDF or release certification."
195
+ ],
196
+ "policy": {
197
+ "pii_fields": {
198
+ "ImmichAlbum": [
199
+ "immichAlbumAlbumName"
200
+ ],
201
+ "ImmichPersonRecord": [
202
+ "immichPersonRecordName"
203
+ ],
204
+ "ImmichUser": [
205
+ "immichUserName"
206
+ ],
207
+ "ImmichTag": [
208
+ "immichTagName"
209
+ ]
210
+ }
211
+ }
212
+ }
File without changes
@@ -0,0 +1,3 @@
1
+ {
2
+ "_comment": "No ingestion preset is enabled. The pinned SDK uses the preset name as stream identity, so this package cannot bind a shared preset to a provider/account scope. The read-only album_metadata_sample tool remains available. Its authoritative=false flag does not govern SDK source mode; no FULL/DELTA sweep is configured."
3
+ }
@@ -0,0 +1 @@
1
+ {"algorithm":"agent-connector-sdk:mcp-tool-contract-compat:v2","connector":"immich-api","tools":{}}