From 857a27735ad6c40669689cd542686803a03aa02a Mon Sep 17 00:00:00 2001 From: Pete Gadomski Date: Wed, 27 Nov 2024 08:46:48 -0800 Subject: [PATCH 1/3] docs: add example for open --- pgstacrs.pyi | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/pgstacrs.pyi b/pgstacrs.pyi index 41a6bb9..d20e46f 100644 --- a/pgstacrs.pyi +++ b/pgstacrs.pyi @@ -13,6 +13,10 @@ class Client: Returns: A pgstac client + + Examples: + >>> from pgstacrs import Client + >>> await Client.open("postgresql://username:password@localhost:5432/pgstac") """ async def print_config(self) -> None: From bab47f3f3654f0d1734612a6d847c2e19d15b5c0 Mon Sep 17 00:00:00 2001 From: Pete Gadomski Date: Wed, 27 Nov 2024 10:22:20 -0800 Subject: [PATCH 2/3] feat: add search --- Cargo.lock | 838 ++++++++++++++++++++++++++++++++++++++++++- Cargo.toml | 4 + pgstacrs.pyi | 44 +++ src/lib.rs | 151 +++++++- tests/test_search.py | 188 ++++++++++ 5 files changed, 1214 insertions(+), 11 deletions(-) create mode 100644 tests/test_search.py diff --git a/Cargo.lock b/Cargo.lock index 8f081bc..f8de509 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -17,6 +17,58 @@ version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "512761e0bb2578dd7380c6baaa0f4ce03e84f95e960231d1dec8bf4d7d6e2627" +[[package]] +name = "ahash" +version = "0.8.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e89da841a80418a9b391ebaea17f5c112ffaaa96f621d2c285b5174da76b9011" +dependencies = [ + "cfg-if", + "getrandom", + "once_cell", + "version_check", + "zerocopy", +] + +[[package]] +name = "aho-corasick" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916" +dependencies = [ + "memchr", +] + +[[package]] +name = "android-tzdata" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0" + +[[package]] +name = "android_system_properties" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" +dependencies = [ + "libc", +] + +[[package]] +name = "appendlist" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e149dc73cd30538307e7ffa2acd3d2221148eaeed4871f246657b1c3eaa1cbd2" + +[[package]] +name = "approx" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cab112f0a86d568ea0e627cc1d6be74a1e9cd55214684db5561995f6dad897c6" +dependencies = [ + "num-traits", +] + [[package]] name = "async-trait" version = "0.1.83" @@ -49,6 +101,12 @@ dependencies = [ "windows-targets", ] +[[package]] +name = "base64" +version = "0.21.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d297deb1925b89f2ccc13d7635fa0714f12c87adce1c75356b39ca9b7178567" + [[package]] name = "base64" version = "0.22.1" @@ -79,6 +137,12 @@ dependencies = [ "tokio-postgres", ] +[[package]] +name = "bitflags" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" + [[package]] name = "bitflags" version = "2.6.0" @@ -94,6 +158,26 @@ dependencies = [ "generic-array", ] +[[package]] +name = "boon" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9672cb0edeadf721484e298c0ed4dd70b0eaa3acaed5b4fd0bd73ca32e51d814" +dependencies = [ + "ahash", + "appendlist", + "base64 0.21.7", + "fluent-uri", + "idna 0.5.0", + "once_cell", + "percent-encoding", + "regex", + "regex-syntax", + "serde", + "serde_json", + "url", +] + [[package]] name = "bumpalo" version = "3.16.0" @@ -112,12 +196,42 @@ version = "1.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9ac0150caa2ae65ca5bd83f25c7de183dea78d4d366469f148435e2acfbad0da" +[[package]] +name = "cc" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fd9de9f2205d5ef3fd67e685b0df337994ddd4495e2a28d185500d0e1edfea47" +dependencies = [ + "shlex", +] + [[package]] name = "cfg-if" version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" +[[package]] +name = "chrono" +version = "0.4.38" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a21f936df1771bf62b77f047b726c4625ff2e8aa607c01ec06e5a05bd8463401" +dependencies = [ + "android-tzdata", + "iana-time-zone", + "js-sys", + "num-traits", + "serde", + "wasm-bindgen", + "windows-targets", +] + +[[package]] +name = "core-foundation-sys" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" + [[package]] name = "cpufeatures" version = "0.2.16" @@ -127,6 +241,26 @@ dependencies = [ "libc", ] +[[package]] +name = "cql2" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f35aedcb831b10cb1f645cf3ed713cd0bc4b0cf66e53a7c8a72cebed24b66a8" +dependencies = [ + "boon", + "geo-types", + "geojson", + "geozero", + "lazy_static", + "pest", + "pest_derive", + "pg_escape", + "serde", + "serde_derive", + "serde_json", + "thiserror 2.0.3", +] + [[package]] name = "crypto-common" version = "0.1.6" @@ -148,12 +282,47 @@ dependencies = [ "subtle", ] +[[package]] +name = "displaydoc" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "equivalent" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" + [[package]] name = "fallible-iterator" version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4443176a9f2c162692bd3d352d745ef9413eec5782a80d8fd6f8a1ac692a07f7" +[[package]] +name = "fluent-uri" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "17c704e9dbe1ddd863da1e6ff3567795087b1eb201ce80d8fa81162e1516500d" +dependencies = [ + "bitflags 1.3.2", +] + +[[package]] +name = "form_urlencoded" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e13624c2627564efccf4934284bdd98cbaa14e79b0b5a141218e507b3a823456" +dependencies = [ + "percent-encoding", +] + [[package]] name = "futures" version = "0.3.31" @@ -253,6 +422,44 @@ dependencies = [ "version_check", ] +[[package]] +name = "geo-types" +version = "0.7.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6f47c611187777bbca61ea7aba780213f5f3441fd36294ab333e96cfa791b65" +dependencies = [ + "approx", + "num-traits", + "serde", +] + +[[package]] +name = "geojson" +version = "0.24.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a5d728c1df1fbf328d74151efe6cb0586f79ee813346ea981add69bd22c9241b" +dependencies = [ + "geo-types", + "log", + "serde", + "serde_json", + "thiserror 1.0.69", +] + +[[package]] +name = "geozero" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5f28f34864745eb2f123c990c6ffd92c1584bd39439b3f27ff2a0f4ea5b309b" +dependencies = [ + "geo-types", + "geojson", + "log", + "serde_json", + "thiserror 1.0.69", + "wkt", +] + [[package]] name = "getrandom" version = "0.2.15" @@ -270,6 +477,12 @@ version = "0.31.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "07e28edb80900c19c28f1072f2e8aeca7fa06b23cd4169cefe1af5aa3260783f" +[[package]] +name = "hashbrown" +version = "0.15.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf151400ff0baff5465007dd2f3e717f3fe502074ca563069ce3a6629d07b289" + [[package]] name = "heck" version = "0.5.0" @@ -291,6 +504,188 @@ dependencies = [ "digest", ] +[[package]] +name = "iana-time-zone" +version = "0.1.61" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "235e081f3925a06703c2d0117ea8b91f042756fd6e7a6e5d901e8ca1a996b220" +dependencies = [ + "android_system_properties", + "core-foundation-sys", + "iana-time-zone-haiku", + "js-sys", + "wasm-bindgen", + "windows-core", +] + +[[package]] +name = "iana-time-zone-haiku" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" +dependencies = [ + "cc", +] + +[[package]] +name = "icu_collections" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db2fa452206ebee18c4b5c2274dbf1de17008e874b4dc4f0aea9d01ca79e4526" +dependencies = [ + "displaydoc", + "yoke", + "zerofrom", + "zerovec", +] + +[[package]] +name = "icu_locid" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13acbb8371917fc971be86fc8057c41a64b521c184808a698c02acc242dbf637" +dependencies = [ + "displaydoc", + "litemap", + "tinystr", + "writeable", + "zerovec", +] + +[[package]] +name = "icu_locid_transform" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "01d11ac35de8e40fdeda00d9e1e9d92525f3f9d887cdd7aa81d727596788b54e" +dependencies = [ + "displaydoc", + "icu_locid", + "icu_locid_transform_data", + "icu_provider", + "tinystr", + "zerovec", +] + +[[package]] +name = "icu_locid_transform_data" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fdc8ff3388f852bede6b579ad4e978ab004f139284d7b28715f773507b946f6e" + +[[package]] +name = "icu_normalizer" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19ce3e0da2ec68599d193c93d088142efd7f9c5d6fc9b803774855747dc6a84f" +dependencies = [ + "displaydoc", + "icu_collections", + "icu_normalizer_data", + "icu_properties", + "icu_provider", + "smallvec", + "utf16_iter", + "utf8_iter", + "write16", + "zerovec", +] + +[[package]] +name = "icu_normalizer_data" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8cafbf7aa791e9b22bec55a167906f9e1215fd475cd22adfcf660e03e989516" + +[[package]] +name = "icu_properties" +version = "1.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93d6020766cfc6302c15dbbc9c8778c37e62c14427cb7f6e601d849e092aeef5" +dependencies = [ + "displaydoc", + "icu_collections", + "icu_locid_transform", + "icu_properties_data", + "icu_provider", + "tinystr", + "zerovec", +] + +[[package]] +name = "icu_properties_data" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67a8effbc3dd3e4ba1afa8ad918d5684b8868b3b26500753effea8d2eed19569" + +[[package]] +name = "icu_provider" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ed421c8a8ef78d3e2dbc98a973be2f3770cb42b606e3ab18d6237c4dfde68d9" +dependencies = [ + "displaydoc", + "icu_locid", + "icu_provider_macros", + "stable_deref_trait", + "tinystr", + "writeable", + "yoke", + "zerofrom", + "zerovec", +] + +[[package]] +name = "icu_provider_macros" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ec89e9337638ecdc08744df490b221a7399bf8d164eb52a665454e60e075ad6" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "idna" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "634d9b1461af396cad843f47fdba5597a4f9e6ddd4bfb6ff5d85028c25cb12f6" +dependencies = [ + "unicode-bidi", + "unicode-normalization", +] + +[[package]] +name = "idna" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "686f825264d630750a544639377bae737628043f20d38bbc029e8f29ea968a7e" +dependencies = [ + "idna_adapter", + "smallvec", + "utf8_iter", +] + +[[package]] +name = "idna_adapter" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "daca1df1c957320b2cf139ac61e7bd64fed304c5040df000a745aa1de3b4ef71" +dependencies = [ + "icu_normalizer", + "icu_properties", +] + +[[package]] +name = "indexmap" +version = "2.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "707907fe3c25f5424cce2cb7e1cbcafee6bdbe735ca90ef77c29e84591e5b9da" +dependencies = [ + "equivalent", + "hashbrown", +] + [[package]] name = "indoc" version = "2.0.5" @@ -312,12 +707,30 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "lazy_static" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" + [[package]] name = "libc" version = "0.2.164" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "433bfe06b8c75da9b2e3fbea6e5329ff87748f0b144ef75306e674c3f6f7c13f" +[[package]] +name = "libm" +version = "0.2.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8355be11b20d696c8f18f6cc018c4e372165b1fa8126cef092399c9951984ffa" + +[[package]] +name = "litemap" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ee93343901ab17bd981295f2cf0026d4ad018c7c31ba84549a4ddbb47a45104" + [[package]] name = "lock_api" version = "0.4.12" @@ -359,6 +772,12 @@ dependencies = [ "autocfg", ] +[[package]] +name = "mime" +version = "0.3.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" + [[package]] name = "miniz_oxide" version = "0.8.0" @@ -380,6 +799,16 @@ dependencies = [ "windows-sys", ] +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", + "libm", +] + [[package]] name = "object" version = "0.36.5" @@ -424,17 +853,75 @@ version = "2.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" +[[package]] +name = "pest" +version = "2.7.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "879952a81a83930934cbf1786752d6dedc3b1f29e8f8fb2ad1d0a36f377cf442" +dependencies = [ + "memchr", + "thiserror 1.0.69", + "ucd-trie", +] + +[[package]] +name = "pest_derive" +version = "2.7.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d214365f632b123a47fd913301e14c946c61d1c183ee245fa76eb752e59a02dd" +dependencies = [ + "pest", + "pest_generator", +] + +[[package]] +name = "pest_generator" +version = "2.7.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eb55586734301717aea2ac313f50b2eb8f60d2fc3dc01d190eefa2e625f60c4e" +dependencies = [ + "pest", + "pest_meta", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "pest_meta" +version = "2.7.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b75da2a70cf4d9cb76833c990ac9cd3923c9a8905a8929789ce347c84564d03d" +dependencies = [ + "once_cell", + "pest", + "sha2", +] + +[[package]] +name = "pg_escape" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44c7bc82ccbe2c7ef7ceed38dcac90d7ff46681e061e9d7310cbcd409113e303" +dependencies = [ + "phf", +] + [[package]] name = "pgstacrs" version = "0.1.0" dependencies = [ "bb8", "bb8-postgres", + "geojson", "pyo3", "pyo3-async-runtimes", "pythonize", + "serde", "serde_json", - "thiserror", + "stac", + "stac-api", + "thiserror 2.0.3", "tokio", "tokio-postgres", ] @@ -445,7 +932,31 @@ version = "0.11.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ade2d8b8f33c7333b51bcf0428d37e217e9f32192ae4772156f65063b8ce03dc" dependencies = [ + "phf_macros", + "phf_shared", +] + +[[package]] +name = "phf_generator" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48e4cc64c2ad9ebe670cb8fd69dd50ae301650392e81c05f9bfcb2d5bdbc24b0" +dependencies = [ + "phf_shared", + "rand", +] + +[[package]] +name = "phf_macros" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3444646e286606587e49f3bcf1679b8cef1dc2c5ecc29ddacaffc305180d464b" +dependencies = [ + "phf_generator", "phf_shared", + "proc-macro2", + "quote", + "syn", ] [[package]] @@ -481,7 +992,7 @@ version = "0.6.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "acda0ebdebc28befa84bee35e651e4c5f09073d668c7aed4cf7e23c3cda84b23" dependencies = [ - "base64", + "base64 0.22.1", "byteorder", "bytes", "fallible-iterator", @@ -655,9 +1166,38 @@ version = "0.5.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9b6dfecf2c74bce2466cabf93f6664d6998a69eb21e39f4207930065b27b771f" dependencies = [ - "bitflags", + "bitflags 2.6.0", ] +[[package]] +name = "regex" +version = "1.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b544ef1b4eac5dc2db33ea63606ae9ffcfac26c1416a2806ae0bf5f56b201191" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "regex-automata" +version = "0.4.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "809e8dc61f6de73b46c85f4c96486310fe304c434cfa43669d7b40f711150908" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c" + [[package]] name = "rustc-demangle" version = "0.1.24" @@ -702,12 +1242,25 @@ version = "1.0.133" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c7fceb2473b9166b2294ef05efcb65a3db80803f0b03ef86a5fc88a2b85ee377" dependencies = [ + "indexmap", "itoa", "memchr", "ryu", "serde", ] +[[package]] +name = "serde_urlencoded" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" +dependencies = [ + "form_urlencoded", + "itoa", + "ryu", + "serde", +] + [[package]] name = "sha2" version = "0.10.8" @@ -719,6 +1272,12 @@ dependencies = [ "digest", ] +[[package]] +name = "shlex" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" + [[package]] name = "siphasher" version = "0.3.11" @@ -750,6 +1309,75 @@ dependencies = [ "windows-sys", ] +[[package]] +name = "stable_deref_trait" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3" + +[[package]] +name = "stac" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "645204dbc1530cc11a70a8fd56cb881868f9d656acbf70ee04bae869320333f1" +dependencies = [ + "bytes", + "chrono", + "geojson", + "log", + "serde", + "serde_json", + "stac-derive", + "stac-types", + "thiserror 1.0.69", + "tracing", + "url", +] + +[[package]] +name = "stac-api" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "35caf5d6f6556e0f5b6d6f767a41f74c74cf1fec7be682cf0ecdd9d7eb056fac" +dependencies = [ + "chrono", + "cql2", + "geojson", + "serde", + "serde_json", + "serde_urlencoded", + "stac", + "stac-derive", + "stac-types", + "thiserror 1.0.69", + "tracing", + "url", +] + +[[package]] +name = "stac-derive" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8d8a9a71949e33dd922de39c213647e84bf401ea4a3100eabe4cb690dba2fcd9" +dependencies = [ + "quote", + "syn", +] + +[[package]] +name = "stac-types" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6f411e0d07b229199353a3a42dbd008fc0728d575e6b1966bd5c51f09062119" +dependencies = [ + "mime", + "serde", + "serde_json", + "thiserror 1.0.69", + "tracing", + "url", +] + [[package]] name = "stringprep" version = "0.1.5" @@ -778,19 +1406,50 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "synstructure" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8af7666ab7b6390ab78131fb5b0fce11d6b7a6951602017c35fa82800708971" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "target-lexicon" version = "0.12.16" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "61c41af27dd6d1e27b1b16b489db798443478cef1f06a660c96db617ba5de3b1" +[[package]] +name = "thiserror" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" +dependencies = [ + "thiserror-impl 1.0.69", +] + [[package]] name = "thiserror" version = "2.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c006c85c7651b3cf2ada4584faa36773bd07bac24acfb39f3c431b36d7e667aa" dependencies = [ - "thiserror-impl", + "thiserror-impl 2.0.3", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" +dependencies = [ + "proc-macro2", + "quote", + "syn", ] [[package]] @@ -804,6 +1463,16 @@ dependencies = [ "syn", ] +[[package]] +name = "tinystr" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9117f5d4db391c1cf6927e7bea3db74b9a1c1add8f7eda9ffd5364f40f57b82f" +dependencies = [ + "displaydoc", + "zerovec", +] + [[package]] name = "tinyvec" version = "1.8.0" @@ -874,12 +1543,49 @@ dependencies = [ "tokio", ] +[[package]] +name = "tracing" +version = "0.1.40" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3523ab5a71916ccf420eebdf5521fcef02141234bbc0b8a49f2fdc4544364ef" +dependencies = [ + "pin-project-lite", + "tracing-attributes", + "tracing-core", +] + +[[package]] +name = "tracing-attributes" +version = "0.1.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "395ae124c09f9e6918a2310af6038fba074bcf474ac352496d5910dd59a2226d" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "tracing-core" +version = "0.1.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e672c95779cf947c5311f83787af4fa8fffd12fb27e4993211a84bdfd9610f9c" +dependencies = [ + "once_cell", +] + [[package]] name = "typenum" version = "1.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "42ff0bf0c66b8238c6f3b578df37d0b7848e55df8577b3f74f92a69acceeb825" +[[package]] +name = "ucd-trie" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2896d95c02a80c6d6a5d6e953d479f5ddf2dfdb6a244441010e373ac0fb88971" + [[package]] name = "unicode-bidi" version = "0.3.17" @@ -913,6 +1619,30 @@ version = "0.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c7de7d73e1754487cb58364ee906a499937a0dfabd86bcb980fa99ec8c8fa2ce" +[[package]] +name = "url" +version = "2.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32f8b686cadd1473f4bd0117a5d28d36b1ade384ea9b5069a1c40aefed7fda60" +dependencies = [ + "form_urlencoded", + "idna 1.0.3", + "percent-encoding", + "serde", +] + +[[package]] +name = "utf16_iter" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8232dd3cdaed5356e0f716d285e4b40b932ac434100fe9b7e0e8e935b9e6246" + +[[package]] +name = "utf8_iter" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" + [[package]] name = "version_check" version = "0.9.5" @@ -1007,6 +1737,15 @@ dependencies = [ "web-sys", ] +[[package]] +name = "windows-core" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33ab640c8d7e35bf8ba19b884ba838ceb4fba93a4e8c65a9059d08afcfc683d9" +dependencies = [ + "windows-targets", +] + [[package]] name = "windows-sys" version = "0.52.0" @@ -1080,6 +1819,54 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" +[[package]] +name = "wkt" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "54f7f1ff4ea4c18936d6cd26a6fd24f0003af37e951a8e0e8b9e9a2d0bd0a46d" +dependencies = [ + "geo-types", + "log", + "num-traits", + "thiserror 1.0.69", +] + +[[package]] +name = "write16" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d1890f4022759daae28ed4fe62859b1236caebfc61ede2f63ed4e695f3f6d936" + +[[package]] +name = "writeable" +version = "0.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e9df38ee2d2c3c5948ea468a8406ff0db0b29ae1ffde1bcf20ef305bcc95c51" + +[[package]] +name = "yoke" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "120e6aef9aa629e3d4f52dc8cc43a015c7724194c97dfaf45180d2daf2b77f40" +dependencies = [ + "serde", + "stable_deref_trait", + "yoke-derive", + "zerofrom", +] + +[[package]] +name = "yoke-derive" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2380878cad4ac9aac1e2435f3eb4020e8374b5f13c296cb75b4620ff8e229154" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "synstructure", +] + [[package]] name = "zerocopy" version = "0.7.35" @@ -1100,3 +1887,46 @@ dependencies = [ "quote", "syn", ] + +[[package]] +name = "zerofrom" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cff3ee08c995dee1859d998dea82f7374f2826091dd9cd47def953cae446cd2e" +dependencies = [ + "zerofrom-derive", +] + +[[package]] +name = "zerofrom-derive" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "595eed982f7d355beb85837f651fa22e90b3c044842dc7f2c2842c086f295808" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "synstructure", +] + +[[package]] +name = "zerovec" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aa2b893d79df23bfb12d5461018d408ea19dfafe76c2c7ef6d4eba614f8ff079" +dependencies = [ + "yoke", + "zerofrom", + "zerovec-derive", +] + +[[package]] +name = "zerovec-derive" +version = "0.10.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6eafa6dfb17584ea3e2bd6e76e0cc15ad7af12b09abdd1ca55961bed9b1063c6" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] diff --git a/Cargo.toml b/Cargo.toml index 3ba3026..894b441 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -20,3 +20,7 @@ bb8 = "0.8.6" bb8-postgres = "0.8.1" serde_json = "1.0.133" pythonize = "0.23.0" +stac-api = "0.6.2" +stac = "0.11.0" +serde = "1.0.215" +geojson = "0.24.1" diff --git a/pgstacrs.pyi b/pgstacrs.pyi index d20e46f..ed51086 100644 --- a/pgstacrs.pyi +++ b/pgstacrs.pyi @@ -158,5 +158,49 @@ class Client: PgstacError: If the item cannot be found """ + async def search( + self, + *, + intersects: str | dict[str, Any] | None = None, + ids: str | list[str] | None = None, + collections: str | list[str] | None = None, + limit: int | None = None, + bbox: list[float] | None = None, + datetime: str | None = None, + include: str | list[str] | None = None, + exclude: str | list[str] | None = None, + sortby: str | list[str] | None = None, + filter: str | dict[str, Any] | None = None, + query: dict[str, Any] | None = None, + ) -> dict[str, Any]: + """ + Searches the database with STAC API item search. + + Args: + collections: Array of one or more Collection IDs that + each matching Item must be in. + ids: Array of Item ids to return. + intersects: Searches items by performing intersection between their + geometry and provided GeoJSON geometry. + bbox: Requested bounding box. + datetime: Single date+time, or a range (`/` separator), formatted to + RFC 3339, section 5.6. Use double dots .. for open date ranges. + include: Fields to include in the response (see [the extension + docs](https://github.com/stac-api-extensions/fields?tab=readme-ov-file#includeexclude-semantics)) + for more on the semantics). + exclude: Fields to exclude from the response (see [the extension + docs](https://github.com/stac-api-extensions/fields?tab=readme-ov-file#includeexclude-semantics)) + for more on the semantics). + sortby: Fields by which to sort results (use `-field` to sort descending). + filter: CQL2 filter expression. Strings will be interpreted as + cql2-text, dictionaries as cql2-json. + query: Additional filtering based on properties. + It is recommended to use filter instead, if possible. + limit: The page size returned from the server. + """ + class PgstacError: """An exception returned from pgstac""" + +class StacError: + """Something doesn't match the STAC specification""" diff --git a/src/lib.rs b/src/lib.rs index 1bfd424..1aa1529 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,5 +1,6 @@ use bb8::{Pool, RunError}; use bb8_postgres::PostgresConnectionManager; +use geojson::Geometry; use pyo3::{ create_exception, exceptions::{PyException, PyValueError}, @@ -7,14 +8,29 @@ use pyo3::{ types::{PyDict, PyList, PyType}, }; use serde_json::Value; +use stac::Bbox; +use stac_api::{Fields, Filter, Items, Search, Sortby}; use std::{future::Future, str::FromStr}; use thiserror::Error; -use tokio_postgres::{Config, NoTls}; +use tokio_postgres::{types::ToSql, Config, NoTls}; create_exception!(pgstacrs, PgstacError, PyException); +create_exception!(pgstacrs, StacError, PyException); type PgstacPool = Pool>; +#[derive(FromPyObject)] +pub enum StringOrDict { + String(String), + Dict(Py), +} + +#[derive(FromPyObject)] +pub enum StringOrList { + String(String), + List(Vec), +} + macro_rules! pgstac { (string $client:expr,$py:expr,$function:expr) => { let function = $function.to_string(); @@ -27,12 +43,16 @@ macro_rules! pgstac { }) }; - (json $client:expr,$py:expr,$function:expr) => { + (json $client:expr,$py:expr,$function:expr,$params:expr) => { let function = $function.to_string(); $client.run($py, |pool: PgstacPool| async move { - let query = format!("SELECT pgstac.{}()", function); + let param_string = (0..$params.len()) + .map(|i| format!("${}", i + 1)) + .collect::>() + .join(", "); + let query = format!("SELECT pgstac.{}({})", function, param_string); let connection = pool.get().await?; - let row = connection.query_one(&query, &[]).await?; + let row = connection.query_one(&query, &$params).await?; let value: Value = row.try_get(function.as_str())?; Ok(Json(value)) }) @@ -71,7 +91,22 @@ macro_rules! pgstac { #[derive(Debug, Error)] enum Error { #[error(transparent)] - RunError(#[from] RunError), + Geojson(#[from] geojson::Error), + + #[error(transparent)] + Run(#[from] RunError), + + #[error(transparent)] + SerdeJson(#[from] serde_json::Error), + + #[error(transparent)] + Stac(#[from] stac::Error), + + #[error(transparent)] + StacApi(#[from] stac_api::Error), + + #[error(transparent)] + Pythonize(#[from] pythonize::PythonizeError), #[error(transparent)] TokioPostgres(#[from] tokio_postgres::Error), @@ -169,7 +204,7 @@ impl Client { fn all_collections<'a>(&self, py: Python<'a>) -> PyResult> { pgstac! { - json self,py,"all_collections" + json self,py,"all_collections",[] as [&(dyn ToSql + Sync); 0] } } @@ -251,6 +286,93 @@ impl Client { void self,py,"delete_item",[&Some(id.as_str()), &collection_id.as_deref()] } } + + #[pyo3(signature = (*, collections=None, ids=None, intersects=None, bbox=None, datetime=None, include=None, exclude=None, sortby=None, filter=None, query=None, limit=None))] + fn search<'a>( + &self, + py: Python<'a>, + collections: Option, + ids: Option, + intersects: Option, + bbox: Option>, + datetime: Option, + include: Option, + exclude: Option, + sortby: Option, + filter: Option, + query: Option>, + limit: Option, + ) -> PyResult> { + // TODO refactor to use https://github.com/gadomski/stacrs/blob/1528d7e1b7185a86efe9fc7c42b0620093c5e9c6/src/search.rs#L128-L162 + let mut fields = Fields::default(); + if let Some(include) = include { + fields.include = include.into(); + } + if let Some(exclude) = exclude { + fields.exclude = exclude.into(); + } + let fields = if fields.include.is_empty() && fields.exclude.is_empty() { + None + } else { + Some(fields) + }; + let query = query + .map(|query| pythonize::depythonize(&query)) + .transpose()?; + let bbox = bbox + .map(|bbox| Bbox::try_from(bbox)) + .transpose() + .map_err(Error::from)?; + let sortby = sortby.map(|sortby| { + Vec::::from(sortby) + .into_iter() + .map(|s| s.parse::().unwrap()) // the parse is infallible + .collect::>() + }); + let filter = filter + .map(|filter| match filter { + StringOrDict::Dict(cql_json) => { + pythonize::depythonize(&cql_json.bind_borrowed(py)).map(Filter::Cql2Json) + } + StringOrDict::String(cql2_text) => Ok(Filter::Cql2Text(cql2_text)), + }) + .transpose()?; + let filter = filter + .map(|filter| filter.into_cql2_json()) + .transpose() + .map_err(Error::from)?; + let items = Items { + limit, + bbox, + datetime, + query, + fields, + sortby, + filter, + ..Default::default() + }; + + let intersects = intersects + .map(|intersects| match intersects { + StringOrDict::Dict(json) => pythonize::depythonize(&json.bind_borrowed(py)) + .map_err(Error::from) + .and_then(|json| Geometry::from_json_object(json).map_err(Error::from)), + StringOrDict::String(s) => s.parse().map_err(Error::from), + }) + .transpose()?; + let ids = ids.map(|ids| ids.into()); + let collections = collections.map(|ids| ids.into()); + let search = Search { + items, + intersects, + ids, + collections, + }; + let search = serde_json::to_value(search).map_err(Error::from)?; + pgstac! { + json self,py,"search",[&search] + } + } } impl Client { @@ -283,15 +405,30 @@ impl<'py> IntoPyObject<'py> for Json { impl From for PyErr { fn from(value: Error) -> Self { match value { - Error::RunError(err) => PgstacError::new_err(err.to_string()), + Error::Stac(err) => StacError::new_err(err.to_string()), + Error::StacApi(err) => StacError::new_err(err.to_string()), + Error::Geojson(err) => PyValueError::new_err(format!("geojson: {}", err)), + Error::SerdeJson(err) => PyValueError::new_err(err.to_string()), + Error::Pythonize(err) => PyValueError::new_err(err.to_string()), + Error::Run(err) => PgstacError::new_err(err.to_string()), Error::TokioPostgres(err) => PgstacError::new_err(format!("postgres: {err}")), } } } +impl From for Vec { + fn from(value: StringOrList) -> Vec { + match value { + StringOrList::List(list) => list, + StringOrList::String(s) => vec![s], + } + } +} + #[pymodule] fn pgstacrs(py: Python<'_>, m: &Bound<'_, PyModule>) -> PyResult<()> { m.add_class::()?; + m.add("StacError", py.get_type::())?; m.add("PgstacError", py.get_type::())?; Ok(()) } diff --git a/tests/test_search.py b/tests/test_search.py new file mode 100644 index 0000000..fae2be0 --- /dev/null +++ b/tests/test_search.py @@ -0,0 +1,188 @@ +import copy +from typing import Any + +import pytest +from pgstacrs import Client + + +async def test_empty_search(client: Client) -> None: + assert await client.search() == { + "features": [], + "links": [ + {"href": ".", "rel": "root", "type": "application/json"}, + {"href": "./search", "rel": "self", "type": "application/json"}, + ], + "numberReturned": 0, + "type": "FeatureCollection", + } + + +async def test_search( + client: Client, collection: dict[str, Any], item: dict[str, Any] +) -> None: + await client.create_collection(collection) + await client.create_item(item) + feature_collection = await client.search() + assert feature_collection["numberReturned"] == 1 + + +async def test_search_fields( + client: Client, collection: dict[str, Any], item: dict[str, Any] +) -> None: + await client.create_collection(collection) + await client.create_item(item) + + feature_collection = await client.search(include="id") + item = feature_collection["features"][0] + assert item == {"id": "20201211_223832_CS2", "collection": "simple-collection"} + + feature_collection = await client.search(exclude="id") + item = feature_collection["features"][0] + assert "id" not in item + + +@pytest.mark.skip("I'm not sure query is implemented properly in pgstac?") +async def test_search_query( + client: Client, collection: dict[str, Any], item: dict[str, Any] +) -> None: + await client.create_collection(collection) + item["properties"]["foo"] = "bar" + await client.create_item(item) + + feature_collection = await client.search(query={"query": {"foo": {"eq": "bar"}}}) + assert feature_collection["numberReturned"] == 1 + + feature_collection = await client.search(query={"query": {"foo": {"eq": "baz"}}}) + assert feature_collection["numberReturned"] == 0 + + +async def test_bbox( + client: Client, collection: dict[str, Any], item: dict[str, Any] +) -> None: + await client.create_collection(collection) + await client.create_item(item) + + feature_collection = await client.search(bbox=[170, 0, 173, 2]) + assert feature_collection["numberReturned"] == 1 + + # Looks like my postgres doesn't like 3d bboxes + # feature_collection = await client.search(bbox=[170, 0, -1000, 173, 2, 20000]) + # assert feature_collection["numberReturned"] == 1 + + feature_collection = await client.search(bbox=[0, 0, 1, 1]) + assert feature_collection["numberReturned"] == 0 + + +async def test_sortby( + client: Client, collection: dict[str, Any], item: dict[str, Any] +) -> None: + await client.create_collection(collection) + item_a = copy.deepcopy(item) + item_a["id"] = "a" + item_a["properties"]["foo"] = "a" + item_a["properties"]["bar"] = 0 + item_b = copy.deepcopy(item) + item_b["id"] = "b" + item_b["properties"]["foo"] = "b" + item_b["properties"]["bar"] = 1 + item_c = copy.deepcopy(item) + item_c["id"] = "c" + item_c["properties"]["foo"] = "c" + item_c["properties"]["bar"] = 1 + await client.create_items([item_a, item_b, item_c]) + + feature_collection = await client.search(sortby="+foo") + assert feature_collection["features"][0]["id"] == "a" + assert feature_collection["features"][1]["id"] == "b" + + feature_collection = await client.search(sortby="foo") + assert feature_collection["features"][0]["id"] == "a" + assert feature_collection["features"][1]["id"] == "b" + + feature_collection = await client.search(sortby="-foo") + assert feature_collection["features"][0]["id"] == "c" + assert feature_collection["features"][1]["id"] == "b" + + feature_collection = await client.search(sortby=["-bar", "+foo"]) + assert feature_collection["features"][0]["id"] == "b" + assert feature_collection["features"][1]["id"] == "c" + + feature_collection = await client.search(sortby=["-bar", "-foo"]) + assert feature_collection["features"][0]["id"] == "c" + assert feature_collection["features"][1]["id"] == "b" + + +async def test_filter( + client: Client, collection: dict[str, Any], item: dict[str, Any] +) -> None: + await client.create_collection(collection) + item["properties"]["foo"] = "bar" + await client.create_item(item) + + feature_collection = await client.search(filter="foo = 'bar'") + assert feature_collection["numberReturned"] == 1 + feature_collection = await client.search(filter="foo != 'bar'") + assert feature_collection["numberReturned"] == 0 + + feature_collection = await client.search( + filter={"op": "=", "args": [{"property": "foo"}, "bar"]} + ) + assert feature_collection["numberReturned"] == 1 + feature_collection = await client.search( + filter={"op": "!=", "args": [{"property": "foo"}, "bar"]} + ) + assert feature_collection["numberReturned"] == 0 + + +async def test_intersects( + client: Client, collection: dict[str, Any], item: dict[str, Any] +) -> None: + await client.create_collection(collection) + await client.create_item(item) + + feature_collection = await client.search( + intersects={"type": "Point", "coordinates": [0, 0]} + ) + assert feature_collection["numberReturned"] == 0 + + feature_collection = await client.search( + intersects={"type": "Point", "coordinates": [172.92, 1.35]} + ) + assert feature_collection["numberReturned"] == 1 + + feature_collection = await client.search( + intersects='{"type": "Point", "coordinates": [172.92, 1.35]}' + ) + assert feature_collection["numberReturned"] == 1 + + +async def test_ids( + client: Client, collection: dict[str, Any], item: dict[str, Any] +) -> None: + await client.create_collection(collection) + await client.create_item(item) + + feature_collection = await client.search(ids="not-an-id") + assert feature_collection["numberReturned"] == 0 + + feature_collection = await client.search(ids="20201211_223832_CS2") + assert feature_collection["numberReturned"] == 1 + + feature_collection = await client.search(ids=["20201211_223832_CS2"]) + assert feature_collection["numberReturned"] == 1 + + +async def test_collections( + client: Client, collection: dict[str, Any], item: dict[str, Any] +) -> None: + await client.create_collection(collection) + await client.create_item(item) + + feature_collection = await client.search(collections="not-an-id") + assert feature_collection["numberReturned"] == 0 + + feature_collection = await client.search(collections="simple-collection") + assert feature_collection["numberReturned"] == 1 + + feature_collection = await client.search(collections=["simple-collection"]) + assert feature_collection["numberReturned"] == 1 From dd404d403f3dfd07c7c4d8e7f6b25628fb050834 Mon Sep 17 00:00:00 2001 From: Pete Gadomski Date: Wed, 27 Nov 2024 10:25:05 -0800 Subject: [PATCH 3/3] feat: add rust cache to ci --- .github/workflows/ci.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 724decc..c4824c8 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -25,6 +25,7 @@ jobs: steps: - uses: actions/checkout@v4 - uses: astral-sh/setup-uv@v3 + - uses: Swatinem/rust-cache@v2 - name: Install postgis run: sudo apt-get install postgis - name: Sync dev