diff --git a/README-OGC-Features.md b/README-OGC-Features.md
new file mode 100644
index 00000000..59356108
--- /dev/null
+++ b/README-OGC-Features.md
@@ -0,0 +1,89 @@
+Prez provides an OGC Features compliant API
+
+The API is mounted as a sub application at `"/catalogs/{catalogId}/collections/{recordsCollectionId}/features"` by default.
+It can be mounted at a different path by setting the configuration setting `ogc_features_mount_path` (or corresponding upper cased environment variable).
+
+Queryables are a part of the OGC Features specifications which provide a listing of which parameters can be queried.
+The queryables are a flat set of properties on features.
+
+Because Prez consumes an RDF Knowledge Graph, it is desirable to query more than top level properties.
+To achieve this, Prez provides a mechanism to declare paths through the graph as queryables.
+To declare these paths, you can use SHACL.
+
+An example is provided below:
+```
+@prefix cql: .
+@prefix dcterms: .
+@prefix dwc: .
+@prefix ex: .
+@prefix sh: .
+@prefix sname: .
+@prefix sosa: .
+@prefix xsd: .
+
+ex:BDRScientificNameQueryableShape
+ a sh:PropertyShape ;
+ a cql:Queryable ;
+ sh:path (
+ [ sh:inversePath sosa:hasFeatureOfInterest ]
+ sosa:hasMember
+ sosa:hasResult
+ dwc:scientificNameID
+ ) ;
+ sh:name "Scientific Name" ;
+ dcterms:identifier "scientificname" ;
+ sh:datatype xsd:string ;
+ sh:in (
+ sname:001
+ sname:002
+) ;
+.
+```
+It is recommended that templated SPARQL queries are used to periodically update the `sh:in` values, which correspond to enumerations.
+# TODO other SHACL predicates can be reused to specify min/max values, etc. where the range is numeric and enumerations are not appropriate.
+
+When Prez starts, it will query the remote repository (typically a triplestore) for all Queryables.
+It queries for them using a CONSTRUCT query, serializes this as JSON-LD, and does a minimal transformation to produce the OGC Features compliant response.
+The query is:
+```
+"""
+ PREFIX cql:
+ PREFIX dcterms:
+ PREFIX sh:
+ PREFIX rdf:
+ CONSTRUCT {
+ ?queryable cql:id ?id ;
+ cql:name ?title ;
+ cql:datatype ?type ;
+ cql:enum ?enums .
+ }
+ WHERE {?queryable a cql:Queryable ;
+ dcterms:identifier ?id ;
+ sh:name ?title ;
+ sh:datatype ?type ;
+ sh:in/rdf:rest*/rdf:first ?enums ;
+ }
+ """
+```
+And the output after transformation is of the form (which is the format required for OGC Features):
+```
+{
+ "$schema": "https://json-schema.org/draft/2019-09/schema",
+ "$id": "http://localhost:8000/catalogs/dtst:bdr/collections/syn:68a782a8-d7fe-4b3e-8377-c76c9cc245cc/features/queryables",
+ "type": "object",
+ "title": "Global Queryables",
+ "description": "Global queryable properties for all collections in the OGC Features API.",
+ "properties": {
+ "scientificname": {
+ "title": "Scientific Name",
+ "type": "string",
+ "enum": [
+ "https://fake-scientific-name-id.com/name/afd/001",
+ "https://fake-scientific-name-id.com/name/afd/002",
+ ]
+ }
+ }
+}
+```
+
+Separately, Prez internally translates the declared SHACL Property Path expression into SPARQL and injects this into queries when the queryable, e.g. `scientificname`, in the example above, is requested.
\ No newline at end of file
diff --git a/poetry.lock b/poetry.lock
index 0944b871..7d2b2913 100644
--- a/poetry.lock
+++ b/poetry.lock
@@ -2,13 +2,13 @@
[[package]]
name = "aiocache"
-version = "0.12.2"
+version = "0.12.3"
description = "multi backend asyncio cache"
optional = false
python-versions = "*"
files = [
- {file = "aiocache-0.12.2-py2.py3-none-any.whl", hash = "sha256:9b6fa30634ab0bfc3ecc44928a91ff07c6ea16d27d55469636b296ebc6eb5918"},
- {file = "aiocache-0.12.2.tar.gz", hash = "sha256:b41c9a145b050a5dcbae1599f847db6dd445193b1f3bd172d8e0fe0cb9e96684"},
+ {file = "aiocache-0.12.3-py2.py3-none-any.whl", hash = "sha256:889086fc24710f431937b87ad3720a289f7fc31c4fd8b68e9f918b9bacd8270d"},
+ {file = "aiocache-0.12.3.tar.gz", hash = "sha256:f528b27bf4d436b497a1d0d1a8f59a542c153ab1e37c3621713cb376d44c4713"},
]
[package.extras]
@@ -29,13 +29,13 @@ files = [
[[package]]
name = "anyio"
-version = "4.4.0"
+version = "4.6.0"
description = "High level compatibility layer for multiple asynchronous event loop implementations"
optional = false
-python-versions = ">=3.8"
+python-versions = ">=3.9"
files = [
- {file = "anyio-4.4.0-py3-none-any.whl", hash = "sha256:c1b2d8f46a8a812513012e1107cb0e68c17159a7a594208005a57dc776e1bdc7"},
- {file = "anyio-4.4.0.tar.gz", hash = "sha256:5aadc6a1bbb7cdb0bede386cac5e2940f5e2ff3aa20277e991cf028e0585ce94"},
+ {file = "anyio-4.6.0-py3-none-any.whl", hash = "sha256:c7d2e9d63e31599eeb636c8c5c03a7e108d73b345f064f1c19fdc87b79036a9a"},
+ {file = "anyio-4.6.0.tar.gz", hash = "sha256:137b4559cbb034c477165047febb6ff83f390fc3b20bf181c1fc0a728cb8beeb"},
]
[package.dependencies]
@@ -43,9 +43,9 @@ idna = ">=2.8"
sniffio = ">=1.1"
[package.extras]
-doc = ["Sphinx (>=7)", "packaging", "sphinx-autodoc-typehints (>=1.2.0)", "sphinx-rtd-theme"]
-test = ["anyio[trio]", "coverage[toml] (>=7)", "exceptiongroup (>=1.2.0)", "hypothesis (>=4.0)", "psutil (>=5.9)", "pytest (>=7.0)", "pytest-mock (>=3.6.1)", "trustme", "uvloop (>=0.17)"]
-trio = ["trio (>=0.23)"]
+doc = ["Sphinx (>=7.4,<8.0)", "packaging", "sphinx-autodoc-typehints (>=1.2.0)", "sphinx-rtd-theme"]
+test = ["anyio[trio]", "coverage[toml] (>=7)", "exceptiongroup (>=1.2.0)", "hypothesis (>=4.0)", "psutil (>=5.9)", "pytest (>=7.0)", "pytest-mock (>=3.6.1)", "trustme", "uvloop (>=0.21.0b1)"]
+trio = ["trio (>=0.26.1)"]
[[package]]
name = "attrs"
@@ -256,17 +256,6 @@ files = [
[package.dependencies]
colorama = {version = "*", markers = "platform_system == \"Windows\""}
-[[package]]
-name = "cloudpickle"
-version = "3.0.0"
-description = "Pickler class to extend the standard pickle.Pickler functionality"
-optional = false
-python-versions = ">=3.8"
-files = [
- {file = "cloudpickle-3.0.0-py3-none-any.whl", hash = "sha256:246ee7d0c295602a036e86369c77fecda4ab17b506496730f2f576d9016fd9c7"},
- {file = "cloudpickle-3.0.0.tar.gz", hash = "sha256:996d9a482c6fb4f33c1a35335cf8afd065d2a56e973270364840712d9131a882"},
-]
-
[[package]]
name = "colorama"
version = "0.4.6"
@@ -375,13 +364,13 @@ files = [
[[package]]
name = "fastapi"
-version = "0.114.1"
+version = "0.114.2"
description = "FastAPI framework, high performance, easy to learn, fast to code, ready for production"
optional = false
python-versions = ">=3.8"
files = [
- {file = "fastapi-0.114.1-py3-none-any.whl", hash = "sha256:5d4746f6e4b7dff0b4f6b6c6d5445645285f662fe75886e99af7ee2d6b58bb3e"},
- {file = "fastapi-0.114.1.tar.gz", hash = "sha256:1d7bbbeabbaae0acb0c22f0ab0b040f642d3093ca3645f8c876b6f91391861d8"},
+ {file = "fastapi-0.114.2-py3-none-any.whl", hash = "sha256:44474a22913057b1acb973ab90f4b671ba5200482e7622816d79105dcece1ac5"},
+ {file = "fastapi-0.114.2.tar.gz", hash = "sha256:0adb148b62edb09e8c6eeefa3ea934e8f276dabc038c5a82989ea6346050c3da"},
]
[package.dependencies]
@@ -395,18 +384,18 @@ standard = ["email-validator (>=2.0.0)", "fastapi-cli[standard] (>=0.0.5)", "htt
[[package]]
name = "filelock"
-version = "3.16.0"
+version = "3.16.1"
description = "A platform independent file lock."
optional = false
python-versions = ">=3.8"
files = [
- {file = "filelock-3.16.0-py3-none-any.whl", hash = "sha256:f6ed4c963184f4c84dd5557ce8fece759a3724b37b80c6c4f20a2f63a4dc6609"},
- {file = "filelock-3.16.0.tar.gz", hash = "sha256:81de9eb8453c769b63369f87f11131a7ab04e367f8d97ad39dc230daa07e3bec"},
+ {file = "filelock-3.16.1-py3-none-any.whl", hash = "sha256:2082e5703d51fbf98ea75855d9d5527e33d8ff23099bec374a134febee6946b0"},
+ {file = "filelock-3.16.1.tar.gz", hash = "sha256:c249fbfcd5db47e5e2d6d62198e565475ee65e4831e2561c8e313fa7eb961435"},
]
[package.extras]
-docs = ["furo (>=2024.8.6)", "sphinx (>=8.0.2)", "sphinx-autodoc-typehints (>=2.4)"]
-testing = ["covdefaults (>=2.3)", "coverage (>=7.6.1)", "diff-cover (>=9.1.1)", "pytest (>=8.3.2)", "pytest-asyncio (>=0.24)", "pytest-cov (>=5)", "pytest-mock (>=3.14)", "pytest-timeout (>=2.3.1)", "virtualenv (>=20.26.3)"]
+docs = ["furo (>=2024.8.6)", "sphinx (>=8.0.2)", "sphinx-autodoc-typehints (>=2.4.1)"]
+testing = ["covdefaults (>=2.3)", "coverage (>=7.6.1)", "diff-cover (>=9.2)", "pytest (>=8.3.3)", "pytest-asyncio (>=0.24)", "pytest-cov (>=5)", "pytest-mock (>=3.14)", "pytest-timeout (>=2.3.1)", "virtualenv (>=20.26.4)"]
typing = ["typing-extensions (>=4.12.2)"]
[[package]]
@@ -565,13 +554,13 @@ zstd = ["zstandard (>=0.18.0)"]
[[package]]
name = "identify"
-version = "2.6.0"
+version = "2.6.1"
description = "File identification library for Python"
optional = false
python-versions = ">=3.8"
files = [
- {file = "identify-2.6.0-py2.py3-none-any.whl", hash = "sha256:e79ae4406387a9d300332b5fd366d8994f1525e8414984e1a59e058b2eda2dd0"},
- {file = "identify-2.6.0.tar.gz", hash = "sha256:cb171c685bdc31bcc4c1734698736a7d5b6c8bf2e0c15117f4d469c8640ae5cf"},
+ {file = "identify-2.6.1-py2.py3-none-any.whl", hash = "sha256:53863bcac7caf8d2ed85bd20312ea5dcfc22226800f6d6881f232d861db5a8f0"},
+ {file = "identify-2.6.1.tar.gz", hash = "sha256:91478c5fb7c3aac5ff7bf9b4344f803843dc586832d5f110d672b19aa1984c98"},
]
[package.extras]
@@ -579,15 +568,18 @@ license = ["ukkonen"]
[[package]]
name = "idna"
-version = "3.8"
+version = "3.10"
description = "Internationalized Domain Names in Applications (IDNA)"
optional = false
python-versions = ">=3.6"
files = [
- {file = "idna-3.8-py3-none-any.whl", hash = "sha256:050b4e5baadcd44d760cedbd2b8e639f2ff89bbc7a5730fcc662954303377aac"},
- {file = "idna-3.8.tar.gz", hash = "sha256:d838c2c0ed6fced7693d5e8ab8e734d5f8fda53a039c0164afb0b82e771e3603"},
+ {file = "idna-3.10-py3-none-any.whl", hash = "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3"},
+ {file = "idna-3.10.tar.gz", hash = "sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9"},
]
+[package.extras]
+all = ["flake8 (>=7.1.1)", "mypy (>=1.11.2)", "pytest (>=8.3.2)", "ruff (>=0.6.2)"]
+
[[package]]
name = "importlib-metadata"
version = "8.5.0"
@@ -842,30 +834,6 @@ html5 = ["html5lib"]
htmlsoup = ["BeautifulSoup4"]
source = ["Cython (>=3.0.11)"]
-[[package]]
-name = "markdown-it-py"
-version = "3.0.0"
-description = "Python port of markdown-it. Markdown parsing, done right!"
-optional = false
-python-versions = ">=3.8"
-files = [
- {file = "markdown-it-py-3.0.0.tar.gz", hash = "sha256:e3f60a94fa066dc52ec76661e37c851cb232d92f9886b15cb560aaada2df8feb"},
- {file = "markdown_it_py-3.0.0-py3-none-any.whl", hash = "sha256:355216845c60bd96232cd8d8c40e8f9765cc86f46880e43a8fd22dc1a1a8cab1"},
-]
-
-[package.dependencies]
-mdurl = ">=0.1,<1.0"
-
-[package.extras]
-benchmarking = ["psutil", "pytest", "pytest-benchmark"]
-code-style = ["pre-commit (>=3.0,<4.0)"]
-compare = ["commonmark (>=0.9,<1.0)", "markdown (>=3.4,<4.0)", "mistletoe (>=1.0,<2.0)", "mistune (>=2.0,<3.0)", "panflute (>=2.3,<3.0)"]
-linkify = ["linkify-it-py (>=1,<3)"]
-plugins = ["mdit-py-plugins"]
-profiling = ["gprof2dot"]
-rtd = ["jupyter_sphinx", "mdit-py-plugins", "myst-parser", "pyyaml", "sphinx", "sphinx-copybutton", "sphinx-design", "sphinx_book_theme"]
-testing = ["coverage", "pytest", "pytest-cov", "pytest-regressions"]
-
[[package]]
name = "markupsafe"
version = "2.1.5"
@@ -935,17 +903,6 @@ files = [
{file = "MarkupSafe-2.1.5.tar.gz", hash = "sha256:d283d37a890ba4c1ae73ffadf8046435c76e7bc2247bbb63c00bd1a709c6544b"},
]
-[[package]]
-name = "mdurl"
-version = "0.1.2"
-description = "Markdown URL utilities"
-optional = false
-python-versions = ">=3.7"
-files = [
- {file = "mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8"},
- {file = "mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba"},
-]
-
[[package]]
name = "mypy-extensions"
version = "1.0.0"
@@ -1108,46 +1065,59 @@ files = [
[[package]]
name = "pandas"
-version = "2.2.2"
+version = "2.2.3"
description = "Powerful data structures for data analysis, time series, and statistics"
optional = false
python-versions = ">=3.9"
files = [
- {file = "pandas-2.2.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:90c6fca2acf139569e74e8781709dccb6fe25940488755716d1d354d6bc58bce"},
- {file = "pandas-2.2.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:c7adfc142dac335d8c1e0dcbd37eb8617eac386596eb9e1a1b77791cf2498238"},
- {file = "pandas-2.2.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4abfe0be0d7221be4f12552995e58723c7422c80a659da13ca382697de830c08"},
- {file = "pandas-2.2.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8635c16bf3d99040fdf3ca3db669a7250ddf49c55dc4aa8fe0ae0fa8d6dcc1f0"},
- {file = "pandas-2.2.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:40ae1dffb3967a52203105a077415a86044a2bea011b5f321c6aa64b379a3f51"},
- {file = "pandas-2.2.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:8e5a0b00e1e56a842f922e7fae8ae4077aee4af0acb5ae3622bd4b4c30aedf99"},
- {file = "pandas-2.2.2-cp310-cp310-win_amd64.whl", hash = "sha256:ddf818e4e6c7c6f4f7c8a12709696d193976b591cc7dc50588d3d1a6b5dc8772"},
- {file = "pandas-2.2.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:696039430f7a562b74fa45f540aca068ea85fa34c244d0deee539cb6d70aa288"},
- {file = "pandas-2.2.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:8e90497254aacacbc4ea6ae5e7a8cd75629d6ad2b30025a4a8b09aa4faf55151"},
- {file = "pandas-2.2.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:58b84b91b0b9f4bafac2a0ac55002280c094dfc6402402332c0913a59654ab2b"},
- {file = "pandas-2.2.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6d2123dc9ad6a814bcdea0f099885276b31b24f7edf40f6cdbc0912672e22eee"},
- {file = "pandas-2.2.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:2925720037f06e89af896c70bca73459d7e6a4be96f9de79e2d440bd499fe0db"},
- {file = "pandas-2.2.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:0cace394b6ea70c01ca1595f839cf193df35d1575986e484ad35c4aeae7266c1"},
- {file = "pandas-2.2.2-cp311-cp311-win_amd64.whl", hash = "sha256:873d13d177501a28b2756375d59816c365e42ed8417b41665f346289adc68d24"},
- {file = "pandas-2.2.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:9dfde2a0ddef507a631dc9dc4af6a9489d5e2e740e226ad426a05cabfbd7c8ef"},
- {file = "pandas-2.2.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:e9b79011ff7a0f4b1d6da6a61aa1aa604fb312d6647de5bad20013682d1429ce"},
- {file = "pandas-2.2.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1cb51fe389360f3b5a4d57dbd2848a5f033350336ca3b340d1c53a1fad33bcad"},
- {file = "pandas-2.2.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:eee3a87076c0756de40b05c5e9a6069c035ba43e8dd71c379e68cab2c20f16ad"},
- {file = "pandas-2.2.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:3e374f59e440d4ab45ca2fffde54b81ac3834cf5ae2cdfa69c90bc03bde04d76"},
- {file = "pandas-2.2.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:43498c0bdb43d55cb162cdc8c06fac328ccb5d2eabe3cadeb3529ae6f0517c32"},
- {file = "pandas-2.2.2-cp312-cp312-win_amd64.whl", hash = "sha256:d187d355ecec3629624fccb01d104da7d7f391db0311145817525281e2804d23"},
- {file = "pandas-2.2.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:0ca6377b8fca51815f382bd0b697a0814c8bda55115678cbc94c30aacbb6eff2"},
- {file = "pandas-2.2.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:9057e6aa78a584bc93a13f0a9bf7e753a5e9770a30b4d758b8d5f2a62a9433cd"},
- {file = "pandas-2.2.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:001910ad31abc7bf06f49dcc903755d2f7f3a9186c0c040b827e522e9cef0863"},
- {file = "pandas-2.2.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:66b479b0bd07204e37583c191535505410daa8df638fd8e75ae1b383851fe921"},
- {file = "pandas-2.2.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:a77e9d1c386196879aa5eb712e77461aaee433e54c68cf253053a73b7e49c33a"},
- {file = "pandas-2.2.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:92fd6b027924a7e178ac202cfbe25e53368db90d56872d20ffae94b96c7acc57"},
- {file = "pandas-2.2.2-cp39-cp39-win_amd64.whl", hash = "sha256:640cef9aa381b60e296db324337a554aeeb883ead99dc8f6c18e81a93942f5f4"},
- {file = "pandas-2.2.2.tar.gz", hash = "sha256:9e79019aba43cb4fda9e4d983f8e88ca0373adbb697ae9c6c43093218de28b54"},
+ {file = "pandas-2.2.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:1948ddde24197a0f7add2bdc4ca83bf2b1ef84a1bc8ccffd95eda17fd836ecb5"},
+ {file = "pandas-2.2.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:381175499d3802cde0eabbaf6324cce0c4f5d52ca6f8c377c29ad442f50f6348"},
+ {file = "pandas-2.2.3-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:d9c45366def9a3dd85a6454c0e7908f2b3b8e9c138f5dc38fed7ce720d8453ed"},
+ {file = "pandas-2.2.3-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:86976a1c5b25ae3f8ccae3a5306e443569ee3c3faf444dfd0f41cda24667ad57"},
+ {file = "pandas-2.2.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:b8661b0238a69d7aafe156b7fa86c44b881387509653fdf857bebc5e4008ad42"},
+ {file = "pandas-2.2.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:37e0aced3e8f539eccf2e099f65cdb9c8aa85109b0be6e93e2baff94264bdc6f"},
+ {file = "pandas-2.2.3-cp310-cp310-win_amd64.whl", hash = "sha256:56534ce0746a58afaf7942ba4863e0ef81c9c50d3f0ae93e9497d6a41a057645"},
+ {file = "pandas-2.2.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:66108071e1b935240e74525006034333f98bcdb87ea116de573a6a0dccb6c039"},
+ {file = "pandas-2.2.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:7c2875855b0ff77b2a64a0365e24455d9990730d6431b9e0ee18ad8acee13dbd"},
+ {file = "pandas-2.2.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:cd8d0c3be0515c12fed0bdbae072551c8b54b7192c7b1fda0ba56059a0179698"},
+ {file = "pandas-2.2.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c124333816c3a9b03fbeef3a9f230ba9a737e9e5bb4060aa2107a86cc0a497fc"},
+ {file = "pandas-2.2.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:63cc132e40a2e084cf01adf0775b15ac515ba905d7dcca47e9a251819c575ef3"},
+ {file = "pandas-2.2.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:29401dbfa9ad77319367d36940cd8a0b3a11aba16063e39632d98b0e931ddf32"},
+ {file = "pandas-2.2.3-cp311-cp311-win_amd64.whl", hash = "sha256:3fc6873a41186404dad67245896a6e440baacc92f5b716ccd1bc9ed2995ab2c5"},
+ {file = "pandas-2.2.3-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:b1d432e8d08679a40e2a6d8b2f9770a5c21793a6f9f47fdd52c5ce1948a5a8a9"},
+ {file = "pandas-2.2.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:a5a1595fe639f5988ba6a8e5bc9649af3baf26df3998a0abe56c02609392e0a4"},
+ {file = "pandas-2.2.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:5de54125a92bb4d1c051c0659e6fcb75256bf799a732a87184e5ea503965bce3"},
+ {file = "pandas-2.2.3-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fffb8ae78d8af97f849404f21411c95062db1496aeb3e56f146f0355c9989319"},
+ {file = "pandas-2.2.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:6dfcb5ee8d4d50c06a51c2fffa6cff6272098ad6540aed1a76d15fb9318194d8"},
+ {file = "pandas-2.2.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:062309c1b9ea12a50e8ce661145c6aab431b1e99530d3cd60640e255778bd43a"},
+ {file = "pandas-2.2.3-cp312-cp312-win_amd64.whl", hash = "sha256:59ef3764d0fe818125a5097d2ae867ca3fa64df032331b7e0917cf5d7bf66b13"},
+ {file = "pandas-2.2.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:f00d1345d84d8c86a63e476bb4955e46458b304b9575dcf71102b5c705320015"},
+ {file = "pandas-2.2.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:3508d914817e153ad359d7e069d752cdd736a247c322d932eb89e6bc84217f28"},
+ {file = "pandas-2.2.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:22a9d949bfc9a502d320aa04e5d02feab689d61da4e7764b62c30b991c42c5f0"},
+ {file = "pandas-2.2.3-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f3a255b2c19987fbbe62a9dfd6cff7ff2aa9ccab3fc75218fd4b7530f01efa24"},
+ {file = "pandas-2.2.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:800250ecdadb6d9c78eae4990da62743b857b470883fa27f652db8bdde7f6659"},
+ {file = "pandas-2.2.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6374c452ff3ec675a8f46fd9ab25c4ad0ba590b71cf0656f8b6daa5202bca3fb"},
+ {file = "pandas-2.2.3-cp313-cp313-win_amd64.whl", hash = "sha256:61c5ad4043f791b61dd4752191d9f07f0ae412515d59ba8f005832a532f8736d"},
+ {file = "pandas-2.2.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:3b71f27954685ee685317063bf13c7709a7ba74fc996b84fc6821c59b0f06468"},
+ {file = "pandas-2.2.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:38cf8125c40dae9d5acc10fa66af8ea6fdf760b2714ee482ca691fc66e6fcb18"},
+ {file = "pandas-2.2.3-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:ba96630bc17c875161df3818780af30e43be9b166ce51c9a18c1feae342906c2"},
+ {file = "pandas-2.2.3-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1db71525a1538b30142094edb9adc10be3f3e176748cd7acc2240c2f2e5aa3a4"},
+ {file = "pandas-2.2.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:15c0e1e02e93116177d29ff83e8b1619c93ddc9c49083f237d4312337a61165d"},
+ {file = "pandas-2.2.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:ad5b65698ab28ed8d7f18790a0dc58005c7629f227be9ecc1072aa74c0c1d43a"},
+ {file = "pandas-2.2.3-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:bc6b93f9b966093cb0fd62ff1a7e4c09e6d546ad7c1de191767baffc57628f39"},
+ {file = "pandas-2.2.3-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:5dbca4c1acd72e8eeef4753eeca07de9b1db4f398669d5994086f788a5d7cc30"},
+ {file = "pandas-2.2.3-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:8cd6d7cc958a3910f934ea8dbdf17b2364827bb4dafc38ce6eef6bb3d65ff09c"},
+ {file = "pandas-2.2.3-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:99df71520d25fade9db7c1076ac94eb994f4d2673ef2aa2e86ee039b6746d20c"},
+ {file = "pandas-2.2.3-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:31d0ced62d4ea3e231a9f228366919a5ea0b07440d9d4dac345376fd8e1477ea"},
+ {file = "pandas-2.2.3-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:7eee9e7cea6adf3e3d24e304ac6b8300646e2a5d1cd3a3c2abed9101b0846761"},
+ {file = "pandas-2.2.3-cp39-cp39-win_amd64.whl", hash = "sha256:4850ba03528b6dd51d6c5d273c46f183f39a9baf3f0143e566b89450965b105e"},
+ {file = "pandas-2.2.3.tar.gz", hash = "sha256:4f18ba62b61d7e192368b84517265a99b4d7ee8912f8708660fb4a366cc82667"},
]
[package.dependencies]
numpy = [
- {version = ">=1.26.0", markers = "python_version >= \"3.12\""},
{version = ">=1.23.2", markers = "python_version == \"3.11\""},
+ {version = ">=1.26.0", markers = "python_version >= \"3.12\""},
]
python-dateutil = ">=2.8.2"
pytz = ">=2020.1"
@@ -1191,13 +1161,13 @@ files = [
[[package]]
name = "platformdirs"
-version = "4.3.2"
+version = "4.3.6"
description = "A small Python package for determining appropriate platform-specific dirs, e.g. a `user data dir`."
optional = false
python-versions = ">=3.8"
files = [
- {file = "platformdirs-4.3.2-py3-none-any.whl", hash = "sha256:eb1c8582560b34ed4ba105009a4badf7f6f85768b30126f351328507b2beb617"},
- {file = "platformdirs-4.3.2.tar.gz", hash = "sha256:9e5e27a08aa095dd127b9f2e764d74254f482fef22b0970773bfba79d091ab8c"},
+ {file = "platformdirs-4.3.6-py3-none-any.whl", hash = "sha256:73e575e1408ab8103900836b97580d5307456908a03e92031bab39e4554cc3fb"},
+ {file = "platformdirs-4.3.6.tar.gz", hash = "sha256:357fb2acbc885b0419afd3ce3ed34564c13c9b95c89360cd9563f73aa5e2b907"},
]
[package.extras]
@@ -1308,21 +1278,21 @@ test = ["cffi", "hypothesis", "pandas", "pytest", "pytz"]
[[package]]
name = "pydantic"
-version = "2.9.1"
+version = "2.9.2"
description = "Data validation using Python type hints"
optional = false
python-versions = ">=3.8"
files = [
- {file = "pydantic-2.9.1-py3-none-any.whl", hash = "sha256:7aff4db5fdf3cf573d4b3c30926a510a10e19a0774d38fc4967f78beb6deb612"},
- {file = "pydantic-2.9.1.tar.gz", hash = "sha256:1363c7d975c7036df0db2b4a61f2e062fbc0aa5ab5f2772e0ffc7191a4f4bce2"},
+ {file = "pydantic-2.9.2-py3-none-any.whl", hash = "sha256:f048cec7b26778210e28a0459867920654d48e5e62db0958433636cde4254f12"},
+ {file = "pydantic-2.9.2.tar.gz", hash = "sha256:d155cef71265d1e9807ed1c32b4c8deec042a44a50a4188b25ac67ecd81a9c0f"},
]
[package.dependencies]
annotated-types = ">=0.6.0"
-pydantic-core = "2.23.3"
+pydantic-core = "2.23.4"
typing-extensions = [
- {version = ">=4.12.2", markers = "python_version >= \"3.13\""},
{version = ">=4.6.1", markers = "python_version < \"3.13\""},
+ {version = ">=4.12.2", markers = "python_version >= \"3.13\""},
]
[package.extras]
@@ -1331,100 +1301,100 @@ timezone = ["tzdata"]
[[package]]
name = "pydantic-core"
-version = "2.23.3"
+version = "2.23.4"
description = "Core functionality for Pydantic validation and serialization"
optional = false
python-versions = ">=3.8"
files = [
- {file = "pydantic_core-2.23.3-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:7f10a5d1b9281392f1bf507d16ac720e78285dfd635b05737c3911637601bae6"},
- {file = "pydantic_core-2.23.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:3c09a7885dd33ee8c65266e5aa7fb7e2f23d49d8043f089989726391dd7350c5"},
- {file = "pydantic_core-2.23.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6470b5a1ec4d1c2e9afe928c6cb37eb33381cab99292a708b8cb9aa89e62429b"},
- {file = "pydantic_core-2.23.3-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:9172d2088e27d9a185ea0a6c8cebe227a9139fd90295221d7d495944d2367700"},
- {file = "pydantic_core-2.23.3-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:86fc6c762ca7ac8fbbdff80d61b2c59fb6b7d144aa46e2d54d9e1b7b0e780e01"},
- {file = "pydantic_core-2.23.3-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f0cb80fd5c2df4898693aa841425ea1727b1b6d2167448253077d2a49003e0ed"},
- {file = "pydantic_core-2.23.3-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:03667cec5daf43ac4995cefa8aaf58f99de036204a37b889c24a80927b629cec"},
- {file = "pydantic_core-2.23.3-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:047531242f8e9c2db733599f1c612925de095e93c9cc0e599e96cf536aaf56ba"},
- {file = "pydantic_core-2.23.3-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:5499798317fff7f25dbef9347f4451b91ac2a4330c6669821c8202fd354c7bee"},
- {file = "pydantic_core-2.23.3-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:bbb5e45eab7624440516ee3722a3044b83fff4c0372efe183fd6ba678ff681fe"},
- {file = "pydantic_core-2.23.3-cp310-none-win32.whl", hash = "sha256:8b5b3ed73abb147704a6e9f556d8c5cb078f8c095be4588e669d315e0d11893b"},
- {file = "pydantic_core-2.23.3-cp310-none-win_amd64.whl", hash = "sha256:2b603cde285322758a0279995b5796d64b63060bfbe214b50a3ca23b5cee3e83"},
- {file = "pydantic_core-2.23.3-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:c889fd87e1f1bbeb877c2ee56b63bb297de4636661cc9bbfcf4b34e5e925bc27"},
- {file = "pydantic_core-2.23.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:ea85bda3189fb27503af4c45273735bcde3dd31c1ab17d11f37b04877859ef45"},
- {file = "pydantic_core-2.23.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a7f7f72f721223f33d3dc98a791666ebc6a91fa023ce63733709f4894a7dc611"},
- {file = "pydantic_core-2.23.3-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:2b2b55b0448e9da68f56b696f313949cda1039e8ec7b5d294285335b53104b61"},
- {file = "pydantic_core-2.23.3-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c24574c7e92e2c56379706b9a3f07c1e0c7f2f87a41b6ee86653100c4ce343e5"},
- {file = "pydantic_core-2.23.3-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f2b05e6ccbee333a8f4b8f4d7c244fdb7a979e90977ad9c51ea31261e2085ce0"},
- {file = "pydantic_core-2.23.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e2c409ce1c219c091e47cb03feb3c4ed8c2b8e004efc940da0166aaee8f9d6c8"},
- {file = "pydantic_core-2.23.3-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d965e8b325f443ed3196db890d85dfebbb09f7384486a77461347f4adb1fa7f8"},
- {file = "pydantic_core-2.23.3-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:f56af3a420fb1ffaf43ece3ea09c2d27c444e7c40dcb7c6e7cf57aae764f2b48"},
- {file = "pydantic_core-2.23.3-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:5b01a078dd4f9a52494370af21aa52964e0a96d4862ac64ff7cea06e0f12d2c5"},
- {file = "pydantic_core-2.23.3-cp311-none-win32.whl", hash = "sha256:560e32f0df04ac69b3dd818f71339983f6d1f70eb99d4d1f8e9705fb6c34a5c1"},
- {file = "pydantic_core-2.23.3-cp311-none-win_amd64.whl", hash = "sha256:c744fa100fdea0d000d8bcddee95213d2de2e95b9c12be083370b2072333a0fa"},
- {file = "pydantic_core-2.23.3-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:e0ec50663feedf64d21bad0809f5857bac1ce91deded203efc4a84b31b2e4305"},
- {file = "pydantic_core-2.23.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:db6e6afcb95edbe6b357786684b71008499836e91f2a4a1e55b840955b341dbb"},
- {file = "pydantic_core-2.23.3-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:98ccd69edcf49f0875d86942f4418a4e83eb3047f20eb897bffa62a5d419c8fa"},
- {file = "pydantic_core-2.23.3-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:a678c1ac5c5ec5685af0133262103defb427114e62eafeda12f1357a12140162"},
- {file = "pydantic_core-2.23.3-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:01491d8b4d8db9f3391d93b0df60701e644ff0894352947f31fff3e52bd5c801"},
- {file = "pydantic_core-2.23.3-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:fcf31facf2796a2d3b7fe338fe8640aa0166e4e55b4cb108dbfd1058049bf4cb"},
- {file = "pydantic_core-2.23.3-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7200fd561fb3be06827340da066df4311d0b6b8eb0c2116a110be5245dceb326"},
- {file = "pydantic_core-2.23.3-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:dc1636770a809dee2bd44dd74b89cc80eb41172bcad8af75dd0bc182c2666d4c"},
- {file = "pydantic_core-2.23.3-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:67a5def279309f2e23014b608c4150b0c2d323bd7bccd27ff07b001c12c2415c"},
- {file = "pydantic_core-2.23.3-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:748bdf985014c6dd3e1e4cc3db90f1c3ecc7246ff5a3cd4ddab20c768b2f1dab"},
- {file = "pydantic_core-2.23.3-cp312-none-win32.whl", hash = "sha256:255ec6dcb899c115f1e2a64bc9ebc24cc0e3ab097775755244f77360d1f3c06c"},
- {file = "pydantic_core-2.23.3-cp312-none-win_amd64.whl", hash = "sha256:40b8441be16c1e940abebed83cd006ddb9e3737a279e339dbd6d31578b802f7b"},
- {file = "pydantic_core-2.23.3-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:6daaf5b1ba1369a22c8b050b643250e3e5efc6a78366d323294aee54953a4d5f"},
- {file = "pydantic_core-2.23.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:d015e63b985a78a3d4ccffd3bdf22b7c20b3bbd4b8227809b3e8e75bc37f9cb2"},
- {file = "pydantic_core-2.23.3-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a3fc572d9b5b5cfe13f8e8a6e26271d5d13f80173724b738557a8c7f3a8a3791"},
- {file = "pydantic_core-2.23.3-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f6bd91345b5163ee7448bee201ed7dd601ca24f43f439109b0212e296eb5b423"},
- {file = "pydantic_core-2.23.3-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:fc379c73fd66606628b866f661e8785088afe2adaba78e6bbe80796baf708a63"},
- {file = "pydantic_core-2.23.3-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:fbdce4b47592f9e296e19ac31667daed8753c8367ebb34b9a9bd89dacaa299c9"},
- {file = "pydantic_core-2.23.3-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fc3cf31edf405a161a0adad83246568647c54404739b614b1ff43dad2b02e6d5"},
- {file = "pydantic_core-2.23.3-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:8e22b477bf90db71c156f89a55bfe4d25177b81fce4aa09294d9e805eec13855"},
- {file = "pydantic_core-2.23.3-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:0a0137ddf462575d9bce863c4c95bac3493ba8e22f8c28ca94634b4a1d3e2bb4"},
- {file = "pydantic_core-2.23.3-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:203171e48946c3164fe7691fc349c79241ff8f28306abd4cad5f4f75ed80bc8d"},
- {file = "pydantic_core-2.23.3-cp313-none-win32.whl", hash = "sha256:76bdab0de4acb3f119c2a4bff740e0c7dc2e6de7692774620f7452ce11ca76c8"},
- {file = "pydantic_core-2.23.3-cp313-none-win_amd64.whl", hash = "sha256:37ba321ac2a46100c578a92e9a6aa33afe9ec99ffa084424291d84e456f490c1"},
- {file = "pydantic_core-2.23.3-cp38-cp38-macosx_10_12_x86_64.whl", hash = "sha256:d063c6b9fed7d992bcbebfc9133f4c24b7a7f215d6b102f3e082b1117cddb72c"},
- {file = "pydantic_core-2.23.3-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:6cb968da9a0746a0cf521b2b5ef25fc5a0bee9b9a1a8214e0a1cfaea5be7e8a4"},
- {file = "pydantic_core-2.23.3-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:edbefe079a520c5984e30e1f1f29325054b59534729c25b874a16a5048028d16"},
- {file = "pydantic_core-2.23.3-cp38-cp38-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:cbaaf2ef20d282659093913da9d402108203f7cb5955020bd8d1ae5a2325d1c4"},
- {file = "pydantic_core-2.23.3-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:fb539d7e5dc4aac345846f290cf504d2fd3c1be26ac4e8b5e4c2b688069ff4cf"},
- {file = "pydantic_core-2.23.3-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7e6f33503c5495059148cc486867e1d24ca35df5fc064686e631e314d959ad5b"},
- {file = "pydantic_core-2.23.3-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:04b07490bc2f6f2717b10c3969e1b830f5720b632f8ae2f3b8b1542394c47a8e"},
- {file = "pydantic_core-2.23.3-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:03795b9e8a5d7fda05f3873efc3f59105e2dcff14231680296b87b80bb327295"},
- {file = "pydantic_core-2.23.3-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:c483dab0f14b8d3f0df0c6c18d70b21b086f74c87ab03c59250dbf6d3c89baba"},
- {file = "pydantic_core-2.23.3-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:8b2682038e255e94baf2c473dca914a7460069171ff5cdd4080be18ab8a7fd6e"},
- {file = "pydantic_core-2.23.3-cp38-none-win32.whl", hash = "sha256:f4a57db8966b3a1d1a350012839c6a0099f0898c56512dfade8a1fe5fb278710"},
- {file = "pydantic_core-2.23.3-cp38-none-win_amd64.whl", hash = "sha256:13dd45ba2561603681a2676ca56006d6dee94493f03d5cadc055d2055615c3ea"},
- {file = "pydantic_core-2.23.3-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:82da2f4703894134a9f000e24965df73cc103e31e8c31906cc1ee89fde72cbd8"},
- {file = "pydantic_core-2.23.3-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:dd9be0a42de08f4b58a3cc73a123f124f65c24698b95a54c1543065baca8cf0e"},
- {file = "pydantic_core-2.23.3-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:89b731f25c80830c76fdb13705c68fef6a2b6dc494402987c7ea9584fe189f5d"},
- {file = "pydantic_core-2.23.3-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:c6de1ec30c4bb94f3a69c9f5f2182baeda5b809f806676675e9ef6b8dc936f28"},
- {file = "pydantic_core-2.23.3-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:bb68b41c3fa64587412b104294b9cbb027509dc2f6958446c502638d481525ef"},
- {file = "pydantic_core-2.23.3-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1c3980f2843de5184656aab58698011b42763ccba11c4a8c35936c8dd6c7068c"},
- {file = "pydantic_core-2.23.3-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:94f85614f2cba13f62c3c6481716e4adeae48e1eaa7e8bac379b9d177d93947a"},
- {file = "pydantic_core-2.23.3-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:510b7fb0a86dc8f10a8bb43bd2f97beb63cffad1203071dc434dac26453955cd"},
- {file = "pydantic_core-2.23.3-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:1eba2f7ce3e30ee2170410e2171867ea73dbd692433b81a93758ab2de6c64835"},
- {file = "pydantic_core-2.23.3-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:4b259fd8409ab84b4041b7b3f24dcc41e4696f180b775961ca8142b5b21d0e70"},
- {file = "pydantic_core-2.23.3-cp39-none-win32.whl", hash = "sha256:40d9bd259538dba2f40963286009bf7caf18b5112b19d2b55b09c14dde6db6a7"},
- {file = "pydantic_core-2.23.3-cp39-none-win_amd64.whl", hash = "sha256:5a8cd3074a98ee70173a8633ad3c10e00dcb991ecec57263aacb4095c5efb958"},
- {file = "pydantic_core-2.23.3-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:f399e8657c67313476a121a6944311fab377085ca7f490648c9af97fc732732d"},
- {file = "pydantic_core-2.23.3-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:6b5547d098c76e1694ba85f05b595720d7c60d342f24d5aad32c3049131fa5c4"},
- {file = "pydantic_core-2.23.3-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0dda0290a6f608504882d9f7650975b4651ff91c85673341789a476b1159f211"},
- {file = "pydantic_core-2.23.3-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:65b6e5da855e9c55a0c67f4db8a492bf13d8d3316a59999cfbaf98cc6e401961"},
- {file = "pydantic_core-2.23.3-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:09e926397f392059ce0afdcac920df29d9c833256354d0c55f1584b0b70cf07e"},
- {file = "pydantic_core-2.23.3-pp310-pypy310_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:87cfa0ed6b8c5bd6ae8b66de941cece179281239d482f363814d2b986b79cedc"},
- {file = "pydantic_core-2.23.3-pp310-pypy310_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:e61328920154b6a44d98cabcb709f10e8b74276bc709c9a513a8c37a18786cc4"},
- {file = "pydantic_core-2.23.3-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:ce3317d155628301d649fe5e16a99528d5680af4ec7aa70b90b8dacd2d725c9b"},
- {file = "pydantic_core-2.23.3-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:e89513f014c6be0d17b00a9a7c81b1c426f4eb9224b15433f3d98c1a071f8433"},
- {file = "pydantic_core-2.23.3-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:4f62c1c953d7ee375df5eb2e44ad50ce2f5aff931723b398b8bc6f0ac159791a"},
- {file = "pydantic_core-2.23.3-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2718443bc671c7ac331de4eef9b673063b10af32a0bb385019ad61dcf2cc8f6c"},
- {file = "pydantic_core-2.23.3-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a0d90e08b2727c5d01af1b5ef4121d2f0c99fbee692c762f4d9d0409c9da6541"},
- {file = "pydantic_core-2.23.3-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2b676583fc459c64146debea14ba3af54e540b61762dfc0613dc4e98c3f66eeb"},
- {file = "pydantic_core-2.23.3-pp39-pypy39_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:50e4661f3337977740fdbfbae084ae5693e505ca2b3130a6d4eb0f2281dc43b8"},
- {file = "pydantic_core-2.23.3-pp39-pypy39_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:68f4cf373f0de6abfe599a38307f4417c1c867ca381c03df27c873a9069cda25"},
- {file = "pydantic_core-2.23.3-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:59d52cf01854cb26c46958552a21acb10dd78a52aa34c86f284e66b209db8cab"},
- {file = "pydantic_core-2.23.3.tar.gz", hash = "sha256:3cb0f65d8b4121c1b015c60104a685feb929a29d7cf204387c7f2688c7974690"},
+ {file = "pydantic_core-2.23.4-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:b10bd51f823d891193d4717448fab065733958bdb6a6b351967bd349d48d5c9b"},
+ {file = "pydantic_core-2.23.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:4fc714bdbfb534f94034efaa6eadd74e5b93c8fa6315565a222f7b6f42ca1166"},
+ {file = "pydantic_core-2.23.4-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:63e46b3169866bd62849936de036f901a9356e36376079b05efa83caeaa02ceb"},
+ {file = "pydantic_core-2.23.4-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ed1a53de42fbe34853ba90513cea21673481cd81ed1be739f7f2efb931b24916"},
+ {file = "pydantic_core-2.23.4-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:cfdd16ab5e59fc31b5e906d1a3f666571abc367598e3e02c83403acabc092e07"},
+ {file = "pydantic_core-2.23.4-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:255a8ef062cbf6674450e668482456abac99a5583bbafb73f9ad469540a3a232"},
+ {file = "pydantic_core-2.23.4-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4a7cd62e831afe623fbb7aabbb4fe583212115b3ef38a9f6b71869ba644624a2"},
+ {file = "pydantic_core-2.23.4-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:f09e2ff1f17c2b51f2bc76d1cc33da96298f0a036a137f5440ab3ec5360b624f"},
+ {file = "pydantic_core-2.23.4-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:e38e63e6f3d1cec5a27e0afe90a085af8b6806ee208b33030e65b6516353f1a3"},
+ {file = "pydantic_core-2.23.4-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:0dbd8dbed2085ed23b5c04afa29d8fd2771674223135dc9bc937f3c09284d071"},
+ {file = "pydantic_core-2.23.4-cp310-none-win32.whl", hash = "sha256:6531b7ca5f951d663c339002e91aaebda765ec7d61b7d1e3991051906ddde119"},
+ {file = "pydantic_core-2.23.4-cp310-none-win_amd64.whl", hash = "sha256:7c9129eb40958b3d4500fa2467e6a83356b3b61bfff1b414c7361d9220f9ae8f"},
+ {file = "pydantic_core-2.23.4-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:77733e3892bb0a7fa797826361ce8a9184d25c8dffaec60b7ffe928153680ba8"},
+ {file = "pydantic_core-2.23.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:1b84d168f6c48fabd1f2027a3d1bdfe62f92cade1fb273a5d68e621da0e44e6d"},
+ {file = "pydantic_core-2.23.4-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:df49e7a0861a8c36d089c1ed57d308623d60416dab2647a4a17fe050ba85de0e"},
+ {file = "pydantic_core-2.23.4-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ff02b6d461a6de369f07ec15e465a88895f3223eb75073ffea56b84d9331f607"},
+ {file = "pydantic_core-2.23.4-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:996a38a83508c54c78a5f41456b0103c30508fed9abcad0a59b876d7398f25fd"},
+ {file = "pydantic_core-2.23.4-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d97683ddee4723ae8c95d1eddac7c192e8c552da0c73a925a89fa8649bf13eea"},
+ {file = "pydantic_core-2.23.4-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:216f9b2d7713eb98cb83c80b9c794de1f6b7e3145eef40400c62e86cee5f4e1e"},
+ {file = "pydantic_core-2.23.4-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:6f783e0ec4803c787bcea93e13e9932edab72068f68ecffdf86a99fd5918878b"},
+ {file = "pydantic_core-2.23.4-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:d0776dea117cf5272382634bd2a5c1b6eb16767c223c6a5317cd3e2a757c61a0"},
+ {file = "pydantic_core-2.23.4-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:d5f7a395a8cf1621939692dba2a6b6a830efa6b3cee787d82c7de1ad2930de64"},
+ {file = "pydantic_core-2.23.4-cp311-none-win32.whl", hash = "sha256:74b9127ffea03643e998e0c5ad9bd3811d3dac8c676e47db17b0ee7c3c3bf35f"},
+ {file = "pydantic_core-2.23.4-cp311-none-win_amd64.whl", hash = "sha256:98d134c954828488b153d88ba1f34e14259284f256180ce659e8d83e9c05eaa3"},
+ {file = "pydantic_core-2.23.4-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:f3e0da4ebaef65158d4dfd7d3678aad692f7666877df0002b8a522cdf088f231"},
+ {file = "pydantic_core-2.23.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f69a8e0b033b747bb3e36a44e7732f0c99f7edd5cea723d45bc0d6e95377ffee"},
+ {file = "pydantic_core-2.23.4-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:723314c1d51722ab28bfcd5240d858512ffd3116449c557a1336cbe3919beb87"},
+ {file = "pydantic_core-2.23.4-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:bb2802e667b7051a1bebbfe93684841cc9351004e2badbd6411bf357ab8d5ac8"},
+ {file = "pydantic_core-2.23.4-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d18ca8148bebe1b0a382a27a8ee60350091a6ddaf475fa05ef50dc35b5df6327"},
+ {file = "pydantic_core-2.23.4-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:33e3d65a85a2a4a0dc3b092b938a4062b1a05f3a9abde65ea93b233bca0e03f2"},
+ {file = "pydantic_core-2.23.4-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:128585782e5bfa515c590ccee4b727fb76925dd04a98864182b22e89a4e6ed36"},
+ {file = "pydantic_core-2.23.4-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:68665f4c17edcceecc112dfed5dbe6f92261fb9d6054b47d01bf6371a6196126"},
+ {file = "pydantic_core-2.23.4-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:20152074317d9bed6b7a95ade3b7d6054845d70584216160860425f4fbd5ee9e"},
+ {file = "pydantic_core-2.23.4-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:9261d3ce84fa1d38ed649c3638feefeae23d32ba9182963e465d58d62203bd24"},
+ {file = "pydantic_core-2.23.4-cp312-none-win32.whl", hash = "sha256:4ba762ed58e8d68657fc1281e9bb72e1c3e79cc5d464be146e260c541ec12d84"},
+ {file = "pydantic_core-2.23.4-cp312-none-win_amd64.whl", hash = "sha256:97df63000f4fea395b2824da80e169731088656d1818a11b95f3b173747b6cd9"},
+ {file = "pydantic_core-2.23.4-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:7530e201d10d7d14abce4fb54cfe5b94a0aefc87da539d0346a484ead376c3cc"},
+ {file = "pydantic_core-2.23.4-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:df933278128ea1cd77772673c73954e53a1c95a4fdf41eef97c2b779271bd0bd"},
+ {file = "pydantic_core-2.23.4-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0cb3da3fd1b6a5d0279a01877713dbda118a2a4fc6f0d821a57da2e464793f05"},
+ {file = "pydantic_core-2.23.4-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:42c6dcb030aefb668a2b7009c85b27f90e51e6a3b4d5c9bc4c57631292015b0d"},
+ {file = "pydantic_core-2.23.4-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:696dd8d674d6ce621ab9d45b205df149399e4bb9aa34102c970b721554828510"},
+ {file = "pydantic_core-2.23.4-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2971bb5ffe72cc0f555c13e19b23c85b654dd2a8f7ab493c262071377bfce9f6"},
+ {file = "pydantic_core-2.23.4-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8394d940e5d400d04cad4f75c0598665cbb81aecefaca82ca85bd28264af7f9b"},
+ {file = "pydantic_core-2.23.4-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:0dff76e0602ca7d4cdaacc1ac4c005e0ce0dcfe095d5b5259163a80d3a10d327"},
+ {file = "pydantic_core-2.23.4-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:7d32706badfe136888bdea71c0def994644e09fff0bfe47441deaed8e96fdbc6"},
+ {file = "pydantic_core-2.23.4-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:ed541d70698978a20eb63d8c5d72f2cc6d7079d9d90f6b50bad07826f1320f5f"},
+ {file = "pydantic_core-2.23.4-cp313-none-win32.whl", hash = "sha256:3d5639516376dce1940ea36edf408c554475369f5da2abd45d44621cb616f769"},
+ {file = "pydantic_core-2.23.4-cp313-none-win_amd64.whl", hash = "sha256:5a1504ad17ba4210df3a045132a7baeeba5a200e930f57512ee02909fc5c4cb5"},
+ {file = "pydantic_core-2.23.4-cp38-cp38-macosx_10_12_x86_64.whl", hash = "sha256:d4488a93b071c04dc20f5cecc3631fc78b9789dd72483ba15d423b5b3689b555"},
+ {file = "pydantic_core-2.23.4-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:81965a16b675b35e1d09dd14df53f190f9129c0202356ed44ab2728b1c905658"},
+ {file = "pydantic_core-2.23.4-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4ffa2ebd4c8530079140dd2d7f794a9d9a73cbb8e9d59ffe24c63436efa8f271"},
+ {file = "pydantic_core-2.23.4-cp38-cp38-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:61817945f2fe7d166e75fbfb28004034b48e44878177fc54d81688e7b85a3665"},
+ {file = "pydantic_core-2.23.4-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:29d2c342c4bc01b88402d60189f3df065fb0dda3654744d5a165a5288a657368"},
+ {file = "pydantic_core-2.23.4-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5e11661ce0fd30a6790e8bcdf263b9ec5988e95e63cf901972107efc49218b13"},
+ {file = "pydantic_core-2.23.4-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9d18368b137c6295db49ce7218b1a9ba15c5bc254c96d7c9f9e924a9bc7825ad"},
+ {file = "pydantic_core-2.23.4-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:ec4e55f79b1c4ffb2eecd8a0cfba9955a2588497d96851f4c8f99aa4a1d39b12"},
+ {file = "pydantic_core-2.23.4-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:374a5e5049eda9e0a44c696c7ade3ff355f06b1fe0bb945ea3cac2bc336478a2"},
+ {file = "pydantic_core-2.23.4-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:5c364564d17da23db1106787675fc7af45f2f7b58b4173bfdd105564e132e6fb"},
+ {file = "pydantic_core-2.23.4-cp38-none-win32.whl", hash = "sha256:d7a80d21d613eec45e3d41eb22f8f94ddc758a6c4720842dc74c0581f54993d6"},
+ {file = "pydantic_core-2.23.4-cp38-none-win_amd64.whl", hash = "sha256:5f5ff8d839f4566a474a969508fe1c5e59c31c80d9e140566f9a37bba7b8d556"},
+ {file = "pydantic_core-2.23.4-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:a4fa4fc04dff799089689f4fd502ce7d59de529fc2f40a2c8836886c03e0175a"},
+ {file = "pydantic_core-2.23.4-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:0a7df63886be5e270da67e0966cf4afbae86069501d35c8c1b3b6c168f42cb36"},
+ {file = "pydantic_core-2.23.4-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dcedcd19a557e182628afa1d553c3895a9f825b936415d0dbd3cd0bbcfd29b4b"},
+ {file = "pydantic_core-2.23.4-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:5f54b118ce5de9ac21c363d9b3caa6c800341e8c47a508787e5868c6b79c9323"},
+ {file = "pydantic_core-2.23.4-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:86d2f57d3e1379a9525c5ab067b27dbb8a0642fb5d454e17a9ac434f9ce523e3"},
+ {file = "pydantic_core-2.23.4-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:de6d1d1b9e5101508cb37ab0d972357cac5235f5c6533d1071964c47139257df"},
+ {file = "pydantic_core-2.23.4-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1278e0d324f6908e872730c9102b0112477a7f7cf88b308e4fc36ce1bdb6d58c"},
+ {file = "pydantic_core-2.23.4-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:9a6b5099eeec78827553827f4c6b8615978bb4b6a88e5d9b93eddf8bb6790f55"},
+ {file = "pydantic_core-2.23.4-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:e55541f756f9b3ee346b840103f32779c695a19826a4c442b7954550a0972040"},
+ {file = "pydantic_core-2.23.4-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:a5c7ba8ffb6d6f8f2ab08743be203654bb1aaa8c9dcb09f82ddd34eadb695605"},
+ {file = "pydantic_core-2.23.4-cp39-none-win32.whl", hash = "sha256:37b0fe330e4a58d3c58b24d91d1eb102aeec675a3db4c292ec3928ecd892a9a6"},
+ {file = "pydantic_core-2.23.4-cp39-none-win_amd64.whl", hash = "sha256:1498bec4c05c9c787bde9125cfdcc63a41004ff167f495063191b863399b1a29"},
+ {file = "pydantic_core-2.23.4-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:f455ee30a9d61d3e1a15abd5068827773d6e4dc513e795f380cdd59932c782d5"},
+ {file = "pydantic_core-2.23.4-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:1e90d2e3bd2c3863d48525d297cd143fe541be8bbf6f579504b9712cb6b643ec"},
+ {file = "pydantic_core-2.23.4-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2e203fdf807ac7e12ab59ca2bfcabb38c7cf0b33c41efeb00f8e5da1d86af480"},
+ {file = "pydantic_core-2.23.4-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e08277a400de01bc72436a0ccd02bdf596631411f592ad985dcee21445bd0068"},
+ {file = "pydantic_core-2.23.4-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:f220b0eea5965dec25480b6333c788fb72ce5f9129e8759ef876a1d805d00801"},
+ {file = "pydantic_core-2.23.4-pp310-pypy310_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:d06b0c8da4f16d1d1e352134427cb194a0a6e19ad5db9161bf32b2113409e728"},
+ {file = "pydantic_core-2.23.4-pp310-pypy310_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:ba1a0996f6c2773bd83e63f18914c1de3c9dd26d55f4ac302a7efe93fb8e7433"},
+ {file = "pydantic_core-2.23.4-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:9a5bce9d23aac8f0cf0836ecfc033896aa8443b501c58d0602dbfd5bd5b37753"},
+ {file = "pydantic_core-2.23.4-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:78ddaaa81421a29574a682b3179d4cf9e6d405a09b99d93ddcf7e5239c742e21"},
+ {file = "pydantic_core-2.23.4-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:883a91b5dd7d26492ff2f04f40fbb652de40fcc0afe07e8129e8ae779c2110eb"},
+ {file = "pydantic_core-2.23.4-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:88ad334a15b32a791ea935af224b9de1bf99bcd62fabf745d5f3442199d86d59"},
+ {file = "pydantic_core-2.23.4-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:233710f069d251feb12a56da21e14cca67994eab08362207785cf8c598e74577"},
+ {file = "pydantic_core-2.23.4-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:19442362866a753485ba5e4be408964644dd6a09123d9416c54cd49171f50744"},
+ {file = "pydantic_core-2.23.4-pp39-pypy39_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:624e278a7d29b6445e4e813af92af37820fafb6dcc55c012c834f9e26f9aaaef"},
+ {file = "pydantic_core-2.23.4-pp39-pypy39_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:f5ef8f42bec47f21d07668a043f077d507e5bf4e668d5c6dfe6aaba89de1a5b8"},
+ {file = "pydantic_core-2.23.4-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:aea443fffa9fbe3af1a9ba721a87f926fe548d32cab71d188a6ede77d0ff244e"},
+ {file = "pydantic_core-2.23.4.tar.gz", hash = "sha256:2584f7cf844ac4d970fba483a717dbe10c1c1c96a969bf65d61ffe94df1b2863"},
]
[package.dependencies]
@@ -1450,20 +1420,6 @@ azure-key-vault = ["azure-identity (>=1.16.0)", "azure-keyvault-secrets (>=4.8.0
toml = ["tomli (>=2.0.1)"]
yaml = ["pyyaml (>=6.0.1)"]
-[[package]]
-name = "pygments"
-version = "2.18.0"
-description = "Pygments is a syntax highlighting package written in Python."
-optional = false
-python-versions = ">=3.8"
-files = [
- {file = "pygments-2.18.0-py3-none-any.whl", hash = "sha256:b8e6aca0523f3ab76fee51799c488e38782ac06eafcf95e7ba832985c8e7b13a"},
- {file = "pygments-2.18.0.tar.gz", hash = "sha256:786ff802f32e91311bff3889f6e9a86e81505fe99f2735bb6d60ae0c5004f199"},
-]
-
-[package.extras]
-windows-terminal = ["colorama (>=0.4.6)"]
-
[[package]]
name = "pyld"
version = "2.0.4"
@@ -1486,17 +1442,6 @@ cachetools = ["cachetools"]
frozendict = ["frozendict"]
requests = ["requests"]
-[[package]]
-name = "pynvml"
-version = "11.5.3"
-description = "Python utilities for the NVIDIA Management Library"
-optional = false
-python-versions = ">=3.6"
-files = [
- {file = "pynvml-11.5.3-py3-none-any.whl", hash = "sha256:a5fba3ab14febda50d19dbda012ef62ae0aed45b7ccc07af0bc5be79223e450c"},
- {file = "pynvml-11.5.3.tar.gz", hash = "sha256:183d223ae487e5f00402d8da06c68c978ef8a9295793ee75559839c6ade7b229"},
-]
-
[[package]]
name = "pyogrio"
version = "0.9.0"
@@ -1659,8 +1604,8 @@ importlib-metadata = {version = ">6", markers = "python_version < \"3.12\""}
owlrl = ">=6.0.2,<7"
packaging = ">=21.3"
prettytable = [
- {version = ">=3.7.0", markers = "python_version >= \"3.12\""},
{version = ">=3.5.0", markers = "python_version >= \"3.8\" and python_version < \"3.12\""},
+ {version = ">=3.7.0", markers = "python_version >= \"3.12\""},
]
rdflib = {version = ">=6.3.2,<8.0", markers = "python_full_version >= \"3.8.1\""}
@@ -1929,24 +1874,6 @@ files = [
[package.dependencies]
six = "*"
-[[package]]
-name = "rich"
-version = "13.8.1"
-description = "Render rich text, tables, progress bars, syntax highlighting, markdown and more to the terminal"
-optional = false
-python-versions = ">=3.7.0"
-files = [
- {file = "rich-13.8.1-py3-none-any.whl", hash = "sha256:1760a3c0848469b97b558fc61c85233e3dafb69c7a071b4d60c38099d3cd4c06"},
- {file = "rich-13.8.1.tar.gz", hash = "sha256:8260cda28e3db6bf04d2d1ef4dbc03ba80a824c88b0e7668a0f23126a424844a"},
-]
-
-[package.dependencies]
-markdown-it-py = ">=2.2.0"
-pygments = ">=2.13.0,<3.0.0"
-
-[package.extras]
-jupyter = ["ipywidgets (>=7.5.1,<9)"]
-
[[package]]
name = "rpds-py"
version = "0.20.0"
@@ -2059,37 +1986,6 @@ files = [
{file = "rpds_py-0.20.0.tar.gz", hash = "sha256:d72a210824facfdaf8768cf2d7ca25a042c30320b3020de2fa04640920d4e121"},
]
-[[package]]
-name = "scalene"
-version = "1.5.19"
-description = "Scalene: A high-resolution, low-overhead CPU, GPU, and memory profiler for Python"
-optional = false
-python-versions = ">=3.8"
-files = [
- {file = "scalene-1.5.19-cp310-cp310-macosx_11_7_universal2.whl", hash = "sha256:78480f92f5098fffdcba4e4cd540fdb6b7a95ff933ee93e1056b0aa2ab746643"},
- {file = "scalene-1.5.19-cp310-cp310-manylinux_2_24_x86_64.whl", hash = "sha256:1da57151f00c70446309c9b52843ce7b437bbf32d336a7c309d4fe0b8deda2a2"},
- {file = "scalene-1.5.19-cp310-cp310-win_amd64.whl", hash = "sha256:cdb9be734cfb6b42eef1105fee1876a5c465eb72e109c0d0ceea6e6bdbce21c6"},
- {file = "scalene-1.5.19-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:dd2b29bd13eaafdd98090108357680c1a6fa88067b4e71313252aa065bbe6ec1"},
- {file = "scalene-1.5.19-cp311-cp311-manylinux_2_24_x86_64.whl", hash = "sha256:2dac52555518c7159d2fd9c8c7c31ffa8b6a74f21d1b40a9d315a77f4a7f97ed"},
- {file = "scalene-1.5.19-cp311-cp311-win_amd64.whl", hash = "sha256:eeef31408df35972e54a39ae2d2f1aeab111f2d53d0a78061e45dcf024c6e01d"},
- {file = "scalene-1.5.19-cp37-cp37m-macosx_10_15_universal2.whl", hash = "sha256:58fb40d031081a55e813f292b2d85b64d7558a372832d8e456f7fe2113adf182"},
- {file = "scalene-1.5.19-cp37-cp37m-manylinux_2_24_x86_64.whl", hash = "sha256:50084cd5c2c4732f845011a7a5407c567df52160b5604a8b4b01ee8b0afee985"},
- {file = "scalene-1.5.19-cp38-cp38-macosx_10_15_universal2.whl", hash = "sha256:0997b1f1ec90e079a73349d702b18df60e6b24c31c255d3e8d6ec6a8b03493c8"},
- {file = "scalene-1.5.19-cp38-cp38-manylinux_2_24_x86_64.whl", hash = "sha256:f67f39321548c06de440319bdd70c41c14b6c50d4748226a101402995677cade"},
- {file = "scalene-1.5.19-cp38-cp38-win_amd64.whl", hash = "sha256:7a1d452ad4d32cf8adb8b86cae4e5b9c7bff866fff1c43618f9ad114b9ecac32"},
- {file = "scalene-1.5.19-cp39-cp39-macosx_11_7_universal2.whl", hash = "sha256:6c08f3bc0b6355db3c34f0e9aea1b291e64675bb832e19758ee751918787cac0"},
- {file = "scalene-1.5.19-cp39-cp39-manylinux_2_24_x86_64.whl", hash = "sha256:d8bc53334b13e486ed6ba03fe4b7595ad4038d05537793eed9999952ba1b34c6"},
- {file = "scalene-1.5.19-cp39-cp39-win_amd64.whl", hash = "sha256:d1fd5d83f3c022ddc46049f29823351b8dbdb8590f5803358fa6034784601f24"},
- {file = "scalene-1.5.19.tar.gz", hash = "sha256:59c5eaaa64f4990444f9606e841b268d49f55dcd7467162e48f9658150a9cd1e"},
-]
-
-[package.dependencies]
-cloudpickle = ">=1.5.0"
-Jinja2 = ">=3.0.3"
-pynvml = ">=11.0.0"
-rich = ">=10.7.0"
-wheel = ">=0.36.1"
-
[[package]]
name = "shapely"
version = "2.0.6"
@@ -2187,13 +2083,13 @@ rdflib = ">=7.0.0,<8.0.0"
[[package]]
name = "starlette"
-version = "0.38.5"
+version = "0.38.6"
description = "The little ASGI library that shines."
optional = false
python-versions = ">=3.8"
files = [
- {file = "starlette-0.38.5-py3-none-any.whl", hash = "sha256:632f420a9d13e3ee2a6f18f437b0a9f1faecb0bc42e1942aa2ea0e379a4c4206"},
- {file = "starlette-0.38.5.tar.gz", hash = "sha256:04a92830a9b6eb1442c766199d62260c3d4dc9c4f9188360626b1e0273cb7077"},
+ {file = "starlette-0.38.6-py3-none-any.whl", hash = "sha256:4517a1409e2e73ee4951214ba012052b9e16f60e90d73cfb06192c19203bbb05"},
+ {file = "starlette-0.38.6.tar.gz", hash = "sha256:863a1588f5574e70a821dadefb41e4881ea451a47a3cd1b4df359d4ffefe5ead"},
]
[package.dependencies]
@@ -2240,13 +2136,13 @@ files = [
[[package]]
name = "tzdata"
-version = "2024.1"
+version = "2024.2"
description = "Provider of IANA time zone data"
optional = false
python-versions = ">=2"
files = [
- {file = "tzdata-2024.1-py2.py3-none-any.whl", hash = "sha256:9068bc196136463f5245e51efda838afa15aaeca9903f49050dfa2679db4d252"},
- {file = "tzdata-2024.1.tar.gz", hash = "sha256:2674120f8d891909751c38abcdfd386ac0a5a1127954fbc332af6b5ceae07efd"},
+ {file = "tzdata-2024.2-py2.py3-none-any.whl", hash = "sha256:a48093786cdcde33cad18c2555e8532f34422074448fbc874186f0abd79565cd"},
+ {file = "tzdata-2024.2.tar.gz", hash = "sha256:7d85cc416e9382e69095b7bdf4afd9e3880418a2413feec7069d533d6b4e31cc"},
]
[[package]]
@@ -2286,13 +2182,13 @@ standard = ["colorama (>=0.4)", "httptools (>=0.5.0)", "python-dotenv (>=0.13)",
[[package]]
name = "virtualenv"
-version = "20.26.4"
+version = "20.26.5"
description = "Virtual Python Environment builder"
optional = false
python-versions = ">=3.7"
files = [
- {file = "virtualenv-20.26.4-py3-none-any.whl", hash = "sha256:48f2695d9809277003f30776d155615ffc11328e6a0a8c1f0ec80188d7874a55"},
- {file = "virtualenv-20.26.4.tar.gz", hash = "sha256:c17f4e0f3e6036e9f26700446f85c76ab11df65ff6d8a9cbfad9f71aabfcf23c"},
+ {file = "virtualenv-20.26.5-py3-none-any.whl", hash = "sha256:4f3ac17b81fba3ce3bd6f4ead2749a72da5929c01774948e243db9ba41df4ff6"},
+ {file = "virtualenv-20.26.5.tar.gz", hash = "sha256:ce489cac131aa58f4b25e321d6d186171f78e6cb13fafbf32a840cee67733ff4"},
]
[package.dependencies]
@@ -2326,29 +2222,15 @@ files = [
{file = "webencodings-0.5.1.tar.gz", hash = "sha256:b36a1c245f2d304965eb4e0a82848379241dc04b865afcc4aab16748587e1923"},
]
-[[package]]
-name = "wheel"
-version = "0.44.0"
-description = "A built-package format for Python"
-optional = false
-python-versions = ">=3.8"
-files = [
- {file = "wheel-0.44.0-py3-none-any.whl", hash = "sha256:2376a90c98cc337d18623527a97c31797bd02bad0033d41547043a1cbfbe448f"},
- {file = "wheel-0.44.0.tar.gz", hash = "sha256:a29c3f2817e95ab89aa4660681ad547c0e9547f20e75b0562fe7723c9a2a9d49"},
-]
-
-[package.extras]
-test = ["pytest (>=6.0.0)", "setuptools (>=65)"]
-
[[package]]
name = "zipp"
-version = "3.20.1"
+version = "3.20.2"
description = "Backport of pathlib-compatible object wrapper for zip files"
optional = false
python-versions = ">=3.8"
files = [
- {file = "zipp-3.20.1-py3-none-any.whl", hash = "sha256:9960cd8967c8f85a56f920d5d507274e74f9ff813a0ab8889a5b5be2daf44064"},
- {file = "zipp-3.20.1.tar.gz", hash = "sha256:c22b14cc4763c5a5b04134207736c107db42e9d3ef2d9779d465f5f1bcba572b"},
+ {file = "zipp-3.20.2-py3-none-any.whl", hash = "sha256:a817ac80d6cf4b23bf7f2828b7cabf326f15a001bea8b1f9b49631780ba28350"},
+ {file = "zipp-3.20.2.tar.gz", hash = "sha256:bc9eb26f4506fda01b81bcde0ca78103b6e62f991b381fec825435c836edbc29"},
]
[package.extras]
@@ -2365,4 +2247,4 @@ server = ["uvicorn"]
[metadata]
lock-version = "2.0"
python-versions = "^3.11"
-content-hash = "ff0e8eddd301b0a3b278d383b26ec73d3e45f100df346ff9a5e2bff3fcb36f60"
+content-hash = "4eb7cb6ad7a00c207d54301cf390d428b7e6e5371125dd004d8087ac855efe0d"
diff --git a/prez/app.py b/prez/app.py
index 986ff6b2..b36321aa 100755
--- a/prez/app.py
+++ b/prez/app.py
@@ -19,6 +19,7 @@
load_system_data_to_oxigraph,
load_annotations_data_to_oxigraph,
get_annotations_store,
+ get_queryable_props,
)
from prez.exceptions.model_exceptions import (
ClassNotFoundException,
@@ -40,6 +41,7 @@
populate_api_info,
prefix_initialisation,
retrieve_remote_template_queries,
+ retrieve_remote_queryable_definitions,
)
from prez.services.exception_catchers import (
catch_400,
@@ -115,8 +117,10 @@ async def lifespan(app: FastAPI):
await count_objects(app.state.repo)
await populate_api_info()
+ app.state.queryable_props = get_queryable_props()
app.state.pyoxi_system_store = get_system_store()
app.state.annotations_store = get_annotations_store()
+ await retrieve_remote_queryable_definitions(app.state, app.state.pyoxi_system_store)
await load_system_data_to_oxigraph(app.state.pyoxi_system_store)
await load_annotations_data_to_oxigraph(app.state.annotations_store)
diff --git a/prez/cache.py b/prez/cache.py
index acd1d035..d82770fc 100755
--- a/prez/cache.py
+++ b/prez/cache.py
@@ -25,6 +25,8 @@
annotations_store = Store()
+queryable_props = {}
+
oxrdflib_store = Graph(store="Oxigraph")
caches.set_config(
diff --git a/prez/dependencies.py b/prez/dependencies.py
index a4558d8b..03a9abd7 100755
--- a/prez/dependencies.py
+++ b/prez/dependencies.py
@@ -15,6 +15,7 @@
endpoints_graph_cache,
annotations_store,
prez_system_graph,
+ queryable_props,
)
from prez.config import settings
from prez.enums import (
@@ -24,7 +25,7 @@
GeoJSONMediaType,
)
from prez.models.query_params import QueryParams
-from prez.reference_data.prez_ns import ALTREXT, ONT, EP, OGCE, OGCFEAT, PREZ
+from prez.reference_data.prez_ns import ALTREXT, ONT, EP, OGCE, OGCFEAT
from prez.repositories import PyoxigraphRepo, RemoteSparqlRepo, OxrdflibRepo, Repo
from prez.services.classes import get_classes_single
from prez.services.connegp_service import NegotiatedPMTs
@@ -63,6 +64,10 @@ def get_oxrdflib_store():
return oxrdflib_store
+def get_queryable_props():
+ return queryable_props
+
+
async def get_data_repo(
request: Request,
http_async_client: httpx.AsyncClient = Depends(get_async_http_client),
@@ -133,13 +138,18 @@ async def load_annotations_data_to_oxigraph(store: Store):
store.load(file_bytes, "application/n-triples")
-async def cql_post_parser_dependency(request: Request) -> CQLParser:
+async def cql_post_parser_dependency(
+ request: Request,
+ queryable_props: list = Depends(get_queryable_props),
+) -> CQLParser:
try:
body = await request.json()
context = json.load(
(Path(__file__).parent / "reference_data/cql/default_context.json").open()
)
- cql_parser = CQLParser(cql=body, context=context)
+ cql_parser = CQLParser(
+ cql=body, context=context, queryable_props=queryable_props
+ )
cql_parser.generate_jsonld()
cql_parser.parse()
return cql_parser
@@ -153,6 +163,7 @@ async def cql_post_parser_dependency(request: Request) -> CQLParser:
async def cql_get_parser_dependency(
query_params: QueryParams = Depends(),
+ queryable_props: list = Depends(get_queryable_props),
) -> CQLParser:
if query_params.filter:
try:
@@ -163,13 +174,15 @@ async def cql_get_parser_dependency(
Path(__file__).parent / "reference_data/cql/default_context.json"
).open()
)
- cql_parser = CQLParser(cql=query, context=context, crs=crs)
+ cql_parser = CQLParser(
+ cql=query, context=context, crs=crs, queryable_props=queryable_props
+ )
cql_parser.generate_jsonld()
cql_parser.parse()
return cql_parser
except json.JSONDecodeError:
raise HTTPException(status_code=400, detail="Invalid JSON format.")
- except Exception as e: # Replace with your specific parsing exception
+ except Exception as e:
raise HTTPException(
status_code=400, detail="Invalid CQL format: Parsing failed."
)
diff --git a/prez/reference_data/cql/default_context.json b/prez/reference_data/cql/default_context.json
index c720041f..aa425d7b 100644
--- a/prez/reference_data/cql/default_context.json
+++ b/prez/reference_data/cql/default_context.json
@@ -1,6 +1,5 @@
{
"@version": 1.1,
- "@base": "http://example.com/",
"@vocab": "http://example.com/vocab/",
"cql": "http://www.opengis.net/doc/IS/cql2/1.0/",
"sf": "http://www.opengis.net/ont/sf#",
diff --git a/prez/renderers/renderer.py b/prez/renderers/renderer.py
index 14ead230..51326e51 100755
--- a/prez/renderers/renderer.py
+++ b/prez/renderers/renderer.py
@@ -13,9 +13,7 @@
from prez.renderers.csv_renderer import render_csv_dropdown
from prez.renderers.json_renderer import render_json_dropdown, NotFoundError
from prez.repositories import Repo
-from prez.services.annotations import (
- get_annotation_properties,
-)
+from prez.services.annotations import get_annotation_properties
from prez.services.connegp_service import RDF_MEDIATYPES, RDF_SERIALIZER_TYPES_MAP
from prez.services.curie_functions import get_curie_id_for_uri
diff --git a/prez/routers/ogc_features_router.py b/prez/routers/ogc_features_router.py
index da434765..9040a780 100755
--- a/prez/routers/ogc_features_router.py
+++ b/prez/routers/ogc_features_router.py
@@ -15,10 +15,10 @@
get_system_repo,
get_endpoint_nodeshapes,
get_profile_nodeshape,
- get_endpoint_uri_type,
get_ogc_features_path_params,
get_template_query,
check_unknown_params,
+ get_endpoint_uri_type,
)
from prez.exceptions.model_exceptions import (
ClassNotFoundException,
@@ -118,6 +118,12 @@ async def ogc_features_api(
methods=ALLOWED_METHODS,
name=OGCFEAT["queryables-global"],
)
+@features_subapi.api_route(
+ "/collections/{collectionId}/queryables",
+ methods=ALLOWED_METHODS,
+ name=OGCFEAT["queryables-local"],
+ openapi_extra=ogc_features_openapi_extras.get("feature-collection"),
+)
@features_subapi.api_route(
"/collections",
methods=ALLOWED_METHODS,
@@ -129,15 +135,9 @@ async def ogc_features_api(
name=OGCFEAT["features"],
openapi_extra=ogc_features_openapi_extras.get("feature-collection"),
)
-@features_subapi.api_route(
- "/collections/{collectionId}/queryables",
- methods=ALLOWED_METHODS,
- name=OGCFEAT["queryables-local"],
- openapi_extra=ogc_features_openapi_extras.get("feature-collection"),
-)
async def listings_with_feature_collection(
validate_unknown_params: bool = Depends(check_unknown_params),
- endpoint_uri_type: tuple = Depends(get_endpoint_uri_type),
+ endpoint_uri_type: str = Depends(get_endpoint_uri_type),
endpoint_nodeshape: NodeShape = Depends(get_endpoint_nodeshapes),
profile_nodeshape: NodeShape = Depends(get_profile_nodeshape),
url: str = Depends(get_url),
diff --git a/prez/services/app_service.py b/prez/services/app_service.py
index afb637a8..ffbf8ff8 100755
--- a/prez/services/app_service.py
+++ b/prez/services/app_service.py
@@ -3,7 +3,7 @@
from pathlib import Path
import httpx
-from rdflib import URIRef, Literal, Graph, RDF, BNode
+from rdflib import URIRef, Literal, Graph, RDF, BNode, DCTERMS
from prez.cache import (
prez_system_graph,
@@ -194,3 +194,25 @@ async def get_remote_endpoint_definitions(repo):
log.info(f"Remote endpoint definition(s) found and added")
else:
log.info("No remote endpoint definitions found")
+
+
+async def retrieve_remote_queryable_definitions(app_state, system_store):
+ query = "DESCRIBE ?queryable { ?queryable a }"
+ g, _ = await app_state.repo.send_queries([query], [])
+ if len(g) > 0:
+ prez_system_graph.__iadd__(g) # use for generating property shapes
+ queryable_bytes = g.serialize(
+ format="nt", encoding="utf-8"
+ ) # use for generating JSON
+ system_store.load(queryable_bytes, "application/n-triples")
+ queryables = list(
+ g.subjects(
+ object=URIRef("http://www.opengis.net/doc/IS/cql2/1.0/Queryable")
+ )
+ )
+ for triple in list(g.triples_choices((queryables, DCTERMS.identifier, None))):
+ app_state.queryable_props[str(triple[2])] = str(triple[0])
+ n_queryables = len(queryables)
+ log.info(f"Remote queryable definition(s) found and added: {n_queryables}")
+ else:
+ log.info("No remote queryable definitions found")
diff --git a/prez/services/listings.py b/prez/services/listings.py
index 4ad61159..af6e6428 100755
--- a/prez/services/listings.py
+++ b/prez/services/listings.py
@@ -7,6 +7,7 @@
from urllib.parse import urlencode
from zoneinfo import ZoneInfo
+from fastapi import Depends
from fastapi.responses import PlainTextResponse
from rdf2geojson import convert
from rdflib import URIRef, Literal
@@ -29,10 +30,12 @@
from prez.cache import endpoints_graph_cache
from prez.config import settings
+from prez.dependencies import get_url, get_endpoint_uri, get_system_repo
from prez.enums import NonAnnotatedRDFMediaType
-from prez.models.ogc_features import Collection, Link, Collections, Links
+from prez.models.ogc_features import Collection, Link, Collections, Links, Queryables
from prez.reference_data.prez_ns import PREZ, ALTREXT, ONT, OGCFEAT
from prez.renderers.renderer import return_from_graph, return_annotated_rdf
+from prez.repositories import Repo
from prez.services.connegp_service import RDF_MEDIATYPES
from prez.services.curie_functions import get_uri_for_curie_id, get_curie_id_for_uri
from prez.services.generate_queryables import generate_queryables_json
@@ -176,28 +179,38 @@ async def ogc_features_listing_function(
OGCFEAT["queryables-local"],
OGCFEAT["queryables-global"],
]:
- queryable_var = Var(value="queryable")
- innser_select_triple = (
- Var(value="focus_node"),
- queryable_var,
- Var(value="queryable_value"),
+ queryables = await generate_queryables_from_shacl_definition(
+ url, endpoint_uri_type[0], system_repo
)
- subselect_kwargs["inner_select_tssp_list"].append(
- TriplesSameSubjectPath.from_spo(*innser_select_triple)
- )
- subselect_kwargs["inner_select_vars"] = [queryable_var]
- construct_triple = (
- queryable_var,
- IRI(value=RDF.type),
- IRI(value="http://www.opengis.net/def/rel/ogc/1.0/Queryable"),
- )
- construct_tss_list = [TriplesSameSubject.from_spo(*construct_triple)]
- query = PrezQueryConstructor(
- construct_tss_list=construct_tss_list,
- profile_triples=profile_nodeshape.tssp_list,
- **subselect_kwargs,
- ).to_string()
- queries.append(query)
+ if queryables: # from shacl definitions
+ content = io.BytesIO(
+ queryables.model_dump_json(exclude_none=True, by_alias=True).encode(
+ "utf-8"
+ )
+ )
+ else:
+ queryable_var = Var(value="queryable")
+ innser_select_triple = (
+ Var(value="focus_node"),
+ queryable_var,
+ Var(value="queryable_value"),
+ )
+ subselect_kwargs["inner_select_tssp_list"].append(
+ TriplesSameSubjectPath.from_spo(*innser_select_triple)
+ )
+ subselect_kwargs["inner_select_vars"] = [queryable_var]
+ construct_triple = (
+ queryable_var,
+ IRI(value=RDF.type),
+ IRI(value="http://www.opengis.net/def/rel/ogc/1.0/Queryable"),
+ )
+ construct_tss_list = [TriplesSameSubject.from_spo(*construct_triple)]
+ query = PrezQueryConstructor(
+ construct_tss_list=construct_tss_list,
+ profile_triples=profile_nodeshape.tssp_list,
+ **subselect_kwargs,
+ ).to_string()
+ queries.append(query)
elif not collectionId: # list Feature Collections
query = PrezQueryConstructor(
construct_tss_list=construct_tss_list,
@@ -268,14 +281,17 @@ async def ogc_features_listing_function(
OGCFEAT["queryables-local"],
OGCFEAT["queryables-global"],
]:
- queryables = generate_queryables_json(
- item_graph, annotations_graph, url, endpoint_uri_type[0]
- )
- content = io.BytesIO(
- queryables.model_dump_json(exclude_none=True, by_alias=True).encode(
- "utf-8"
+ if queryables: # queryables were generated from SHACL
+ pass
+ else: # generate them from the data
+ queryables = generate_queryables_json(
+ item_graph, annotations_graph, url, endpoint_uri_type[0]
+ )
+ content = io.BytesIO(
+ queryables.model_dump_json(exclude_none=True, by_alias=True).encode(
+ "utf-8"
+ )
)
- )
else:
collections = create_collections_json(
item_graph,
@@ -361,7 +377,7 @@ def create_collections_json(
return collections
-def create_self_alt_links(selected_mediatype, url, query_params = None, count = None):
+def create_self_alt_links(selected_mediatype, url, query_params=None, count=None):
self_alt_links = []
for mt in [selected_mediatype, *RDF_MEDIATYPES]:
self_alt_links.append(
@@ -439,3 +455,68 @@ def get_brisbane_timestamp():
# Insert colon in timezone offset
return f"{timestamp[:-2]}:{timestamp[-2:]}"
+
+
+# TODO cache this
+async def generate_queryables_from_shacl_definition(
+ url: str = Depends(get_url),
+ endpoint_uri: URIRef = Depends(get_endpoint_uri),
+ system_repo: Repo = Depends(get_system_repo),
+):
+ query = """
+ PREFIX cql:
+ PREFIX dcterms:
+ PREFIX sh:
+ PREFIX rdf:
+ CONSTRUCT {
+ ?queryable cql:id ?id ;
+ cql:name ?title ;
+ cql:datatype ?type ;
+ cql:enum ?enums .
+ }
+ WHERE {?queryable a cql:Queryable ;
+ dcterms:identifier ?id ;
+ sh:name ?title ;
+ sh:datatype ?type ;
+ sh:in/rdf:rest*/rdf:first ?enums ;
+ }
+ """
+ g, _ = await system_repo.send_queries([query], [])
+ if (
+ len(g) == 0
+ ): # will auto generate queryables from data - less preferable approach
+ return None
+ jsonld_string = g.serialize(format="json-ld")
+ jsonld = json.loads(jsonld_string)
+ queryable_props = {}
+ for item in jsonld:
+ id_value = item["http://www.opengis.net/doc/IS/cql2/1.0/id"][0]["@value"]
+ queryable_props[id_value] = {
+ "title": item["http://www.opengis.net/doc/IS/cql2/1.0/name"][0]["@value"],
+ "type": item["http://www.opengis.net/doc/IS/cql2/1.0/datatype"][0][
+ "@id"
+ ].split("#")[
+ -1
+ ], # hack
+ "enum": [
+ enum_item["@id"]
+ for enum_item in item["http://www.opengis.net/doc/IS/cql2/1.0/enum"]
+ ],
+ }
+ if endpoint_uri == OGCFEAT["queryables-global"]:
+ title = "Global Queryables"
+ description = (
+ "Global queryable properties for all collections in the OGC Features API."
+ )
+ else:
+ title = "Local Queryables"
+ description = (
+ "Local queryable properties for the collection in the OGC Features API."
+ )
+ queryable_params = {
+ "$id": f"{settings.system_uri}{url.path}",
+ "title": title,
+ "description": description,
+ "properties": queryable_props,
+ }
+ return Queryables(**queryable_params)
diff --git a/prez/services/objects.py b/prez/services/objects.py
index a3ebebb1..4fc835c2 100755
--- a/prez/services/objects.py
+++ b/prez/services/objects.py
@@ -19,8 +19,12 @@
from prez.services.connegp_service import RDF_MEDIATYPES
from prez.services.curie_functions import get_uri_for_curie_id, get_curie_id_for_uri
from prez.services.link_generation import add_prez_links
-from prez.services.listings import listing_function, generate_link_headers, create_self_alt_links, \
- get_brisbane_timestamp
+from prez.services.listings import (
+ listing_function,
+ generate_link_headers,
+ create_self_alt_links,
+ get_brisbane_timestamp,
+)
from prez.services.query_generation.umbrella import (
PrezQueryConstructor,
)
@@ -102,11 +106,10 @@ def create_parent_link(url):
return Link(
href=f"{settings.system_uri}{url.path.split('/items')[0]}",
rel="collection",
- type="application/geo+json"
+ type="application/geo+json",
)
-
async def ogc_features_object_function(
template_query,
selected_mediatype,
@@ -144,11 +147,17 @@ async def ogc_features_object_function(
feature_iri = IRI(value=feature_uri)
triples = [
(feature_iri, Var(value="prop"), Var(value="val")),
- (feature_iri, IRI(value=GEO.hasGeometry), Var(value="bn")), # Pyoxigraph DESCRIBE does not follow blank nodes, so specify the geometry path
- (Var(value="bn"), IRI(value=GEO.asWKT), Var(value="wkt"))
+ (
+ feature_iri,
+ IRI(value=GEO.hasGeometry),
+ Var(value="bn"),
+ ), # Pyoxigraph DESCRIBE does not follow blank nodes, so specify the geometry path
+ (Var(value="bn"), IRI(value=GEO.asWKT), Var(value="wkt")),
]
tssp_list = [TriplesSameSubjectPath.from_spo(*triple) for triple in triples]
- construct_tss_list = [TriplesSameSubject.from_spo(*triple) for triple in triples]
+ construct_tss_list = [
+ TriplesSameSubject.from_spo(*triple) for triple in triples
+ ]
query = PrezQueryConstructor(
construct_tss_list=construct_tss_list,
profile_triples=tssp_list,
diff --git a/prez/services/query_generation/cql.py b/prez/services/query_generation/cql.py
index 0d3eb73a..6e37543f 100755
--- a/prez/services/query_generation/cql.py
+++ b/prez/services/query_generation/cql.py
@@ -4,6 +4,7 @@
from pathlib import Path
from typing import Generator
+from fastapi import Depends
from pyld import jsonld
from rdf2geojson.contrib.geomet.util import flatten_multi_dim
from rdf2geojson.contrib.geomet.wkt import dumps
@@ -50,23 +51,16 @@
BooleanLiteral,
)
+from prez.cache import prez_system_graph
from prez.models.query_params import parse_datetime
from prez.reference_data.cql.geo_function_mapping import (
cql_sparql_spatial_mapping,
)
+from prez.repositories import Repo
+from prez.services.query_generation.shacl import PropertyShape
CQL = Namespace("http://www.opengis.net/doc/IS/cql2/1.0/")
-# SUPPORTED_CQL_TIME_OPERATORS = {
-# "t_after",
-# "t_before",
-# "t_equals",
-# "t_disjoint",
-# "t_intersects",
-# }
-
-
-# all CQL time operators
SUPPORTED_CQL_TIME_OPERATORS = {
"t_after",
"t_before",
@@ -92,11 +86,21 @@
)
relations = json.loads(relations_path.read_text())
+SHACL_FILTER_NAMESPACE = Namespace("https://cql-shacl-filter/")
+
class CQLParser:
- def __init__(self, cql=None, context: dict = None, cql_json: dict = None, crs=None):
+ def __init__(
+ self,
+ cql=None,
+ context: dict = None,
+ cql_json: dict = None,
+ crs=None,
+ queryable_props=None,
+ ):
self.ggps_inner_select = None
self.inner_select_gpnt_list = None
+ self.inner_select_vars: list[Var] = []
self.cql: dict = cql
self.context = context
self.cql_json = cql_json
@@ -107,6 +111,7 @@ def __init__(self, cql=None, context: dict = None, cql_json: dict = None, crs=No
self.tss_list = []
self.tssp_list = []
self.crs = crs
+ self.queryable_props = queryable_props
def generate_jsonld(self):
combined = {"@context": self.context, **self.cql}
@@ -118,9 +123,11 @@ def parse(self):
where = WhereClause(
group_graph_pattern=GroupGraphPattern(content=self.ggps_inner_select)
)
- construct_template = ConstructTemplate(
- construct_triples=ConstructTriples.from_tss_list(self.tss_list)
- )
+ if self.tss_list:
+ construct_triples = ConstructTriples.from_tss_list(self.tss_list)
+ else:
+ construct_triples = None
+ construct_template = ConstructTemplate(construct_triples=construct_triples)
solution_modifier = SolutionModifier()
self.query_object = ConstructQuery(
construct_template=construct_template,
@@ -204,14 +211,8 @@ def _add_triple(self, ggps, subject, predicate, object):
ggps.triples_block = TriplesBlock(triples=tssp)
def _handle_comparison(self, operator, args, existing_ggps=None):
- self.var_counter += 1
- ggps = existing_ggps if existing_ggps is not None else GroupGraphPatternSub()
+ ggps, object = self._add_tss_tssp(args, existing_ggps)
- prop = args[0].get(str(CQL.property))[0].get("@id")
- inverse = False # for inverse properties
- if prop.startswith("^"):
- prop = prop[1:]
- inverse = True
val = args[1].get("@value")
if not val: # then should be an IRI
val = args[1].get("@id")
@@ -220,10 +221,7 @@ def _handle_comparison(self, operator, args, existing_ggps=None):
value = RDFLiteral(value=val)
elif isinstance(val, (int, float)): # literal numeric
value = NumericLiteral(value=val)
- subject = Var(value="focus_node")
- predicate = IRI(value=prop)
- object = Var(value=f"var_{self.var_counter}")
object_pe = PrimaryExpression(content=object)
if operator == "=":
iri_db_vals = [DataBlockValue(value=value)]
@@ -240,21 +238,24 @@ def _handle_comparison(self, operator, args, existing_ggps=None):
gpnt = GraphPatternNotTriples(content=values_constraint)
ggps.add_pattern(gpnt)
- if inverse:
- self._add_triple(ggps, object, predicate, subject)
- else:
- self._add_triple(ggps, subject, predicate, object)
-
yield ggps
- def _handle_like(self, args, existing_ggps=None):
+ def _add_tss_tssp(self, args, existing_ggps):
self.var_counter += 1
ggps = existing_ggps if existing_ggps is not None else GroupGraphPatternSub()
prop = args[0].get(str(CQL.property))[0].get("@id")
- inverse = False
- if prop.startswith("^"):
- prop = prop[1:]
- inverse = True
+ if prop in self.queryable_props:
+ object = self._handle_shacl_defined_prop(prop)
+ else:
+ subject = Var(value="focus_node")
+ predicate = IRI(value=prop)
+ object = Var(value=f"var_{self.var_counter}")
+ self._add_triple(ggps, subject, predicate, object)
+ return ggps, object
+
+ def _handle_like(self, args, existing_ggps=None):
+ ggps, object = self._add_tss_tssp(args, existing_ggps)
+
value = (
args[1]
.get("@value")
@@ -263,21 +264,13 @@ def _handle_like(self, args, existing_ggps=None):
.replace("\\", "\\\\")
)
- subject = Var(value="focus_node")
- predicate = IRI(value=URIRef(prop))
- obj = Var(value=f"var_{self.var_counter}")
- if inverse:
- self._add_triple(ggps, obj, predicate, subject)
- else:
- self._add_triple(ggps, subject, predicate, obj)
-
filter_gpnt = GraphPatternNotTriples(
content=Filter(
constraint=Constraint(
content=BuiltInCall(
other_expressions=RegexExpression(
text_expression=Expression.from_primary_expression(
- primary_expression=PrimaryExpression(content=obj)
+ primary_expression=PrimaryExpression(content=object)
),
pattern_expression=Expression.from_primary_expression(
primary_expression=PrimaryExpression(
@@ -333,16 +326,14 @@ def _handle_spatial(self, operator, args, existing_ggps=None):
yield ggps
def _handle_in(self, args, existing_ggps=None):
- self.var_counter += 1
- ggps = existing_ggps if existing_ggps is not None else GroupGraphPatternSub()
+ ggps, object = self._add_tss_tssp(args, existing_ggps)
- prop = args[0].get(str(CQL.property))[0].get("@id")
- inverse = False
- if prop.startswith("^"):
- prop = prop[1:]
- inverse = True
literal_values = [item["@value"] for item in args if "@value" in item]
uri_values = [item["@id"] for item in args if "@id" in item]
+ for i, lit_val in enumerate(literal_values):
+ if lit_val.startswith("http"): # hack
+ uri_values.append(literal_values.pop(i))
+ grammar_uri_values = [IRI(value=URIRef(value)) for value in uri_values]
grammar_literal_values = []
for val in literal_values:
if isinstance(val, str):
@@ -350,15 +341,7 @@ def _handle_in(self, args, existing_ggps=None):
elif isinstance(val, (int, float)):
value = NumericLiteral(value=val)
grammar_literal_values.append(value)
- grammar_uri_values = [IRI(value=URIRef(value)) for value in uri_values]
all_values = grammar_literal_values + grammar_uri_values
- subject = Var(value="focus_node")
- predicate = IRI(value=URIRef(prop))
- object = Var(value=f"var_{self.var_counter}")
- if inverse:
- self._add_triple(ggps, object, predicate, subject)
- else:
- self._add_triple(ggps, subject, predicate, object)
iri_db_vals = [DataBlockValue(value=p) for p in all_values]
ildov = InlineDataOneVar(variable=object, datablockvalues=iri_db_vals)
@@ -369,6 +352,18 @@ def _handle_in(self, args, existing_ggps=None):
ggps.add_pattern(gpnt)
yield ggps
+ def _handle_shacl_defined_prop(self, prop):
+ tssp_list, object = self.queryable_id_to_tssp(self.queryable_props[prop])
+ tss_triple = (
+ Var(value="focus_node"),
+ IRI(value=SHACL_FILTER_NAMESPACE[prop]),
+ object,
+ )
+ self.tss_list.append(TriplesSameSubject.from_spo(*tss_triple))
+ self.tssp_list.extend(tssp_list)
+ self.inner_select_vars.append(object)
+ return object
+
def _extract_spatial_info(self, coordinates_list, args):
coordinates = []
geom_type = None
@@ -518,6 +513,26 @@ def _dt_to_rdf_literal(self, i, dt_str, label, operands):
datatype=IRI(value="http://www.w3.org/2001/XMLSchema#dateTime"),
)
+ def queryable_id_to_tssp(
+ self,
+ queryable_uri,
+ ):
+ queryable_shape = prez_system_graph.cbd(URIRef(queryable_uri))
+ ps = PropertyShape(
+ uri=URIRef(queryable_uri),
+ graph=queryable_shape,
+ kind="endpoint", # could be renamed - originally only endpoint nodeshapes filtered the nodes to be selected
+ focus_node=Var(value="focus_node"),
+ )
+ obj_var_name = (
+ ps.tssp_list[0]
+ .content[1]
+ .first_pair[1]
+ .object_paths[0]
+ .graph_node_path.varorterm_or_triplesnodepath.varorterm
+ )
+ return ps.tssp_list, obj_var_name
+
def format_coordinates_as_wkt(bbox_values):
if len(bbox_values) == 4:
diff --git a/prez/services/query_generation/shacl.py b/prez/services/query_generation/shacl.py
index 6ce6f2ad..19b7a7ea 100755
--- a/prez/services/query_generation/shacl.py
+++ b/prez/services/query_generation/shacl.py
@@ -333,7 +333,7 @@ def _process_property_path(self, pp, union: bool = False):
self._process_union(pp, union)
elif bn_pred in PRED_TO_PATH_CLASS:
path_class = PRED_TO_PATH_CLASS[bn_pred]
- self._add_path(path_class(value=bn_obj), union)
+ self._add_path(path_class(value=Path(value=bn_obj)), union)
else: # sequence paths
self._process_sequence(pp, union)
@@ -348,16 +348,31 @@ def _process_union(self, pp, union: bool):
def _process_sequence(self, pp, union: bool):
paths = list(Collection(self.graph, pp))
sp_list = []
- for path in paths:
+
+ def process_path(path, parent_path_class=None):
if isinstance(path, BNode):
pred_objects = list(self.graph.predicate_objects(subject=path))
if pred_objects:
bn_pred, bn_obj = pred_objects[0]
if bn_pred in PRED_TO_PATH_CLASS:
path_class = PRED_TO_PATH_CLASS[bn_pred]
- sp_list.append(path_class(value=bn_obj))
+ if isinstance(bn_obj, URIRef):
+ if not parent_path_class:
+ sp_list.append(path_class(value=Path(value=bn_obj)))
+ else:
+ sp_list.append(
+ parent_path_class(
+ value=path_class(value=Path(value=bn_obj))
+ )
+ )
+ elif isinstance(bn_obj, BNode):
+ process_path(bn_obj, path_class)
elif isinstance(path, URIRef):
sp_list.append(Path(value=path))
+
+ for path in paths:
+ process_path(path)
+
self._add_path(SequencePath(value=sp_list), union)
def _add_path(self, path: PropertyPath, union: bool):
@@ -526,7 +541,11 @@ def process_property_paths(self, property_paths, path_or_prop, tssp_list, pp_i):
pp_i += 1
elif isinstance(property_path, InversePath):
- triple = (path_node_1, IRI(value=property_path.value), self.focus_node)
+ triple = (
+ path_node_1,
+ IRI(value=property_path.value.value),
+ self.focus_node,
+ )
self.tss_list.append(TriplesSameSubject.from_spo(*triple))
current_tssp.append(TriplesSameSubjectPath.from_spo(*triple))
pp_i += 1
@@ -534,12 +553,13 @@ def process_property_paths(self, property_paths, path_or_prop, tssp_list, pp_i):
elif isinstance(
property_path, Union[ZeroOrMorePath, OneOrMorePath, ZeroOrOnePath]
):
- triple = (self.focus_node, IRI(value=property_path.value), path_node_1)
- self.tss_list.append(TriplesSameSubject.from_spo(*triple))
+ # triple = (self.focus_node, IRI(value=property_path.value), path_node_1)
+ # self.tss_list.append(TriplesSameSubject.from_spo(*triple))
+ # remove TSS as it cannot capture the full set of triples possibly created by the path expression
self.tssp_list.append(
_tssp_for_pathmods(
self.focus_node,
- IRI(value=property_path.value),
+ IRI(value=property_path.value.value),
path_node_1,
property_path.operand,
)
@@ -547,28 +567,64 @@ def process_property_paths(self, property_paths, path_or_prop, tssp_list, pp_i):
pp_i += 1
elif isinstance(property_path, SequencePath):
+ preds_pathmods_inverse = []
for j, path in enumerate(property_path.value):
if isinstance(path, Path):
- if j == 0:
- triple = (
- self.focus_node,
- IRI(value=path.value),
- path_node_1,
+ if self.kind == "endpoint":
+ preds_pathmods_inverse.append(
+ (IRI(value=path.value), None, False)
)
- else:
- triple = (path_node_1, IRI(value=path.value), path_node_2)
+ elif self.kind == "profile":
+ if j == 0:
+ triple = (
+ self.focus_node,
+ IRI(value=path.value),
+ path_node_1,
+ )
+ else:
+ triple = (
+ path_node_1,
+ IRI(value=path.value),
+ path_node_2,
+ )
elif isinstance(path, InversePath):
- if j == 0:
- triple = (
- path_node_1,
- IRI(value=path.value),
- self.focus_node,
+ if self.kind == "endpoint":
+ preds_pathmods_inverse.append(
+ (IRI(value=path.value.value), None, True)
+ )
+ elif self.kind == "profile":
+ if j == 0:
+ triple = (
+ path_node_1,
+ IRI(value=path.value),
+ self.focus_node,
+ )
+ else:
+ triple = (
+ path_node_2,
+ IRI(value=path.value),
+ path_node_1,
+ )
+ elif isinstance(
+ path, Union[ZeroOrMorePath, OneOrMorePath, ZeroOrOnePath]
+ ):
+ if isinstance(path.value, Path):
+ preds_pathmods_inverse.append(
+ (IRI(value=path.value.value), path.operand, False)
)
- else:
- triple = (path_node_2, IRI(value=path.value), path_node_1)
- self.tss_list.append(TriplesSameSubject.from_spo(*triple))
- current_tssp.append(TriplesSameSubjectPath.from_spo(*triple))
+ elif isinstance(path.value, InversePath):
+ preds_pathmods_inverse.append(
+ (IRI(value=path.value.value.value), path.operand, True)
+ )
+ if self.kind == "profile":
+ self.tss_list.append(TriplesSameSubject.from_spo(*triple))
+ current_tssp.append(TriplesSameSubjectPath.from_spo(*triple))
pp_i += len(property_path.value)
+ if self.kind == "endpoint":
+ tssp = _tssp_for_sequence(
+ self.focus_node, preds_pathmods_inverse, path_node_2
+ )
+ current_tssp.append(tssp)
if current_tssp:
tssp_list.append(current_tssp)
@@ -576,12 +632,12 @@ def process_property_paths(self, property_paths, path_or_prop, tssp_list, pp_i):
return pp_i
-def _tssp_for_pathmods(focus_node, pred, obj, pathmod):
+def _tssp_for_pathmods(focus_node: IRI | Var, pred, obj, pathmod):
"""
Creates path modifier TriplesSameSubjectPath objects.
"""
if isinstance(focus_node, IRI):
- focus_node = GraphTerm(value=focus_node)
+ focus_node = GraphTerm(content=focus_node)
return TriplesSameSubjectPath(
content=(
VarOrTerm(varorterm=focus_node),
@@ -624,6 +680,75 @@ def _tssp_for_pathmods(focus_node, pred, obj, pathmod):
)
+def _tssp_for_sequence(
+ focus_node, preds_pathmods_inverse: list[tuple[IRI, str | None, bool]], obj
+):
+ """
+ Creates TSSP for Sequence Paths, supporting *?+ pathmods and inverse paths TriplesSameSubjectPath objects.
+ """
+ if isinstance(focus_node, IRI):
+ focus_node = GraphTerm(content=focus_node)
+ if isinstance(obj, IRI):
+ obj = GraphTerm(content=obj)
+ list_path_elt_or_inverse = []
+ for pred, pathmod, inverse in preds_pathmods_inverse:
+ if pathmod:
+ list_path_elt_or_inverse.append(
+ PathEltOrInverse(
+ path_elt=PathElt(
+ path_primary=PathPrimary(
+ value=pred,
+ ),
+ path_mod=PathMod(pathmod=pathmod),
+ ),
+ inverse=inverse,
+ )
+ )
+ else:
+ list_path_elt_or_inverse.append(
+ PathEltOrInverse(
+ path_elt=PathElt(
+ path_primary=PathPrimary(
+ value=pred,
+ ),
+ ),
+ inverse=inverse,
+ )
+ )
+
+ return TriplesSameSubjectPath(
+ content=(
+ VarOrTerm(varorterm=focus_node),
+ PropertyListPathNotEmpty(
+ first_pair=(
+ VerbPath(
+ path=SG_Path(
+ path_alternative=PathAlternative(
+ sequence_paths=[
+ PathSequence(
+ list_path_elt_or_inverse=list_path_elt_or_inverse
+ )
+ ]
+ )
+ )
+ ),
+ ObjectListPath(
+ object_paths=[
+ ObjectPath(
+ graph_node_path=GraphNodePath(
+ varorterm_or_triplesnodepath=VarOrTerm(
+ varorterm=obj
+ )
+ )
+ )
+ ]
+ ),
+ )
+ ),
+ )
+ )
+
+
class PropertyPath(BaseModel):
class Config:
arbitrary_types_allowed = True
@@ -646,21 +771,21 @@ def __len__(self):
class InversePath(PropertyPath):
- value: URIRef
+ value: PropertyPath
class ZeroOrMorePath(PropertyPath):
- value: URIRef
+ value: PropertyPath
operand: str = "*"
class OneOrMorePath(PropertyPath):
- value: URIRef
+ value: PropertyPath
operand: str = "+"
class ZeroOrOnePath(PropertyPath):
- value: URIRef
+ value: PropertyPath
operand: str = "?"
diff --git a/prez/services/query_generation/umbrella.py b/prez/services/query_generation/umbrella.py
index d83ee027..a4130dbb 100755
--- a/prez/services/query_generation/umbrella.py
+++ b/prez/services/query_generation/umbrella.py
@@ -189,7 +189,7 @@ def merge_listing_query_grammar_inputs(
"""
kwargs = {
"construct_tss_list": [],
- "inner_select_vars": [],
+ "inner_select_vars": [Var(value="focus_node")],
"inner_select_tssp_list": [],
"inner_select_gpnt": [],
"limit": None,
@@ -234,6 +234,7 @@ def merge_listing_query_grammar_inputs(
kwargs["order_by_direction"] = "ASC"
if cql_parser:
+ kwargs["inner_select_vars"].extend(cql_parser.inner_select_vars)
kwargs["construct_tss_list"].extend(cql_parser.tss_list)
kwargs["inner_select_tssp_list"].extend(cql_parser.tssp_list)
kwargs["inner_select_gpnt"].extend(cql_parser.inner_select_gpnt_list)
diff --git a/pyproject.toml b/pyproject.toml
index ebeb2b1c..5b45c0a9 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -26,7 +26,6 @@ rdflib = "^7.0.0"
toml = "^0.10.2"
fastapi = "^0.114.0"
jinja2 = "^3.1.2"
-oxrdflib = "^0.3.6"
pydantic = "^2.9.1"
pydantic-settings = "^2.5.0"
pyld = "^2.0.4"
@@ -34,6 +33,8 @@ aiocache = "^0.12.2"
sparql-grammar-pydantic = "^0.1.2"
rdf2geojson = {git = "https://github.com/ashleysommer/rdf2geojson.git", rev = "v0.2.1"}
python-multipart = "^0.0.9"
+pyoxigraph = "^0.3.22"
+oxrdflib = "^0.3.7"
[tool.poetry.extras]
server = ["uvicorn"]
@@ -44,9 +45,7 @@ pre-commit = "^2.15.0"
black = "^24.4.2"
pytest-asyncio = "^0.23.7"
requests = "^2.28.1"
-scalene = "^1.5.18"
python-dotenv = "^1.0.0"
-pyoxigraph = "^0.3.19"
coverage = "^7.3.2"
tabulate = "^0.9.0"
ogctests = "^0.1.15"
diff --git a/test_data/cql/expected_generated_queries/additional_temporal_disjoint_instant.rq b/test_data/cql/expected_generated_queries/additional_temporal_disjoint_instant.rq
index 3f61c22d..b6c8fd8c 100644
--- a/test_data/cql/expected_generated_queries/additional_temporal_disjoint_instant.rq
+++ b/test_data/cql/expected_generated_queries/additional_temporal_disjoint_instant.rq
@@ -1,7 +1,7 @@
CONSTRUCT {
-?focus_node ?dt_1_instant
+?focus_node ?dt_1_instant
}
WHERE {
-?focus_node ?dt_1_instant
+?focus_node ?dt_1_instant
FILTER (?dt_1_instant > "2012-08-10T05:30:00+00:00"^^ || ?dt_1_instant < "2012-08-10T05:30:00+00:00"^^)
}
\ No newline at end of file
diff --git a/test_data/cql/expected_generated_queries/additional_temporal_during_intervals.rq b/test_data/cql/expected_generated_queries/additional_temporal_during_intervals.rq
index c3d32c5d..3d1b71ad 100644
--- a/test_data/cql/expected_generated_queries/additional_temporal_during_intervals.rq
+++ b/test_data/cql/expected_generated_queries/additional_temporal_during_intervals.rq
@@ -1,10 +1,10 @@
CONSTRUCT {
-?focus_node ?dt_1_end .
-?focus_node ?dt_1_start
+?focus_node ?dt_1_end .
+?focus_node ?dt_1_start
}
WHERE {
-?focus_node ?dt_1_end .
-?focus_node ?dt_1_start
+?focus_node ?dt_1_end .
+?focus_node ?dt_1_start
FILTER (?dt_1_start > "2017-06-10T07:30:00+00:00"^^ && ?dt_1_end < "2017-06-11T10:30:00+00:00"^^)
}
\ No newline at end of file
diff --git a/test_data/cql/expected_generated_queries/clause7_12.rq b/test_data/cql/expected_generated_queries/clause7_12.rq
index dd4a5abe..76f8874c 100644
--- a/test_data/cql/expected_generated_queries/clause7_12.rq
+++ b/test_data/cql/expected_generated_queries/clause7_12.rq
@@ -1,7 +1,7 @@
CONSTRUCT {
-?focus_node ?dt_1_instant
+?focus_node ?dt_1_instant
}
WHERE {
-?focus_node ?dt_1_instant
+?focus_node ?dt_1_instant
FILTER (! (?dt_1_instant > "1969-07-24T16:50:35+00:00"^^ || ?dt_1_instant < "1969-07-16T05:32:00+00:00"^^))
}
\ No newline at end of file
diff --git a/test_data/cql/expected_generated_queries/clause7_13.rq b/test_data/cql/expected_generated_queries/clause7_13.rq
index a1ce5696..8a6f8424 100644
--- a/test_data/cql/expected_generated_queries/clause7_13.rq
+++ b/test_data/cql/expected_generated_queries/clause7_13.rq
@@ -1,10 +1,10 @@
CONSTRUCT {
-?focus_node ?dt_1_end .
-?focus_node ?dt_1_start
+?focus_node ?dt_1_end .
+?focus_node ?dt_1_start
}
WHERE {
-?focus_node ?dt_1_end .
-?focus_node ?dt_1_start
+?focus_node ?dt_1_end .
+?focus_node ?dt_1_start
FILTER (?dt_1_start > "1969-07-16T13:32:00+00:00"^^ && ?dt_1_end < "1969-07-24T16:50:35+00:00"^^)
}
\ No newline at end of file
diff --git a/test_data/cql/expected_generated_queries/example20.rq b/test_data/cql/expected_generated_queries/example20.rq
index ce6a0ae3..bd5c659b 100644
--- a/test_data/cql/expected_generated_queries/example20.rq
+++ b/test_data/cql/expected_generated_queries/example20.rq
@@ -1,7 +1,7 @@
CONSTRUCT {
-?focus_node ?dt_1_instant
+?focus_node ?dt_1_instant
}
WHERE {
-?focus_node ?dt_1_instant
+?focus_node ?dt_1_instant
FILTER (?dt_1_instant < "2015-01-01T00:00:00"^^)
}
\ No newline at end of file
diff --git a/test_data/cql/expected_generated_queries/example21.rq b/test_data/cql/expected_generated_queries/example21.rq
index 1ef93f98..7ee79960 100644
--- a/test_data/cql/expected_generated_queries/example21.rq
+++ b/test_data/cql/expected_generated_queries/example21.rq
@@ -1,7 +1,7 @@
CONSTRUCT {
-?focus_node ?dt_1_instant
+?focus_node ?dt_1_instant
}
WHERE {
-?focus_node ?dt_1_instant
+?focus_node ?dt_1_instant
FILTER (?dt_1_instant > "2012-06-05T00:00:00"^^)
}
\ No newline at end of file
diff --git a/test_data/cql/expected_generated_queries/example22.rq b/test_data/cql/expected_generated_queries/example22.rq
index c3d32c5d..3d1b71ad 100644
--- a/test_data/cql/expected_generated_queries/example22.rq
+++ b/test_data/cql/expected_generated_queries/example22.rq
@@ -1,10 +1,10 @@
CONSTRUCT {
-?focus_node ?dt_1_end .
-?focus_node ?dt_1_start
+?focus_node ?dt_1_end .
+?focus_node ?dt_1_start
}
WHERE {
-?focus_node ?dt_1_end .
-?focus_node ?dt_1_start
+?focus_node ?dt_1_end .
+?focus_node ?dt_1_start
FILTER (?dt_1_start > "2017-06-10T07:30:00+00:00"^^ && ?dt_1_end < "2017-06-11T10:30:00+00:00"^^)
}
\ No newline at end of file
diff --git a/test_data/cql/expected_generated_queries/example27.rq b/test_data/cql/expected_generated_queries/example27.rq
index 373d83d4..5188a92c 100644
--- a/test_data/cql/expected_generated_queries/example27.rq
+++ b/test_data/cql/expected_generated_queries/example27.rq
@@ -1,7 +1,7 @@
CONSTRUCT {
-?focus_node ?datetime
+?focus_node ?datetime
}
WHERE {
-?focus_node ?datetime
+?focus_node ?datetime
FILTER (?datetime > "2012-06-05T00:00:00"^^)
}
\ No newline at end of file
diff --git a/test_data/cql/expected_generated_queries/example53.rq b/test_data/cql/expected_generated_queries/example53.rq
index 4f1ae886..35303e88 100644
--- a/test_data/cql/expected_generated_queries/example53.rq
+++ b/test_data/cql/expected_generated_queries/example53.rq
@@ -1,7 +1,7 @@
CONSTRUCT {
-?focus_node ?dt_1_instant
+?focus_node ?dt_1_instant
}
WHERE {
-?focus_node ?dt_1_instant
+?focus_node ?dt_1_instant
FILTER (?dt_1_instant > "2010-02-10T00:00:00"^^)
}
\ No newline at end of file
diff --git a/test_data/cql/expected_generated_queries/example54.rq b/test_data/cql/expected_generated_queries/example54.rq
index 8d1ae435..02235108 100644
--- a/test_data/cql/expected_generated_queries/example54.rq
+++ b/test_data/cql/expected_generated_queries/example54.rq
@@ -1,7 +1,7 @@
CONSTRUCT {
-?focus_node ?dt_1_instant
+?focus_node ?dt_1_instant
}
WHERE {
-?focus_node ?dt_1_instant
+?focus_node ?dt_1_instant
FILTER (?dt_1_instant < "2012-08-10T05:30:00+00:00"^^)
}
\ No newline at end of file
diff --git a/test_data/cql/expected_generated_queries/example55.rq b/test_data/cql/expected_generated_queries/example55.rq
index 1c71d701..e57ab27f 100644
--- a/test_data/cql/expected_generated_queries/example55.rq
+++ b/test_data/cql/expected_generated_queries/example55.rq
@@ -1,10 +1,10 @@
CONSTRUCT {
-?focus_node ?dt_2_end .
-?focus_node ?dt_2_start
+?focus_node ?dt_2_end .
+?focus_node ?dt_2_start
}
WHERE {
-?focus_node ?dt_2_end .
-?focus_node ?dt_2_start
+?focus_node ?dt_2_end .
+?focus_node ?dt_2_start
FILTER ("2000-01-01T00:00:00+00:00"^^ < ?dt_2_start && "2005-01-10T01:01:01.393216+00:00"^^ > ?dt_2_end)
}
\ No newline at end of file
diff --git a/test_data/cql/expected_generated_queries/example56.rq b/test_data/cql/expected_generated_queries/example56.rq
index 4cf45fcc..1c71181b 100644
--- a/test_data/cql/expected_generated_queries/example56.rq
+++ b/test_data/cql/expected_generated_queries/example56.rq
@@ -1,10 +1,10 @@
CONSTRUCT {
-?focus_node ?dt_2_end .
-?focus_node ?dt_2_start
+?focus_node ?dt_2_end .
+?focus_node ?dt_2_start
}
WHERE {
-?focus_node ?dt_2_end .
-?focus_node ?dt_2_start
+?focus_node ?dt_2_end .
+?focus_node ?dt_2_start
FILTER ("2005-01-10T01:01:01.393216+00:00"^^ < ?dt_2_start)
}
\ No newline at end of file
diff --git a/test_data/cql/expected_generated_queries/example57.rq b/test_data/cql/expected_generated_queries/example57.rq
index 1e25ab9c..879a2297 100644
--- a/test_data/cql/expected_generated_queries/example57.rq
+++ b/test_data/cql/expected_generated_queries/example57.rq
@@ -1,10 +1,10 @@
CONSTRUCT {
-?focus_node ?dt_1_end .
-?focus_node ?dt_1_start
+?focus_node ?dt_1_end .
+?focus_node ?dt_1_start
}
WHERE {
-?focus_node ?dt_1_end .
-?focus_node ?dt_1_start
+?focus_node ?dt_1_end .
+?focus_node ?dt_1_start
FILTER (?dt_1_start > "2005-01-10T00:00:00"^^ && ?dt_1_end < "2010-02-10T00:00:00"^^)
}
\ No newline at end of file
diff --git a/test_data/cql/expected_generated_queries/example58.rq b/test_data/cql/expected_generated_queries/example58.rq
index f12a8efc..94354339 100644
--- a/test_data/cql/expected_generated_queries/example58.rq
+++ b/test_data/cql/expected_generated_queries/example58.rq
@@ -1,7 +1,7 @@
CONSTRUCT {
-?focus_node ?dt_1_instant
+?focus_node ?dt_1_instant
}
WHERE {
-?focus_node ?dt_1_instant
+?focus_node ?dt_1_instant
FILTER (?dt_1_instant = "1851-04-29T00:00:00"^^)
}
\ No newline at end of file
diff --git a/test_data/cql/expected_generated_queries/example59.rq b/test_data/cql/expected_generated_queries/example59.rq
index 0990ae97..9fb46ab2 100644
--- a/test_data/cql/expected_generated_queries/example59.rq
+++ b/test_data/cql/expected_generated_queries/example59.rq
@@ -1,10 +1,10 @@
CONSTRUCT {
-?focus_node ?dt_1_end .
-?focus_node ?dt_1_start
+?focus_node ?dt_1_end .
+?focus_node ?dt_1_start
}
WHERE {
-?focus_node ?dt_1_end .
-?focus_node ?dt_1_start
+?focus_node ?dt_1_end .
+?focus_node ?dt_1_start
FILTER (?dt_1_start < "1991-10-07T08:21:06.393262+00:00"^^ && ?dt_1_end = "2010-02-10T05:29:20.073225+00:00"^^)
}
\ No newline at end of file
diff --git a/test_data/cql/expected_generated_queries/example60.rq b/test_data/cql/expected_generated_queries/example60.rq
index e601b608..7b7dd108 100644
--- a/test_data/cql/expected_generated_queries/example60.rq
+++ b/test_data/cql/expected_generated_queries/example60.rq
@@ -1,10 +1,10 @@
CONSTRUCT {
-?focus_node ?dt_1_end .
-?focus_node ?dt_1_start
+?focus_node ?dt_1_end .
+?focus_node ?dt_1_start
}
WHERE {
-?focus_node ?dt_1_end .
-?focus_node ?dt_1_start
+?focus_node ?dt_1_end .
+?focus_node ?dt_1_start
FILTER (?dt_1_start > "1991-10-07T00:00:00"^^ && ?dt_1_end = "2010-02-10T05:29:20.073225+00:00"^^)
}
\ No newline at end of file
diff --git a/test_data/cql/expected_generated_queries/example61.rq b/test_data/cql/expected_generated_queries/example61.rq
index e8ff1c78..832fad71 100644
--- a/test_data/cql/expected_generated_queries/example61.rq
+++ b/test_data/cql/expected_generated_queries/example61.rq
@@ -1,10 +1,10 @@
CONSTRUCT {
-?focus_node ?dt_1_end .
-?focus_node ?dt_1_start
+?focus_node ?dt_1_end .
+?focus_node ?dt_1_start
}
WHERE {
-?focus_node ?dt_1_end .
-?focus_node ?dt_1_start
+?focus_node ?dt_1_end .
+?focus_node ?dt_1_start
FILTER (! (?dt_1_end < "1991-10-07T08:21:06.393262+00:00"^^ || ?dt_1_start > "2010-02-10T05:29:20.073225+00:00"^^))
}
\ No newline at end of file
diff --git a/test_data/cql/expected_generated_queries/example62.rq b/test_data/cql/expected_generated_queries/example62.rq
index f9fb02ce..6ba6fbbf 100644
--- a/test_data/cql/expected_generated_queries/example62.rq
+++ b/test_data/cql/expected_generated_queries/example62.rq
@@ -1,10 +1,10 @@
CONSTRUCT {
-?focus_node ?dt_2_end .
-?focus_node ?dt_2_start
+?focus_node ?dt_2_end .
+?focus_node ?dt_2_start
}
WHERE {
-?focus_node ?dt_2_end .
-?focus_node ?dt_2_start
+?focus_node ?dt_2_end .
+?focus_node ?dt_2_start
FILTER ("2010-02-10T00:00:00"^^ = ?dt_2_start)
}
\ No newline at end of file
diff --git a/test_data/cql/expected_generated_queries/example63.rq b/test_data/cql/expected_generated_queries/example63.rq
index 0fd6c765..3170d7bd 100644
--- a/test_data/cql/expected_generated_queries/example63.rq
+++ b/test_data/cql/expected_generated_queries/example63.rq
@@ -1,10 +1,10 @@
CONSTRUCT {
-?focus_node ?dt_2_end .
-?focus_node ?dt_2_start
+?focus_node ?dt_2_end .
+?focus_node ?dt_2_start
}
WHERE {
-?focus_node ?dt_2_end .
-?focus_node ?dt_2_start
+?focus_node ?dt_2_end .
+?focus_node ?dt_2_start
FILTER ("2010-02-10T05:29:20.073225+00:00"^^ = ?dt_2_end)
}
\ No newline at end of file
diff --git a/test_data/cql/expected_generated_queries/example64.rq b/test_data/cql/expected_generated_queries/example64.rq
index d709e1bb..7aac2a9f 100644
--- a/test_data/cql/expected_generated_queries/example64.rq
+++ b/test_data/cql/expected_generated_queries/example64.rq
@@ -1,10 +1,10 @@
CONSTRUCT {
-?focus_node ?dt_2_end .
-?focus_node ?dt_2_start
+?focus_node ?dt_2_end .
+?focus_node ?dt_2_start
}
WHERE {
-?focus_node ?dt_2_end .
-?focus_node ?dt_2_start
+?focus_node ?dt_2_end .
+?focus_node ?dt_2_start
FILTER ("1991-10-07T08:21:06.393262+00:00"^^ > ?dt_2_start && "1991-10-07T08:21:06.393262+00:00"^^ < ?dt_2_end && "2010-02-10T05:29:20.073225+00:00"^^ > ?dt_2_end)
}
\ No newline at end of file
diff --git a/test_data/cql/expected_generated_queries/example65.rq b/test_data/cql/expected_generated_queries/example65.rq
index 5801e893..f7a629ab 100644
--- a/test_data/cql/expected_generated_queries/example65.rq
+++ b/test_data/cql/expected_generated_queries/example65.rq
@@ -1,10 +1,10 @@
CONSTRUCT {
-?focus_node ?dt_1_end .
-?focus_node ?dt_1_start
+?focus_node ?dt_1_end .
+?focus_node ?dt_1_start
}
WHERE {
-?focus_node ?dt_1_end .
-?focus_node ?dt_1_start
+?focus_node ?dt_1_end .
+?focus_node ?dt_1_start
FILTER (?dt_1_start < "1991-10-07T08:21:06.393262+00:00"^^ && ?dt_1_end > "1991-10-07T08:21:06.393262+00:00"^^ && ?dt_1_end < "1992-10-09T08:08:08.393473+00:00"^^)
}
\ No newline at end of file
diff --git a/test_data/cql/expected_generated_queries/example66.rq b/test_data/cql/expected_generated_queries/example66.rq
index b5d0c198..66bf2613 100644
--- a/test_data/cql/expected_generated_queries/example66.rq
+++ b/test_data/cql/expected_generated_queries/example66.rq
@@ -1,10 +1,10 @@
CONSTRUCT {
-?focus_node ?dt_2_end .
-?focus_node ?dt_2_start
+?focus_node ?dt_2_end .
+?focus_node ?dt_2_start
}
WHERE {
-?focus_node ?dt_2_end .
-?focus_node ?dt_2_start
+?focus_node ?dt_2_end .
+?focus_node ?dt_2_start
FILTER ("1991-10-07T08:21:06.393262+00:00"^^ = ?dt_2_start && "2010-02-10T05:29:20.073225+00:00"^^ > ?dt_2_end)
}
\ No newline at end of file
diff --git a/test_data/cql/expected_generated_queries/example67.rq b/test_data/cql/expected_generated_queries/example67.rq
index eca86355..809fb421 100644
--- a/test_data/cql/expected_generated_queries/example67.rq
+++ b/test_data/cql/expected_generated_queries/example67.rq
@@ -1,10 +1,10 @@
CONSTRUCT {
-?focus_node ?dt_1_end .
-?focus_node ?dt_1_start
+?focus_node ?dt_1_end .
+?focus_node ?dt_1_start
}
WHERE {
-?focus_node ?dt_1_end .
-?focus_node ?dt_1_start
+?focus_node ?dt_1_end .
+?focus_node ?dt_1_start
FILTER (?dt_1_start = "1991-10-07T08:21:06.393262+00:00"^^)
}
\ No newline at end of file
diff --git a/test_data/cql_queryable_shapes.ttl b/test_data/cql_queryable_shapes.ttl
new file mode 100644
index 00000000..5c58ec0e
--- /dev/null
+++ b/test_data/cql_queryable_shapes.ttl
@@ -0,0 +1,30 @@
+@prefix cql: .
+@prefix dcterms: .
+@prefix dwc: .
+@prefix ex: .
+@prefix sh: .
+@prefix sname: .
+@prefix sosa: .
+@prefix xsd: .
+
+ex:SpeciesQueryableShape
+ a sh:PropertyShape ;
+ a cql:Queryable ;
+ sh:path (
+ [ sh:inversePath ex:hasFeatureOfInterest ]
+ [
+ sh:zeroOrMorePath [ sh:inversePath ex:hasMember ]
+ ]
+ ex:hasSimpleResult
+ ) ;
+ sh:datatype xsd:string ;
+ sh:in (
+ "Homo sapiens"
+ "Canis lupus familiaris"
+ "Felis catus"
+ "Mus musculus"
+ "Rattus norvegicus"
+ ) ;
+ sh:name "Species Name" ;
+ dcterms:identifier "specname" ;
+ .
diff --git a/test_data/cql_queryable_shapes_bdr.ttl b/test_data/cql_queryable_shapes_bdr.ttl
new file mode 100644
index 00000000..1674879d
--- /dev/null
+++ b/test_data/cql_queryable_shapes_bdr.ttl
@@ -0,0 +1,34 @@
+@prefix cql: .
+@prefix dcterms: .
+@prefix dwc: .
+@prefix ex: .
+@prefix sh: .
+@prefix sname: .
+@prefix sosa: .
+@prefix xsd: .
+
+ex:BDRScientificNameQueryableShape
+ a sh:PropertyShape ;
+ a cql:Queryable ;
+ sh:path (
+ [ sh:inversePath sosa:hasFeatureOfInterest ]
+ sosa:hasMember
+ sosa:hasResult
+ dwc:scientificNameID
+ ) ;
+ sh:name "Scientific Name" ;
+ dcterms:identifier "scientificname" ;
+ sh:datatype xsd:string ;
+ sh:in (
+ sname:001
+ sname:002
+ sname:003
+ sname:004
+ sname:005
+ sname:006
+ sname:007
+ sname:008
+ sname:009
+ sname:010
+) ;
+.
\ No newline at end of file
diff --git a/tests/test_cql_queryable.py b/tests/test_cql_queryable.py
new file mode 100755
index 00000000..bab43b24
--- /dev/null
+++ b/tests/test_cql_queryable.py
@@ -0,0 +1,37 @@
+from pathlib import Path
+
+from rdflib import Graph, URIRef
+from sparql_grammar_pydantic import Var
+
+from prez.services.query_generation.shacl import PropertyShape
+
+test_file_1 = Path(__file__).parent.parent / f"test_data/cql_queryable_shapes.ttl"
+test_file_2 = Path(__file__).parent.parent / f"test_data/cql_queryable_shapes_bdr.ttl"
+data = Graph().parse(test_file_1, format="turtle")
+data.parse(test_file_2, format="turtle")
+
+
+def test_ps_1():
+ ps = PropertyShape(
+ uri=URIRef("http://example.com/SpeciesQueryableShape"),
+ graph=data,
+ kind="endpoint",
+ focus_node=Var(value="focus_node"),
+ )
+ assert (
+ ps.tssp_list[0].to_string()
+ == "?focus_node ^/^*/ ?path_node_2"
+ )
+
+
+def test_ps_2():
+ ps = PropertyShape(
+ uri=URIRef("http://example.com/BDRScientificNameQueryableShape"),
+ graph=data,
+ kind="endpoint",
+ focus_node=Var(value="focus_node"),
+ )
+ assert (
+ ps.tssp_list[0].to_string()
+ == "?focus_node ^/// ?path_node_2"
+ )
diff --git a/tests/test_endpoints_object.py b/tests/test_endpoints_object.py
index 3028b674..f3462422 100755
--- a/tests/test_endpoints_object.py
+++ b/tests/test_endpoints_object.py
@@ -15,7 +15,9 @@ def test_feature_collection(client):
def test_feature(client):
- r = client.get(f"/object?uri=https://example.com/spaceprez/Feature1&_mediatype=text/turtle")
+ r = client.get(
+ f"/object?uri=https://example.com/spaceprez/Feature1&_mediatype=text/turtle"
+ )
response_graph = Graph().parse(data=r.text)
assert (
URIRef("https://example.com/spaceprez/Feature1"),
diff --git a/tests/test_ogc_features_manual.py b/tests/test_ogc_features_manual.py
index 51bf074d..818e24b7 100644
--- a/tests/test_ogc_features_manual.py
+++ b/tests/test_ogc_features_manual.py
@@ -1,16 +1,24 @@
-from rdflib import Graph
-from rdflib.namespace import RDF, GEO
-
-
def test_ogc_features_root(client):
r = client.get(f"/catalogs/ex:DemoCatalog/collections/ex:GeoDataset/features")
assert r.status_code == 200
-#
-# def test_bbox_query(client):
-# r = client.get(f"/catalogs/ex:DemoCatalog/collections/ex:GeoDataset/features/collections/ex:FeatureCollection/items?bbox=4.0,4.0,6.0,6.0")
-# assert r.status_code == 200
-# g = Graph().parse(data=r.text, format="turtle")
-# # this should filter one feature but not the other
-# assert len(list(g.triples((None, RDF.type, GEO.Feature)))) == 1
+def test_ogc_features_queryables(client):
+ r = client.get(
+ f"/catalogs/ex:DemoCatalog/collections/ex:GeoDataset/features/queryables"
+ )
+ assert r.status_code == 200
+
+
+def test_bbox_200(client):
+ r = client.get(
+ f"/catalogs/ex:DemoCatalog/collections/ex:GeoDataset/features/collections/ex:FeatureCollection/items?bbox=4.0,4.0,6.0,6.0&_mediatype=application/sparql-query"
+ )
+ assert r.status_code == 200
+
+
+def test_datetime_200(client):
+ r = client.get(
+ f"/catalogs/ex:DemoCatalog/collections/ex:GeoDataset/features/collections/ex:FeatureCollection/items?datetime=2021-01-01T00:00:00Z/2021-01-02T00:00:00Z&_mediatype=application/sparql-query"
+ )
+ assert r.status_code == 200