diff --git a/poetry.lock b/poetry.lock index f76dcac2..7197fcb8 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1,5 +1,17 @@ # This file is automatically @generated by Poetry 2.2.1 and should not be changed by hand. +[[package]] +name = "annotated-types" +version = "0.7.0" +description = "Reusable constraint types to use with typing.Annotated" +optional = false +python-versions = ">=3.8" +groups = ["main"] +files = [ + {file = "annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53"}, + {file = "annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89"}, +] + [[package]] name = "anyio" version = "4.11.0" @@ -573,6 +585,27 @@ files = [ {file = "distlib-0.4.0.tar.gz", hash = "sha256:feec40075be03a04501a973d81f633735b4b69f98b05450592310c0f401a4e0d"}, ] +[[package]] +name = "dnspython" +version = "2.8.0" +description = "DNS toolkit" +optional = false +python-versions = ">=3.10" +groups = ["main"] +files = [ + {file = "dnspython-2.8.0-py3-none-any.whl", hash = "sha256:01d9bbc4a2d76bf0db7c1f729812ded6d912bd318d3b1cf81d30c0f845dbf3af"}, + {file = "dnspython-2.8.0.tar.gz", hash = "sha256:181d3c6996452cb1189c4046c61599b84a5a86e099562ffde77d26984ff26d0f"}, +] + +[package.extras] +dev = ["black (>=25.1.0)", "coverage (>=7.0)", "flake8 (>=7)", "hypercorn (>=0.17.0)", "mypy (>=1.17)", "pylint (>=3)", "pytest (>=8.4)", "pytest-cov (>=6.2.0)", "quart-trio (>=0.12.0)", "sphinx (>=8.2.0)", "sphinx-rtd-theme (>=3.0.0)", "twine (>=6.1.0)", "wheel (>=0.45.0)"] +dnssec = ["cryptography (>=45)"] +doh = ["h2 (>=4.2.0)", "httpcore (>=1.0.0)", "httpx (>=0.28.0)"] +doq = ["aioquic (>=1.2.0)"] +idna = ["idna (>=3.10)"] +trio = ["trio (>=0.30)"] +wmi = ["wmi (>=1.5.1) ; platform_system == \"Windows\""] + [[package]] name = "docutils" version = "0.22.3" @@ -641,6 +674,22 @@ paramiko = ["paramiko"] patiencediff = ["patiencediff"] pgp = ["gpg"] +[[package]] +name = "email-validator" +version = "2.3.0" +description = "A robust email address syntax and deliverability validation library." +optional = false +python-versions = ">=3.8" +groups = ["main"] +files = [ + {file = "email_validator-2.3.0-py3-none-any.whl", hash = "sha256:80f13f623413e6b197ae73bb10bf4eb0908faf509ad8362c5edeb0be7fd450b4"}, + {file = "email_validator-2.3.0.tar.gz", hash = "sha256:9fc05c37f2f6cf439ff414f8fc46d917929974a82244c20eb10231ba60c54426"}, +] + +[package.dependencies] +dnspython = ">=2.0.0" +idna = ">=2.0.0" + [[package]] name = "fastjsonschema" version = "2.21.2" @@ -1782,6 +1831,163 @@ files = [ {file = "pycparser-2.23.tar.gz", hash = "sha256:78816d4f24add8f10a06d6f05b4d424ad9e96cfebf68a4ddc99c65c0720d00c2"}, ] +[[package]] +name = "pydantic" +version = "2.12.5" +description = "Data validation using Python type hints" +optional = false +python-versions = ">=3.9" +groups = ["main"] +files = [ + {file = "pydantic-2.12.5-py3-none-any.whl", hash = "sha256:e561593fccf61e8a20fc46dfc2dfe075b8be7d0188df33f221ad1f0139180f9d"}, + {file = "pydantic-2.12.5.tar.gz", hash = "sha256:4d351024c75c0f085a9febbb665ce8c0c6ec5d30e903bdb6394b7ede26aebb49"}, +] + +[package.dependencies] +annotated-types = ">=0.6.0" +email-validator = {version = ">=2.0.0", optional = true, markers = "extra == \"email\""} +pydantic-core = "2.41.5" +typing-extensions = ">=4.14.1" +typing-inspection = ">=0.4.2" + +[package.extras] +email = ["email-validator (>=2.0.0)"] +timezone = ["tzdata ; python_version >= \"3.9\" and platform_system == \"Windows\""] + +[[package]] +name = "pydantic-core" +version = "2.41.5" +description = "Core functionality for Pydantic validation and serialization" +optional = false +python-versions = ">=3.9" +groups = ["main"] +files = [ + {file = "pydantic_core-2.41.5-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:77b63866ca88d804225eaa4af3e664c5faf3568cea95360d21f4725ab6e07146"}, + {file = "pydantic_core-2.41.5-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:dfa8a0c812ac681395907e71e1274819dec685fec28273a28905df579ef137e2"}, + {file = "pydantic_core-2.41.5-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5921a4d3ca3aee735d9fd163808f5e8dd6c6972101e4adbda9a4667908849b97"}, + {file = "pydantic_core-2.41.5-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e25c479382d26a2a41b7ebea1043564a937db462816ea07afa8a44c0866d52f9"}, + {file = "pydantic_core-2.41.5-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f547144f2966e1e16ae626d8ce72b4cfa0caedc7fa28052001c94fb2fcaa1c52"}, + {file = "pydantic_core-2.41.5-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:6f52298fbd394f9ed112d56f3d11aabd0d5bd27beb3084cc3d8ad069483b8941"}, + {file = "pydantic_core-2.41.5-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:100baa204bb412b74fe285fb0f3a385256dad1d1879f0a5cb1499ed2e83d132a"}, + {file = "pydantic_core-2.41.5-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:05a2c8852530ad2812cb7914dc61a1125dc4e06252ee98e5638a12da6cc6fb6c"}, + {file = "pydantic_core-2.41.5-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:29452c56df2ed968d18d7e21f4ab0ac55e71dc59524872f6fc57dcf4a3249ed2"}, + {file = "pydantic_core-2.41.5-cp310-cp310-musllinux_1_1_armv7l.whl", hash = "sha256:d5160812ea7a8a2ffbe233d8da666880cad0cbaf5d4de74ae15c313213d62556"}, + {file = "pydantic_core-2.41.5-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:df3959765b553b9440adfd3c795617c352154e497a4eaf3752555cfb5da8fc49"}, + {file = "pydantic_core-2.41.5-cp310-cp310-win32.whl", hash = "sha256:1f8d33a7f4d5a7889e60dc39856d76d09333d8a6ed0f5f1190635cbec70ec4ba"}, + {file = "pydantic_core-2.41.5-cp310-cp310-win_amd64.whl", hash = "sha256:62de39db01b8d593e45871af2af9e497295db8d73b085f6bfd0b18c83c70a8f9"}, + {file = "pydantic_core-2.41.5-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:a3a52f6156e73e7ccb0f8cced536adccb7042be67cb45f9562e12b319c119da6"}, + {file = "pydantic_core-2.41.5-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:7f3bf998340c6d4b0c9a2f02d6a400e51f123b59565d74dc60d252ce888c260b"}, + {file = "pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:378bec5c66998815d224c9ca994f1e14c0c21cb95d2f52b6021cc0b2a58f2a5a"}, + {file = "pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e7b576130c69225432866fe2f4a469a85a54ade141d96fd396dffcf607b558f8"}, + {file = "pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6cb58b9c66f7e4179a2d5e0f849c48eff5c1fca560994d6eb6543abf955a149e"}, + {file = "pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:88942d3a3dff3afc8288c21e565e476fc278902ae4d6d134f1eeda118cc830b1"}, + {file = "pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f31d95a179f8d64d90f6831d71fa93290893a33148d890ba15de25642c5d075b"}, + {file = "pydantic_core-2.41.5-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:c1df3d34aced70add6f867a8cf413e299177e0c22660cc767218373d0779487b"}, + {file = "pydantic_core-2.41.5-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:4009935984bd36bd2c774e13f9a09563ce8de4abaa7226f5108262fa3e637284"}, + {file = "pydantic_core-2.41.5-cp311-cp311-musllinux_1_1_armv7l.whl", hash = "sha256:34a64bc3441dc1213096a20fe27e8e128bd3ff89921706e83c0b1ac971276594"}, + {file = "pydantic_core-2.41.5-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:c9e19dd6e28fdcaa5a1de679aec4141f691023916427ef9bae8584f9c2fb3b0e"}, + {file = "pydantic_core-2.41.5-cp311-cp311-win32.whl", hash = "sha256:2c010c6ded393148374c0f6f0bf89d206bf3217f201faa0635dcd56bd1520f6b"}, + {file = "pydantic_core-2.41.5-cp311-cp311-win_amd64.whl", hash = "sha256:76ee27c6e9c7f16f47db7a94157112a2f3a00e958bc626e2f4ee8bec5c328fbe"}, + {file = "pydantic_core-2.41.5-cp311-cp311-win_arm64.whl", hash = "sha256:4bc36bbc0b7584de96561184ad7f012478987882ebf9f9c389b23f432ea3d90f"}, + {file = "pydantic_core-2.41.5-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:f41a7489d32336dbf2199c8c0a215390a751c5b014c2c1c5366e817202e9cdf7"}, + {file = "pydantic_core-2.41.5-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:070259a8818988b9a84a449a2a7337c7f430a22acc0859c6b110aa7212a6d9c0"}, + {file = "pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e96cea19e34778f8d59fe40775a7a574d95816eb150850a85a7a4c8f4b94ac69"}, + {file = "pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ed2e99c456e3fadd05c991f8f437ef902e00eedf34320ba2b0842bd1c3ca3a75"}, + {file = "pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:65840751b72fbfd82c3c640cff9284545342a4f1eb1586ad0636955b261b0b05"}, + {file = "pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e536c98a7626a98feb2d3eaf75944ef6f3dbee447e1f841eae16f2f0a72d8ddc"}, + {file = "pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:eceb81a8d74f9267ef4081e246ffd6d129da5d87e37a77c9bde550cb04870c1c"}, + {file = "pydantic_core-2.41.5-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d38548150c39b74aeeb0ce8ee1d8e82696f4a4e16ddc6de7b1d8823f7de4b9b5"}, + {file = "pydantic_core-2.41.5-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:c23e27686783f60290e36827f9c626e63154b82b116d7fe9adba1fda36da706c"}, + {file = "pydantic_core-2.41.5-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:482c982f814460eabe1d3bb0adfdc583387bd4691ef00b90575ca0d2b6fe2294"}, + {file = "pydantic_core-2.41.5-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:bfea2a5f0b4d8d43adf9d7b8bf019fb46fdd10a2e5cde477fbcb9d1fa08c68e1"}, + {file = "pydantic_core-2.41.5-cp312-cp312-win32.whl", hash = "sha256:b74557b16e390ec12dca509bce9264c3bbd128f8a2c376eaa68003d7f327276d"}, + {file = "pydantic_core-2.41.5-cp312-cp312-win_amd64.whl", hash = "sha256:1962293292865bca8e54702b08a4f26da73adc83dd1fcf26fbc875b35d81c815"}, + {file = "pydantic_core-2.41.5-cp312-cp312-win_arm64.whl", hash = "sha256:1746d4a3d9a794cacae06a5eaaccb4b8643a131d45fbc9af23e353dc0a5ba5c3"}, + {file = "pydantic_core-2.41.5-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:941103c9be18ac8daf7b7adca8228f8ed6bb7a1849020f643b3a14d15b1924d9"}, + {file = "pydantic_core-2.41.5-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:112e305c3314f40c93998e567879e887a3160bb8689ef3d2c04b6cc62c33ac34"}, + {file = "pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0cbaad15cb0c90aa221d43c00e77bb33c93e8d36e0bf74760cd00e732d10a6a0"}, + {file = "pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:03ca43e12fab6023fc79d28ca6b39b05f794ad08ec2feccc59a339b02f2b3d33"}, + {file = "pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:dc799088c08fa04e43144b164feb0c13f9a0bc40503f8df3e9fde58a3c0c101e"}, + {file = "pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:97aeba56665b4c3235a0e52b2c2f5ae9cd071b8a8310ad27bddb3f7fb30e9aa2"}, + {file = "pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:406bf18d345822d6c21366031003612b9c77b3e29ffdb0f612367352aab7d586"}, + {file = "pydantic_core-2.41.5-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:b93590ae81f7010dbe380cdeab6f515902ebcbefe0b9327cc4804d74e93ae69d"}, + {file = "pydantic_core-2.41.5-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:01a3d0ab748ee531f4ea6c3e48ad9dac84ddba4b0d82291f87248f2f9de8d740"}, + {file = "pydantic_core-2.41.5-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:6561e94ba9dacc9c61bce40e2d6bdc3bfaa0259d3ff36ace3b1e6901936d2e3e"}, + {file = "pydantic_core-2.41.5-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:915c3d10f81bec3a74fbd4faebe8391013ba61e5a1a8d48c4455b923bdda7858"}, + {file = "pydantic_core-2.41.5-cp313-cp313-win32.whl", hash = "sha256:650ae77860b45cfa6e2cdafc42618ceafab3a2d9a3811fcfbd3bbf8ac3c40d36"}, + {file = "pydantic_core-2.41.5-cp313-cp313-win_amd64.whl", hash = "sha256:79ec52ec461e99e13791ec6508c722742ad745571f234ea6255bed38c6480f11"}, + {file = "pydantic_core-2.41.5-cp313-cp313-win_arm64.whl", hash = "sha256:3f84d5c1b4ab906093bdc1ff10484838aca54ef08de4afa9de0f5f14d69639cd"}, + {file = "pydantic_core-2.41.5-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:3f37a19d7ebcdd20b96485056ba9e8b304e27d9904d233d7b1015db320e51f0a"}, + {file = "pydantic_core-2.41.5-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:1d1d9764366c73f996edd17abb6d9d7649a7eb690006ab6adbda117717099b14"}, + {file = "pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:25e1c2af0fce638d5f1988b686f3b3ea8cd7de5f244ca147c777769e798a9cd1"}, + {file = "pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:506d766a8727beef16b7adaeb8ee6217c64fc813646b424d0804d67c16eddb66"}, + {file = "pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4819fa52133c9aa3c387b3328f25c1facc356491e6135b459f1de698ff64d869"}, + {file = "pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2b761d210c9ea91feda40d25b4efe82a1707da2ef62901466a42492c028553a2"}, + {file = "pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:22f0fb8c1c583a3b6f24df2470833b40207e907b90c928cc8d3594b76f874375"}, + {file = "pydantic_core-2.41.5-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2782c870e99878c634505236d81e5443092fba820f0373997ff75f90f68cd553"}, + {file = "pydantic_core-2.41.5-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:0177272f88ab8312479336e1d777f6b124537d47f2123f89cb37e0accea97f90"}, + {file = "pydantic_core-2.41.5-cp314-cp314-musllinux_1_1_armv7l.whl", hash = "sha256:63510af5e38f8955b8ee5687740d6ebf7c2a0886d15a6d65c32814613681bc07"}, + {file = "pydantic_core-2.41.5-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:e56ba91f47764cc14f1daacd723e3e82d1a89d783f0f5afe9c364b8bb491ccdb"}, + {file = "pydantic_core-2.41.5-cp314-cp314-win32.whl", hash = "sha256:aec5cf2fd867b4ff45b9959f8b20ea3993fc93e63c7363fe6851424c8a7e7c23"}, + {file = "pydantic_core-2.41.5-cp314-cp314-win_amd64.whl", hash = "sha256:8e7c86f27c585ef37c35e56a96363ab8de4e549a95512445b85c96d3e2f7c1bf"}, + {file = "pydantic_core-2.41.5-cp314-cp314-win_arm64.whl", hash = "sha256:e672ba74fbc2dc8eea59fb6d4aed6845e6905fc2a8afe93175d94a83ba2a01a0"}, + {file = "pydantic_core-2.41.5-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:8566def80554c3faa0e65ac30ab0932b9e3a5cd7f8323764303d468e5c37595a"}, + {file = "pydantic_core-2.41.5-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:b80aa5095cd3109962a298ce14110ae16b8c1aece8b72f9dafe81cf597ad80b3"}, + {file = "pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3006c3dd9ba34b0c094c544c6006cc79e87d8612999f1a5d43b769b89181f23c"}, + {file = "pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:72f6c8b11857a856bcfa48c86f5368439f74453563f951e473514579d44aa612"}, + {file = "pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5cb1b2f9742240e4bb26b652a5aeb840aa4b417c7748b6f8387927bc6e45e40d"}, + {file = "pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:bd3d54f38609ff308209bd43acea66061494157703364ae40c951f83ba99a1a9"}, + {file = "pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2ff4321e56e879ee8d2a879501c8e469414d948f4aba74a2d4593184eb326660"}, + {file = "pydantic_core-2.41.5-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d0d2568a8c11bf8225044aa94409e21da0cb09dcdafe9ecd10250b2baad531a9"}, + {file = "pydantic_core-2.41.5-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:a39455728aabd58ceabb03c90e12f71fd30fa69615760a075b9fec596456ccc3"}, + {file = "pydantic_core-2.41.5-cp314-cp314t-musllinux_1_1_armv7l.whl", hash = "sha256:239edca560d05757817c13dc17c50766136d21f7cd0fac50295499ae24f90fdf"}, + {file = "pydantic_core-2.41.5-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:2a5e06546e19f24c6a96a129142a75cee553cc018ffee48a460059b1185f4470"}, + {file = "pydantic_core-2.41.5-cp314-cp314t-win32.whl", hash = "sha256:b4ececa40ac28afa90871c2cc2b9ffd2ff0bf749380fbdf57d165fd23da353aa"}, + {file = "pydantic_core-2.41.5-cp314-cp314t-win_amd64.whl", hash = "sha256:80aa89cad80b32a912a65332f64a4450ed00966111b6615ca6816153d3585a8c"}, + {file = "pydantic_core-2.41.5-cp314-cp314t-win_arm64.whl", hash = "sha256:35b44f37a3199f771c3eaa53051bc8a70cd7b54f333531c59e29fd4db5d15008"}, + {file = "pydantic_core-2.41.5-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:8bfeaf8735be79f225f3fefab7f941c712aaca36f1128c9d7e2352ee1aa87bdf"}, + {file = "pydantic_core-2.41.5-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:346285d28e4c8017da95144c7f3acd42740d637ff41946af5ce6e5e420502dd5"}, + {file = "pydantic_core-2.41.5-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a75dafbf87d6276ddc5b2bf6fae5254e3d0876b626eb24969a574fff9149ee5d"}, + {file = "pydantic_core-2.41.5-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:7b93a4d08587e2b7e7882de461e82b6ed76d9026ce91ca7915e740ecc7855f60"}, + {file = "pydantic_core-2.41.5-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e8465ab91a4bd96d36dde3263f06caa6a8a6019e4113f24dc753d79a8b3a3f82"}, + {file = "pydantic_core-2.41.5-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:299e0a22e7ae2b85c1a57f104538b2656e8ab1873511fd718a1c1c6f149b77b5"}, + {file = "pydantic_core-2.41.5-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:707625ef0983fcfb461acfaf14de2067c5942c6bb0f3b4c99158bed6fedd3cf3"}, + {file = "pydantic_core-2.41.5-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:f41eb9797986d6ebac5e8edff36d5cef9de40def462311b3eb3eeded1431e425"}, + {file = "pydantic_core-2.41.5-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:0384e2e1021894b1ff5a786dbf94771e2986ebe2869533874d7e43bc79c6f504"}, + {file = "pydantic_core-2.41.5-cp39-cp39-musllinux_1_1_armv7l.whl", hash = "sha256:f0cd744688278965817fd0839c4a4116add48d23890d468bc436f78beb28abf5"}, + {file = "pydantic_core-2.41.5-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:753e230374206729bf0a807954bcc6c150d3743928a73faffee51ac6557a03c3"}, + {file = "pydantic_core-2.41.5-cp39-cp39-win32.whl", hash = "sha256:873e0d5b4fb9b89ef7c2d2a963ea7d02879d9da0da8d9d4933dee8ee86a8b460"}, + {file = "pydantic_core-2.41.5-cp39-cp39-win_amd64.whl", hash = "sha256:e4f4a984405e91527a0d62649ee21138f8e3d0ef103be488c1dc11a80d7f184b"}, + {file = "pydantic_core-2.41.5-graalpy311-graalpy242_311_native-macosx_10_12_x86_64.whl", hash = "sha256:b96d5f26b05d03cc60f11a7761a5ded1741da411e7fe0909e27a5e6a0cb7b034"}, + {file = "pydantic_core-2.41.5-graalpy311-graalpy242_311_native-macosx_11_0_arm64.whl", hash = "sha256:634e8609e89ceecea15e2d61bc9ac3718caaaa71963717bf3c8f38bfde64242c"}, + {file = "pydantic_core-2.41.5-graalpy311-graalpy242_311_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:93e8740d7503eb008aa2df04d3b9735f845d43ae845e6dcd2be0b55a2da43cd2"}, + {file = "pydantic_core-2.41.5-graalpy311-graalpy242_311_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f15489ba13d61f670dcc96772e733aad1a6f9c429cc27574c6cdaed82d0146ad"}, + {file = "pydantic_core-2.41.5-graalpy312-graalpy250_312_native-macosx_10_12_x86_64.whl", hash = "sha256:7da7087d756b19037bc2c06edc6c170eeef3c3bafcb8f532ff17d64dc427adfd"}, + {file = "pydantic_core-2.41.5-graalpy312-graalpy250_312_native-macosx_11_0_arm64.whl", hash = "sha256:aabf5777b5c8ca26f7824cb4a120a740c9588ed58df9b2d196ce92fba42ff8dc"}, + {file = "pydantic_core-2.41.5-graalpy312-graalpy250_312_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c007fe8a43d43b3969e8469004e9845944f1a80e6acd47c150856bb87f230c56"}, + {file = "pydantic_core-2.41.5-graalpy312-graalpy250_312_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:76d0819de158cd855d1cbb8fcafdf6f5cf1eb8e470abe056d5d161106e38062b"}, + {file = "pydantic_core-2.41.5-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:b5819cd790dbf0c5eb9f82c73c16b39a65dd6dd4d1439dcdea7816ec9adddab8"}, + {file = "pydantic_core-2.41.5-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:5a4e67afbc95fa5c34cf27d9089bca7fcab4e51e57278d710320a70b956d1b9a"}, + {file = "pydantic_core-2.41.5-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ece5c59f0ce7d001e017643d8d24da587ea1f74f6993467d85ae8a5ef9d4f42b"}, + {file = "pydantic_core-2.41.5-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:16f80f7abe3351f8ea6858914ddc8c77e02578544a0ebc15b4c2e1a0e813b0b2"}, + {file = "pydantic_core-2.41.5-pp310-pypy310_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:33cb885e759a705b426baada1fe68cbb0a2e68e34c5d0d0289a364cf01709093"}, + {file = "pydantic_core-2.41.5-pp310-pypy310_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:c8d8b4eb992936023be7dee581270af5c6e0697a8559895f527f5b7105ecd36a"}, + {file = "pydantic_core-2.41.5-pp310-pypy310_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:242a206cd0318f95cd21bdacff3fcc3aab23e79bba5cac3db5a841c9ef9c6963"}, + {file = "pydantic_core-2.41.5-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:d3a978c4f57a597908b7e697229d996d77a6d3c94901e9edee593adada95ce1a"}, + {file = "pydantic_core-2.41.5-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:b2379fa7ed44ddecb5bfe4e48577d752db9fc10be00a6b7446e9663ba143de26"}, + {file = "pydantic_core-2.41.5-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:266fb4cbf5e3cbd0b53669a6d1b039c45e3ce651fd5442eff4d07c2cc8d66808"}, + {file = "pydantic_core-2.41.5-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:58133647260ea01e4d0500089a8c4f07bd7aa6ce109682b1426394988d8aaacc"}, + {file = "pydantic_core-2.41.5-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:287dad91cfb551c363dc62899a80e9e14da1f0e2b6ebde82c806612ca2a13ef1"}, + {file = "pydantic_core-2.41.5-pp311-pypy311_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:03b77d184b9eb40240ae9fd676ca364ce1085f203e1b1256f8ab9984dca80a84"}, + {file = "pydantic_core-2.41.5-pp311-pypy311_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:a668ce24de96165bb239160b3d854943128f4334822900534f2fe947930e5770"}, + {file = "pydantic_core-2.41.5-pp311-pypy311_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:f14f8f046c14563f8eb3f45f499cc658ab8d10072961e07225e507adb700e93f"}, + {file = "pydantic_core-2.41.5-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:56121965f7a4dc965bff783d70b907ddf3d57f6eba29b6d2e5dabfaf07799c51"}, + {file = "pydantic_core-2.41.5.tar.gz", hash = "sha256:08daa51ea16ad373ffd5e7606252cc32f07bc72b28284b6bc9c6df804816476e"}, +] + +[package.dependencies] +typing-extensions = ">=4.14.1" + [[package]] name = "pygments" version = "2.19.2" @@ -2723,6 +2929,21 @@ files = [ ] markers = {dev = "python_version < \"3.13\""} +[[package]] +name = "typing-inspection" +version = "0.4.2" +description = "Runtime typing introspection tools" +optional = false +python-versions = ">=3.9" +groups = ["main"] +files = [ + {file = "typing_inspection-0.4.2-py3-none-any.whl", hash = "sha256:4ed1cacbdc298c220f1bd249ed5287caa16f34d44ef4e9c3d0cbad5b521545e7"}, + {file = "typing_inspection-0.4.2.tar.gz", hash = "sha256:ba561c48a67c5958007083d386c3295464928b01faa735ab8547c5692e87f464"}, +] + +[package.dependencies] +typing-extensions = ">=4.12.0" + [[package]] name = "urllib3" version = "2.5.0" @@ -2980,4 +3201,4 @@ cffi = ["cffi (>=1.17,<2.0) ; platform_python_implementation != \"PyPy\" and pyt [metadata] lock-version = "2.1" python-versions = "^3.11" -content-hash = "6b3c6b2de2d5ea6aa1f27f14afe34e410b47ad4e1d78f310c0bf71d652213536" +content-hash = "24d86cc6deb1abf05569dd83eb4a9ba23430086cb04d852a8f7d50d9202bb15d" diff --git a/pyproject.toml b/pyproject.toml index f270177c..002292e5 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "cdot-wzdx-translator" -version = "1.4.5" +version = "1.4.6rc1" description = "CDOT Work Zone WZDx Translators" authors = ["CDOT "] license = "MIT" @@ -22,6 +22,8 @@ regex = ">=2024.4.16,<2026.0.0" pyproj = "^3.4.0" google-cloud-monitoring = "^2.13.0" google-cloud-storage = ">=2.7.0,<4.0.0" +pydantic = {extras = ["email"], version = "^2.12.5"} +email-validator = "^2.3.0" [tool.poetry.group.dev.dependencies] pytest = ">=7.0.0,<10.0.0" diff --git a/requirements.txt b/requirements.txt index fdb311b2..51dc4136 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,3 +1,4 @@ +annotated-types==0.7.0 ; python_version >= "3.11" and python_version < "4.0" anyio==4.11.0 ; python_version >= "3.11" and python_version < "4.0" attrs==25.4.0 ; python_version >= "3.11" and python_version < "4.0" backports-tarfile==1.2.0 ; python_version == "3.11" @@ -13,8 +14,10 @@ coverage==7.11.3 ; python_version >= "3.11" and python_version < "4.0" crashtest==0.4.1 ; python_version >= "3.11" and python_version < "4.0" cryptography==46.0.3 ; python_version >= "3.11" and python_version < "4.0" and sys_platform == "linux" distlib==0.4.0 ; python_version >= "3.11" and python_version < "4.0" +dnspython==2.8.0 ; python_version >= "3.11" and python_version < "4.0" docutils==0.22.3 ; python_version >= "3.11" and python_version < "4.0" dulwich==0.24.10 ; python_version >= "3.11" and python_version < "4.0" +email-validator==2.3.0 ; python_version >= "3.11" and python_version < "4.0" fastjsonschema==2.21.2 ; python_version >= "3.11" and python_version < "4.0" filelock==3.20.0 ; python_version >= "3.11" and python_version < "4.0" findpython==0.7.0 ; python_version >= "3.11" and python_version < "4.0" @@ -64,6 +67,8 @@ protobuf==6.33.0 ; python_version >= "3.11" and python_version < "4.0" pyasn1-modules==0.4.2 ; python_version >= "3.11" and python_version < "4.0" pyasn1==0.6.1 ; python_version >= "3.11" and python_version < "4.0" pycparser==2.23 ; python_version >= "3.11" and python_version < "4.0" and (sys_platform == "darwin" or platform_python_implementation != "PyPy") and (sys_platform == "darwin" or sys_platform == "linux") and implementation_name != "PyPy" +pydantic-core==2.41.5 ; python_version >= "3.11" and python_version < "4.0" +pydantic==2.12.5 ; python_version >= "3.11" and python_version < "4.0" pygments==2.19.2 ; python_version >= "3.11" and python_version < "4.0" pyproj==3.7.2 ; python_version >= "3.11" and python_version < "4.0" pyproject-hooks==1.2.0 ; python_version >= "3.11" and python_version < "4.0" @@ -92,6 +97,7 @@ tomlkit==0.13.3 ; python_version >= "3.11" and python_version < "4.0" trove-classifiers==2025.9.11.17 ; python_version >= "3.11" and python_version < "4.0" twine==6.2.0 ; python_version >= "3.11" and python_version < "4.0" typing-extensions==4.15.0 ; python_version >= "3.11" and python_version < "4.0" +typing-inspection==0.4.2 ; python_version >= "3.11" and python_version < "4.0" urllib3==2.5.0 ; python_version >= "3.11" and python_version < "4.0" virtualenv==20.35.4 ; python_version >= "3.11" and python_version < "4.0" xattr==1.3.0 ; python_version >= "3.11" and python_version < "4.0" and sys_platform == "darwin" diff --git a/tests/data/models/field_device_feed_icone_final.json b/tests/data/models/field_device_feed_icone_final.json new file mode 100644 index 00000000..d24acc0b --- /dev/null +++ b/tests/data/models/field_device_feed_icone_final.json @@ -0,0 +1,57 @@ +{ + "feed_info": { + "update_date": "2025-12-18T20:34:51.150000Z", + "publisher": "iCone Products LLC", + "contact_email": "support@iconeproducts.com", + "version": "4.2", + "data_sources": [ + { + "data_source_id": "67899A97-0F3E-4683-B169-75C09C3B8F67", + "update_date": "2025-12-18T20:34:51.150000Z", + "organization_name": "iCone Products LLC", + "contact_email": "support@iconeproducts.com" + } + ] + }, + "type": "FeatureCollection", + "features": [ + { + "id": "E595E296-B1DE-4911-9454-1F2D54AC2EBD", + "type": "Feature", + "geometry": { + "type": "Point", + "coordinates": [-104.7752009, 39.4983242] + }, + "properties": { + "core_details": { + "device_type": "arrow-board", + "data_source_id": "67899A97-0F3E-4683-B169-75C09C3B8F67", + "device_status": "ok", + "update_date": "2025-12-18T20:30:27Z", + "has_automatic_location": true, + "description": "Roadwork - Caution" + }, + "pattern": "four-corners-flashing" + } + }, + { + "id": "0E1E3B5B-D06E-4390-ABB3-C89091E246F0", + "type": "Feature", + "geometry": { + "type": "Point", + "coordinates": [-106.0079266, 39.6531149] + }, + "properties": { + "core_details": { + "device_type": "location-marker", + "data_source_id": "67899A97-0F3E-4683-B169-75C09C3B8F67", + "device_status": "ok", + "update_date": "2025-12-18T20:19:13Z", + "has_automatic_location": true, + "description": "Roadwork Active" + }, + "marked_locations": [{"type": "work-truck-with-lights-flashing"}] + } + } + ] + } \ No newline at end of file diff --git a/tests/data/models/field_device_feed_icone_raw.json b/tests/data/models/field_device_feed_icone_raw.json new file mode 100644 index 00000000..cfd22292 --- /dev/null +++ b/tests/data/models/field_device_feed_icone_raw.json @@ -0,0 +1,94 @@ +[ + { + "feed_info": { + "update_date": "2025-12-18T20:34:51.1500000Z", + "publisher": "iCone Products LLC", + "contact_email": "support@iconeproducts.com", + "version": "4.2", + "data_sources": [ + { + "data_source_id": "67899A97-0F3E-4683-B169-75C09C3B8F67", + "update_date": "2025-12-18T20:34:51.1500000Z", + "organization_name": "iCone Products LLC", + "contact_email": "support@iconeproducts.com" + } + ], + "custom": { + "oldest_feature": "2025-12-17T20:34:51.0300000Z", + "oldest_location": "2025-12-17T20:34:51.0300000Z", + "username": "cdotfeeds", + "active_only": false, + "require_location": false, + "allow_custom_enums": true, + "include_custom": true, + "force_spec_required": false + } + }, + "type": "FeatureCollection", + "features": [ + { + "id": "E595E296-B1DE-4911-9454-1F2D54AC2EBD", + "type": "Feature", + "geometry": { + "type": "Point", + "coordinates": [ + -104.7752009, + 39.4983242 + ] + }, + "properties": { + "core_details": { + "device_type": "arrow-board", + "data_source_id": "67899A97-0F3E-4683-B169-75C09C3B8F67", + "device_status": "ok", + "update_date": "2025-12-18T20:30:27Z", + "has_automatic_location": true, + "description": "Roadwork - Caution" + }, + "pattern": "four-corners-flashing", + "custom": { + "start_date": "2025-12-16T16:16:08", + "waze_incident": { + "type": "CONSTRUCTION", + "description": "Roadwork - Caution" + } + } + } + }, + { + "id": "0E1E3B5B-D06E-4390-ABB3-C89091E246F0", + "type": "Feature", + "geometry": { + "type": "Point", + "coordinates": [ + -106.0079266, + 39.6531149 + ] + }, + "properties": { + "core_details": { + "device_type": "location-marker", + "data_source_id": "67899A97-0F3E-4683-B169-75C09C3B8F67", + "device_status": "ok", + "update_date": "2025-12-18T20:19:13Z", + "has_automatic_location": true, + "description": "Roadwork Active" + }, + "marked_locations": [ + { + "type": "work-truck-with-lights-flashing" + } + ], + "custom": { + "isActive": true, + "start_date": "2025-12-18T20:08:16.1200000", + "waze_incident": { + "type": "HAZARD", + "description": "Roadwork Active" + } + } + } + } + ] + } + ] \ No newline at end of file diff --git a/tests/models/field_device_feed_test.py b/tests/models/field_device_feed_test.py new file mode 100644 index 00000000..bc2c44b4 --- /dev/null +++ b/tests/models/field_device_feed_test.py @@ -0,0 +1,57 @@ +from pydantic import TypeAdapter +from wzdx.models.field_device_feed.device_feed import DeviceFeed +import json + + +def test_deserialization(): + """Test deserialization and serialization of field device feed""" + # Load and deserialize JSON + with open("./tests/data/models/field_device_feed_icone_raw.json") as f: + json_string = f.read() + + adapter = TypeAdapter(list[DeviceFeed]) + device_feed_list: list[DeviceFeed] = adapter.validate_json(json_string) + + # Validate structure + assert len(device_feed_list) == 1, "Expected exactly one device feed" + device_feed = device_feed_list[0] + assert len(device_feed.features) > 0, "Expected at least one feature" + + # Validate first feature properties + first_feature = device_feed.features[0] + assert first_feature.id is not None, "Feature should have an ID" + assert first_feature.properties.core_details.device_status is not None + assert first_feature.properties.core_details.update_date is not None + + # Load expected output + with open("./tests/data/models/field_device_feed_icone_final.json") as f: + expected_object = json.load(f) + + # Compare serialized output with expected + actual_output = device_feed.model_dump( + by_alias=True, exclude_none=True, mode="json" + ) + assert actual_output == expected_object, "Serialized output should match expected" + + +def test_roundtrip_serialization(): + """Test that serialize -> deserialize produces identical results""" + # Load original data + with open("./tests/data/models/field_device_feed_icone_raw.json") as f: + json_string = f.read() + + adapter = TypeAdapter(list[DeviceFeed]) + + # First deserialization + device_feed_list_1 = adapter.validate_json(json_string) + + # Serialize and deserialize again + json_output = adapter.dump_json( + device_feed_list_1, by_alias=True, exclude_none=True + ) + device_feed_list_2 = adapter.validate_json(json_output) + + # Should be identical + assert ( + device_feed_list_1 == device_feed_list_2 + ), "Roundtrip serialization should be stable" diff --git a/tests/tools/date_tools_test.py b/tests/tools/date_tools_test.py index 26a600fa..bf5aac94 100644 --- a/tests/tools/date_tools_test.py +++ b/tests/tools/date_tools_test.py @@ -40,6 +40,28 @@ def test_parse_datetime_from_unix_invalid_dict(): assert actual == expected +# --------------------------------------------------------------------------------unit test for datetime_from_unix function-------------------------------------------------------------------------------- +def test_datetime_from_unix_valid(): + time = 1609398000 + expected = datetime(2020, 12, 31, 7, tzinfo=timezone.utc) + actual = date_tools.datetime_from_unix(time) + assert actual == expected + + +def test_datetime_from_unix_decimal(): + time = 1615866698.393723 + expected = datetime(2021, 3, 16, 3, 51, 38, tzinfo=timezone.utc) + actual = date_tools.datetime_from_unix(time) + assert abs(actual - expected) < timedelta(seconds=1) + + +def test_datetime_from_unix_too_large(): + time = 32536850400 + expected = datetime(1971, 1, 12, 14, 0, 50, tzinfo=timezone.utc) + actual = date_tools.datetime_from_unix(time) + assert abs(actual - expected) < timedelta(seconds=1) + + # --------------------------------------------------------------------------------unit test for parse_datetime_from_iso_string function-------------------------------------------------------------------------------- def test_parse_datetime_from_iso_string_valid(): time_string = "2020-12-31T07:00:00Z" diff --git a/wzdx/models/__init__.py b/wzdx/models/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/wzdx/models/feed_info/__init__.py b/wzdx/models/feed_info/__init__.py new file mode 100644 index 00000000..75e046d3 --- /dev/null +++ b/wzdx/models/feed_info/__init__.py @@ -0,0 +1,7 @@ +from .feed_info import FeedInfo +from .feed_data_source import FeedDataSource + +__all__ = [ + "FeedInfo", + "FeedDataSource" +] diff --git a/wzdx/models/feed_info/feed_data_source.py b/wzdx/models/feed_info/feed_data_source.py new file mode 100644 index 00000000..ed0e1b57 --- /dev/null +++ b/wzdx/models/feed_info/feed_data_source.py @@ -0,0 +1,40 @@ +from typing import Optional +from datetime import datetime +from pydantic import BaseModel, EmailStr, Field + + +class FeedDataSource(BaseModel): + """ + The FeedDataSource object describes information about a specific data source used to build a data feed. A WZDx feed must contain at least one FeedDataSource, included as an entry in the data_sources array of the FeedInfo object. + + Documentation: https://github.com/usdot-jpo-ode/wzdx/blob/develop/spec-content/objects/FeedDataSource.md + """ + + data_source_id: str = Field( + alias="data_source_id", + description="A unique identifier for the data source organization providing work zone data. It is recommended that this identifier is a Universally Unique Identifier (UUID) as defined in RFC 4122 to guarantee uniqueness between feeds and over time.", + ) + organization_name: str = Field( + alias="organization_name", + description="The name of the organization for the authoritative source of the work zone data.", + ) + update_date: Optional[datetime] = Field( + default=None, + alias="update_date", + description="The UTC date and time when the data source was last updated.", + ) + update_frequency: Optional[int] = Field( + default=None, + alias="update_frequency", + description="The frequency in seconds at which the data source is updated.", + ) + contact_name: Optional[str] = Field( + default=None, + alias="contact_name", + description="The name of the individual or group responsible for the data source.", + ) + contact_email: Optional[EmailStr] = Field( + default=None, + alias="contact_email", + description="The email address of the individual or group responsible for the data source.", + ) diff --git a/wzdx/models/feed_info/feed_info.py b/wzdx/models/feed_info/feed_info.py new file mode 100644 index 00000000..9d77ad70 --- /dev/null +++ b/wzdx/models/feed_info/feed_info.py @@ -0,0 +1,49 @@ +from typing import Optional +from pydantic import BaseModel, Field, EmailStr +from .feed_data_source import FeedDataSource +from datetime import datetime + + +class FeedInfo(BaseModel): + """ + The FeedInfo object describes WZDx feed header information such as metadata, contact information, and data sources. There is one FeedInfo per WZDx GeoJSON document. + + Documentation: https://github.com/usdot-jpo-ode/wzdx/blob/develop/spec-content/objects/FeedInfo.md + """ + + publisher: str = Field( + alias="publisher", + description="The organization responsible for publishing the feed.", + ) + version: str = Field( + alias="version", + description="The WZDx specification version used to create the data feed in major.minor format. Note this mandates that all data in a WZDx feed complies to a single version of WZDx.", + ) + license: Optional[str] = Field( + default=None, + alias="license", + description='The URL of the license that applies to the data in the WZDx feed. This must be the string "https://creativecommons.org/publicdomain/zero/1.0/"', + ) + data_sources: list[FeedDataSource] = Field( + alias="data_sources", + description="A list of specific data sources for the road event data in the feed.", + ) + update_date: datetime = Field( + alias="update_date", + description="The UTC date and time when the GeoJSON file (representing the instance of the feed) was generated.", + ) + update_frequency: Optional[int] = Field( + default=None, + alias="update_frequency", + description="The frequency in seconds at which the data feed is updated.", + ) + contact_name: Optional[str] = Field( + default=None, + alias="contact_name", + description="The name of the individual or group responsible for the data feed.", + ) + contact_email: Optional[EmailStr] = Field( + default=None, + alias="contact_email", + description="The email address of the individual or group responsible for the data feed.", + ) diff --git a/wzdx/models/field_device_feed/__init__.py b/wzdx/models/field_device_feed/__init__.py new file mode 100644 index 00000000..4c6b6dfd --- /dev/null +++ b/wzdx/models/field_device_feed/__init__.py @@ -0,0 +1,47 @@ +from .device_feed import DeviceFeed +from .field_device_feature import FieldDeviceFeature +from .field_device_core_details import FieldDeviceCoreDetails +from .field_device_status import FieldDeviceStatus +from .field_device_type import FieldDeviceType +from .properties import ( + FieldDeviceProperties, + ArrowBoard, + ArrowBoardPattern, + Camera, + DynamicMessageSign, + FlashingBeacon, + FlashingBeaconFunction, + HybridSign, + HybridSignDynamicMessageFunction, + LocationMarker, + MarkedLocation, + MarkedLocationType, + TrafficSensor, + TrafficSensorLaneData, + TrafficSignal, + TrafficSignalMode, +) + +__all__ = [ + "DeviceFeed", + "FieldDeviceFeature", + "FieldDeviceCoreDetails", + "FieldDeviceStatus", + "FieldDeviceType", + "FieldDeviceProperties", + "ArrowBoard", + "ArrowBoardPattern", + "Camera", + "DynamicMessageSign", + "FlashingBeacon", + "FlashingBeaconFunction", + "HybridSign", + "HybridSignDynamicMessageFunction", + "LocationMarker", + "MarkedLocation", + "MarkedLocationType", + "TrafficSensor", + "TrafficSensorLaneData", + "TrafficSignal", + "TrafficSignalMode", +] diff --git a/wzdx/models/field_device_feed/device_feed.py b/wzdx/models/field_device_feed/device_feed.py new file mode 100644 index 00000000..b437eb6e --- /dev/null +++ b/wzdx/models/field_device_feed/device_feed.py @@ -0,0 +1,33 @@ +from typing import Literal, Optional +from pydantic import BaseModel, Field + +from ..feed_info.feed_info import FeedInfo + +from .field_device_feature import FieldDeviceFeature + + +class DeviceFeed(BaseModel): + """ + The DeviceFeed object is the root (highest level) object of a WZDx Device Feed. There is one DeviceFeed object per feed GeoJSON document. The DeviceFeed is a [GeoJSON FeatureCollection](https://datatracker.ietf.org/doc/html/rfc7946#section-3.3) object. + + The DeviceFeed contains information (location, status, live data) about field devices deployed on the roadway in work zones. + + Documentation: https://github.com/usdot-jpo-ode/wzdx/blob/develop/spec-content/objects/DeviceFeed.md + """ + + feed_info: FeedInfo = Field( + alias="feed_info", description="Information about the data feed." + ) + type: Literal["FeatureCollection"] = Field( + default="FeatureCollection", + alias="type", + description="The GeoJSON object type. For WZDx, this must be the string FeatureCollection.", + ) + features: list[FieldDeviceFeature] = Field( + alias="features", + description="An array of GeoJSON [Feature](https://datatracker.ietf.org/doc/html/rfc7946#section-3.2) objects which each represent a field device deployed in a work zone.", + ) + bbox: Optional[list[float]] = Field( + default=None, + description="Information on the coordinate range for all FieldDeviceFeatures in the feed. The value must be an array of length 2n where n is the number of dimensions represented in the contained geometries, with all axes of the most southwesterly point followed by all axes of the more northeasterly point. The axes order of a bbox follows the axes order of geometries.", + ) diff --git a/wzdx/models/field_device_feed/field_device_core_details.py b/wzdx/models/field_device_feed/field_device_core_details.py new file mode 100644 index 00000000..0044ab03 --- /dev/null +++ b/wzdx/models/field_device_feed/field_device_core_details.py @@ -0,0 +1,92 @@ +from typing import Optional +from pydantic import BaseModel, Field +from datetime import datetime + +from .field_device_type import FieldDeviceType +from .field_device_status import FieldDeviceStatus +from ..enums import Direction + + +class FieldDeviceCoreDetails(BaseModel): + """ + The FieldDeviceCoreDetails object represents the core details—both configuration and current state—of a field device that are shared by all types of field devices. + The FieldDeviceCoreDetails object cannot occur directly in a data feed and does not represent a field device on its own. It is used as the value of the core_details + property on every specific type of field device, each represented by its own object. + + Documentation: https://github.com/usdot-jpo-ode/wzdx/blob/develop/spec-content/objects/FieldDeviceCoreDetails.md + """ + + device_type: FieldDeviceType = Field( + alias="device_type", description="The type of field device." + ) + data_source_id: str = Field( + alias="data_source_id", + description="Identifies the data source from which the field device data originates.", + ) + device_status: FieldDeviceStatus = Field( + alias="device_status", + description="The operational status of the field device. The value of this property indicates if the device is ok or in an error or warning state.", + ) + update_date: datetime = Field( + alias="update_date", + description="The UTC time and date when the field device information was updated.", + ) + has_automatic_location: bool = Field( + alias="has_automatic_location", + description="A yes/no value indicating if the field device location (parent FieldDeviceFeature's geometry) is determined automatically from an onboard GPS (true) or manually set/overridden (false).", + ) + road_direction: Optional[Direction] = Field( + None, + alias="road_direction", + description="The direction of the road that the field device is on. This value indicates the direction of the traffic flow of the road, not a real heading angle.", + ) + road_names: Optional[list[str]] = Field( + None, + alias="road_names", + description="A list of publicly known names of the road on which the device is located. This may include the road number designated by a jurisdiction such as a county, state or interstate (e.g. I-5, VT 133).", + ) + name: Optional[str] = Field( + None, alias="name", description="A human-readable name for the field device." + ) + description: Optional[str] = Field( + None, alias="description", description="A description of the field device." + ) + status_messages: Optional[list[str]] = Field( + None, + alias="status_messages", + description="A list of messages associated with the device's status, if applicable. Used to provide additional information about the status such as specific warning or error messages.", + ) + is_moving: Optional[bool] = Field( + None, + alias="is_moving", + description="A yes/no value indicating if the device is actively moving (not statically placed) as part of a mobile work zone operation.", + ) + road_event_ids: Optional[list[str]] = Field( + None, + alias="road_event_ids", + description="A list of one or more IDs of a RoadEventFeature that the device is associated with.", + ) + milepost: Optional[float] = Field( + None, + alias="milepost", + description="The linear distance measured against a milepost marker along a roadway where the device is located.", + ) + make: Optional[str] = Field( + None, alias="make", description="The make or manufacturer of the device." + ) + model: Optional[str] = Field( + None, alias="model", description="The model of the device." + ) + serial_number: Optional[str] = Field( + None, alias="serial_number", description="The serial number of the device." + ) + firmware_version: Optional[str] = Field( + None, + alias="firmware_version", + description="The version of firmware the device is using to operate.", + ) + velocity_kph: Optional[float] = Field( + None, + alias="velocity_kph", + description="The velocity of the device in kilometers per hour.", + ) diff --git a/wzdx/models/field_device_feed/field_device_feature.py b/wzdx/models/field_device_feed/field_device_feature.py new file mode 100644 index 00000000..31869530 --- /dev/null +++ b/wzdx/models/field_device_feed/field_device_feature.py @@ -0,0 +1,38 @@ +from typing import Literal, Optional +from pydantic import BaseModel, Field + +from ..geometry.geojson_geometry import GeoJsonGeometry +from .properties.field_device_properties import FieldDeviceProperties + + +class FieldDeviceFeature(BaseModel): + """ + The FieldDeviceFeature object is a GeoJSON Feature representing a deployed field device. This object contains the specific details of the field device, similar to how the RoadEventFeature object in a WZDx Feed contains the road event object (WorkZoneRoadEvent or DetourRoadEvent. + + Currently, only point devices are supported. + + Documentation: https://github.com/usdot-jpo-ode/wzdx/blob/develop/spec-content/objects/FieldDeviceFeature.md + """ + + id: str = Field( + alias="id", + description="A unique identifier issued by the data feed provider to identify the field device. It is recommended that this identifier is a Universally Unique Identifier (UUID) as defined in RFC 4122 to guarantee uniqueness between feeds and over time.", + ) + type: Literal["Feature"] = Field( + default="Feature", + alias="type", + description="The GeoJSON object type. This MUST be the string Feature.", + ) + properties: FieldDeviceProperties = Field( + alias="properties", + description="The specific details of the field device.", + ) + geometry: GeoJsonGeometry = Field( + alias="geometry", + description="The geometry of the field device, indicating its location. The Geometry object's type property MUST be Point.", + ) + bbox: Optional[list[float]] = Field( + default=None, + alias="bbox", + description="Information on the coordinate range for this field device. Must be an array of length 2n where n is the number of dimensions represented in the geometry property, with all axes of the most southwesterly point followed by all axes of the more northeasterly point. The axes order of a bbox follows the axes order of the geometry.", + ) diff --git a/wzdx/models/field_device_feed/field_device_status.py b/wzdx/models/field_device_feed/field_device_status.py new file mode 100644 index 00000000..55c5733f --- /dev/null +++ b/wzdx/models/field_device_feed/field_device_status.py @@ -0,0 +1,14 @@ +from enum import Enum + + +class FieldDeviceStatus(str, Enum): + """ + The FieldDeviceStatus enumerated type describes the operational status of a field device. The status indicates the health of the device. + + Documentation: https://github.com/usdot-jpo-ode/wzdx/blob/develop/spec-content/enumerated-types/FieldDeviceStatus.md + """ + + OK = "ok" # The device is turned on and working without issue. + WARNING = "warning" # The device is functional but is impaired or impacted in a way that is not critical to operation. + ERROR = "error" # The device is impaired such that it is not able to perform one or more necessary functions. + UNKNOWN = "unknown" # The device's operational status is not known. diff --git a/wzdx/models/field_device_feed/field_device_type.py b/wzdx/models/field_device_feed/field_device_type.py new file mode 100644 index 00000000..c3ceada4 --- /dev/null +++ b/wzdx/models/field_device_feed/field_device_type.py @@ -0,0 +1,17 @@ +from typing_extensions import Literal + +""" +The FieldDeviceType enumerated type enumerates all types of field devices described by the specification. + +Documentation: https://github.com/usdot-jpo-ode/wzdx/blob/develop/spec-content/enumerated-types/FieldDeviceType.md +""" +FieldDeviceType = Literal[ + "arrow-board", + "camera", + "dynamic-message-sign", + "flashing-beacon", + "hybrid-sign", + "location-marker", + "traffic-sensor", + "traffic-signal", +] diff --git a/wzdx/models/field_device_feed/properties/__init__.py b/wzdx/models/field_device_feed/properties/__init__.py new file mode 100644 index 00000000..7f4e09f1 --- /dev/null +++ b/wzdx/models/field_device_feed/properties/__init__.py @@ -0,0 +1,28 @@ +from .field_device_properties import FieldDeviceProperties +from .arrow_board import ArrowBoard, ArrowBoardPattern +from .camera import Camera +from .dynamic_message_sign import DynamicMessageSign +from .flashing_beacon import FlashingBeacon, FlashingBeaconFunction +from .hybrid_sign import HybridSign, HybridSignDynamicMessageFunction +from .location_marker import LocationMarker, MarkedLocation, MarkedLocationType +from .traffic_sensor import TrafficSensor, TrafficSensorLaneData +from .traffic_signal import TrafficSignal, TrafficSignalMode + +__all__ = [ + "FieldDeviceProperties", + "ArrowBoard", + "ArrowBoardPattern", + "Camera", + "DynamicMessageSign", + "FlashingBeacon", + "FlashingBeaconFunction", + "HybridSign", + "HybridSignDynamicMessageFunction", + "LocationMarker", + "MarkedLocation", + "MarkedLocationType", + "TrafficSensor", + "TrafficSensorLaneData", + "TrafficSignal", + "TrafficSignalMode", +] diff --git a/wzdx/models/field_device_feed/properties/arrow_board.py b/wzdx/models/field_device_feed/properties/arrow_board.py new file mode 100644 index 00000000..ed5d0008 --- /dev/null +++ b/wzdx/models/field_device_feed/properties/arrow_board.py @@ -0,0 +1,66 @@ +from typing import Literal, Optional +from pydantic import BaseModel, Field +from enum import Enum +from ..field_device_core_details import FieldDeviceCoreDetails + + +class ArrowBoardPattern(str, Enum): + """ + The ArrowBoardPattern enumerated type defines a list of options for the posted pattern on an ArrowBoard. + + If the arrow board pattern does not exactly match one of the values described, the closest pattern should be used. + + Documentation: https://github.com/usdot-jpo-ode/wzdx/blob/develop/spec-content/enumerated-types/ArrowBoardPattern.md + """ + + BLANK = "blank" # No pattern; the board is not displaying anything. + RIGHT_ARROW_STATIC = "right-arrow-static" # Merge right represented by an arrow pattern (e.g. -->) that does not flash or move. + RIGHT_ARROW_FLASHING = "right-arrow-flashing" # Merge right represented by an arrow pattern (e.g. -->) that flashes on/off. + RIGHT_ARROW_SEQUENTIAL = "right-arrow-sequential" # Merge right represented by an arrow pattern (e.g. -->) that is displayed in a progressing sequence (e.g. > -> --> or - -- -->). + RIGHT_CHEVRON_STATIC = "right-chevron-static" # Merge right represented by a pattern of chevrons (e.g. >>>) that does not flash or move. + RIGHT_CHEVRON_FLASHING = "right-chevron-flashing" # Merge right represented by a pattern of chevrons (e.g. >>>) that flashes on/off. + RIGHT_CHEVRON_SEQUENTIAL = "right-chevron-sequential" # Merge right represented by a pattern of chevrons that is displayed in a progressing sequence. + LEFT_ARROW_STATIC = "left-arrow-static" # Merge left represented by an arrow pattern (e.g. <--) that does not flash or move. + LEFT_ARROW_FLASHING = "left-arrow-flashing" # Merge left represented by an arrow pattern (e.g. <--) that flashes on/off. + LEFT_ARROW_SEQUENTIAL = "left-arrow-sequential" # Merge left represented by an arrow pattern (e.g. <--) that is displayed in a progressing sequence (e.g. < <- <-- or - -- <--). + LEFT_CHEVRON_STATIC = "left-chevron-static" # Merge left represented by a pattern of chevrons (e.g. <<<) that does not flash or move. + LEFT_CHEVRON_FLASHING = "left-chevron-flashing" # Merge left represented by a pattern of chevrons (e.g. <<<) that flashes on/off. + LEFT_CHEVRON_SEQUENTIAL = "left-chevron-sequential" # Merge left represented by a pattern of chevrons that is displayed in a progressing sequence. + BIDIRECTIONAL_ARROW_STATIC = "bidirectional-arrow-static" # Split (merge left or right) represented by arrows pointing both left and right (e.g. <-->) that does not flash or move. + BIDIRECTIONAL_ARROW_FLASHING = "bidirectional-arrow-flashing" # Split (merge left or right) represented by arrows pointing both left and right (e.g. <-->) that flashes on/off. + LINE_FLASHING = "line-flashing" # A flashing line or bar (e.g. ---), indicating warning/caution, not a merge. + DIAMONDS_ALTERNATING = "diamonds-alternating" # An alternating display of two diamond shapes (e.g. ◇ ◇), indicating warning/caution, not a merge. + FOUR_CORNERS_FLASHING = "four-corners-flashing" # Four dots on the corners of the board which flash, indicating warning/caution, not a merge. + UNKNOWN = "unknown" # The arrow board pattern is not known. + + +class ArrowBoardCoreDetails(FieldDeviceCoreDetails): + device_type: Literal["arrow-board"] = "arrow-board" + + +class ArrowBoard(BaseModel): + """ + The ArrowBoard object describes an electronic, connected arrow board which can display + an arrow pattern to direct traffic. Arrow boards are often placed at the beginning of a + lane closure—thus knowing the location of an arrow board can assist in programmatically + generating a WZDx road event with verified spatial information. + + The ArrowBoard is a type of field device; it has a :class:`core_details` property which + contains the :class:`FieldDeviceCoreDetails` and exists within a :class:`FieldDeviceFeature`. + + Documentation: https://github.com/usdot-jpo-ode/wzdx/blob/develop/spec-content/objects/ArrowBoard.md + """ + + core_details: ArrowBoardCoreDetails = Field( + alias="core_details", + description="The core details of the field device that are shared by all types of field devices, not specific to arrow boards.", + ) + pattern: ArrowBoardPattern = Field( + alias="pattern", + description="The current pattern displayed on the arrow board. Note this includes blank, which indicates that nothing is shown on the arrow board.", + ) + is_in_transport_position: Optional[bool] = Field( + default=None, + alias="is_in_transport_position", + description="A yes/no value indicating if the arrow board is in the stowed/transport position (true) or deployed/upright position (false).", + ) diff --git a/wzdx/models/field_device_feed/properties/camera.py b/wzdx/models/field_device_feed/properties/camera.py new file mode 100644 index 00000000..8f78d40c --- /dev/null +++ b/wzdx/models/field_device_feed/properties/camera.py @@ -0,0 +1,40 @@ +from typing import Literal, Optional +from datetime import datetime +from pydantic import BaseModel, Field, HttpUrl, model_validator +from ..field_device_core_details import FieldDeviceCoreDetails + + +class CameraCoreDetails(FieldDeviceCoreDetails): + device_type: Literal["camera"] = "camera" + + +class Camera(BaseModel): + """ + The Camera object describes a camera device deployed in the field, capable of capturing still images. + + The Camera is a type of field device; it has a core_details property which contains the :class:`FieldDeviceCoreDetails` and exists within a :class:`FieldDeviceFeature`. + + Documentation: https://github.com/usdot-jpo-ode/wzdx/blob/develop/spec-content/objects/Camera.md + """ + + core_details: CameraCoreDetails = Field( + alias="core_details", + description="The core details of the field device that are shared by all types of field devices, not specific to cameras.", + ) + image_url: Optional[HttpUrl] = Field( + None, + alias="image_url", + description="A URL pointing to an image file for the camera image still.", + ) + image_timestamp: Optional[datetime] = Field( + None, + alias="image_timestamp", + description="The UTC date and time when the image was captured.", + ) + + @model_validator(mode="after") + def validate_image_timestamp(self) -> "Camera": + """Validate that image_timestamp is provided when image_url is set.""" + if self.image_url is not None and self.image_timestamp is None: + raise ValueError("image_timestamp is required when image_url is provided") + return self diff --git a/wzdx/models/field_device_feed/properties/dynamic_message_sign.py b/wzdx/models/field_device_feed/properties/dynamic_message_sign.py new file mode 100644 index 00000000..bbfc4b23 --- /dev/null +++ b/wzdx/models/field_device_feed/properties/dynamic_message_sign.py @@ -0,0 +1,26 @@ +from typing import Literal +from pydantic import BaseModel, Field +from ..field_device_core_details import FieldDeviceCoreDetails + + +class DynamicMessageSignCoreDetails(FieldDeviceCoreDetails): + device_type: Literal["dynamic-message-sign"] = "dynamic-message-sign" + + +class DynamicMessageSign(BaseModel): + """ + The DynamicMessageSign object describes a dynamic message sign (DMS)—also known as changeable message sign (CMS) or variable message sign (VMS)—which is an electronic traffic sign deployed on the roadway used to provide information to travelers. + + The DynamicMessageSign is a type of field device; it has a core_details property which contains the :class:`FieldDeviceCoreDetails` and exists within a :class:`FieldDeviceFeature`. + + Documentation: https://github.com/usdot-jpo-ode/wzdx/blob/develop/spec-content/objects/DynamicMessageSign.md + """ + + core_details: DynamicMessageSignCoreDetails = Field( + alias="core_details", + description="The core details of the field device that are shared by all types of field devices, not specific to dynamic message signs.", + ) + message_multi_string: str = Field( + alias="message_multi_string", + description="The MULTI (Mark-Up Language for Transportation Information, see [NTCIP 1203 v03](https://www.ntcip.org/file/2018/11/NTCIP1203v03f.pdf)) formatted string describing the message currently posted to the sign.", + ) diff --git a/wzdx/models/field_device_feed/properties/field_device_properties.py b/wzdx/models/field_device_feed/properties/field_device_properties.py new file mode 100644 index 00000000..8e77a48d --- /dev/null +++ b/wzdx/models/field_device_feed/properties/field_device_properties.py @@ -0,0 +1,49 @@ +from typing import Annotated + +from pydantic import Discriminator, Tag +from .arrow_board import ArrowBoard +from .camera import Camera +from .dynamic_message_sign import DynamicMessageSign +from .flashing_beacon import FlashingBeacon +from .hybrid_sign import HybridSign +from .location_marker import LocationMarker +from .traffic_sensor import TrafficSensor +from .traffic_signal import TrafficSignal + + +def get_device_type(v): + """ + Extract the ``device_type`` discriminator value from a field device object. + + This helper is used by the Pydantic ``Discriminator`` on ``FieldDeviceProperties`` + to implement a discriminated union based on ``core_details.device_type``. + It supports both raw dictionaries and model instances that expose a + ``core_details`` attribute. + + Args: + v: Either a mapping-like object (typically a ``dict``) containing a + ``"core_details"`` key, or a model instance with a ``core_details`` + attribute that in turn has a ``device_type`` attribute. + + Returns: + The value of ``device_type`` used to select the appropriate concrete + field device model in the ``FieldDeviceProperties`` union, or ``None`` + if it cannot be determined. + """ + if isinstance(v, dict): + return v.get("core_details", {}).get("device_type") + return getattr(v.core_details, "device_type", None) + + +# Discriminated union based on core_details.device_type +FieldDeviceProperties = Annotated[ + Annotated[ArrowBoard, Tag("arrow-board")] + | Annotated[Camera, Tag("camera")] + | Annotated[DynamicMessageSign, Tag("dynamic-message-sign")] + | Annotated[FlashingBeacon, Tag("flashing-beacon")] + | Annotated[HybridSign, Tag("hybrid-sign")] + | Annotated[LocationMarker, Tag("location-marker")] + | Annotated[TrafficSensor, Tag("traffic-sensor")] + | Annotated[TrafficSignal, Tag("traffic-signal")], + Discriminator(get_device_type), +] diff --git a/wzdx/models/field_device_feed/properties/flashing_beacon.py b/wzdx/models/field_device_feed/properties/flashing_beacon.py new file mode 100644 index 00000000..694f281f --- /dev/null +++ b/wzdx/models/field_device_feed/properties/flashing_beacon.py @@ -0,0 +1,52 @@ +from typing import Literal, Optional +from pydantic import BaseModel, Field +from enum import Enum +from ..field_device_core_details import FieldDeviceCoreDetails + + +class FlashingBeaconFunction(str, Enum): + """ + The FlashingBeaconFunction enumerated type describes a list of options for what a :class:`FlashingBeacon` is being used to indicate. + + Documentation: https://github.com/usdot-jpo-ode/wzdx/blob/develop/spec-content/enumerated-types/FlashingBeaconFunction.md + """ + + VEHICLE_ENTERING = "vehicle-entering" # Vehicles are entering the roadway. + QUEUE_WARNING = "queue-warning" # There is a queue of vehicles. + REDUCED_SPEED = "reduced-speed" # There is a reduced speed limit. + WORKERS_PRESENT = ( + "workers-present" # There are workers present on or near the roadway. + ) + + +class FlashingBeaconCoreDetails(FieldDeviceCoreDetails): + device_type: Literal["flashing-beacon"] = "flashing-beacon" + + +class FlashingBeacon(BaseModel): + """ + The FlashingBeacon object describes a flashing warning beacon used to supplement a temporary traffic control device. A flashing warning beacon is mounted on a sign or channelizing device and used to indicate a warning condition and capture driver attention. + + The FlashingBeacon is a type of field device; it has a core_details property which contains the :class:`FieldDeviceCoreDetails` and exists within a :class:`FieldDeviceFeature`. + + Documentation: https://github.com/usdot-jpo-ode/wzdx/blob/develop/spec-content/objects/FlashingBeacon.md + """ + + core_details: FlashingBeaconCoreDetails = Field( + alias="core_details", + description="The core details of the field device that are shared by all types of field devices, not specific to flashing beacons.", + ) + function: FlashingBeaconFunction = Field( + alias="function", + description="Describes the function or purpose of the flashing beacon, i.e. what it is being used to indicate.", + ) + is_flashing: Optional[bool] = Field( + None, + alias="is_flashing", + description="A yes/no value indicating if the flashing beacon is currently in use and flashing.", + ) + sign_text: Optional[str] = Field( + None, + alias="sign_text", + description="The text on the sign the beacon is mounted on, if applicable.", + ) diff --git a/wzdx/models/field_device_feed/properties/hybrid_sign.py b/wzdx/models/field_device_feed/properties/hybrid_sign.py new file mode 100644 index 00000000..85cbbc62 --- /dev/null +++ b/wzdx/models/field_device_feed/properties/hybrid_sign.py @@ -0,0 +1,49 @@ +from typing import Literal, Optional +from pydantic import BaseModel, Field +from enum import Enum +from ..field_device_core_details import FieldDeviceCoreDetails + + +class HybridSignDynamicMessageFunction(str, Enum): + """ + The HybridSignDynamicMessageFunction enumerated type describes options for the function of the dynamic message displayed by the electronic display on a :class:`HybridSign`. + + Documentation: https://github.com/usdot-jpo-ode/wzdx/blob/develop/spec-content/enumerated-types/HybridSignDynamicMessageFunction.md + """ + + SPEED_LIMIT = "speed-limit" # The message is a speed limit. + TRAVEL_TIME = "travel-time" # The message is a travel time. + OTHER = "other" # The hybrid sign message function is not one of the other options described by this enumerated type. + + +class HybridSignCoreDetails(FieldDeviceCoreDetails): + device_type: Literal["hybrid-sign"] = "hybrid-sign" + + +class HybridSign(BaseModel): + """ + The HybridSign object describes a hybrid sign that contains static text (e.g. on an aluminum sign) along with a single electronic message display, used to provide information to travelers. This object is intended to be general to represent hybrid signs with multiple functions, such as variable speed limit signs (VSLS), hybrid travel time signs, and other similar systems. + + The HybridSign is a type of field device; it has a core_details property which contains the :class:`FieldDeviceCoreDetails` and exists within a :class:`FieldDeviceFeature`. + + Documentation: https://github.com/usdot-jpo-ode/wzdx/blob/develop/spec-content/objects/HybridSign.md + """ + + core_details: HybridSignCoreDetails = Field( + alias="core_details", + description="The core details of the field device shared by all field devices types, not specific to hybrid signs.", + ) + dynamic_message_function: HybridSignDynamicMessageFunction = Field( + alias="dynamic_message_function", + description="The function the dynamic message displayed (e.g. a speed limit).", + ) + dynamic_message_text: Optional[str] = Field( + None, + alias="dynamic_message_text", + description="A text representation of the message currently posted to the electronic component of the hybrid sign.", + ) + static_sign_text: Optional[str] = Field( + None, + alias="static_sign_text", + description="The static text on the non-electronic component of the hybrid sign.", + ) diff --git a/wzdx/models/field_device_feed/properties/location_marker.py b/wzdx/models/field_device_feed/properties/location_marker.py new file mode 100644 index 00000000..cdd50066 --- /dev/null +++ b/wzdx/models/field_device_feed/properties/location_marker.py @@ -0,0 +1,69 @@ +from typing import Literal, Optional +from pydantic import BaseModel, Field +from enum import Enum +from ..field_device_core_details import FieldDeviceCoreDetails + + +class MarkedLocationType(str, Enum): + """ + The MarkedLocationType enumerated type describes options for what a :class:`MarkedLocation` can mark, such as the start or end of a road event. + + https://github.com/usdot-jpo-ode/wzdx/blob/develop/spec-content/enumerated-types/MarkedLocationType.md + """ + + AFAD = "afad" # An automatic flagger assistance device. + DELINEATOR = "delineator" # A generic delineation point in a work zone. This value can be used for most types of marked locations that don't match any of the other values. + FLAGGER = "flagger" # A human who is directing traffic. + LANE_SHIFT = "lane-shift" # A lane shift. + LANE_CLOSURE = "lane-closure" # One or more lanes are closed. + PERSONAL_DEVICE = "personal-device" # A connected device that is worn or carried by an individual worker in a work zone. + RAMP_CLOSURE = ( + "ramp-closure" # The start of a closed ramp onto or off a main road or highway. + ) + ROAD_CLOSURE = "road-closure" # The start of a closed road. + ROAD_EVENT_START = "road-event-start" # The start point of a road event. + ROAD_EVENT_END = "road-event-end" # The end point of a road event. + WORK_TRUCK_WITH_LIGHTS_FLASHING = "work-truck-with-lights-flashing" # A work truck with lights flashing, actively engaged in construction or maintenance activity on the roadway. + WORK_ZONE_START = "work-zone-start" # The start point of a work zone. + WORK_ZONE_END = "work-zone-end" # The end point of a work zone. + + +class MarkedLocation(BaseModel): + """ + The MarkedLocation object describes a specific location where a LocationMarker is placed, such as the start or end of a work zone road event. The marked location is typically within a road event, but is not required to be. + + Documentation: https://github.com/usdot-jpo-ode/wzdx/blob/develop/spec-content/objects/MarkedLocation.md + """ + + type: MarkedLocationType = Field( + alias="type", + description="The type of location (e.g. start or end) that is marked.", + ) + road_event_id: Optional[str] = Field( + None, + alias="road_event_id", + description="The ID of a RoadEventFeature that the MarkedLocation applies to.", + ) + + +class MarkedLocationCoreDetails(FieldDeviceCoreDetails): + device_type: Literal["location-marker"] = "location-marker" + + +class LocationMarker(BaseModel): + """ + The LocationMarker object describes any GPS-enabled ITS device that is placed at a point on a roadway to dynamically know the location of something (often the beginning or end of a work zone). The LocationMarker contains a list of one or more MarkedLocation objects which indicate the type of location (such as the start or end) and optionally the ID of a RoadEventFeature that the location is associated with. + + The LocationMarker is a type of field device; it has a core_details property which contains the FieldDeviceCoreDetails and exists within a FieldDeviceFeature. + + Documentation: https://github.com/usdot-jpo-ode/wzdx/blob/develop/spec-content/objects/LocationMarker.md + """ + + core_details: MarkedLocationCoreDetails = Field( + alias="core_details", + description="The core details of the field device shared by all field devices types, not specific to the location marker.", + ) + marked_locations: list[MarkedLocation] = Field( + alias="marked_locations", + description="A list of locations that the LocationMarker is marking.", + ) diff --git a/wzdx/models/field_device_feed/properties/traffic_sensor.py b/wzdx/models/field_device_feed/properties/traffic_sensor.py new file mode 100644 index 00000000..34a31431 --- /dev/null +++ b/wzdx/models/field_device_feed/properties/traffic_sensor.py @@ -0,0 +1,85 @@ +from typing import Literal, Optional +from pydantic import BaseModel, Field +from ..field_device_core_details import FieldDeviceCoreDetails + + +class TrafficSensorLaneData(BaseModel): + """ + The TrafficSensorLaneData object describes data for a single lane measured by a :class:`TrafficSensor` deployed on the roadway. + + Note this structure allows a single :class:`TrafficSensor` to provide data across lanes on multiple road events. + + Description: https://github.com/usdot-jpo-ode/wzdx/blob/develop/spec-content/objects/TrafficSensorLaneData.md + """ + + lane_order: int = Field( + alias="lane_order", + description="The lane's position in sequence on the roadway. If road_event_id is provided, the value of this property corresponds to the associated road event's Lane's order property.", + ) + road_event_id: Optional[str] = Field( + None, + alias="road_event_id", + description="The ID of a RoadEventFeature which the measured lane occurs in, if applicable.", + ) + average_speed_kph: Optional[float] = Field( + None, + alias="average_speed_kph", + description="The average speed of traffic in the lane over the collection interval (in kilometers per hour).", + ) + volume_vph: Optional[float] = Field( + None, + alias="volume_vph", + description="The rate of vehicles passing by the sensor in the lane during the collection interval (in vehicles per hour).", + ) + occupancy_percent: Optional[float] = Field( + None, + alias="occupancy_percent", + description="The percent of time the lane monitored by the sensor was occupied by a vehicle over the collection interval.", + ) + + +class TrafficSensorCoreDetails(FieldDeviceCoreDetails): + device_type: Literal["traffic-sensor"] = "traffic-sensor" + + +class TrafficSensor(BaseModel): + """ + The TrafficSensor object describes a traffic sensor deployed on a roadway which captures traffic metrics (e.g. speed, volume, occupancy) over a collection interval. The TrafficSensor can describe lane-level traffic data if available and if associated with a road event (e.g. :class:`WorkZoneRoadEvent`). + + The TrafficSensor is a type of field device; it has a core_details property which contains the :class:`FieldDeviceCoreDetails` and exists within a :class:`FieldDeviceFeature`. + + Documentation: https://github.com/usdot-jpo-ode/wzdx/blob/develop/spec-content/objects/TrafficSensor.md + """ + + core_details: TrafficSensorCoreDetails = Field( + alias="core_details", + description="The core details of the field device shared by all field devices types, not specific to traffic sensors.", + ) + collection_interval_start_date: str = Field( + alias="collection_interval_start_date", + description="The UTC date and time where the TrafficSensor data began being collected at. The averages and totals contained in the TrafficSensor data apply to the inclusive interval of collection_interval_start_date to collection_interval_end_date.", + ) + collection_interval_end_date: str = Field( + alias="collection_interval_end_date", + description="The UTC date and time where the TrafficSensor collection interval ended. The averages and totals contained in the TrafficSensor data apply to the inclusive interval of collection_interval_start_date to collection_interval_end_date.", + ) + average_speed_kph: Optional[float] = Field( + None, + alias="average_speed_kph", + description="The average speed of vehicles across all lanes over the collection interval in kilometers per hour.", + ) + volume_vph: Optional[float] = Field( + None, + alias="volume_vph", + description="The rate of vehicles passing by the sensor during the collection interval in vehicles per hour.", + ) + occupancy_percent: Optional[float] = Field( + None, + alias="occupancy_percent", + description="The percent of time the roadway section monitored by the sensor was occupied by a vehicle over the collection interval.", + ) + lane_data: Optional[list[TrafficSensorLaneData]] = Field( + None, + alias="lane_data", + description="A list of objects each describing traffic data for a specific lane.", + ) diff --git a/wzdx/models/field_device_feed/properties/traffic_signal.py b/wzdx/models/field_device_feed/properties/traffic_signal.py new file mode 100644 index 00000000..e73eaf29 --- /dev/null +++ b/wzdx/models/field_device_feed/properties/traffic_signal.py @@ -0,0 +1,45 @@ +from typing import Literal +from pydantic import BaseModel, Field +from enum import Enum +from ..field_device_core_details import FieldDeviceCoreDetails + + +class TrafficSignalMode(str, Enum): + """ + The TrafficSignalMode enumerated type describes the current operating mode of a :class:`TrafficSignal`. + + Documentation: https://github.com/usdot-jpo-ode/wzdx/blob/develop/spec-content/enumerated-types/TrafficSignalMode.md + """ + + BLANK = "blank" # The signal is not displaying anything. + FLASHING_RED = "flashing-red" # The signal is in a flashing red state that could be part of startup or fault. + FLASHING_YELLOW = "flashing-yellow" # The signal is in a flashing yellow state that could be part of startup or fault. + FULLY_ACTUATED = ( + "fully-actuated" # The signal is using an external trigger for all movements. + ) + MANUAL = "manual" # The signal is using a manual trigger. + PRE_TIMED = "pre-timed" # The signal is using a timed cycle. + SEMI_ACTUATED = "semi-actuated" # The signal is using an external trigger only for the minor movements. + UNKNOWN = "unknown" # The current operating mode is not known. + + +class TrafficSignalCoreDetails(FieldDeviceCoreDetails): + device_type: Literal["traffic-signal"] = "traffic-signal" + + +class TrafficSignal(BaseModel): + """ + The TrafficSignal object describes a temporary traffic signal deployed on a roadway. + + The TrafficSignal is a type of field device; it has a core_details property which contains the :class:`FieldDeviceCoreDetails` and exists within a :class:`FieldDeviceFeature`. + + Documentation: https://github.com/usdot-jpo-ode/wzdx/blob/develop/spec-content/objects/TrafficSignal.md + """ + + core_details: TrafficSignalCoreDetails = Field( + alias="core_details", + description="The core details of the traffic signal device.", + ) + mode: TrafficSignalMode = Field( + alias="mode", description="The current operating mode of the traffic signal." + ) diff --git a/wzdx/models/geometry/__init__.py b/wzdx/models/geometry/__init__.py new file mode 100644 index 00000000..a739838e --- /dev/null +++ b/wzdx/models/geometry/__init__.py @@ -0,0 +1,13 @@ +from .geojson_geometry import GeoJsonGeometry +from .geojson_point import GeoJsonPoint +from .geojson_linestring import GeoJsonLineString +from .geojson_multipoint import GeoJsonMultiPoint +from .geojson_polygon import GeoJsonPolygon + +__all__ = [ + "GeoJsonGeometry", + "GeoJsonPoint", + "GeoJsonLineString", + "GeoJsonMultiPoint", + "GeoJsonPolygon", +] diff --git a/wzdx/models/geometry/geojson_geometry.py b/wzdx/models/geometry/geojson_geometry.py new file mode 100644 index 00000000..e6922b80 --- /dev/null +++ b/wzdx/models/geometry/geojson_geometry.py @@ -0,0 +1,17 @@ +from typing import Annotated, Union + +from pydantic import Field +from .geojson_point import GeoJsonPoint +from .geojson_linestring import GeoJsonLineString +from .geojson_multipoint import GeoJsonMultiPoint +from .geojson_polygon import GeoJsonPolygon + +GeoJsonGeometry = Annotated[ + Union[ + GeoJsonPoint, + GeoJsonLineString, + GeoJsonMultiPoint, + GeoJsonPolygon, + ], + Field(discriminator="type"), +] diff --git a/wzdx/models/geometry/geojson_linestring.py b/wzdx/models/geometry/geojson_linestring.py new file mode 100644 index 00000000..34ef5e23 --- /dev/null +++ b/wzdx/models/geometry/geojson_linestring.py @@ -0,0 +1,16 @@ +from typing import Literal +from pydantic import BaseModel, Field + + +class GeoJsonLineString(BaseModel): + """GeoJSON LineString geometry""" + + type: Literal["LineString"] = Field( + default="LineString", + alias="type", + description="The GeoJSON object type. This MUST be the string LineString.", + ) + coordinates: list[list[float]] = Field( + alias="coordinates", + description="A list of two or more positions that make up the LineString.", + ) diff --git a/wzdx/models/geometry/geojson_multipoint.py b/wzdx/models/geometry/geojson_multipoint.py new file mode 100644 index 00000000..644bd6a9 --- /dev/null +++ b/wzdx/models/geometry/geojson_multipoint.py @@ -0,0 +1,16 @@ +from typing import Literal +from pydantic import BaseModel, Field + + +class GeoJsonMultiPoint(BaseModel): + """GeoJSON MultiPoint geometry""" + + type: Literal["MultiPoint"] = Field( + default="MultiPoint", + alias="type", + description="The GeoJSON object type. This MUST be the string MultiPoint.", + ) + coordinates: list[list[float]] = Field( + alias="coordinates", + description="A list of positions that make up the MultiPoint.", + ) diff --git a/wzdx/models/geometry/geojson_point.py b/wzdx/models/geometry/geojson_point.py new file mode 100644 index 00000000..661f19e1 --- /dev/null +++ b/wzdx/models/geometry/geojson_point.py @@ -0,0 +1,16 @@ +from typing import Literal +from pydantic import BaseModel, Field + + +class GeoJsonPoint(BaseModel): + """GeoJSON Point geometry""" + + type: Literal["Point"] = Field( + default="Point", + alias="type", + description="The GeoJSON object type. This MUST be the string Point.", + ) + coordinates: list[float] = Field( + alias="coordinates", + description="A single position that makes up the Point.", + ) diff --git a/wzdx/models/geometry/geojson_polygon.py b/wzdx/models/geometry/geojson_polygon.py new file mode 100644 index 00000000..b13fe4e0 --- /dev/null +++ b/wzdx/models/geometry/geojson_polygon.py @@ -0,0 +1,16 @@ +from typing import Literal +from pydantic import BaseModel, Field + + +class GeoJsonPolygon(BaseModel): + """GeoJSON Polygon geometry""" + + type: Literal["Polygon"] = Field( + default="Polygon", + alias="type", + description="The GeoJSON object type. This MUST be the string Polygon.", + ) + coordinates: list[list[list[float]]] = Field( + alias="coordinates", + description="A list of linear rings that make up the Polygon.", + ) diff --git a/wzdx/sample_files/validation_schema/connected_work_zone_feed_v10.py b/wzdx/sample_files/validation_schema/connected_work_zone_feed_v10.py index b3f568ad..33e54389 100644 --- a/wzdx/sample_files/validation_schema/connected_work_zone_feed_v10.py +++ b/wzdx/sample_files/validation_schema/connected_work_zone_feed_v10.py @@ -73,7 +73,7 @@ ], "properties": { "data_source_id": { - "description": "Unique identifier for the organization providing work zone data. This identifier is a Universally Unique IDentifier (UUID) as defined in [RFC 4122](https://datatracker.ietf.org/doc/html/rfc4122).", + "description": "Unique identifier for the organization providing work zone data. This identifier is a Universally Unique Identifier (UUID) as defined in [RFC 4122](https://datatracker.ietf.org/doc/html/rfc4122).", "type": "string", }, "organization_name": { @@ -136,7 +136,7 @@ "required": ["id", "type", "properties", "geometry"], "properties": { "id": { - "description": "A unique identifier issued by the data feed provider to identify the CWZ road event. This identifier is a Universally Unique IDentifier (UUID) as defined in [RFC 4122](https://datatracker.ietf.org/doc/html/rfc4122).", + "description": "A unique identifier issued by the data feed provider to identify the CWZ road event. This identifier is a Universally Unique Identifier (UUID) as defined in [RFC 4122](https://datatracker.ietf.org/doc/html/rfc4122).", "type": "string", }, "type": { diff --git a/wzdx/sample_files/validation_schema/work_zone_feed_v42.py b/wzdx/sample_files/validation_schema/work_zone_feed_v42.py index b48220ad..7b135614 100644 --- a/wzdx/sample_files/validation_schema/work_zone_feed_v42.py +++ b/wzdx/sample_files/validation_schema/work_zone_feed_v42.py @@ -14,53 +14,44 @@ "properties": { "publisher": { "description": "The organization responsible for publishing the feed", - "type": "string" + "type": "string", }, "contact_name": { "description": "The name of the individual or group responsible for the data feed", - "type": "string" + "type": "string", }, "contact_email": { "description": "The email address of the individual or group responsible for the data feed", "type": "string", - "format": "email" + "format": "email", }, "update_frequency": { "description": "The frequency in seconds at which the data feed is updated", "type": "integer", - "minimum": 1 + "minimum": 1, }, "update_date": { "description": "The UTC date and time when the GeoJSON file (representing the instance of the feed) was generated", "type": "string", - "format": "date-time" + "format": "date-time", }, "version": { "description": "The WZDx specification version used to create the data feed, in 'major.minor' format", "type": "string", - "pattern": "^(0|[1-9][0-9]*)\\.(0|[1-9][0-9]*)$" + "pattern": "^(0|[1-9][0-9]*)\\.(0|[1-9][0-9]*)$", }, "license": { - "description": "The URL of the license that applies to the data in the WZDx feed. This *must* be the string \"https://creativecommons.org/publicdomain/zero/1.0/\"", - "enum": [ - "https://creativecommons.org/publicdomain/zero/1.0/" - ] + "description": 'The URL of the license that applies to the data in the WZDx feed. This *must* be the string "https://creativecommons.org/publicdomain/zero/1.0/"', + "enum": ["https://creativecommons.org/publicdomain/zero/1.0/"], }, "data_sources": { "description": "A list of specific data sources for the road event data in the feed", "type": "array", - "items": { - "$ref": "#/definitions/FeedDataSource" - }, - "minItems": 1 - } + "items": {"$ref": "#/definitions/FeedDataSource"}, + "minItems": 1, + }, }, - "required": [ - "update_date", - "version", - "publisher", - "data_sources" - ], + "required": ["update_date", "version", "publisher", "data_sources"], "definitions": { "FeedDataSource": { "title": "WZDx Feed Data Source", @@ -68,59 +59,51 @@ "type": "object", "properties": { "data_source_id": { - "description": "Unique identifier for the organization providing work zone data. It is recommended that this identifier is a Universally Unique IDentifier (UUID) as defined in [RFC 4122](https://datatracker.ietf.org/doc/html/rfc4122).", - "type": "string" + "description": "Unique identifier for the organization providing work zone data. It is recommended that this identifier is a Universally Unique Identifier (UUID) as defined in [RFC 4122](https://datatracker.ietf.org/doc/html/rfc4122).", + "type": "string", }, "organization_name": { "description": "The name of the organization for the authoritative source of the work zone data", - "type": "string" + "type": "string", }, "contact_name": { "description": "The name of the individual or group responsible for the data source", - "type": "string" + "type": "string", }, "contact_email": { "description": "The email address of the individual or group responsible for the data source", "type": "string", - "format": "email" + "format": "email", }, "update_frequency": { "description": "The frequency in seconds at which the data source is updated", "type": "integer", - "minimum": 1 + "minimum": 1, }, "update_date": { "description": "The UTC date and time when the data source was last updated", "type": "string", - "format": "date-time" + "format": "date-time", }, "lrs_type": { "description": "**DEPRECATED** Describes the type of linear referencing system used for the milepost measurements", - "type": "string" + "type": "string", }, "lrs_url": { "description": "**DEPRECATED** A URL where additional information on the LRS information and transformation information is stored", "type": "string", - "format": "uri" + "format": "uri", }, "location_verify_method": { "description": "***DEPRECATED***The method used to verify the accuracy of the location information", - "type": "string" - } + "type": "string", + }, }, - "required": [ - "data_source_id", - "organization_name" - ] + "required": ["data_source_id", "organization_name"], } - } - }, - "type": { - "description": "The GeoJSON type", - "enum": [ - "FeatureCollection" - ] + }, }, + "type": {"description": "The GeoJSON type", "enum": ["FeatureCollection"]}, "features": { "description": "An array of GeoJSON Feature objects which represent WZDx road events", "type": "array", @@ -133,25 +116,16 @@ "core_details": { "properties": { "event_type": { - "enum": [ - "work-zone", - "detour" - ] + "enum": ["work-zone", "detour"] } }, - "required": [ - "event_type" - ] + "required": ["event_type"], } }, - "required": [ - "core_details" - ] + "required": ["core_details"], } }, - "required": [ - "properties" - ] + "required": ["properties"], }, { "$id": "https://raw.githubusercontent.com/usdot-jpo-ode/wzdx/main/schemas/4.2/RoadEventFeature.json", @@ -161,14 +135,12 @@ "type": "object", "properties": { "id": { - "description": "A unique identifier issued by the data feed provider to identify the WZDx road event. It is recommended that this identifier is a Universally Unique IDentifier (UUID) as defined in [RFC 4122](https://datatracker.ietf.org/doc/html/rfc4122).", - "type": "string" + "description": "A unique identifier issued by the data feed provider to identify the WZDx road event. It is recommended that this identifier is a Universally Unique Identifier (UUID) as defined in [RFC 4122](https://datatracker.ietf.org/doc/html/rfc4122).", + "type": "string", }, "type": { "description": "The GeoJSON object type; must be 'Feature'", - "enum": [ - "Feature" - ] + "enum": ["Feature"], }, "properties": { "type": "object", @@ -177,17 +149,11 @@ "$ref": "#/definitions/RoadEventCoreDetails" } }, - "required": [ - "core_details" - ], + "required": ["core_details"], "oneOf": [ - { - "$ref": "#/definitions/WorkZoneRoadEvent" - }, - { - "$ref": "#/definitions/DetourRoadEvent" - } - ] + {"$ref": "#/definitions/WorkZoneRoadEvent"}, + {"$ref": "#/definitions/DetourRoadEvent"}, + ], }, "geometry": { "oneOf": [ @@ -196,16 +162,11 @@ "$id": "https://geojson.org/schema/LineString.json", "title": "GeoJSON LineString", "type": "object", - "required": [ - "type", - "coordinates" - ], + "required": ["type", "coordinates"], "properties": { "type": { "type": "string", - "enum": [ - "LineString" - ] + "enum": ["LineString"], }, "coordinates": { "type": "array", @@ -213,55 +174,42 @@ "items": { "type": "array", "minItems": 2, - "items": { - "type": "number" - } - } + "items": {"type": "number"}, + }, }, "bbox": { "type": "array", "minItems": 4, - "items": { - "type": "number" - } - } - } + "items": {"type": "number"}, + }, + }, }, { "$schema": "http://json-schema.org/draft-07/schema#", "$id": "https://geojson.org/schema/MultiPoint.json", "title": "GeoJSON MultiPoint", "type": "object", - "required": [ - "type", - "coordinates" - ], + "required": ["type", "coordinates"], "properties": { "type": { "type": "string", - "enum": [ - "MultiPoint" - ] + "enum": ["MultiPoint"], }, "coordinates": { "type": "array", "items": { "type": "array", "minItems": 2, - "items": { - "type": "number" - } - } + "items": {"type": "number"}, + }, }, "bbox": { "type": "array", "minItems": 4, - "items": { - "type": "number" - } - } - } - } + "items": {"type": "number"}, + }, + }, + }, ] }, "bbox": { @@ -271,17 +219,10 @@ "description": "Information on the coordinate range for a Geometry, Feature, or FeatureCollection", "type": "array", "minItems": 4, - "items": { - "type": "number" - } - } + "items": {"type": "number"}, + }, }, - "required": [ - "id", - "type", - "properties", - "geometry" - ], + "required": ["id", "type", "properties", "geometry"], "definitions": { "WorkZoneRoadEvent": { "title": "Work Zone Road Event", @@ -292,18 +233,12 @@ "properties": { "core_details": { "properties": { - "event_type": { - "const": "work-zone" - } + "event_type": {"const": "work-zone"} }, - "required": [ - "event_type" - ] + "required": ["event_type"], } }, - "required": [ - "core_details" - ] + "required": ["core_details"], }, { "properties": { @@ -312,51 +247,51 @@ }, "beginning_cross_street": { "description": "Name or number of the nearest cross street along the roadway where the event begins", - "type": "string" + "type": "string", }, "ending_cross_street": { "description": "Name or number of the nearest cross street along the roadway where the event ends", - "type": "string" + "type": "string", }, "beginning_milepost": { "description": "The linear distance measured against a milepost marker along a roadway where the event begins", "type": "number", - "minimum": 0 + "minimum": 0, }, "ending_milepost": { "description": "The linear distance measured against a milepost marker along a roadway where the event ends", "type": "number", - "minimum": 0 + "minimum": 0, }, "is_start_position_verified": { "description": "Indicates if the start position (first geometric coordinate pair) is based on actual reported data from a GPS-equipped device that measured the location of the start of the work zone.", - "type": "boolean" + "type": "boolean", }, "is_end_position_verified": { "description": "Indicates if the end position (last geometric coordinate pair) is based on actual reported data from a GPS-equipped device that measured the location of the end of the work zone.", - "type": "boolean" + "type": "boolean", }, "start_date": { "description": "The UTC date and time (formatted according to RFC 3339, Section 5.6) when the road event begins (e.g. 2020-11-03T19:37:00Z)", "type": "string", - "format": "date-time" + "format": "date-time", }, "end_date": { "description": "The UTC date and time (formatted according to RFC 3339, Section 5.6) when the road event ends (e.g. 2020-11-03T19:37:00Z)", "type": "string", - "format": "date-time" + "format": "date-time", }, "is_start_date_verified": { "description": "Indicates if work has been confirmed to have started, such as from a person or field device", - "type": "boolean" + "type": "boolean", }, "is_end_date_verified": { "description": "Indicates if work has been confirmed to have ended, such as from a person or field device", - "type": "boolean" + "type": "boolean", }, "work_zone_type": { "description": "The type of work zone road event", - "$ref": "#/definitions/WorkZoneType" + "$ref": "#/definitions/WorkZoneType", }, "vehicle_impact": { "$ref": "#/definitions/VehicleImpact" @@ -370,63 +305,61 @@ "reduced_speed_limit_kph": { "description": "If applicable, the reduced speed limit posted within the road event, in kilometers per hour", "type": "number", - "minimum": 0 + "minimum": 0, }, "restrictions": { "description": "A list of zero or more restrictions applying to the road event", "type": "array", "items": { "$ref": "#/definitions/Restriction" - } + }, }, "types_of_work": { "description": "A list of the types of work being done in a road event", "type": "array", "items": { "$ref": "#/definitions/TypeOfWork" - } + }, }, "lanes": { "description": "A list of individual lanes within a road event (roadway segment)", "type": "array", - "items": { - "$ref": "#/definitions/Lane" - } + "items": {"$ref": "#/definitions/Lane"}, }, "impacted_cds_curb_zones": { "description": "A list of references to external CDS Curb Zones impacted by the work zone", "type": "array", "items": { "$ref": "#/definitions/CdsCurbZonesReference" - } + }, }, "event_status": { "description": "**DEPRECATED**", - "$ref": "#/definitions/EventStatus" + "$ref": "#/definitions/EventStatus", }, "start_date_accuracy": { "description": "**DEPRECATED** Use is_start_date_verified instead", - "$ref": "#/definitions/TimeVerification" + "$ref": "#/definitions/TimeVerification", }, "end_date_accuracy": { "description": "**DEPRECATED** Use is_end_date_verified instead", - "$ref": "#/definitions/TimeVerification" + "$ref": "#/definitions/TimeVerification", }, "beginning_accuracy": { "description": "**DEPRECATED** Use is_start_position_verified instead.", - "$ref": "#/definitions/SpatialVerification" + "$ref": "#/definitions/SpatialVerification", }, "ending_accuracy": { "description": "**DEPRECATED** Use is_end_position_verified instead.", - "$ref": "#/definitions/SpatialVerification" - } + "$ref": "#/definitions/SpatialVerification", + }, }, "required": [ "core_details", "start_date", "end_date", "vehicle_impact", - "location_method" + "location_method", ], "allOf": [ { @@ -440,7 +373,7 @@ "required": [ "start_date_accuracy" ] - } + }, ] }, { @@ -454,7 +387,7 @@ "required": [ "beginning_accuracy" ] - } + }, ] }, { @@ -464,11 +397,7 @@ "is_end_date_verified" ] }, - { - "required": [ - "end_date_accuracy" - ] - } + {"required": ["end_date_accuracy"]}, ] }, { @@ -478,16 +407,12 @@ "is_end_position_verified" ] }, - { - "required": [ - "ending_accuracy" - ] - } + {"required": ["ending_accuracy"]}, ] - } - ] - } - ] + }, + ], + }, + ], }, "DetourRoadEvent": { "title": "Detour Road Event", @@ -498,18 +423,12 @@ "properties": { "core_details": { "properties": { - "event_type": { - "const": "detour" - } + "event_type": {"const": "detour"} }, - "required": [ - "event_type" - ] + "required": ["event_type"], } }, - "required": [ - "core_details" - ] + "required": ["core_details"], }, { "properties": { @@ -518,57 +437,57 @@ }, "beginning_cross_street": { "description": "Name or number of the nearest cross street along the roadway where the event begins", - "type": "string" + "type": "string", }, "ending_cross_street": { "description": "Name or number of the nearest cross street along the roadway where the event ends", - "type": "string" + "type": "string", }, "beginning_milepost": { "description": "The linear distance measured against a milepost marker along a roadway where the event begins", "type": "number", - "minimum": 0 + "minimum": 0, }, "ending_milepost": { "description": "The linear distance measured against a milepost marker along a roadway where the event ends", "type": "number", - "minimum": 0 + "minimum": 0, }, "start_date": { "description": "The UTC date and time (formatted according to RFC 3339, Section 5.6) when the road event begins (e.g. 2020-11-03T19:37:00Z)", "type": "string", - "format": "date-time" + "format": "date-time", }, "end_date": { "description": "The UTC date and time (formatted according to RFC 3339, Section 5.6) when the road event ends (e.g. 2020-11-03T19:37:00Z)", "type": "string", - "format": "date-time" + "format": "date-time", }, "is_start_date_verified": { "description": "Indicates if the detour has been confirmed to have started, such as from a person or device in the field or a report from a traffic management center", - "type": "boolean" + "type": "boolean", }, "is_end_date_verified": { "description": "Indicates if the detour has been confirmed to have ended, such as from a person or device in the field or a report from a traffic management center", - "type": "boolean" + "type": "boolean", }, "event_status": { "description": "**DEPRECATED**", - "$ref": "#/definitions/EventStatus" + "$ref": "#/definitions/EventStatus", }, "start_date_accuracy": { "description": "**DEPRECATED** Use is_start_date_verified instead", - "$ref": "#/definitions/TimeVerification" + "$ref": "#/definitions/TimeVerification", }, "end_date_accuracy": { "description": "**DEPRECATED** Use is_end_date_verified instead", - "$ref": "#/definitions/TimeVerification" - } + "$ref": "#/definitions/TimeVerification", + }, }, "required": [ "core_details", "start_date", - "end_date" + "end_date", ], "allOf": [ { @@ -582,7 +501,7 @@ "required": [ "start_date_accuracy" ] - } + }, ] }, { @@ -592,16 +511,12 @@ "is_end_date_verified" ] }, - { - "required": [ - "end_date_accuracy" - ] - } + {"required": ["end_date_accuracy"]}, ] - } - ] - } - ] + }, + ], + }, + ], }, "RoadEventCoreDetails": { "title": "Road Event Core Details", @@ -610,25 +525,21 @@ "properties": { "data_source_id": { "description": "Identifies the data source from which the road event data is sourced from", - "type": "string" - }, - "event_type": { - "$ref": "#/definitions/EventType" + "type": "string", }, + "event_type": {"$ref": "#/definitions/EventType"}, "related_road_events": { "description": "A list describing one or more road events which are related to this road event, such as a work zone project it is part of or another road event that occurs before or after it in sequence.", "type": "array", "items": { "$ref": "#/definitions/RelatedRoadEvent" - } + }, }, "road_names": { "description": "A list of publicly known names of the road on which the event occurs. This may include the road number designated by a jurisdiction such as a county, state or interstate (e.g. I-5, VT 133)", "type": "array", "minItems": 1, - "items": { - "type": "string" - } + "items": {"type": "string"}, }, "direction": { "$id": "https://raw.githubusercontent.com/usdot-jpo-ode/wzdx/main/schemas/4.2/Direction.json", @@ -643,38 +554,38 @@ "undefined", "unknown", "inner-loop", - "outer-loop" - ] + "outer-loop", + ], }, "name": { "description": "A human-readable name for the road event", - "type": "string" + "type": "string", }, "description": { "description": "Short free text description of the road event", - "type": "string" + "type": "string", }, "creation_date": { "description": "The UTC date and time (formatted according to RFC 3339, Section 5.6) when the road event was created (e.g. 2020-11-03T19:37:00Z)", "type": "string", - "format": "date-time" + "format": "date-time", }, "update_date": { "description": "The UTC date and time (formatted according to RFC 3339, Section 5.6) when any information in the RoadEventFeature (including child objects) that the RoadEventCoreDetails applies to was most recently updated or confirmed as up to date", "type": "string", - "format": "date-time" + "format": "date-time", }, "relationship": { "description": "**DEPRECATED**", - "$ref": "#/definitions/Relationship" - } + "$ref": "#/definitions/Relationship", + }, }, "required": [ "event_type", "data_source_id", "direction", - "road_names" - ] + "road_names", + ], }, "LocationMethod": { "title": "Location Method Enumerated Type", @@ -684,8 +595,8 @@ "sign-method", "junction-method", "other", - "unknown" - ] + "unknown", + ], }, "Relationship": { "title": "Relationship", @@ -696,35 +607,27 @@ "description": "Indicates the first (can be multiple) road event in a sequence of road events by RoadEventFeature 'id'", "type": "array", "minItems": 1, - "items": { - "type": "string" - } + "items": {"type": "string"}, }, "next": { "description": "Indicates the next (can be multiple) road event in a sequence of road events by RoadEventFeature 'id'", "type": "array", "minItems": 1, - "items": { - "type": "string" - } + "items": {"type": "string"}, }, "parents": { "description": "Indicates entities that the road event with this relationship is a part of, such as a work zone project or phase. Values can but do not have to correspond to a WZDx entity", "type": "array", "minItems": 1, - "items": { - "type": "string" - } + "items": {"type": "string"}, }, "children": { "description": "Indicates entities that are part of the road event with this relationship, such as a detour or piece of equipment. Values can but do not have to correspond to a WZDx entity", "type": "array", "minItems": 1, - "items": { - "type": "string" - } - } - } + "items": {"type": "string"}, + }, + }, }, "RelatedRoadEvent": { "title": "RelatedRoadEvent", @@ -733,34 +636,27 @@ "properties": { "type": { "description": "The type of road event being identified, such as another sequence of related work zones, a detour, or next road event in sequence.", - "$ref": "#/definitions/RelatedRoadEventType" + "$ref": "#/definitions/RelatedRoadEventType", }, "id": { "description": "An identifier for the related road event by the type property.", - "type": "string" - } + "type": "string", + }, }, - "required": [ - "type", - "id" - ] + "required": ["type", "id"], }, "TypeOfWork": { "title": "Type of Work", "description": "A description of the type of work being done in a road event and an indication of if that work will result in an architectural change to the roadway", "type": "object", "properties": { - "type_name": { - "$ref": "#/definitions/WorkTypeName" - }, + "type_name": {"$ref": "#/definitions/WorkTypeName"}, "is_architectural_change": { "description": "A flag indicating whether the type of work will result in an architectural change to the roadway", - "type": "boolean" - } + "type": "boolean", + }, }, - "required": [ - "type_name" - ] + "required": ["type_name"], }, "Lane": { "title": "Lane", @@ -770,56 +666,34 @@ "order": { "description": "The position (index) of the lane in sequence on the roadway, where '1' represents the left-most lane", "type": "integer", - "minimum": 1 - }, - "status": { - "$ref": "#/definitions/LaneStatus" - }, - "type": { - "$ref": "#/definitions/LaneType" + "minimum": 1, }, + "status": {"$ref": "#/definitions/LaneStatus"}, + "type": {"$ref": "#/definitions/LaneType"}, "lane_number": { "description": "***DEPRECATED*** The number assigned to the lane to help identify its position. Flexible, but usually used for regular, drivable lanes", "type": "integer", - "minimum": 1 + "minimum": 1, }, "restrictions": { "description": "A list of zero or more restrictions specific to the lane", "type": "array", - "items": { - "$ref": "#/definitions/Restriction" - } - } + "items": {"$ref": "#/definitions/Restriction"}, + }, }, - "required": [ - "status", - "type", - "order" - ] + "required": ["status", "type", "order"], }, "Restriction": { "title": "Restriction", "description": "A restriction on a roadway or lane, including type and value", "type": "object", "properties": { - "type": { - "$ref": "#/definitions/RestrictionType" - }, - "value": { - "type": "number" - }, - "unit": { - "$ref": "#/definitions/UnitOfMeasurement" - } + "type": {"$ref": "#/definitions/RestrictionType"}, + "value": {"type": "number"}, + "unit": {"$ref": "#/definitions/UnitOfMeasurement"}, }, - "required": [ - "type" - ], - "dependencies": { - "value": [ - "unit" - ] - } + "required": ["type"], + "dependencies": {"value": ["unit"]}, }, "CdsCurbZonesReference": { "title": "CdsCurbZonesReference", @@ -829,20 +703,15 @@ "cds_curb_zone_ids": { "description": "A list of CDS Curb Zone ids", "type": "array", - "items": { - "type": "string" - } + "items": {"type": "string"}, }, "cds_curbs_api_url": { "description": "An identifier for the source of the requested CDS Curbs API", "type": "string", - "format": "uri" - } + "format": "uri", + }, }, - "required": [ - "cds_curb_zone_ids", - "cds_curbs_api_url" - ] + "required": ["cds_curb_zone_ids", "cds_curbs_api_url"], }, "WorkerPresence": { "title": "Worker Presence", @@ -851,7 +720,7 @@ "properties": { "are_workers_present": { "description": "Whether workers are present in the work zone event area, following the definition provided in the \u2018definition\u2019 property on the WorkerPresence object", - "type": "boolean" + "type": "boolean", }, "method": { "$ref": "#/definitions/WorkerPresenceMethod" @@ -859,7 +728,7 @@ "worker_presence_last_confirmed_date": { "description": "The UTC date and time at which the presence of workers was last confirmed", "type": "string", - "format": "date-time" + "format": "date-time", }, "confidence": { "$ref": "#/definitions/WorkerPresenceConfidence" @@ -870,37 +739,25 @@ "items": { "$ref": "#/definitions/WorkerPresenceDefinition" }, - "uniqueItems": True - } + "uniqueItems": True, + }, }, - "required": [ - "are_workers_present" - ] + "required": ["are_workers_present"], }, "EventType": { "title": "Road Event Type Enumerated Type", "description": "The type of WZDx road event", - "enum": [ - "work-zone", - "detour", - "restriction" - ] + "enum": ["work-zone", "detour", "restriction"], }, "SpatialVerification": { "title": "Spatial Verification Enumerated Type", "description": "An indication of how a geographical coordinate was defined", - "enum": [ - "estimated", - "verified" - ] + "enum": ["estimated", "verified"], }, "TimeVerification": { "title": "Time Verification Enumerated Type", "description": "A measure of how accurate a date-time is", - "enum": [ - "estimated", - "verified" - ] + "enum": ["estimated", "verified"], }, "EventStatus": { "title": "Event Status Enumerated Type", @@ -910,17 +767,13 @@ "pending", "active", "completed", - "cancelled" - ] + "cancelled", + ], }, "WorkZoneType": { "title": "Work Zone Type Enumerated Type", "description": "The type of work zone road event", - "enum": [ - "static", - "moving", - "planned-moving-area" - ] + "enum": ["static", "moving", "planned-moving-area"], }, "VehicleImpact": { "title": "Vehicle Impact Enumerated Type", @@ -937,8 +790,8 @@ "some-lanes-closed-split", "flagging", "temporary-traffic-signal", - "unknown" - ] + "unknown", + ], }, "RestrictionType": { "title": "Restriction Type Enumerated Type", @@ -958,8 +811,8 @@ "towing-prohibited", "permitted-oversize-loads-prohibited", "local-access-only", - "no-passing" - ] + "no-passing", + ], }, "WorkTypeName": { "title": "Work Type Name Enumerated Type", @@ -974,8 +827,8 @@ "surface-work", "painting", "roadway-relocation", - "roadway-creation" - ] + "roadway-creation", + ], }, "LaneStatus": { "title": "Lane Status Enumerated Type", @@ -987,8 +840,8 @@ "shift-right", "merge-left", "merge-right", - "alternating-flow" - ] + "alternating-flow", + ], }, "LaneType": { "title": "Lane Type Enumerated Type", @@ -1005,8 +858,8 @@ "parking", "median", "two-way-center-turn-lane", - "center-left-turn-lane" - ] + "center-left-turn-lane", + ], }, "UnitOfMeasurement": { "title": "Unit of Measurement Enumerated Type", @@ -1017,8 +870,8 @@ "centimeters", "pounds", "tons", - "kilograms" - ] + "kilograms", + ], }, "WorkerPresenceMethod": { "title": "Worker Presence Method Enumerated Type", @@ -1032,8 +885,8 @@ "mobile-device-present", "check-in-app", "check-in-verbal", - "scheduled" - ] + "scheduled", + ], }, "WorkerPresenceDefinition": { "title": "Worker Presence Definition Enumerated Type", @@ -1045,17 +898,13 @@ "mobile-equipment-in-work-zone-not-moving", "fixed-equipment-in-work-zone", "humans-behind-barrier", - "humans-in-right-of-way" - ] + "humans-in-right-of-way", + ], }, "WorkerPresenceConfidence": { "title": "Worker Presence Confidence Enumerated Type", "description": "A high-level description of the feed publisher's confidence in the reported WorkerPresence value of are_workers_present", - "enum": [ - "low", - "medium", - "high" - ] + "enum": ["low", "medium", "high"], }, "RelatedRoadEventType": { "title": "Related Road Event Type Enumerated Type", @@ -1068,13 +917,13 @@ "related-work-zone", "related-detour", "planned-moving-operation", - "active-moving-operation" - ] - } - } - } + "active-moving-operation", + ], + }, + }, + }, ] - } + }, }, "bbox": { "$id": "https://raw.githubusercontent.com/usdot-jpo-ode/wzdx/main/schemas/4.2/BoundingBox.json", @@ -1083,9 +932,7 @@ "description": "Information on the coordinate range for a Geometry, Feature, or FeatureCollection", "type": "array", "minItems": 4, - "items": { - "type": "number" - } + "items": {"type": "number"}, }, "road_event_feed_info": { "$id": "https://raw.githubusercontent.com/usdot-jpo-ode/wzdx/main/schemas/4.2/FeedInfo.json", @@ -1096,53 +943,44 @@ "properties": { "publisher": { "description": "The organization responsible for publishing the feed", - "type": "string" + "type": "string", }, "contact_name": { "description": "The name of the individual or group responsible for the data feed", - "type": "string" + "type": "string", }, "contact_email": { "description": "The email address of the individual or group responsible for the data feed", "type": "string", - "format": "email" + "format": "email", }, "update_frequency": { "description": "The frequency in seconds at which the data feed is updated", "type": "integer", - "minimum": 1 + "minimum": 1, }, "update_date": { "description": "The UTC date and time when the GeoJSON file (representing the instance of the feed) was generated", "type": "string", - "format": "date-time" + "format": "date-time", }, "version": { "description": "The WZDx specification version used to create the data feed, in 'major.minor' format", "type": "string", - "pattern": "^(0|[1-9][0-9]*)\\.(0|[1-9][0-9]*)$" + "pattern": "^(0|[1-9][0-9]*)\\.(0|[1-9][0-9]*)$", }, "license": { - "description": "The URL of the license that applies to the data in the WZDx feed. This *must* be the string \"https://creativecommons.org/publicdomain/zero/1.0/\"", - "enum": [ - "https://creativecommons.org/publicdomain/zero/1.0/" - ] + "description": 'The URL of the license that applies to the data in the WZDx feed. This *must* be the string "https://creativecommons.org/publicdomain/zero/1.0/"', + "enum": ["https://creativecommons.org/publicdomain/zero/1.0/"], }, "data_sources": { "description": "A list of specific data sources for the road event data in the feed", "type": "array", - "items": { - "$ref": "#/definitions/FeedDataSource" - }, - "minItems": 1 - } + "items": {"$ref": "#/definitions/FeedDataSource"}, + "minItems": 1, + }, }, - "required": [ - "update_date", - "version", - "publisher", - "data_sources" - ], + "required": ["update_date", "version", "publisher", "data_sources"], "definitions": { "FeedDataSource": { "title": "WZDx Feed Data Source", @@ -1150,68 +988,51 @@ "type": "object", "properties": { "data_source_id": { - "description": "Unique identifier for the organization providing work zone data. It is recommended that this identifier is a Universally Unique IDentifier (UUID) as defined in [RFC 4122](https://datatracker.ietf.org/doc/html/rfc4122).", - "type": "string" + "description": "Unique identifier for the organization providing work zone data. It is recommended that this identifier is a Universally Unique Identifier (UUID) as defined in [RFC 4122](https://datatracker.ietf.org/doc/html/rfc4122).", + "type": "string", }, "organization_name": { "description": "The name of the organization for the authoritative source of the work zone data", - "type": "string" + "type": "string", }, "contact_name": { "description": "The name of the individual or group responsible for the data source", - "type": "string" + "type": "string", }, "contact_email": { "description": "The email address of the individual or group responsible for the data source", "type": "string", - "format": "email" + "format": "email", }, "update_frequency": { "description": "The frequency in seconds at which the data source is updated", "type": "integer", - "minimum": 1 + "minimum": 1, }, "update_date": { "description": "The UTC date and time when the data source was last updated", "type": "string", - "format": "date-time" + "format": "date-time", }, "lrs_type": { "description": "**DEPRECATED** Describes the type of linear referencing system used for the milepost measurements", - "type": "string" + "type": "string", }, "lrs_url": { "description": "**DEPRECATED** A URL where additional information on the LRS information and transformation information is stored", "type": "string", - "format": "uri" + "format": "uri", }, "location_verify_method": { "description": "***DEPRECATED***The method used to verify the accuracy of the location information", - "type": "string" - } + "type": "string", + }, }, - "required": [ - "data_source_id", - "organization_name" - ] + "required": ["data_source_id", "organization_name"], } - } - } - }, - "required": [ - "type", - "features" - ], - "anyOf": [ - { - "required": [ - "feed_info" - ] + }, }, - { - "required": [ - "road_event_feed_info" - ] - } - ] -} \ No newline at end of file + }, + "required": ["type", "features"], + "anyOf": [{"required": ["feed_info"]}, {"required": ["road_event_feed_info"]}], +} diff --git a/wzdx/tools/date_tools.py b/wzdx/tools/date_tools.py index 39599765..33db2514 100644 --- a/wzdx/tools/date_tools.py +++ b/wzdx/tools/date_tools.py @@ -1,98 +1,98 @@ -import datetime as dt -import logging -from datetime import datetime, timedelta, timezone - -from dateutil import parser - -ISO_8601_FORMAT_STRING = "%Y-%m-%dT%H:%M:%SZ" - - -def get_unix_from_iso_string(time_string): - return date_to_unix(parse_datetime_from_iso_string(time_string)) - - -def get_iso_string_from_unix(time_string): - return get_iso_string_from_datetime(parse_datetime_from_unix(time_string)) - - -def get_iso_string_from_datetime(date): - # This is added for unit test mocking (dt.datetime instead of just datetime) - if not date or type(date) != dt.datetime: - return None - return date.astimezone(timezone.utc).strftime(ISO_8601_FORMAT_STRING) - - -def parse_datetime_from_iso_string(time_string): - """Parse ISO string to datetime. Handles many different datetime formats""" - if not time_string or type(time_string) != str: - return None - - try: - return parser.parse(time_string) - except ValueError: - logging.warning("invalid datetime string: " + time_string) - return None - - -def parse_datetime_from_unix(time): - if not time: - return None - - if type(time) == str: - try: - return datetime_from_unix(float(time)) - except ValueError: - return None - elif type(time) == int or type(time) == float: - return datetime_from_unix(time) - - -def datetime_from_unix(time): - # I tested this method, and this value makes it fail (3001, 1, 19, 21, 59, 59) - if time > 32536850399: - return datetime.fromtimestamp(time / 1000, tz=timezone.utc) - else: - return datetime.fromtimestamp(time, tz=timezone.utc) - - -def date_to_unix(time: datetime): - if not time or type(time) != datetime: - return None - - return round(time.timestamp() * 1000) - - -# function to get event status from start and end datetimes -def get_event_status(start_time, end_time): - if not start_time or type(start_time) != dt.datetime: - return None - - event_status = "active" - - current_time = datetime.now() - - # check if datetime is time zone aware. If it is, get utc time - if ( - start_time.tzinfo is not None - and start_time.tzinfo.utcoffset(start_time) is not None - ): - current_time = datetime.now(timezone.utc) - - future_date_after_2weeks = current_time + timedelta(days=14) - past_date_2weeks_ago = current_time - timedelta(days=14) - - if current_time < start_time: - if start_time < future_date_after_2weeks: - event_status = "pending" - else: - event_status = "planned" - elif end_time and type(end_time) == dt.datetime and end_time < current_time: - if end_time > past_date_2weeks_ago: - event_status = "completed_recently" - else: - event_status = "completed" - return event_status - - -def get_current_ts_millis(): - return datetime.now(timezone.utc).timestamp() +import datetime as dt +import logging +from datetime import datetime, timedelta, timezone + +from dateutil import parser + +ISO_8601_FORMAT_STRING = "%Y-%m-%dT%H:%M:%SZ" + + +def get_unix_from_iso_string(time_string): + return date_to_unix(parse_datetime_from_iso_string(time_string)) + + +def get_iso_string_from_unix(time_string): + return get_iso_string_from_datetime(parse_datetime_from_unix(time_string)) + + +def get_iso_string_from_datetime(date): + # This is added for unit test mocking (dt.datetime instead of just datetime) + if not date or type(date) is not dt.datetime: + return None + return date.astimezone(timezone.utc).strftime(ISO_8601_FORMAT_STRING) + + +def parse_datetime_from_iso_string(time_string): + """Parse ISO string to datetime. Handles many different datetime formats""" + if not time_string or type(time_string) is not str: + return None + + try: + return parser.parse(time_string) + except ValueError: + logging.warning("invalid datetime string: " + time_string) + return None + + +def parse_datetime_from_unix(time): + if not time: + return None + + if type(time) is str: + try: + return datetime_from_unix(float(time)) + except ValueError: + return None + elif type(time) is int or type(time) is float: + return datetime_from_unix(time) + + +def datetime_from_unix(time): + # Maximum unix value of 32536850399 due to windows 32-bit signed integer (max valid date is 3001, 1, 19, 21, 59, 59) + if time > 32536850399: + return datetime.fromtimestamp(time / 1000, tz=timezone.utc) + else: + return datetime.fromtimestamp(time, tz=timezone.utc) + + +def date_to_unix(time: datetime): + if not time or type(time) is not datetime: + return None + + return round(time.timestamp() * 1000) + + +# function to get event status from start and end datetimes +def get_event_status(start_time, end_time): + if not start_time or type(start_time) is not dt.datetime: + return None + + event_status = "active" + + current_time = datetime.now() + + # check if datetime is time zone aware. If it is, get utc time + if ( + start_time.tzinfo is not None + and start_time.tzinfo.utcoffset(start_time) is not None + ): + current_time = datetime.now(timezone.utc) + + future_date_after_2weeks = current_time + timedelta(days=14) + past_date_2weeks_ago = current_time - timedelta(days=14) + + if current_time < start_time: + if start_time < future_date_after_2weeks: + event_status = "pending" + else: + event_status = "planned" + elif end_time and type(end_time) is dt.datetime and end_time < current_time: + if end_time > past_date_2weeks_ago: + event_status = "completed_recently" + else: + event_status = "completed" + return event_status + + +def get_current_ts_millis(): + return datetime.now(timezone.utc).timestamp()