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