inputlayer-client-dev 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.
inputlayer/__init__.py ADDED
@@ -0,0 +1,151 @@
1
+ """inputlayer - Python Object-Logic Mapper for InputLayer knowledge graph engine."""
2
+
3
+ # Core types
4
+ from inputlayer.types import Timestamp, Vector, VectorInt8
5
+
6
+ # Relation system
7
+ from inputlayer.relation import Relation
8
+ from inputlayer.derived import Derived, From, RuleClause
9
+
10
+ # Aggregations
11
+ from inputlayer.aggregations import (
12
+ avg,
13
+ count,
14
+ count_distinct,
15
+ max_,
16
+ min_,
17
+ sum_,
18
+ top_k,
19
+ top_k_threshold,
20
+ within_radius,
21
+ )
22
+
23
+ # Functions (re-export all)
24
+ from inputlayer import functions
25
+
26
+ # Index
27
+ from inputlayer.index import HnswIndex
28
+
29
+ # Exceptions
30
+ from inputlayer.exceptions import (
31
+ AuthenticationError,
32
+ CannotDropError,
33
+ ConnectionError,
34
+ IndexNotFoundError,
35
+ InputLayerError,
36
+ InternalError,
37
+ KnowledgeGraphExistsError,
38
+ KnowledgeGraphNotFoundError,
39
+ PermissionError,
40
+ QueryTimeoutError,
41
+ RelationNotFoundError,
42
+ RuleNotFoundError,
43
+ SchemaConflictError,
44
+ ValidationError,
45
+ )
46
+
47
+ # Result
48
+ from inputlayer.result import ResultSet
49
+
50
+ # Client
51
+ from inputlayer.client import InputLayer
52
+ from inputlayer.client_sync import InputLayerSync
53
+
54
+ # Knowledge Graph
55
+ from inputlayer.knowledge_graph import (
56
+ ClearResult,
57
+ ColumnInfo,
58
+ DeleteResult,
59
+ ExplainResult,
60
+ IndexInfo,
61
+ IndexStats,
62
+ InsertResult,
63
+ KnowledgeGraph,
64
+ RelationDescription,
65
+ RelationInfo,
66
+ RuleInfo,
67
+ ServerStatus,
68
+ )
69
+
70
+ # Auth
71
+ from inputlayer.auth import AclEntry, ApiKeyInfo, UserInfo
72
+
73
+ # Session
74
+ from inputlayer.session import Session
75
+
76
+ # Notifications
77
+ from inputlayer.notifications import NotificationEvent
78
+
79
+ # Migrations
80
+ from inputlayer.migrations import Migration
81
+
82
+ __version__ = "0.1.0"
83
+
84
+ __all__ = [
85
+ # Types
86
+ "Vector",
87
+ "VectorInt8",
88
+ "Timestamp",
89
+ # Relation
90
+ "Relation",
91
+ "Derived",
92
+ "From",
93
+ "RuleClause",
94
+ # Aggregations
95
+ "count",
96
+ "count_distinct",
97
+ "sum_",
98
+ "min_",
99
+ "max_",
100
+ "avg",
101
+ "top_k",
102
+ "top_k_threshold",
103
+ "within_radius",
104
+ # Functions
105
+ "functions",
106
+ # Index
107
+ "HnswIndex",
108
+ # Exceptions
109
+ "InputLayerError",
110
+ "ConnectionError",
111
+ "AuthenticationError",
112
+ "SchemaConflictError",
113
+ "ValidationError",
114
+ "QueryTimeoutError",
115
+ "PermissionError",
116
+ "KnowledgeGraphNotFoundError",
117
+ "KnowledgeGraphExistsError",
118
+ "CannotDropError",
119
+ "RelationNotFoundError",
120
+ "RuleNotFoundError",
121
+ "IndexNotFoundError",
122
+ "InternalError",
123
+ # Result
124
+ "ResultSet",
125
+ # Client
126
+ "InputLayer",
127
+ "InputLayerSync",
128
+ # KG
129
+ "KnowledgeGraph",
130
+ "RelationInfo",
131
+ "RelationDescription",
132
+ "ColumnInfo",
133
+ "RuleInfo",
134
+ "IndexInfo",
135
+ "IndexStats",
136
+ "InsertResult",
137
+ "DeleteResult",
138
+ "ClearResult",
139
+ "ExplainResult",
140
+ "ServerStatus",
141
+ # Auth
142
+ "UserInfo",
143
+ "ApiKeyInfo",
144
+ "AclEntry",
145
+ # Session
146
+ "Session",
147
+ # Notifications
148
+ "NotificationEvent",
149
+ # Migrations
150
+ "Migration",
151
+ ]
inputlayer/_ast.py ADDED
@@ -0,0 +1,133 @@
1
+ """Internal AST nodes for expression trees compiled to Datalog."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from dataclasses import dataclass, field
6
+ from typing import Any
7
+
8
+
9
+ # ── Base ──────────────────────────────────────────────────────────────
10
+
11
+ @dataclass(frozen=True)
12
+ class Expr:
13
+ """Base class for all expression AST nodes."""
14
+
15
+
16
+ @dataclass(frozen=True)
17
+ class BoolExpr:
18
+ """Base class for boolean expression AST nodes (conditions)."""
19
+
20
+
21
+ # ── Leaf nodes ────────────────────────────────────────────────────────
22
+
23
+ @dataclass(frozen=True)
24
+ class Column(Expr):
25
+ """Reference to a relation column."""
26
+ relation: str
27
+ name: str
28
+ ref_alias: str | None = None # For self-join disambiguation
29
+
30
+ @property
31
+ def qualified(self) -> str:
32
+ prefix = self.ref_alias or self.relation
33
+ return f"{prefix}.{self.name}"
34
+
35
+
36
+ @dataclass(frozen=True)
37
+ class Literal(Expr):
38
+ """A constant value."""
39
+ value: Any # int, float, str, bool, list (vector), None
40
+
41
+
42
+ # ── Arithmetic ────────────────────────────────────────────────────────
43
+
44
+ @dataclass(frozen=True)
45
+ class Arithmetic(Expr):
46
+ """Binary arithmetic: +, -, *, /, %."""
47
+ op: str # "+", "-", "*", "/", "%"
48
+ left: Expr
49
+ right: Expr
50
+
51
+
52
+ # ── Function call ─────────────────────────────────────────────────────
53
+
54
+ @dataclass(frozen=True)
55
+ class FuncCall(Expr):
56
+ """Built-in function call: distance(V1, V2), upper(S), etc."""
57
+ name: str
58
+ args: tuple[Expr, ...] = ()
59
+
60
+
61
+ # ── Aggregation ───────────────────────────────────────────────────────
62
+
63
+ @dataclass(frozen=True)
64
+ class AggExpr(Expr):
65
+ """Aggregation expression: count<X>, sum<X>, top_k<k, ...>, etc."""
66
+ func: str # "count", "sum", "min", "max", "avg", "count_distinct",
67
+ # "top_k", "top_k_threshold", "within_radius"
68
+ column: Expr | None = None # The aggregated column (None for count(*))
69
+ params: tuple[Any, ...] = () # Extra params (k, threshold, etc.)
70
+ passthrough: tuple[Expr, ...] = () # Passthrough columns
71
+ order_column: Expr | None = None # For top_k: the ordering column
72
+ desc: bool = True # For top_k: descending order
73
+
74
+
75
+ # ── Ordering ──────────────────────────────────────────────────────────
76
+
77
+ @dataclass(frozen=True)
78
+ class OrderedColumn(Expr):
79
+ """A column with sort direction."""
80
+ column: Expr
81
+ descending: bool = False
82
+
83
+
84
+ # ── Boolean expressions ──────────────────────────────────────────────
85
+
86
+ @dataclass(frozen=True)
87
+ class Comparison(BoolExpr):
88
+ """Binary comparison: ==, !=, <, <=, >, >=."""
89
+ op: str # "=", "!=", "<", "<=", ">", ">="
90
+ left: Expr
91
+ right: Expr
92
+
93
+
94
+ @dataclass(frozen=True)
95
+ class And(BoolExpr):
96
+ """Logical AND of two conditions (Datalog comma)."""
97
+ left: BoolExpr
98
+ right: BoolExpr
99
+
100
+
101
+ @dataclass(frozen=True)
102
+ class Or(BoolExpr):
103
+ """Logical OR - requires splitting into multiple queries."""
104
+ left: BoolExpr
105
+ right: BoolExpr
106
+
107
+
108
+ @dataclass(frozen=True)
109
+ class Not(BoolExpr):
110
+ """Negation: !relation(X, Y) in Datalog."""
111
+ operand: BoolExpr
112
+
113
+
114
+ @dataclass(frozen=True)
115
+ class InExpr(BoolExpr):
116
+ """Membership test: Column appears in another relation."""
117
+ column: Expr
118
+ target_column: Expr
119
+
120
+
121
+ @dataclass(frozen=True)
122
+ class NegatedIn(BoolExpr):
123
+ """Negated membership test."""
124
+ column: Expr
125
+ target_column: Expr
126
+
127
+
128
+ @dataclass(frozen=True)
129
+ class MatchExpr(BoolExpr):
130
+ """Multi-column negation/existence check against a relation."""
131
+ relation: str
132
+ bindings: dict[str, Expr] # target_col -> source expr
133
+ negated: bool = False
inputlayer/_naming.py ADDED
@@ -0,0 +1,44 @@
1
+ """Naming convention utilities: CamelCase ↔ snake_case, column → Datalog variable."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import re
6
+
7
+
8
+ def camel_to_snake(name: str) -> str:
9
+ """Convert CamelCase class name to snake_case relation name.
10
+
11
+ Examples:
12
+ Employee -> employee
13
+ UserProfile -> user_profile
14
+ HTTPRequest -> http_request
15
+ ABCDef -> abc_def
16
+ """
17
+ # Insert underscore between sequences of uppercase and a following lower/digit
18
+ s = re.sub(r"([A-Z]+)([A-Z][a-z])", r"\1_\2", name)
19
+ # Insert underscore between lowercase/digit and uppercase
20
+ s = re.sub(r"([a-z0-9])([A-Z])", r"\1_\2", s)
21
+ return s.lower()
22
+
23
+
24
+ def snake_to_camel(name: str) -> str:
25
+ """Convert snake_case to CamelCase.
26
+
27
+ Examples:
28
+ employee -> Employee
29
+ user_profile -> UserProfile
30
+ http_request -> HttpRequest
31
+ """
32
+ return "".join(part.capitalize() for part in name.split("_"))
33
+
34
+
35
+ def column_to_variable(column_name: str) -> str:
36
+ """Convert a snake_case column name to a Datalog variable (Capitalized).
37
+
38
+ Examples:
39
+ id -> Id
40
+ name -> Name
41
+ department_name -> DepartmentName
42
+ x -> X
43
+ """
44
+ return snake_to_camel(column_name)
@@ -0,0 +1,225 @@
1
+ """WebSocket wire protocol: message serialization and deserialization.
2
+
3
+ Matches the AsyncAPI spec at ``docs/spec/asyncapi.yaml``.
4
+ """
5
+
6
+ from __future__ import annotations
7
+
8
+ import json
9
+ from dataclasses import dataclass, field
10
+ from typing import Any
11
+
12
+
13
+ # ── Client → Server messages ──────────────────────────────────────────
14
+
15
+ @dataclass(frozen=True)
16
+ class LoginMessage:
17
+ username: str
18
+ password: str
19
+
20
+ def to_json(self) -> str:
21
+ return json.dumps({
22
+ "type": "login",
23
+ "username": self.username,
24
+ "password": self.password,
25
+ })
26
+
27
+
28
+ @dataclass(frozen=True)
29
+ class AuthenticateMessage:
30
+ api_key: str
31
+
32
+ def to_json(self) -> str:
33
+ return json.dumps({
34
+ "type": "authenticate",
35
+ "api_key": self.api_key,
36
+ })
37
+
38
+
39
+ @dataclass(frozen=True)
40
+ class ExecuteMessage:
41
+ program: str
42
+
43
+ def to_json(self) -> str:
44
+ return json.dumps({
45
+ "type": "execute",
46
+ "program": self.program,
47
+ })
48
+
49
+
50
+ @dataclass(frozen=True)
51
+ class PingMessage:
52
+ def to_json(self) -> str:
53
+ return json.dumps({"type": "ping"})
54
+
55
+
56
+ # ── Server → Client messages ─────────────────────────────────────────
57
+
58
+ @dataclass(frozen=True)
59
+ class AuthenticatedResponse:
60
+ session_id: str
61
+ knowledge_graph: str
62
+ version: str
63
+ role: str
64
+
65
+
66
+ @dataclass(frozen=True)
67
+ class AuthErrorResponse:
68
+ message: str
69
+
70
+
71
+ @dataclass(frozen=True)
72
+ class ResultResponse:
73
+ columns: list[str]
74
+ rows: list[list[Any]]
75
+ row_count: int
76
+ total_count: int
77
+ truncated: bool
78
+ execution_time_ms: int
79
+ row_provenance: list[str] | None = None
80
+ metadata: dict[str, Any] | None = None
81
+ switched_kg: str | None = None
82
+
83
+
84
+ @dataclass(frozen=True)
85
+ class ErrorResponse:
86
+ message: str
87
+ validation_errors: list[dict[str, Any]] | None = None
88
+
89
+
90
+ @dataclass(frozen=True)
91
+ class ResultStartResponse:
92
+ columns: list[str]
93
+ total_count: int
94
+ truncated: bool
95
+ execution_time_ms: int
96
+ metadata: dict[str, Any] | None = None
97
+ switched_kg: str | None = None
98
+
99
+
100
+ @dataclass(frozen=True)
101
+ class ResultChunkResponse:
102
+ rows: list[list[Any]]
103
+ chunk_index: int
104
+ row_provenance: list[str] | None = None
105
+
106
+
107
+ @dataclass(frozen=True)
108
+ class ResultEndResponse:
109
+ row_count: int
110
+ chunk_count: int
111
+
112
+
113
+ @dataclass(frozen=True)
114
+ class PongResponse:
115
+ pass
116
+
117
+
118
+ @dataclass(frozen=True)
119
+ class NotificationResponse:
120
+ type: str # persistent_update, rule_change, kg_change, schema_change
121
+ seq: int
122
+ timestamp_ms: int
123
+ session_id: str | None = None
124
+ knowledge_graph: str | None = None
125
+ # persistent_update fields
126
+ relation: str | None = None
127
+ operation: str | None = None
128
+ count: int | None = None
129
+ # rule_change fields
130
+ rule_name: str | None = None
131
+ # schema_change fields
132
+ entity: str | None = None
133
+
134
+
135
+ # ── Type alias ────────────────────────────────────────────────────────
136
+
137
+ ServerMessage = (
138
+ AuthenticatedResponse
139
+ | AuthErrorResponse
140
+ | ResultResponse
141
+ | ErrorResponse
142
+ | ResultStartResponse
143
+ | ResultChunkResponse
144
+ | ResultEndResponse
145
+ | PongResponse
146
+ | NotificationResponse
147
+ )
148
+
149
+
150
+ # ── Serialization / Deserialization ───────────────────────────────────
151
+
152
+ def serialize_message(msg: LoginMessage | AuthenticateMessage | ExecuteMessage | PingMessage) -> str:
153
+ """Serialize a client message to JSON."""
154
+ return msg.to_json()
155
+
156
+
157
+ def deserialize_message(data: str | bytes) -> ServerMessage:
158
+ """Deserialize a server JSON message into a typed response object."""
159
+ if isinstance(data, bytes):
160
+ data = data.decode("utf-8")
161
+ obj = json.loads(data)
162
+ msg_type = obj.get("type")
163
+
164
+ if msg_type == "authenticated":
165
+ return AuthenticatedResponse(
166
+ session_id=obj["session_id"],
167
+ knowledge_graph=obj["knowledge_graph"],
168
+ version=obj["version"],
169
+ role=obj["role"],
170
+ )
171
+ if msg_type == "auth_error":
172
+ return AuthErrorResponse(message=obj["message"])
173
+ if msg_type == "result":
174
+ return ResultResponse(
175
+ columns=obj["columns"],
176
+ rows=obj["rows"],
177
+ row_count=obj["row_count"],
178
+ total_count=obj["total_count"],
179
+ truncated=obj["truncated"],
180
+ execution_time_ms=obj["execution_time_ms"],
181
+ row_provenance=obj.get("row_provenance"),
182
+ metadata=obj.get("metadata"),
183
+ switched_kg=obj.get("switched_kg"),
184
+ )
185
+ if msg_type == "error":
186
+ return ErrorResponse(
187
+ message=obj["message"],
188
+ validation_errors=obj.get("validation_errors"),
189
+ )
190
+ if msg_type == "result_start":
191
+ return ResultStartResponse(
192
+ columns=obj["columns"],
193
+ total_count=obj["total_count"],
194
+ truncated=obj["truncated"],
195
+ execution_time_ms=obj["execution_time_ms"],
196
+ metadata=obj.get("metadata"),
197
+ switched_kg=obj.get("switched_kg"),
198
+ )
199
+ if msg_type == "result_chunk":
200
+ return ResultChunkResponse(
201
+ rows=obj["rows"],
202
+ chunk_index=obj["chunk_index"],
203
+ row_provenance=obj.get("row_provenance"),
204
+ )
205
+ if msg_type == "result_end":
206
+ return ResultEndResponse(
207
+ row_count=obj["row_count"],
208
+ chunk_count=obj["chunk_count"],
209
+ )
210
+ if msg_type == "pong":
211
+ return PongResponse()
212
+ if msg_type in ("persistent_update", "rule_change", "kg_change", "schema_change"):
213
+ return NotificationResponse(
214
+ type=msg_type,
215
+ seq=obj["seq"],
216
+ timestamp_ms=obj["timestamp_ms"],
217
+ session_id=obj.get("session_id"),
218
+ knowledge_graph=obj.get("knowledge_graph"),
219
+ relation=obj.get("relation"),
220
+ operation=obj.get("operation"),
221
+ count=obj.get("count"),
222
+ rule_name=obj.get("rule_name"),
223
+ entity=obj.get("entity"),
224
+ )
225
+ raise ValueError(f"Unknown message type: {msg_type!r}")