upp-python 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.
- upp/__init__.py +183 -0
- upp/backends/.gitkeep +0 -0
- upp/backends/__init__.py +23 -0
- upp/backends/ingest.py +122 -0
- upp/backends/ontology.py +41 -0
- upp/backends/retriever.py +79 -0
- upp/client.py +148 -0
- upp/models/.gitkeep +0 -0
- upp/models/__init__.py +38 -0
- upp/models/client.py +113 -0
- upp/models/enums.py +124 -0
- upp/models/events.py +124 -0
- upp/models/labels.py +71 -0
- upp/ontologies/__init__.py +0 -0
- upp/ontologies/user_v1.py +112 -0
- upp/py.typed +0 -0
- upp/rpc/.gitkeep +0 -0
- upp/rpc/__init__.py +137 -0
- upp/rpc/codec.py +112 -0
- upp/rpc/errors.py +127 -0
- upp/rpc/messages.py +354 -0
- upp/rpc/methods.py +73 -0
- upp_python-0.1.0.dist-info/METADATA +137 -0
- upp_python-0.1.0.dist-info/RECORD +25 -0
- upp_python-0.1.0.dist-info/WHEEL +4 -0
upp/models/client.py
ADDED
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
"""UPP Client Protocol.
|
|
2
|
+
|
|
3
|
+
Defines :class:`UPPClientProtocol`, the top-level interface that any
|
|
4
|
+
UPP-compliant client must satisfy. It declares all ten operations
|
|
5
|
+
specified by the Universal Personalization Protocol.
|
|
6
|
+
|
|
7
|
+
Design note — why this Protocol does NOT inherit from the backend protocols
|
|
8
|
+
(``IngestBackend``, ``RetrieverBackend``, ``OntologyBackend``):
|
|
9
|
+
|
|
10
|
+
Backends are *infrastructure* components — each one owns a single
|
|
11
|
+
responsibility (persistence, retrieval, ontology metadata). A client
|
|
12
|
+
is an *orchestrator*: it delegates to backends but lives at a higher
|
|
13
|
+
abstraction level. Inheriting from the backend protocols would
|
|
14
|
+
conflate these two roles and allow a client instance to be passed
|
|
15
|
+
where a backend is expected, creating a misleading extra layer of
|
|
16
|
+
indirection.
|
|
17
|
+
|
|
18
|
+
Instead, ``UPPClientProtocol`` declares the same method signatures
|
|
19
|
+
independently. This keeps the type hierarchy honest — a client *is
|
|
20
|
+
not* a backend — while still guaranteeing that every compliant client
|
|
21
|
+
exposes the full set of UPP operations.
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
from __future__ import annotations
|
|
25
|
+
|
|
26
|
+
from typing import Any, Protocol, runtime_checkable
|
|
27
|
+
|
|
28
|
+
from upp.models.events import ContextualizeResult, Event, StoredEvent, TaskResult
|
|
29
|
+
from upp.models.labels import LabelDefinition
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
@runtime_checkable
|
|
33
|
+
class UPPClientProtocol(Protocol):
|
|
34
|
+
"""Top-level protocol for a UPP-compliant client.
|
|
35
|
+
|
|
36
|
+
Any implementation that satisfies this protocol exposes the ten
|
|
37
|
+
operations defined by the Universal Personalization Protocol:
|
|
38
|
+
|
|
39
|
+
Core (write):
|
|
40
|
+
1. ingest — Persist extracted events.
|
|
41
|
+
2. delete_events — Delete events (GDPR / CCPA compliance).
|
|
42
|
+
|
|
43
|
+
Core (read + write):
|
|
44
|
+
3. retrieve — Intelligent, query-driven event retrieval.
|
|
45
|
+
4. get_events — Raw listing of all stored events.
|
|
46
|
+
5. contextualize — Retrieve context and ingest in the background.
|
|
47
|
+
|
|
48
|
+
Discovery:
|
|
49
|
+
6. info — Server metadata and capabilities.
|
|
50
|
+
7. get_labels — Available labels in the ontology.
|
|
51
|
+
8. get_tasks — Check status of background tasks.
|
|
52
|
+
|
|
53
|
+
Portability:
|
|
54
|
+
9. export_events — Export events for migration.
|
|
55
|
+
10. import_events — Import events from another server.
|
|
56
|
+
"""
|
|
57
|
+
|
|
58
|
+
# ── Core (write) ─────────────────────────────────────────────────
|
|
59
|
+
|
|
60
|
+
async def ingest(
|
|
61
|
+
self,
|
|
62
|
+
entity_key: str,
|
|
63
|
+
text: str,
|
|
64
|
+
) -> list[StoredEvent]: ...
|
|
65
|
+
|
|
66
|
+
async def delete_events(
|
|
67
|
+
self,
|
|
68
|
+
entity_key: str,
|
|
69
|
+
event_ids: list[str] | None = None,
|
|
70
|
+
) -> int: ...
|
|
71
|
+
|
|
72
|
+
# ── Core (read) ──────────────────────────────────────────────────
|
|
73
|
+
|
|
74
|
+
async def retrieve(
|
|
75
|
+
self,
|
|
76
|
+
entity_key: str,
|
|
77
|
+
query: str,
|
|
78
|
+
) -> list[StoredEvent]: ...
|
|
79
|
+
|
|
80
|
+
async def get_events(
|
|
81
|
+
self,
|
|
82
|
+
entity_key: str,
|
|
83
|
+
) -> list[StoredEvent]: ...
|
|
84
|
+
|
|
85
|
+
async def contextualize(
|
|
86
|
+
self,
|
|
87
|
+
entity_key: str,
|
|
88
|
+
text: str,
|
|
89
|
+
) -> ContextualizeResult: ...
|
|
90
|
+
|
|
91
|
+
# ── Discovery ────────────────────────────────────────────────────
|
|
92
|
+
|
|
93
|
+
def info(self) -> dict[str, Any]: ...
|
|
94
|
+
|
|
95
|
+
def get_labels(self) -> list[LabelDefinition]: ...
|
|
96
|
+
|
|
97
|
+
async def get_tasks(
|
|
98
|
+
self,
|
|
99
|
+
task_ids: list[str],
|
|
100
|
+
) -> list[TaskResult]: ...
|
|
101
|
+
|
|
102
|
+
# ── Portability ──────────────────────────────────────────────────
|
|
103
|
+
|
|
104
|
+
async def export_events(
|
|
105
|
+
self,
|
|
106
|
+
entity_key: str,
|
|
107
|
+
) -> list[StoredEvent]: ...
|
|
108
|
+
|
|
109
|
+
async def import_events(
|
|
110
|
+
self,
|
|
111
|
+
entity_key: str,
|
|
112
|
+
events: list[Event],
|
|
113
|
+
) -> list[StoredEvent]: ...
|
upp/models/enums.py
ADDED
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
"""UPP Enumerations.
|
|
2
|
+
|
|
3
|
+
Defines the enumeration types used throughout the Universal Personalization
|
|
4
|
+
Protocol. All enums inherit from ``str`` and ``enum.Enum`` so they
|
|
5
|
+
serialize naturally to their string values in JSON.
|
|
6
|
+
|
|
7
|
+
Enumerations:
|
|
8
|
+
EventStatus: Lifecycle state of a stored event.
|
|
9
|
+
SourceType: Provenance classification of an extracted fact.
|
|
10
|
+
SensitivityTier: Privacy classification for ontology labels.
|
|
11
|
+
Cardinality: Whether a label accepts one or many values.
|
|
12
|
+
Durability: Expected lifespan of a fact.
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
from __future__ import annotations
|
|
16
|
+
|
|
17
|
+
import enum
|
|
18
|
+
|
|
19
|
+
__all__ = [
|
|
20
|
+
"Cardinality",
|
|
21
|
+
"Durability",
|
|
22
|
+
"EventStatus",
|
|
23
|
+
"SensitivityTier",
|
|
24
|
+
"SourceType",
|
|
25
|
+
"TaskStatus",
|
|
26
|
+
]
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class EventStatus(enum.StrEnum):
|
|
30
|
+
"""Lifecycle state of a stored event.
|
|
31
|
+
|
|
32
|
+
Events follow an immutable event-sourcing pattern:
|
|
33
|
+
|
|
34
|
+
- ``VALID`` — Active and authoritative.
|
|
35
|
+
- ``STAGED`` — Low confidence, pending reinforcement.
|
|
36
|
+
- ``SUPERSEDED`` — Replaced by a newer event, retained for audit.
|
|
37
|
+
"""
|
|
38
|
+
|
|
39
|
+
VALID = "valid"
|
|
40
|
+
"""Current, retrievable event."""
|
|
41
|
+
|
|
42
|
+
STAGED = "staged"
|
|
43
|
+
"""Low-confidence event awaiting reinforcement."""
|
|
44
|
+
|
|
45
|
+
SUPERSEDED = "superseded"
|
|
46
|
+
"""Replaced by a newer event. Retained for audit trail."""
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
class SourceType(enum.StrEnum):
|
|
50
|
+
"""Classifies how a fact was obtained.
|
|
51
|
+
|
|
52
|
+
Source types indicate the provenance of an extracted fact.
|
|
53
|
+
"""
|
|
54
|
+
|
|
55
|
+
USER_STATED = "user_stated"
|
|
56
|
+
"""The user explicitly stated this fact."""
|
|
57
|
+
|
|
58
|
+
AGENT_OBSERVED = "agent_observed"
|
|
59
|
+
"""The agent observed or derived this fact from conversation context."""
|
|
60
|
+
|
|
61
|
+
INFERRED = "inferred"
|
|
62
|
+
"""The system inferred this fact from indirect signals."""
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
class SensitivityTier(enum.StrEnum):
|
|
66
|
+
"""Privacy classification for ontology labels.
|
|
67
|
+
|
|
68
|
+
Ordered from least to most sensitive:
|
|
69
|
+
``TIER_PUBLIC < TIER_WORK < TIER_PERSONAL < TIER_SENSITIVE < TIER_INTERNAL``
|
|
70
|
+
"""
|
|
71
|
+
|
|
72
|
+
TIER_PUBLIC = "tier_public"
|
|
73
|
+
"""Safe to share broadly."""
|
|
74
|
+
|
|
75
|
+
TIER_WORK = "tier_work"
|
|
76
|
+
"""Professional context."""
|
|
77
|
+
|
|
78
|
+
TIER_PERSONAL = "tier_personal"
|
|
79
|
+
"""Personal but non-sensitive."""
|
|
80
|
+
|
|
81
|
+
TIER_SENSITIVE = "tier_sensitive"
|
|
82
|
+
"""Sensitive personal data."""
|
|
83
|
+
|
|
84
|
+
TIER_INTERNAL = "tier_internal"
|
|
85
|
+
"""Never shared externally."""
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
class Cardinality(enum.StrEnum):
|
|
89
|
+
"""Defines whether a label accepts one or many concurrent values.
|
|
90
|
+
|
|
91
|
+
Cardinality determines supersession behavior: singular labels cause
|
|
92
|
+
new events to supersede old ones, while plural labels accumulate.
|
|
93
|
+
"""
|
|
94
|
+
|
|
95
|
+
SINGULAR = "singular"
|
|
96
|
+
"""Only one value at a time. New values supersede old ones."""
|
|
97
|
+
|
|
98
|
+
PLURAL = "plural"
|
|
99
|
+
"""Multiple values coexist. New values are added alongside existing ones."""
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
class Durability(enum.StrEnum):
|
|
103
|
+
"""Indicates how long a fact is expected to remain valid.
|
|
104
|
+
|
|
105
|
+
Ordering (longest to shortest): ``PERMANENT > TRANSIENT > EPHEMERAL``
|
|
106
|
+
"""
|
|
107
|
+
|
|
108
|
+
PERMANENT = "permanent"
|
|
109
|
+
"""Unlikely to change over a person's lifetime."""
|
|
110
|
+
|
|
111
|
+
TRANSIENT = "transient"
|
|
112
|
+
"""Changes over months or years."""
|
|
113
|
+
|
|
114
|
+
EPHEMERAL = "ephemeral"
|
|
115
|
+
"""Changes frequently — days or weeks."""
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
class TaskStatus(enum.StrEnum):
|
|
119
|
+
"""Status of a background task."""
|
|
120
|
+
|
|
121
|
+
PENDING = "pending"
|
|
122
|
+
RUNNING = "running"
|
|
123
|
+
COMPLETED = "completed"
|
|
124
|
+
FAILED = "failed"
|
upp/models/events.py
ADDED
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
"""UPP Event Models.
|
|
2
|
+
|
|
3
|
+
Defines the core event entities in the Universal Personalization Protocol:
|
|
4
|
+
|
|
5
|
+
- :class:`Event` — The atomic unit of extracted personal information.
|
|
6
|
+
- :class:`StoredEvent` — An Event after persistence, enriched with server metadata.
|
|
7
|
+
|
|
8
|
+
Both models are immutable (frozen). Events follow an event-sourcing pattern
|
|
9
|
+
where they are never modified after creation.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
from datetime import datetime
|
|
15
|
+
|
|
16
|
+
from pydantic import BaseModel, Field
|
|
17
|
+
|
|
18
|
+
from upp.models.enums import EventStatus, SourceType, TaskStatus
|
|
19
|
+
|
|
20
|
+
__all__ = [
|
|
21
|
+
"ContextualizeResult",
|
|
22
|
+
"Event",
|
|
23
|
+
"StoredEvent",
|
|
24
|
+
"TaskResult",
|
|
25
|
+
]
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class Event(BaseModel):
|
|
29
|
+
"""An atomic unit of extracted personal information.
|
|
30
|
+
|
|
31
|
+
Events are produced by the extraction pipeline and consumed by the
|
|
32
|
+
store layer. They are immutable — once created, they are never modified.
|
|
33
|
+
|
|
34
|
+
Attributes:
|
|
35
|
+
value: The extracted fact as natural-language text.
|
|
36
|
+
labels: One or more ontology label keys this fact maps to.
|
|
37
|
+
confidence: Extraction confidence score in [0.0, 1.0].
|
|
38
|
+
source_type: Provenance classification of the fact.
|
|
39
|
+
"""
|
|
40
|
+
|
|
41
|
+
model_config = {"frozen": True}
|
|
42
|
+
|
|
43
|
+
value: str = Field(
|
|
44
|
+
min_length=1,
|
|
45
|
+
description="The extracted fact as natural-language text.",
|
|
46
|
+
)
|
|
47
|
+
labels: list[str] = Field(
|
|
48
|
+
min_length=1,
|
|
49
|
+
description="One or more ontology label keys this fact maps to.",
|
|
50
|
+
)
|
|
51
|
+
confidence: float = Field(
|
|
52
|
+
ge=0.0,
|
|
53
|
+
le=1.0,
|
|
54
|
+
description="Extraction confidence score in [0.0, 1.0].",
|
|
55
|
+
)
|
|
56
|
+
source_type: SourceType = Field(
|
|
57
|
+
description="Provenance classification: user_stated, agent_observed, or inferred.",
|
|
58
|
+
)
|
|
59
|
+
valid_from: str | None = Field(
|
|
60
|
+
default=None,
|
|
61
|
+
description="ISO-8601 start of the fact's validity window.",
|
|
62
|
+
)
|
|
63
|
+
valid_until: str | None = Field(
|
|
64
|
+
default=None,
|
|
65
|
+
description="ISO-8601 end of the fact's validity window.",
|
|
66
|
+
)
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
class StoredEvent(Event):
|
|
70
|
+
"""A persisted event with server-assigned metadata.
|
|
71
|
+
|
|
72
|
+
Extends :class:`Event` with fields assigned by the server upon
|
|
73
|
+
persistence: unique ID, user key, lifecycle status, creation
|
|
74
|
+
timestamp, and optional supersession reference.
|
|
75
|
+
|
|
76
|
+
Like ``Event``, ``StoredEvent`` is immutable.
|
|
77
|
+
|
|
78
|
+
Attributes:
|
|
79
|
+
id: Unique event identifier (UUID v4).
|
|
80
|
+
entity_key: Unique identifier of the user who owns this event.
|
|
81
|
+
status: Lifecycle status (valid, staged, superseded).
|
|
82
|
+
created_at: Timestamp of creation.
|
|
83
|
+
superseded_by: ID of the event that replaced this one.
|
|
84
|
+
"""
|
|
85
|
+
|
|
86
|
+
id: str = Field(
|
|
87
|
+
description="Unique event identifier (UUID v4).",
|
|
88
|
+
)
|
|
89
|
+
entity_key: str = Field(
|
|
90
|
+
description="Unique identifier of the user who owns this event.",
|
|
91
|
+
)
|
|
92
|
+
status: EventStatus = Field(
|
|
93
|
+
default=EventStatus.VALID,
|
|
94
|
+
description="Lifecycle status: valid, staged, or superseded.",
|
|
95
|
+
)
|
|
96
|
+
created_at: datetime = Field(
|
|
97
|
+
description="Timestamp of creation (UTC).",
|
|
98
|
+
)
|
|
99
|
+
superseded_by: str | None = Field(
|
|
100
|
+
default=None,
|
|
101
|
+
description="ID of the event that replaced this one (if superseded).",
|
|
102
|
+
)
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
class TaskResult(BaseModel):
|
|
106
|
+
"""Status and result of a background task."""
|
|
107
|
+
|
|
108
|
+
model_config = {"frozen": True}
|
|
109
|
+
|
|
110
|
+
task_id: str = Field(description="The task identifier.")
|
|
111
|
+
status: TaskStatus = Field(description="Current task status.")
|
|
112
|
+
result: list[StoredEvent] | None = Field(default=None, description="Resulting events when completed.")
|
|
113
|
+
error: str | None = Field(default=None, description="Error message when failed.")
|
|
114
|
+
created_at: datetime = Field(description="When the task was created (UTC).")
|
|
115
|
+
completed_at: datetime | None = Field(default=None, description="When the task finished (UTC).")
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
class ContextualizeResult(BaseModel):
|
|
119
|
+
"""Result of a contextualize operation."""
|
|
120
|
+
|
|
121
|
+
model_config = {"frozen": True}
|
|
122
|
+
|
|
123
|
+
events: list[StoredEvent] = Field(description="Relevant existing events.")
|
|
124
|
+
task_id: str = Field(description="Reference to the background ingest task.")
|
upp/models/labels.py
ADDED
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
"""UPP Label Definition Model.
|
|
2
|
+
|
|
3
|
+
Defines the :class:`LabelDefinition` model — the fundamental unit of the
|
|
4
|
+
UPP ontology taxonomy. Labels categorize personal facts using the 5W+H
|
|
5
|
+
framework (Who, What, Where, When, Why, How) plus Preferences, Relationships,
|
|
6
|
+
and Meta categories.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
from pydantic import BaseModel, Field
|
|
12
|
+
|
|
13
|
+
from upp.models.enums import Cardinality, Durability, SensitivityTier
|
|
14
|
+
|
|
15
|
+
__all__ = [
|
|
16
|
+
"LabelDefinition",
|
|
17
|
+
]
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class LabelDefinition(BaseModel):
|
|
21
|
+
"""Schema definition for a single ontology label.
|
|
22
|
+
|
|
23
|
+
A ``LabelDefinition`` describes a category of personal fact in the
|
|
24
|
+
UPP taxonomy, including its privacy sensitivity, cardinality
|
|
25
|
+
(singular vs. plural), and expected durability.
|
|
26
|
+
|
|
27
|
+
Attributes:
|
|
28
|
+
name: Machine-readable label key (e.g., ``"who_name"``).
|
|
29
|
+
display_name: Human-readable name (e.g., ``"Name"``).
|
|
30
|
+
description: Concise description of what this label captures.
|
|
31
|
+
category: Top-level grouping (WHO, WHAT, WHERE, WHEN, WHY, HOW, PREF, REL, META).
|
|
32
|
+
sensitivity: Privacy sensitivity tier.
|
|
33
|
+
cardinality: Whether the label accepts one or many values.
|
|
34
|
+
durability: Expected lifespan of facts under this label.
|
|
35
|
+
examples: Optional example values to guide classification.
|
|
36
|
+
"""
|
|
37
|
+
|
|
38
|
+
model_config = {"frozen": True}
|
|
39
|
+
|
|
40
|
+
name: str = Field(
|
|
41
|
+
description="Machine-readable label key (e.g., 'who_name').",
|
|
42
|
+
)
|
|
43
|
+
display_name: str = Field(
|
|
44
|
+
description="Human-readable name (e.g., 'Name').",
|
|
45
|
+
)
|
|
46
|
+
description: str = Field(
|
|
47
|
+
description="Concise description of what this label captures.",
|
|
48
|
+
)
|
|
49
|
+
classification_guidance: str | None = Field(
|
|
50
|
+
default=None,
|
|
51
|
+
description="Classification and disambiguation guidance for label extraction.",
|
|
52
|
+
)
|
|
53
|
+
category: str = Field(
|
|
54
|
+
description=("Top-level grouping defined by the ontology (e.g., user/v1 uses WHO, WHAT, WHERE, etc.)."),
|
|
55
|
+
)
|
|
56
|
+
sensitivity: SensitivityTier = Field(
|
|
57
|
+
description="Privacy sensitivity tier.",
|
|
58
|
+
)
|
|
59
|
+
cardinality: Cardinality = Field(
|
|
60
|
+
description="Whether the label accepts one value (singular) or many (plural).",
|
|
61
|
+
)
|
|
62
|
+
durability: Durability = Field(
|
|
63
|
+
description="Expected lifespan: permanent, transient, or ephemeral.",
|
|
64
|
+
)
|
|
65
|
+
examples: list[str] = Field(
|
|
66
|
+
description="Example values to guide classification and extraction.",
|
|
67
|
+
)
|
|
68
|
+
anti_examples: list[str] | None = Field(
|
|
69
|
+
default=None,
|
|
70
|
+
description="Counter-examples to clarify label boundaries and prevent misclassification.",
|
|
71
|
+
)
|
|
File without changes
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
"""UPP user/v1 Ontology — Loader and Implementation.
|
|
2
|
+
|
|
3
|
+
Provides :class:`OntologyUserV1`, an implementation of
|
|
4
|
+
:class:`~upp.backends.ontology.OntologyBackend` backed by
|
|
5
|
+
``ontologies/user/v1.json``.
|
|
6
|
+
|
|
7
|
+
Usage::
|
|
8
|
+
|
|
9
|
+
from upp.ontologies.user_v1 import OntologyUserV1
|
|
10
|
+
|
|
11
|
+
ontology = OntologyUserV1()
|
|
12
|
+
all_labels = ontology.get_labels()
|
|
13
|
+
label = ontology.get_label_by_name("who_name")
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
from __future__ import annotations
|
|
17
|
+
|
|
18
|
+
import json
|
|
19
|
+
from pathlib import Path
|
|
20
|
+
|
|
21
|
+
from upp.backends.ontology import OntologyBackend
|
|
22
|
+
from upp.models.labels import LabelDefinition
|
|
23
|
+
|
|
24
|
+
__all__ = ["OntologyUserV1"]
|
|
25
|
+
|
|
26
|
+
#: Default path to the ontology file, resolved relative to this package.
|
|
27
|
+
_DEFAULT_ONTOLOGY_PATH = Path(__file__).resolve().parent.parent.parent.parent.parent.parent / "ontologies" / "user" / "v1.json"
|
|
28
|
+
|
|
29
|
+
# Module-level cache
|
|
30
|
+
_cached_labels: list[LabelDefinition] | None = None
|
|
31
|
+
_cached_labels_by_name: dict[str, LabelDefinition] | None = None
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def _load_labels(path: Path | None = None) -> list[LabelDefinition]:
|
|
35
|
+
"""Load label definitions from the ontology JSON file.
|
|
36
|
+
|
|
37
|
+
Args:
|
|
38
|
+
path: Optional path to the ontology file.
|
|
39
|
+
|
|
40
|
+
Returns:
|
|
41
|
+
A list of :class:`LabelDefinition` objects.
|
|
42
|
+
|
|
43
|
+
Raises:
|
|
44
|
+
FileNotFoundError: If the ontology file does not exist.
|
|
45
|
+
json.JSONDecodeError: If the file contains invalid JSON.
|
|
46
|
+
"""
|
|
47
|
+
ontology_path = path or _DEFAULT_ONTOLOGY_PATH
|
|
48
|
+
|
|
49
|
+
if not ontology_path.exists():
|
|
50
|
+
raise FileNotFoundError(
|
|
51
|
+
f"Default ontology file not found at {ontology_path}. Ensure the ontologies/user/v1.json file exists in the UPP repository root."
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
raw = json.loads(ontology_path.read_text(encoding="utf-8"))
|
|
55
|
+
raw_labels = raw.get("labels", [])
|
|
56
|
+
|
|
57
|
+
labels: list[LabelDefinition] = []
|
|
58
|
+
|
|
59
|
+
# Support both array format and dict format
|
|
60
|
+
if isinstance(raw_labels, list):
|
|
61
|
+
for label_data in raw_labels:
|
|
62
|
+
labels.append(LabelDefinition.model_validate(label_data))
|
|
63
|
+
elif isinstance(raw_labels, dict):
|
|
64
|
+
for key, label_data in raw_labels.items():
|
|
65
|
+
if "name" not in label_data:
|
|
66
|
+
label_data["name"] = key
|
|
67
|
+
labels.append(LabelDefinition.model_validate(label_data))
|
|
68
|
+
|
|
69
|
+
return labels
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def _ensure_loaded() -> tuple[list[LabelDefinition], dict[str, LabelDefinition]]:
|
|
73
|
+
"""Ensure labels are loaded and cached.
|
|
74
|
+
|
|
75
|
+
Returns:
|
|
76
|
+
Tuple of (labels list, labels-by-name dict).
|
|
77
|
+
"""
|
|
78
|
+
global _cached_labels, _cached_labels_by_name
|
|
79
|
+
if _cached_labels is None:
|
|
80
|
+
_cached_labels = _load_labels()
|
|
81
|
+
_cached_labels_by_name = {label.name: label for label in _cached_labels}
|
|
82
|
+
return _cached_labels, _cached_labels_by_name # type: ignore[return-value]
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
class OntologyUserV1(OntologyBackend):
|
|
86
|
+
"""Ontology implementation backed by ontologies/user/v1.json."""
|
|
87
|
+
|
|
88
|
+
def get_labels(self) -> list[LabelDefinition]:
|
|
89
|
+
"""Return all label definitions for the ontology."""
|
|
90
|
+
labels, _ = _ensure_loaded()
|
|
91
|
+
return list(labels)
|
|
92
|
+
|
|
93
|
+
def get_label_by_name(self, name: str) -> LabelDefinition:
|
|
94
|
+
"""Get a label definition by its name.
|
|
95
|
+
|
|
96
|
+
Args:
|
|
97
|
+
name: The name of the label to retrieve.
|
|
98
|
+
|
|
99
|
+
Returns:
|
|
100
|
+
The :class:`LabelDefinition` with the specified name.
|
|
101
|
+
|
|
102
|
+
Raises:
|
|
103
|
+
KeyError: If no label with the given name exists.
|
|
104
|
+
"""
|
|
105
|
+
_, labels_by_name = _ensure_loaded()
|
|
106
|
+
if name not in labels_by_name:
|
|
107
|
+
raise KeyError(f"Label with name '{name}' not found in ontology.")
|
|
108
|
+
return labels_by_name[name]
|
|
109
|
+
|
|
110
|
+
def get_version(self) -> str:
|
|
111
|
+
"""Return the ontology identifier for this server instance."""
|
|
112
|
+
return "user/v1"
|
upp/py.typed
ADDED
|
File without changes
|
upp/rpc/.gitkeep
ADDED
|
File without changes
|
upp/rpc/__init__.py
ADDED
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
"""UPP JSON-RPC 2.0 Support.
|
|
2
|
+
|
|
3
|
+
This package provides JSON-RPC 2.0 message types, codec functions,
|
|
4
|
+
method constants, error definitions, and typed request/response models
|
|
5
|
+
for all ten UPP operations.
|
|
6
|
+
|
|
7
|
+
Message Types:
|
|
8
|
+
JsonRpcRequest, JsonRpcResponse, JsonRpcError, JsonRpcNotification
|
|
9
|
+
|
|
10
|
+
Operation Models:
|
|
11
|
+
IngestRequest/Response, RetrieveRequest/Response, EventsRequest/Response,
|
|
12
|
+
DeleteRequest/Response, InfoRequest/Response, LabelsRequest/Response,
|
|
13
|
+
ExportRequest/Response, ImportRequest/Response
|
|
14
|
+
|
|
15
|
+
Codec:
|
|
16
|
+
encode_request, decode_request, encode_response, decode_response
|
|
17
|
+
|
|
18
|
+
Methods:
|
|
19
|
+
UPP_INGEST, UPP_RETRIEVE, UPP_EVENTS, UPP_DELETE,
|
|
20
|
+
UPP_INFO, UPP_LABELS, UPP_EXPORT, UPP_IMPORT, ALL_METHODS
|
|
21
|
+
|
|
22
|
+
Errors:
|
|
23
|
+
UppError, standard and UPP-specific error code constants
|
|
24
|
+
"""
|
|
25
|
+
|
|
26
|
+
from __future__ import annotations
|
|
27
|
+
|
|
28
|
+
from upp.rpc.codec import decode_request, decode_response, encode_request, encode_response
|
|
29
|
+
from upp.rpc.errors import (
|
|
30
|
+
EXTRACTION_FAILED,
|
|
31
|
+
INGEST_FAILED,
|
|
32
|
+
INTERNAL_ERROR,
|
|
33
|
+
INVALID_PARAMS,
|
|
34
|
+
INVALID_REQUEST,
|
|
35
|
+
METHOD_NOT_FOUND,
|
|
36
|
+
ONTOLOGY_NOT_FOUND,
|
|
37
|
+
PARSE_ERROR,
|
|
38
|
+
USER_NOT_FOUND,
|
|
39
|
+
UppError,
|
|
40
|
+
)
|
|
41
|
+
from upp.rpc.messages import (
|
|
42
|
+
ContextualizeRequest,
|
|
43
|
+
ContextualizeResponse,
|
|
44
|
+
DeleteRequest,
|
|
45
|
+
DeleteResponse,
|
|
46
|
+
EventsRequest,
|
|
47
|
+
EventsResponse,
|
|
48
|
+
ExportRequest,
|
|
49
|
+
ExportResponse,
|
|
50
|
+
GetTasksRequest,
|
|
51
|
+
GetTasksResponse,
|
|
52
|
+
ImportRequest,
|
|
53
|
+
ImportResponse,
|
|
54
|
+
InfoRequest,
|
|
55
|
+
InfoResponse,
|
|
56
|
+
IngestRequest,
|
|
57
|
+
IngestResponse,
|
|
58
|
+
JsonRpcError,
|
|
59
|
+
JsonRpcNotification,
|
|
60
|
+
JsonRpcRequest,
|
|
61
|
+
JsonRpcResponse,
|
|
62
|
+
LabelsRequest,
|
|
63
|
+
LabelsResponse,
|
|
64
|
+
RetrieveRequest,
|
|
65
|
+
RetrieveResponse,
|
|
66
|
+
)
|
|
67
|
+
from upp.rpc.methods import (
|
|
68
|
+
ALL_METHODS,
|
|
69
|
+
UPP_CONTEXTUALIZE,
|
|
70
|
+
UPP_DELETE,
|
|
71
|
+
UPP_EVENTS,
|
|
72
|
+
UPP_EXPORT,
|
|
73
|
+
UPP_GET_TASKS,
|
|
74
|
+
UPP_IMPORT,
|
|
75
|
+
UPP_INFO,
|
|
76
|
+
UPP_INGEST,
|
|
77
|
+
UPP_LABELS,
|
|
78
|
+
UPP_RETRIEVE,
|
|
79
|
+
)
|
|
80
|
+
|
|
81
|
+
__all__ = [
|
|
82
|
+
# Message types
|
|
83
|
+
"JsonRpcError",
|
|
84
|
+
"JsonRpcNotification",
|
|
85
|
+
"JsonRpcRequest",
|
|
86
|
+
"JsonRpcResponse",
|
|
87
|
+
# Operation request/response models
|
|
88
|
+
"ContextualizeRequest",
|
|
89
|
+
"ContextualizeResponse",
|
|
90
|
+
"DeleteRequest",
|
|
91
|
+
"DeleteResponse",
|
|
92
|
+
"EventsRequest",
|
|
93
|
+
"EventsResponse",
|
|
94
|
+
"ExportRequest",
|
|
95
|
+
"ExportResponse",
|
|
96
|
+
"GetTasksRequest",
|
|
97
|
+
"GetTasksResponse",
|
|
98
|
+
"ImportRequest",
|
|
99
|
+
"ImportResponse",
|
|
100
|
+
"InfoRequest",
|
|
101
|
+
"InfoResponse",
|
|
102
|
+
"LabelsRequest",
|
|
103
|
+
"LabelsResponse",
|
|
104
|
+
"IngestRequest",
|
|
105
|
+
"IngestResponse",
|
|
106
|
+
"RetrieveRequest",
|
|
107
|
+
"RetrieveResponse",
|
|
108
|
+
# Codec
|
|
109
|
+
"decode_request",
|
|
110
|
+
"decode_response",
|
|
111
|
+
"encode_request",
|
|
112
|
+
"encode_response",
|
|
113
|
+
# Method constants
|
|
114
|
+
"ALL_METHODS",
|
|
115
|
+
"UPP_CONTEXTUALIZE",
|
|
116
|
+
"UPP_DELETE",
|
|
117
|
+
"UPP_EVENTS",
|
|
118
|
+
"UPP_EXPORT",
|
|
119
|
+
"UPP_GET_TASKS",
|
|
120
|
+
"UPP_IMPORT",
|
|
121
|
+
"UPP_INFO",
|
|
122
|
+
"UPP_LABELS",
|
|
123
|
+
"UPP_INGEST",
|
|
124
|
+
"UPP_RETRIEVE",
|
|
125
|
+
# Error codes
|
|
126
|
+
"EXTRACTION_FAILED",
|
|
127
|
+
"INTERNAL_ERROR",
|
|
128
|
+
"INVALID_PARAMS",
|
|
129
|
+
"INVALID_REQUEST",
|
|
130
|
+
"METHOD_NOT_FOUND",
|
|
131
|
+
"ONTOLOGY_NOT_FOUND",
|
|
132
|
+
"INGEST_FAILED",
|
|
133
|
+
"PARSE_ERROR",
|
|
134
|
+
"USER_NOT_FOUND",
|
|
135
|
+
# Exception
|
|
136
|
+
"UppError",
|
|
137
|
+
]
|