web-algebra 2.0.2__tar.gz → 2.0.3__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.
- {web_algebra-2.0.2 → web_algebra-2.0.3}/PKG-INFO +1 -1
- {web_algebra-2.0.2 → web_algebra-2.0.3}/pyproject.toml +1 -1
- {web_algebra-2.0.2 → web_algebra-2.0.3}/src/web_algebra/client.py +19 -1
- web_algebra-2.0.3/src/web_algebra/operations/linkeddatahub/create.py +51 -0
- {web_algebra-2.0.2 → web_algebra-2.0.3}/src/web_algebra/operations/linkeddatahub/create_container.py +34 -9
- {web_algebra-2.0.2 → web_algebra-2.0.3}/src/web_algebra/operations/linkeddatahub/create_item.py +34 -9
- {web_algebra-2.0.2 → web_algebra-2.0.3}/src/web_algebra/prompts/system.md +4 -4
- {web_algebra-2.0.2 → web_algebra-2.0.3}/src/web_algebra.egg-info/PKG-INFO +1 -1
- {web_algebra-2.0.2 → web_algebra-2.0.3}/src/web_algebra.egg-info/SOURCES.txt +1 -0
- {web_algebra-2.0.2 → web_algebra-2.0.3}/tests/test_operation_kind.py +5 -5
- {web_algebra-2.0.2 → web_algebra-2.0.3}/LICENSE +0 -0
- {web_algebra-2.0.2 → web_algebra-2.0.3}/README.md +0 -0
- {web_algebra-2.0.2 → web_algebra-2.0.3}/setup.cfg +0 -0
- {web_algebra-2.0.2 → web_algebra-2.0.3}/src/web_algebra/__init__.py +0 -0
- {web_algebra-2.0.2 → web_algebra-2.0.3}/src/web_algebra/__main__.py +0 -0
- {web_algebra-2.0.2 → web_algebra-2.0.3}/src/web_algebra/client_operation.py +0 -0
- {web_algebra-2.0.2 → web_algebra-2.0.3}/src/web_algebra/exceptions.py +0 -0
- {web_algebra-2.0.2 → web_algebra-2.0.3}/src/web_algebra/focus.py +0 -0
- {web_algebra-2.0.2 → web_algebra-2.0.3}/src/web_algebra/json_result.py +0 -0
- {web_algebra-2.0.2 → web_algebra-2.0.3}/src/web_algebra/main.py +0 -0
- {web_algebra-2.0.2 → web_algebra-2.0.3}/src/web_algebra/mcp_tool.py +0 -0
- {web_algebra-2.0.2 → web_algebra-2.0.3}/src/web_algebra/operation.py +0 -0
- {web_algebra-2.0.2 → web_algebra-2.0.3}/src/web_algebra/operations/__init__.py +0 -0
- {web_algebra-2.0.2 → web_algebra-2.0.3}/src/web_algebra/operations/bindings.py +0 -0
- {web_algebra-2.0.2 → web_algebra-2.0.3}/src/web_algebra/operations/current.py +0 -0
- {web_algebra-2.0.2 → web_algebra-2.0.3}/src/web_algebra/operations/filter.py +0 -0
- {web_algebra-2.0.2 → web_algebra-2.0.3}/src/web_algebra/operations/for_each.py +0 -0
- {web_algebra-2.0.2 → web_algebra-2.0.3}/src/web_algebra/operations/iterate.py +0 -0
- {web_algebra-2.0.2 → web_algebra-2.0.3}/src/web_algebra/operations/last.py +0 -0
- {web_algebra-2.0.2 → web_algebra-2.0.3}/src/web_algebra/operations/linked_data/__init__.py +0 -0
- {web_algebra-2.0.2 → web_algebra-2.0.3}/src/web_algebra/operations/linked_data/get.py +0 -0
- {web_algebra-2.0.2 → web_algebra-2.0.3}/src/web_algebra/operations/linked_data/patch.py +0 -0
- {web_algebra-2.0.2 → web_algebra-2.0.3}/src/web_algebra/operations/linked_data/post.py +0 -0
- {web_algebra-2.0.2 → web_algebra-2.0.3}/src/web_algebra/operations/linked_data/put.py +0 -0
- {web_algebra-2.0.2 → web_algebra-2.0.3}/src/web_algebra/operations/linkeddatahub/__init__.py +0 -0
- {web_algebra-2.0.2 → web_algebra-2.0.3}/src/web_algebra/operations/linkeddatahub/add_construct.py +0 -0
- {web_algebra-2.0.2 → web_algebra-2.0.3}/src/web_algebra/operations/linkeddatahub/add_file.py +0 -0
- {web_algebra-2.0.2 → web_algebra-2.0.3}/src/web_algebra/operations/linkeddatahub/add_generic_service.py +0 -0
- {web_algebra-2.0.2 → web_algebra-2.0.3}/src/web_algebra/operations/linkeddatahub/add_result_set_chart.py +0 -0
- {web_algebra-2.0.2 → web_algebra-2.0.3}/src/web_algebra/operations/linkeddatahub/add_select.py +0 -0
- {web_algebra-2.0.2 → web_algebra-2.0.3}/src/web_algebra/operations/linkeddatahub/add_view.py +0 -0
- {web_algebra-2.0.2 → web_algebra-2.0.3}/src/web_algebra/operations/linkeddatahub/content/__init__.py +0 -0
- {web_algebra-2.0.2 → web_algebra-2.0.3}/src/web_algebra/operations/linkeddatahub/content/add_object_block.py +0 -0
- {web_algebra-2.0.2 → web_algebra-2.0.3}/src/web_algebra/operations/linkeddatahub/content/add_xhtml_block.py +0 -0
- {web_algebra-2.0.2 → web_algebra-2.0.3}/src/web_algebra/operations/linkeddatahub/content/generate_class_containers.py +0 -0
- {web_algebra-2.0.2 → web_algebra-2.0.3}/src/web_algebra/operations/linkeddatahub/content/generate_ontology_views.py +0 -0
- {web_algebra-2.0.2 → web_algebra-2.0.3}/src/web_algebra/operations/linkeddatahub/content/generate_portal.py +0 -0
- {web_algebra-2.0.2 → web_algebra-2.0.3}/src/web_algebra/operations/linkeddatahub/content/remove_block.py +0 -0
- {web_algebra-2.0.2 → web_algebra-2.0.3}/src/web_algebra/operations/linkeddatahub/fragments.py +0 -0
- {web_algebra-2.0.2 → web_algebra-2.0.3}/src/web_algebra/operations/linkeddatahub/list.py +0 -0
- {web_algebra-2.0.2 → web_algebra-2.0.3}/src/web_algebra/operations/merge.py +0 -0
- {web_algebra-2.0.2 → web_algebra-2.0.3}/src/web_algebra/operations/now.py +0 -0
- {web_algebra-2.0.2 → web_algebra-2.0.3}/src/web_algebra/operations/position.py +0 -0
- {web_algebra-2.0.2 → web_algebra-2.0.3}/src/web_algebra/operations/resolve_uri.py +0 -0
- {web_algebra-2.0.2 → web_algebra-2.0.3}/src/web_algebra/operations/schema/__init__.py +0 -0
- {web_algebra-2.0.2 → web_algebra-2.0.3}/src/web_algebra/operations/schema/extract_classes.py +0 -0
- {web_algebra-2.0.2 → web_algebra-2.0.3}/src/web_algebra/operations/schema/extract_datatype_properties.py +0 -0
- {web_algebra-2.0.2 → web_algebra-2.0.3}/src/web_algebra/operations/schema/extract_object_properties.py +0 -0
- {web_algebra-2.0.2 → web_algebra-2.0.3}/src/web_algebra/operations/schema/extract_ontology.py +0 -0
- {web_algebra-2.0.2 → web_algebra-2.0.3}/src/web_algebra/operations/sparql/__init__.py +0 -0
- {web_algebra-2.0.2 → web_algebra-2.0.3}/src/web_algebra/operations/sparql/construct.py +0 -0
- {web_algebra-2.0.2 → web_algebra-2.0.3}/src/web_algebra/operations/sparql/describe.py +0 -0
- {web_algebra-2.0.2 → web_algebra-2.0.3}/src/web_algebra/operations/sparql/select.py +0 -0
- {web_algebra-2.0.2 → web_algebra-2.0.3}/src/web_algebra/operations/sparql/substitute.py +0 -0
- {web_algebra-2.0.2 → web_algebra-2.0.3}/src/web_algebra/operations/sparql/values.py +0 -0
- {web_algebra-2.0.2 → web_algebra-2.0.3}/src/web_algebra/operations/sparql_string.py +0 -0
- {web_algebra-2.0.2 → web_algebra-2.0.3}/src/web_algebra/operations/str.py +0 -0
- {web_algebra-2.0.2 → web_algebra-2.0.3}/src/web_algebra/operations/string/__init__.py +0 -0
- {web_algebra-2.0.2 → web_algebra-2.0.3}/src/web_algebra/operations/string/concat.py +0 -0
- {web_algebra-2.0.2 → web_algebra-2.0.3}/src/web_algebra/operations/string/encode_for_uri.py +0 -0
- {web_algebra-2.0.2 → web_algebra-2.0.3}/src/web_algebra/operations/string/replace.py +0 -0
- {web_algebra-2.0.2 → web_algebra-2.0.3}/src/web_algebra/operations/struuid.py +0 -0
- {web_algebra-2.0.2 → web_algebra-2.0.3}/src/web_algebra/operations/uri.py +0 -0
- {web_algebra-2.0.2 → web_algebra-2.0.3}/src/web_algebra/operations/value.py +0 -0
- {web_algebra-2.0.2 → web_algebra-2.0.3}/src/web_algebra/operations/variable.py +0 -0
- {web_algebra-2.0.2 → web_algebra-2.0.3}/src/web_algebra/prompts/user.template.txt +0 -0
- {web_algebra-2.0.2 → web_algebra-2.0.3}/src/web_algebra/py.typed +0 -0
- {web_algebra-2.0.2 → web_algebra-2.0.3}/src/web_algebra/query_source.py +0 -0
- {web_algebra-2.0.2 → web_algebra-2.0.3}/src/web_algebra/rfc3986.py +0 -0
- {web_algebra-2.0.2 → web_algebra-2.0.3}/src/web_algebra/schema_extraction.py +0 -0
- {web_algebra-2.0.2 → web_algebra-2.0.3}/src/web_algebra/server.py +0 -0
- {web_algebra-2.0.2 → web_algebra-2.0.3}/src/web_algebra/sparql_restrict.py +0 -0
- {web_algebra-2.0.2 → web_algebra-2.0.3}/src/web_algebra.egg-info/dependency_links.txt +0 -0
- {web_algebra-2.0.2 → web_algebra-2.0.3}/src/web_algebra.egg-info/entry_points.txt +0 -0
- {web_algebra-2.0.2 → web_algebra-2.0.3}/src/web_algebra.egg-info/requires.txt +0 -0
- {web_algebra-2.0.2 → web_algebra-2.0.3}/src/web_algebra.egg-info/top_level.txt +0 -0
- {web_algebra-2.0.2 → web_algebra-2.0.3}/tests/test_client_writes.py +0 -0
- {web_algebra-2.0.2 → web_algebra-2.0.3}/tests/test_examples.py +0 -0
- {web_algebra-2.0.2 → web_algebra-2.0.3}/tests/test_ldh_cli_parity.py +0 -0
- {web_algebra-2.0.2 → web_algebra-2.0.3}/tests/test_mcp_server.py +0 -0
- {web_algebra-2.0.2 → web_algebra-2.0.3}/tests/test_operation_names.py +0 -0
- {web_algebra-2.0.2 → web_algebra-2.0.3}/tests/test_packaging.py +0 -0
- {web_algebra-2.0.2 → web_algebra-2.0.3}/tests/test_planner.py +0 -0
- {web_algebra-2.0.2 → web_algebra-2.0.3}/tests/test_sparql_string_ask.py +0 -0
- {web_algebra-2.0.2 → web_algebra-2.0.3}/tests/test_system_prompt.py +0 -0
|
@@ -210,6 +210,7 @@ class LinkedDataClient:
|
|
|
210
210
|
body: Optional[bytes] = None,
|
|
211
211
|
content_type: Optional[str] = None,
|
|
212
212
|
accept: str = "application/n-triples",
|
|
213
|
+
preconditions: Optional[dict] = None,
|
|
213
214
|
) -> HTTPResponse:
|
|
214
215
|
"""Send a write, conditional on the resource's current entity tag.
|
|
215
216
|
|
|
@@ -217,11 +218,14 @@ class LinkedDataClient:
|
|
|
217
218
|
them can leave out the `If-Match` a server like LinkedDataHub requires of
|
|
218
219
|
a write to an existing document (formal-semantics.md §4.4) - as the
|
|
219
220
|
LinkedDataHub CLI's `LDHClient` adds it in each of its write methods.
|
|
221
|
+
A write that brings its own `preconditions` is sent with those instead:
|
|
222
|
+
`If-None-Match: *` creates only, and quoting an existing tag as well
|
|
223
|
+
would turn it back into a replace.
|
|
220
224
|
"""
|
|
221
225
|
headers = {"Accept": accept}
|
|
222
226
|
if content_type is not None:
|
|
223
227
|
headers["Content-Type"] = content_type
|
|
224
|
-
headers.update(self.conditional(url, accept))
|
|
228
|
+
headers.update(preconditions if preconditions is not None else self.conditional(url, accept))
|
|
225
229
|
request = urllib.request.Request(url, data=body, headers=headers, method=method)
|
|
226
230
|
|
|
227
231
|
return self._send(request)
|
|
@@ -311,6 +315,20 @@ class LinkedDataClient:
|
|
|
311
315
|
data = graph.serialize(format="nt")
|
|
312
316
|
return self._write("PUT", url, data.encode("utf-8"), "application/n-triples")
|
|
313
317
|
|
|
318
|
+
def create(self, url: str, graph: Graph) -> HTTPResponse:
|
|
319
|
+
"""
|
|
320
|
+
Sends RDF data to the given URL using HTTP PUT with `If-None-Match: *`,
|
|
321
|
+
so that it only ever creates: a server answers `412` where a resource
|
|
322
|
+
already exists, and leaves it as it is (formal-semantics.md Appendix A).
|
|
323
|
+
|
|
324
|
+
:param url: The URL to create the resource at.
|
|
325
|
+
:param graph: An RDFLib Graph containing the data to send.
|
|
326
|
+
:return: The HTTPResponse object.
|
|
327
|
+
"""
|
|
328
|
+
data = graph.serialize(format="nt")
|
|
329
|
+
return self._write("PUT", url, data.encode("utf-8"), "application/n-triples",
|
|
330
|
+
preconditions={"If-None-Match": "*"})
|
|
331
|
+
|
|
314
332
|
def delete(self, url: str) -> HTTPResponse:
|
|
315
333
|
"""
|
|
316
334
|
Sends an HTTP DELETE request to the given URL.
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
from typing import Optional
|
|
3
|
+
|
|
4
|
+
from rdflib import Graph, Literal, URIRef
|
|
5
|
+
from rdflib.namespace import XSD
|
|
6
|
+
from rdflib.query import Result
|
|
7
|
+
|
|
8
|
+
from web_algebra.client_operation import ClientOperation
|
|
9
|
+
from web_algebra.exceptions import WriteRefusedError
|
|
10
|
+
from web_algebra.json_result import JSONResult
|
|
11
|
+
|
|
12
|
+
PRECONDITION_FAILED = 412
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def create(op: ClientOperation, url: URIRef, graph: Graph, if_absent: Optional[Literal]) -> Result:
|
|
16
|
+
"""Write `graph` as a new document at `url`, never replacing one.
|
|
17
|
+
|
|
18
|
+
The `PUT` carries `If-None-Match: *` (formal-semantics.md Appendix A), as
|
|
19
|
+
LinkedDataHub's CLI create commands and its client's create forms send it,
|
|
20
|
+
so a server refuses it `412` where a document already exists and leaves
|
|
21
|
+
that document as it is. That refusal is the operation's error, unless
|
|
22
|
+
`if_absent` is true: then the existing document is the result, as an
|
|
23
|
+
existing directory is for `mkdir -p`. Nothing was written, so it is not
|
|
24
|
+
reported to an enclosing `ForEach` as a write (§3.6).
|
|
25
|
+
"""
|
|
26
|
+
try:
|
|
27
|
+
response = op.client.create(str(url), graph)
|
|
28
|
+
except WriteRefusedError as e:
|
|
29
|
+
if e.status != PRECONDITION_FAILED:
|
|
30
|
+
raise
|
|
31
|
+
if if_absent is None or not if_absent.toPython():
|
|
32
|
+
raise WriteRefusedError(
|
|
33
|
+
e.method, e.url, e.status,
|
|
34
|
+
"a document already exists at this URI and was left as it is; "
|
|
35
|
+
"pass if_absent to accept one that does",
|
|
36
|
+
) from None
|
|
37
|
+
logging.info("A document already exists at <%s>; left as it is", url)
|
|
38
|
+
return JSONResult(
|
|
39
|
+
vars=["status", "url"],
|
|
40
|
+
bindings=[{"status": Literal(e.status, datatype=XSD.integer), "url": url}],
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
logging.info("PUT operation status: %s", response.status)
|
|
44
|
+
return op.written_response(response)
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def if_absent_flag(value: Optional[Literal], op_name: str) -> Optional[Literal]:
|
|
48
|
+
"""`if_absent` as an operation takes it: absent, or an xsd:boolean Literal."""
|
|
49
|
+
if value is not None and not (isinstance(value, Literal) and value.datatype == XSD.boolean):
|
|
50
|
+
raise TypeError(f"{op_name} expects if_absent to be an xsd:boolean Literal, got {value!r}")
|
|
51
|
+
return value
|
{web_algebra-2.0.2 → web_algebra-2.0.3}/src/web_algebra/operations/linkeddatahub/create_container.py
RENAMED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
from typing import Any, Optional
|
|
1
|
+
from typing import Any, ClassVar, Optional
|
|
2
2
|
import logging
|
|
3
3
|
import urllib.parse
|
|
4
4
|
import uuid
|
|
@@ -7,11 +7,17 @@ from rdflib.namespace import XSD
|
|
|
7
7
|
from mcp import types
|
|
8
8
|
from web_algebra.operation import Operation
|
|
9
9
|
from web_algebra.rfc3986 import resolve
|
|
10
|
+
from web_algebra.operation import OperationKind
|
|
10
11
|
from web_algebra.operations.linked_data.put import PUT
|
|
12
|
+
from web_algebra.operations.linkeddatahub.create import create, if_absent_flag
|
|
11
13
|
from rdflib.query import Result
|
|
12
14
|
|
|
13
15
|
|
|
14
16
|
class CreateContainer(PUT):
|
|
17
|
+
# creates only, with If-None-Match: *: a document already at the URI is left as it is, so
|
|
18
|
+
# nothing is ever removed
|
|
19
|
+
kind: ClassVar[OperationKind] = OperationKind.WRITE
|
|
20
|
+
|
|
15
21
|
@classmethod
|
|
16
22
|
def name(cls):
|
|
17
23
|
return "ldh-CreateContainer"
|
|
@@ -26,7 +32,9 @@ class CreateContainer(PUT):
|
|
|
26
32
|
- Example: if https://localhost/blog/ exists, you cannot create https://localhost/blog/posts/something/
|
|
27
33
|
before creating https://localhost/blog/posts/
|
|
28
34
|
- LinkedDataHub automatically manages timestamps (dct:created, dct:modified)
|
|
29
|
-
- LinkedDataHub automatically manages parent-child relationships (sioc:has_parent) based on URI hierarchy
|
|
35
|
+
- LinkedDataHub automatically manages parent-child relationships (sioc:has_parent) based on URI hierarchy
|
|
36
|
+
- It only creates: a document already at the URI is left as it is and the operation fails, unless
|
|
37
|
+
if_absent is true, which accepts it as the result (like mkdir -p). To replace a document, PUT it."""
|
|
30
38
|
|
|
31
39
|
@classmethod
|
|
32
40
|
def inputSchema(cls) -> dict:
|
|
@@ -43,7 +51,7 @@ class CreateContainer(PUT):
|
|
|
43
51
|
},
|
|
44
52
|
"slug": {
|
|
45
53
|
"type": "string",
|
|
46
|
-
"description": "URL path segment (optional). Defaults to a random UUID.
|
|
54
|
+
"description": "URL path segment (optional). Defaults to a random UUID.",
|
|
47
55
|
},
|
|
48
56
|
"description": {
|
|
49
57
|
"type": "string",
|
|
@@ -57,6 +65,10 @@ class CreateContainer(PUT):
|
|
|
57
65
|
"type": "string",
|
|
58
66
|
"description": "URI reference of what the container is about, resolved against its URI (optional), e.g. #this.",
|
|
59
67
|
},
|
|
68
|
+
"if_absent": {
|
|
69
|
+
"type": "boolean",
|
|
70
|
+
"description": "Optional: whether a document already at the URI counts as success, left as it is (like mkdir -p). Without it the operation fails there.",
|
|
71
|
+
},
|
|
60
72
|
},
|
|
61
73
|
"required": ["parent", "title"],
|
|
62
74
|
}
|
|
@@ -69,6 +81,7 @@ class CreateContainer(PUT):
|
|
|
69
81
|
description: Optional[Literal] = None,
|
|
70
82
|
children_view: Optional[Literal] = None,
|
|
71
83
|
primary_topic: Optional[Literal] = None,
|
|
84
|
+
if_absent: Optional[Literal] = None,
|
|
72
85
|
) -> Result:
|
|
73
86
|
"""Pure function: create container with RDFLib terms"""
|
|
74
87
|
if not isinstance(parent_uri, URIRef):
|
|
@@ -98,6 +111,8 @@ class CreateContainer(PUT):
|
|
|
98
111
|
f"CreateContainer.execute expects primary_topic to be Literal or None, got {type(primary_topic)}"
|
|
99
112
|
)
|
|
100
113
|
|
|
114
|
+
if_absent = if_absent_flag(if_absent, "CreateContainer.execute")
|
|
115
|
+
|
|
101
116
|
# URL-encode slug, or mint a fresh one - a lowercase UUID, as the LinkedDataHub CLI does - so that
|
|
102
117
|
# leaving it out never lands on an existing document; deriving it from the title did, on a repeated title
|
|
103
118
|
slug_str = urllib.parse.quote(str(slug) if slug else str(uuid.uuid4()), safe="")
|
|
@@ -141,8 +156,7 @@ class CreateContainer(PUT):
|
|
|
141
156
|
# Convert the document to a Graph and call parent PUT operation
|
|
142
157
|
graph = self.to_graph(data, base=url)
|
|
143
158
|
|
|
144
|
-
|
|
145
|
-
return super().execute(URIRef(url), graph)
|
|
159
|
+
return create(self, URIRef(url), graph, if_absent)
|
|
146
160
|
|
|
147
161
|
def execute_json(self, arguments: dict, variable_stack: list = None) -> Result:
|
|
148
162
|
"""JSON execution: process arguments and call pure function"""
|
|
@@ -210,7 +224,14 @@ class CreateContainer(PUT):
|
|
|
210
224
|
f"ldh-CreateContainer operation expects 'primary_topic' to be Literal, got {type(primary_topic)}"
|
|
211
225
|
)
|
|
212
226
|
|
|
213
|
-
|
|
227
|
+
# Process optional if_absent
|
|
228
|
+
if_absent = None
|
|
229
|
+
if "if_absent" in arguments:
|
|
230
|
+
if_absent = Operation.json_to_rdflib(Operation.process_json(
|
|
231
|
+
self.settings, arguments["if_absent"], self.context, variable_stack
|
|
232
|
+
))
|
|
233
|
+
|
|
234
|
+
return self.execute(parent_uri, title, slug, description, children_view, primary_topic, if_absent)
|
|
214
235
|
|
|
215
236
|
def mcp_run(self, arguments: dict, context: Any = None) -> Any:
|
|
216
237
|
"""MCP execution: plain args → plain results"""
|
|
@@ -235,12 +256,16 @@ class CreateContainer(PUT):
|
|
|
235
256
|
if "primary_topic" in arguments
|
|
236
257
|
else None
|
|
237
258
|
)
|
|
238
|
-
|
|
259
|
+
if_absent = Literal(bool(arguments["if_absent"])) if "if_absent" in arguments else None
|
|
260
|
+
result = self.execute(parent_uri, title, slug, description, children_view, primary_topic, if_absent)
|
|
239
261
|
|
|
240
|
-
# Extract URL from PUT result
|
|
262
|
+
# Extract URL from PUT result; a 412 is the existing document if_absent accepted
|
|
241
263
|
url_binding = result.bindings[0]["url"]
|
|
264
|
+
created = int(result.bindings[0]["status"]) != 412
|
|
242
265
|
return [
|
|
243
266
|
types.TextContent(
|
|
244
|
-
type="text",
|
|
267
|
+
type="text",
|
|
268
|
+
text=f"Container created successfully at: {url_binding}" if created
|
|
269
|
+
else f"A document already exists at {url_binding}; left as it is",
|
|
245
270
|
)
|
|
246
271
|
]
|
{web_algebra-2.0.2 → web_algebra-2.0.3}/src/web_algebra/operations/linkeddatahub/create_item.py
RENAMED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
from typing import Any, Optional
|
|
1
|
+
from typing import Any, ClassVar, Optional
|
|
2
2
|
import logging
|
|
3
3
|
import urllib.parse
|
|
4
4
|
import uuid
|
|
@@ -7,11 +7,17 @@ from rdflib.namespace import XSD
|
|
|
7
7
|
from mcp import types
|
|
8
8
|
from web_algebra.operation import Operation
|
|
9
9
|
from web_algebra.rfc3986 import resolve
|
|
10
|
+
from web_algebra.operation import OperationKind
|
|
10
11
|
from web_algebra.operations.linked_data.put import PUT
|
|
12
|
+
from web_algebra.operations.linkeddatahub.create import create, if_absent_flag
|
|
11
13
|
from rdflib.query import Result
|
|
12
14
|
|
|
13
15
|
|
|
14
16
|
class CreateItem(PUT):
|
|
17
|
+
# creates only, with If-None-Match: *: a document already at the URI is left as it is, so
|
|
18
|
+
# nothing is ever removed
|
|
19
|
+
kind: ClassVar[OperationKind] = OperationKind.WRITE
|
|
20
|
+
|
|
15
21
|
@classmethod
|
|
16
22
|
def name(cls):
|
|
17
23
|
return "ldh-CreateItem"
|
|
@@ -26,7 +32,9 @@ class CreateItem(PUT):
|
|
|
26
32
|
- Example: if https://localhost/blog/ exists, you cannot create https://localhost/blog/posts/something/
|
|
27
33
|
before creating https://localhost/blog/posts/
|
|
28
34
|
- LinkedDataHub automatically manages timestamps (dct:created, dct:modified)
|
|
29
|
-
- LinkedDataHub automatically manages parent-child relationships (sioc:has_parent) based on URI hierarchy
|
|
35
|
+
- LinkedDataHub automatically manages parent-child relationships (sioc:has_parent) based on URI hierarchy
|
|
36
|
+
- It only creates: a document already at the URI is left as it is and the operation fails, unless
|
|
37
|
+
if_absent is true, which accepts it as the result (like mkdir -p). To replace a document, PUT it."""
|
|
30
38
|
|
|
31
39
|
@classmethod
|
|
32
40
|
def inputSchema(cls) -> dict:
|
|
@@ -40,13 +48,17 @@ class CreateItem(PUT):
|
|
|
40
48
|
"title": {"type": "string", "description": "The title of the item"},
|
|
41
49
|
"slug": {
|
|
42
50
|
"type": "string",
|
|
43
|
-
"description": "URL path segment (optional). Defaults to a random UUID.
|
|
51
|
+
"description": "URL path segment (optional). Defaults to a random UUID.",
|
|
44
52
|
},
|
|
45
53
|
"description": {"type": "string", "description": "Description (optional)"},
|
|
46
54
|
"primary_topic": {
|
|
47
55
|
"type": "string",
|
|
48
56
|
"description": "URI reference of what the item is about, resolved against its URI (optional), e.g. #this.",
|
|
49
57
|
},
|
|
58
|
+
"if_absent": {
|
|
59
|
+
"type": "boolean",
|
|
60
|
+
"description": "Optional: whether a document already at the URI counts as success, left as it is (like mkdir -p). Without it the operation fails there.",
|
|
61
|
+
},
|
|
50
62
|
},
|
|
51
63
|
"required": ["container", "title"],
|
|
52
64
|
}
|
|
@@ -58,6 +70,7 @@ class CreateItem(PUT):
|
|
|
58
70
|
slug: Optional[Literal] = None,
|
|
59
71
|
description: Optional[Literal] = None,
|
|
60
72
|
primary_topic: Optional[Literal] = None,
|
|
73
|
+
if_absent: Optional[Literal] = None,
|
|
61
74
|
) -> Result:
|
|
62
75
|
"""Pure function: create item with RDFLib terms"""
|
|
63
76
|
if not isinstance(container_uri, URIRef):
|
|
@@ -82,6 +95,8 @@ class CreateItem(PUT):
|
|
|
82
95
|
f"CreateItem.execute expects primary_topic to be Literal or None, got {type(primary_topic)}"
|
|
83
96
|
)
|
|
84
97
|
|
|
98
|
+
if_absent = if_absent_flag(if_absent, "CreateItem.execute")
|
|
99
|
+
|
|
85
100
|
# URL-encode slug, or mint a fresh one - a lowercase UUID, as the LinkedDataHub CLI does - so that
|
|
86
101
|
# leaving it out never lands on an existing document; deriving it from the title did, on a repeated title
|
|
87
102
|
slug_str = urllib.parse.quote(str(slug) if slug else str(uuid.uuid4()), safe="")
|
|
@@ -118,8 +133,7 @@ class CreateItem(PUT):
|
|
|
118
133
|
# Convert the document to a Graph and call parent PUT operation
|
|
119
134
|
graph = self.to_graph(data, base=url)
|
|
120
135
|
|
|
121
|
-
|
|
122
|
-
return super().execute(URIRef(url), graph)
|
|
136
|
+
return create(self, URIRef(url), graph, if_absent)
|
|
123
137
|
|
|
124
138
|
def execute_json(self, arguments: dict, variable_stack: list = None) -> Result:
|
|
125
139
|
"""JSON execution: process arguments with strict type checking"""
|
|
@@ -180,7 +194,14 @@ class CreateItem(PUT):
|
|
|
180
194
|
f"ldh-CreateItem operation expects 'primary_topic' to be Literal, got {type(primary_topic)}"
|
|
181
195
|
)
|
|
182
196
|
|
|
183
|
-
|
|
197
|
+
# Process optional if_absent
|
|
198
|
+
if_absent = None
|
|
199
|
+
if "if_absent" in arguments:
|
|
200
|
+
if_absent = Operation.json_to_rdflib(Operation.process_json(
|
|
201
|
+
self.settings, arguments["if_absent"], self.context, variable_stack
|
|
202
|
+
))
|
|
203
|
+
|
|
204
|
+
return self.execute(container_uri, title, slug, description, primary_topic, if_absent)
|
|
184
205
|
|
|
185
206
|
def mcp_run(self, arguments: dict, context: Any = None) -> Any:
|
|
186
207
|
"""MCP execution: plain args → plain results"""
|
|
@@ -202,12 +223,16 @@ class CreateItem(PUT):
|
|
|
202
223
|
if "primary_topic" in arguments
|
|
203
224
|
else None
|
|
204
225
|
)
|
|
205
|
-
|
|
226
|
+
if_absent = Literal(bool(arguments["if_absent"])) if "if_absent" in arguments else None
|
|
227
|
+
result = self.execute(container_uri, title, slug, description, primary_topic, if_absent)
|
|
206
228
|
|
|
207
|
-
# Extract URL from PUT result
|
|
229
|
+
# Extract URL from PUT result; a 412 is the existing document if_absent accepted
|
|
208
230
|
url_binding = result.bindings[0]["url"]
|
|
231
|
+
created = int(result.bindings[0]["status"]) != 412
|
|
209
232
|
return [
|
|
210
233
|
types.TextContent(
|
|
211
|
-
type="text",
|
|
234
|
+
type="text",
|
|
235
|
+
text=f"Item created successfully at: {url_binding}" if created
|
|
236
|
+
else f"A document already exists at {url_binding}; left as it is",
|
|
212
237
|
)
|
|
213
238
|
]
|
|
@@ -560,9 +560,9 @@ The classes, datatype properties and object properties of the data at a SPARQL e
|
|
|
560
560
|
|
|
561
561
|
These write to a LinkedDataHub instance. Every write returns the one-row `status`/`url` result. A document's **content** (what its page shows) is its sequence of blocks: only `ldh-AddXHTMLBlock` and `ldh-AddObjectBlock` add to it. The other `ldh-Add*` operations write a resource *into* the document (a query, a view, a chart, a service, a file) without showing it; to show it, place it with `ldh-AddObjectBlock`, its URI as `value` — so give it a `fragment`.
|
|
562
562
|
|
|
563
|
-
## ldh-CreateContainer(parent: URI, title: Literal, slug?: Literal, description?: Literal, children_view?: Literal, primary_topic?: Literal) → Result
|
|
563
|
+
## ldh-CreateContainer(parent: URI, title: Literal, slug?: Literal, description?: Literal, children_view?: Literal, primary_topic?: Literal, if_absent?: Literal) → Result
|
|
564
564
|
|
|
565
|
-
Creates a container document at `parent` + `slug` + `/` (without `slug`, a random UUID), showing its children.
|
|
565
|
+
Creates a container document at `parent` + `slug` + `/` (without `slug`, a random UUID), showing its children. It only creates: a document that already exists at that URL is left as it is and the operation fails, unless `if_absent` is `true` (a JSON boolean), which takes the existing document as the result and writes nothing, like `mkdir -p`. Pass it when the program means "make sure it exists"; to replace a document, `PUT` it. Its first content block lists its children through the shared `ldh:ChildrenView`, unless `children_view` is `false` — a JSON boolean, never the string `"false"`. To list them in another layout, pass `false`, add a view of `https://w3id.org/atomgraph/linkeddatahub#SelectChildren` in that mode to the container with `ldh-AddView` (with a `fragment`), and place it with `ldh-AddObjectBlock`. `primary_topic` is what the container is about, a URI reference resolved against its URL (`#this` for a resource described in it).
|
|
566
566
|
|
|
567
567
|
### Example JSON
|
|
568
568
|
```json
|
|
@@ -578,9 +578,9 @@ A container whose children are listed in another layout, here on a map:
|
|
|
578
578
|
]
|
|
579
579
|
```
|
|
580
580
|
|
|
581
|
-
## ldh-CreateItem(container: URI, title: Literal, slug?: Literal, description?: Literal, primary_topic?: Literal) → Result
|
|
581
|
+
## ldh-CreateItem(container: URI, title: Literal, slug?: Literal, description?: Literal, primary_topic?: Literal, if_absent?: Literal) → Result
|
|
582
582
|
|
|
583
|
-
Creates an item document at `container` + `slug` + `/` (without `slug`, a random UUID).
|
|
583
|
+
Creates an item document at `container` + `slug` + `/` (without `slug`, a random UUID). It only creates: a document that already exists at that URL is left as it is and the operation fails, unless `if_absent` is `true`, which takes the existing document as the result and writes nothing. To replace a document, `PUT` it. `primary_topic` is what the item is about, a URI reference resolved against its URL (`#this` for a resource described in it).
|
|
584
584
|
|
|
585
585
|
### Example JSON
|
|
586
586
|
```json
|
|
@@ -52,6 +52,7 @@ src/web_algebra/operations/linkeddatahub/add_generic_service.py
|
|
|
52
52
|
src/web_algebra/operations/linkeddatahub/add_result_set_chart.py
|
|
53
53
|
src/web_algebra/operations/linkeddatahub/add_select.py
|
|
54
54
|
src/web_algebra/operations/linkeddatahub/add_view.py
|
|
55
|
+
src/web_algebra/operations/linkeddatahub/create.py
|
|
55
56
|
src/web_algebra/operations/linkeddatahub/create_container.py
|
|
56
57
|
src/web_algebra/operations/linkeddatahub/create_item.py
|
|
57
58
|
src/web_algebra/operations/linkeddatahub/fragments.py
|
|
@@ -37,14 +37,14 @@ def test_the_worst_of_several_kinds_is_the_most_destructive():
|
|
|
37
37
|
assert sorted([DESTRUCTIVE, READ, WRITE]) == [READ, WRITE, DESTRUCTIVE]
|
|
38
38
|
|
|
39
39
|
|
|
40
|
-
def
|
|
40
|
+
def test_an_upsert_is_destructive_but_a_create_is_not():
|
|
41
41
|
# PUT replaces the whole document at a URL that already holds one;
|
|
42
|
-
# ldh-CreateContainer and ldh-CreateItem PUT
|
|
43
|
-
#
|
|
42
|
+
# ldh-CreateContainer and ldh-CreateItem PUT with If-None-Match: *, so on
|
|
43
|
+
# an existing URL they are refused and remove nothing
|
|
44
44
|
from web_algebra.operations.linked_data.put import PUT
|
|
45
45
|
from web_algebra.operations.linkeddatahub.create_container import CreateContainer
|
|
46
46
|
from web_algebra.operations.linkeddatahub.create_item import CreateItem
|
|
47
47
|
|
|
48
48
|
assert PUT.kind is DESTRUCTIVE
|
|
49
|
-
assert CreateContainer.kind is
|
|
50
|
-
assert CreateItem.kind is
|
|
49
|
+
assert CreateContainer.kind is WRITE
|
|
50
|
+
assert CreateItem.kind is WRITE
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{web_algebra-2.0.2 → web_algebra-2.0.3}/src/web_algebra/operations/linkeddatahub/__init__.py
RENAMED
|
File without changes
|
{web_algebra-2.0.2 → web_algebra-2.0.3}/src/web_algebra/operations/linkeddatahub/add_construct.py
RENAMED
|
File without changes
|
{web_algebra-2.0.2 → web_algebra-2.0.3}/src/web_algebra/operations/linkeddatahub/add_file.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{web_algebra-2.0.2 → web_algebra-2.0.3}/src/web_algebra/operations/linkeddatahub/add_select.py
RENAMED
|
File without changes
|
{web_algebra-2.0.2 → web_algebra-2.0.3}/src/web_algebra/operations/linkeddatahub/add_view.py
RENAMED
|
File without changes
|
{web_algebra-2.0.2 → web_algebra-2.0.3}/src/web_algebra/operations/linkeddatahub/content/__init__.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{web_algebra-2.0.2 → web_algebra-2.0.3}/src/web_algebra/operations/linkeddatahub/fragments.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{web_algebra-2.0.2 → web_algebra-2.0.3}/src/web_algebra/operations/schema/extract_classes.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{web_algebra-2.0.2 → web_algebra-2.0.3}/src/web_algebra/operations/schema/extract_ontology.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|