diff --git a/Pipfile b/Pipfile index 2ad8ee7c..17f8b6bd 100644 --- a/Pipfile +++ b/Pipfile @@ -11,3 +11,4 @@ pytest-cov = "*" yapf = "*" toml = "*" # see https://github.com/google/yapf/issues/936 exceptiongroup = "*" +pytz = "*" diff --git a/Pipfile.lock b/Pipfile.lock index 83549d27..cfeae7e8 100644 --- a/Pipfile.lock +++ b/Pipfile.lock @@ -1,7 +1,7 @@ { "_meta": { "hash": { - "sha256": "1220820085e149c2e02ab3ec1c792084d31d4c4fae3a09c39e5864ab1800abf9" + "sha256": "cc84dcb9f7b861c7a6d0ee00f24bdd08f9d6bdb153f18fce2762730e168c851d" }, "pipfile-spec": 6, "requires": {}, @@ -20,85 +20,87 @@ "toml" ], "hashes": [ - "sha256:06a9a2be0b5b576c3f18f1a241f0473575c4a26021b52b2a85263a00f034d51f", - "sha256:06fb182e69f33f6cd1d39a6c597294cff3143554b64b9825d1dc69d18cc2fff2", - "sha256:0a5f9e1dbd7fbe30196578ca36f3fba75376fb99888c395c5880b355e2875f8a", - "sha256:0e1f928eaf5469c11e886fe0885ad2bf1ec606434e79842a879277895a50942a", - "sha256:171717c7cb6b453aebac9a2ef603699da237f341b38eebfee9be75d27dc38e01", - "sha256:1e9d683426464e4a252bf70c3498756055016f99ddaec3774bf368e76bbe02b6", - "sha256:201e7389591af40950a6480bd9edfa8ed04346ff80002cec1a66cac4549c1ad7", - "sha256:245167dd26180ab4c91d5e1496a30be4cd721a5cf2abf52974f965f10f11419f", - "sha256:2aee274c46590717f38ae5e4650988d1af340fe06167546cc32fe2f58ed05b02", - "sha256:2e07b54284e381531c87f785f613b833569c14ecacdcb85d56b25c4622c16c3c", - "sha256:31563e97dae5598556600466ad9beea39fb04e0229e61c12eaa206e0aa202063", - "sha256:33d6d3ea29d5b3a1a632b3c4e4f4ecae24ef170b0b9ee493883f2df10039959a", - "sha256:3d376df58cc111dc8e21e3b6e24606b5bb5dee6024f46a5abca99124b2229ef5", - "sha256:419bfd2caae268623dd469eff96d510a920c90928b60f2073d79f8fe2bbc5959", - "sha256:48c19d2159d433ccc99e729ceae7d5293fbffa0bdb94952d3579983d1c8c9d97", - "sha256:49969a9f7ffa086d973d91cec8d2e31080436ef0fb4a359cae927e742abfaaa6", - "sha256:52edc1a60c0d34afa421c9c37078817b2e67a392cab17d97283b64c5833f427f", - "sha256:537891ae8ce59ef63d0123f7ac9e2ae0fc8b72c7ccbe5296fec45fd68967b6c9", - "sha256:54b896376ab563bd38453cecb813c295cf347cf5906e8b41d340b0321a5433e5", - "sha256:58c2ccc2f00ecb51253cbe5d8d7122a34590fac9646a960d1430d5b15321d95f", - "sha256:5b7540161790b2f28143191f5f8ec02fb132660ff175b7747b95dcb77ac26562", - "sha256:5baa06420f837184130752b7c5ea0808762083bf3487b5038d68b012e5937dbe", - "sha256:5e330fc79bd7207e46c7d7fd2bb4af2963f5f635703925543a70b99574b0fea9", - "sha256:61b9a528fb348373c433e8966535074b802c7a5d7f23c4f421e6c6e2f1697a6f", - "sha256:63426706118b7f5cf6bb6c895dc215d8a418d5952544042c8a2d9fe87fcf09cb", - "sha256:6d040ef7c9859bb11dfeb056ff5b3872436e3b5e401817d87a31e1750b9ae2fb", - "sha256:6f48351d66575f535669306aa7d6d6f71bc43372473b54a832222803eb956fd1", - "sha256:7ee7d9d4822c8acc74a5e26c50604dff824710bc8de424904c0982e25c39c6cb", - "sha256:81c13a1fc7468c40f13420732805a4c38a105d89848b7c10af65a90beff25250", - "sha256:8d13c64ee2d33eccf7437961b6ea7ad8673e2be040b4f7fd4fd4d4d28d9ccb1e", - "sha256:8de8bb0e5ad103888d65abef8bca41ab93721647590a3f740100cd65c3b00511", - "sha256:8fa03bce9bfbeeef9f3b160a8bed39a221d82308b4152b27d82d8daa7041fee5", - "sha256:924d94291ca674905fe9481f12294eb11f2d3d3fd1adb20314ba89e94f44ed59", - "sha256:975d70ab7e3c80a3fe86001d8751f6778905ec723f5b110aed1e450da9d4b7f2", - "sha256:976b9c42fb2a43ebf304fa7d4a310e5f16cc99992f33eced91ef6f908bd8f33d", - "sha256:9e31cb64d7de6b6f09702bb27c02d1904b3aebfca610c12772452c4e6c21a0d3", - "sha256:a342242fe22407f3c17f4b499276a02b01e80f861f1682ad1d95b04018e0c0d4", - "sha256:a3d33a6b3eae87ceaefa91ffdc130b5e8536182cd6dfdbfc1aa56b46ff8c86de", - "sha256:a895fcc7b15c3fc72beb43cdcbdf0ddb7d2ebc959edac9cef390b0d14f39f8a9", - "sha256:afb17f84d56068a7c29f5fa37bfd38d5aba69e3304af08ee94da8ed5b0865833", - "sha256:b1c546aca0ca4d028901d825015dc8e4d56aac4b541877690eb76490f1dc8ed0", - "sha256:b29019c76039dc3c0fd815c41392a044ce555d9bcdd38b0fb60fb4cd8e475ba9", - "sha256:b46517c02ccd08092f4fa99f24c3b83d8f92f739b4657b0f146246a0ca6a831d", - "sha256:b7aa5f8a41217360e600da646004f878250a0d6738bcdc11a0a39928d7dc2050", - "sha256:b7b4c971f05e6ae490fef852c218b0e79d4e52f79ef0c8475566584a8fb3e01d", - "sha256:ba90a9563ba44a72fda2e85302c3abc71c5589cea608ca16c22b9804262aaeb6", - "sha256:cb017fd1b2603ef59e374ba2063f593abe0fc45f2ad9abdde5b4d83bd922a353", - "sha256:d22656368f0e6189e24722214ed8d66b8022db19d182927b9a248a2a8a2f67eb", - "sha256:d2c2db7fd82e9b72937969bceac4d6ca89660db0a0967614ce2481e81a0b771e", - "sha256:d39b5b4f2a66ccae8b7263ac3c8170994b65266797fb96cbbfd3fb5b23921db8", - "sha256:d62a5c7dad11015c66fbb9d881bc4caa5b12f16292f857842d9d1871595f4495", - "sha256:e7d9405291c6928619403db1d10bd07888888ec1abcbd9748fdaa971d7d661b2", - "sha256:e84606b74eb7de6ff581a7915e2dab7a28a0517fbe1c9239eb227e1354064dcd", - "sha256:eb393e5ebc85245347950143969b241d08b52b88a3dc39479822e073a1a8eb27", - "sha256:ebba1cd308ef115925421d3e6a586e655ca5a77b5bf41e02eb0e4562a111f2d1", - "sha256:ee57190f24fba796e36bb6d3aa8a8783c643d8fa9760c89f7a98ab5455fbf818", - "sha256:f2f67fe12b22cd130d34d0ef79206061bfb5eda52feb6ce0dba0644e20a03cf4", - "sha256:f6951407391b639504e3b3be51b7ba5f3528adbf1a8ac3302b687ecababf929e", - "sha256:f75f7168ab25dd93110c8a8117a22450c19976afbc44234cbf71481094c1b850", - "sha256:fdec9e8cbf13a5bf63290fc6013d216a4c7232efb51548594ca3631a7f13c3a3" + "sha256:0193657651f5399d433c92f8ae264aff31fc1d066deee4b831549526433f3f61", + "sha256:02f2edb575d62172aa28fe00efe821ae31f25dc3d589055b3fb64d51e52e4ab1", + "sha256:0491275c3b9971cdbd28a4595c2cb5838f08036bca31765bad5e17edf900b2c7", + "sha256:077d366e724f24fc02dbfe9d946534357fda71af9764ff99d73c3c596001bbd7", + "sha256:10e88e7f41e6197ea0429ae18f21ff521d4f4490aa33048f6c6f94c6045a6a75", + "sha256:18e961aa13b6d47f758cc5879383d27b5b3f3dcd9ce8cdbfdc2571fe86feb4dd", + "sha256:1a78b656a4d12b0490ca72651fe4d9f5e07e3c6461063a9b6265ee45eb2bdd35", + "sha256:1ed4b95480952b1a26d863e546fa5094564aa0065e1e5f0d4d0041f293251d04", + "sha256:23b27b8a698e749b61809fb637eb98ebf0e505710ec46a8aa6f1be7dc0dc43a6", + "sha256:23f5881362dcb0e1a92b84b3c2809bdc90db892332daab81ad8f642d8ed55042", + "sha256:32a8d985462e37cfdab611a6f95b09d7c091d07668fdc26e47a725ee575fe166", + "sha256:3468cc8720402af37b6c6e7e2a9cdb9f6c16c728638a2ebc768ba1ef6f26c3a1", + "sha256:379d4c7abad5afbe9d88cc31ea8ca262296480a86af945b08214eb1a556a3e4d", + "sha256:3cacfaefe6089d477264001f90f55b7881ba615953414999c46cc9713ff93c8c", + "sha256:3e3424c554391dc9ef4a92ad28665756566a28fecf47308f91841f6c49288e66", + "sha256:46342fed0fff72efcda77040b14728049200cbba1279e0bf1188f1f2078c1d70", + "sha256:536d609c6963c50055bab766d9951b6c394759190d03311f3e9fcf194ca909e1", + "sha256:5d6850e6e36e332d5511a48a251790ddc545e16e8beaf046c03985c69ccb2676", + "sha256:6008adeca04a445ea6ef31b2cbaf1d01d02986047606f7da266629afee982630", + "sha256:64e723ca82a84053dd7bfcc986bdb34af8d9da83c521c19d6b472bc6880e191a", + "sha256:6b00e21f86598b6330f0019b40fb397e705135040dbedc2ca9a93c7441178e74", + "sha256:6d224f0c4c9c98290a6990259073f496fcec1b5cc613eecbd22786d398ded3ad", + "sha256:6dceb61d40cbfcf45f51e59933c784a50846dc03211054bd76b421a713dcdf19", + "sha256:7ac8f8eb153724f84885a1374999b7e45734bf93a87d8df1e7ce2146860edef6", + "sha256:85ccc5fa54c2ed64bd91ed3b4a627b9cce04646a659512a051fa82a92c04a448", + "sha256:869b5046d41abfea3e381dd143407b0d29b8282a904a19cb908fa24d090cc018", + "sha256:8bdb0285a0202888d19ec6b6d23d5990410decb932b709f2b0dfe216d031d218", + "sha256:8dfc5e195bbef80aabd81596ef52a1277ee7143fe419efc3c4d8ba2754671756", + "sha256:8e738a492b6221f8dcf281b67129510835461132b03024830ac0e554311a5c54", + "sha256:918440dea04521f499721c039863ef95433314b1db00ff826a02580c1f503e45", + "sha256:9641e21670c68c7e57d2053ddf6c443e4f0a6e18e547e86af3fad0795414a628", + "sha256:9d2f9d4cc2a53b38cabc2d6d80f7f9b7e3da26b2f53d48f05876fef7956b6968", + "sha256:a07f61fc452c43cd5328b392e52555f7d1952400a1ad09086c4a8addccbd138d", + "sha256:a3277f5fa7483c927fe3a7b017b39351610265308f5267ac6d4c2b64cc1d8d25", + "sha256:a4a3907011d39dbc3e37bdc5df0a8c93853c369039b59efa33a7b6669de04c60", + "sha256:aeb2c2688ed93b027eb0d26aa188ada34acb22dceea256d76390eea135083950", + "sha256:b094116f0b6155e36a304ff912f89bbb5067157aff5f94060ff20bbabdc8da06", + "sha256:b8ffb498a83d7e0305968289441914154fb0ef5d8b3157df02a90c6695978295", + "sha256:b9bb62fac84d5f2ff523304e59e5c439955fb3b7f44e3d7b2085184db74d733b", + "sha256:c61f66d93d712f6e03369b6a7769233bfda880b12f417eefdd4f16d1deb2fc4c", + "sha256:ca6e61dc52f601d1d224526360cdeab0d0712ec104a2ce6cc5ccef6ed9a233bc", + "sha256:ca7b26a5e456a843b9b6683eada193fc1f65c761b3a473941efe5a291f604c74", + "sha256:d12c923757de24e4e2110cf8832d83a886a4cf215c6e61ed506006872b43a6d1", + "sha256:d17bbc946f52ca67adf72a5ee783cd7cd3477f8f8796f59b4974a9b59cacc9ee", + "sha256:dfd1e1b9f0898817babf840b77ce9fe655ecbe8b1b327983df485b30df8cc011", + "sha256:e0860a348bf7004c812c8368d1fc7f77fe8e4c095d661a579196a9533778e156", + "sha256:f2f5968608b1fe2a1d00d01ad1017ee27efd99b3437e08b83ded9b7af3f6f766", + "sha256:f3771b23bb3675a06f5d885c3630b1d01ea6cac9e84a01aaf5508706dba546c5", + "sha256:f68ef3660677e6624c8cace943e4765545f8191313a07288a53d3da188bd8581", + "sha256:f86f368e1c7ce897bf2457b9eb61169a44e2ef797099fb5728482b8d69f3f016", + "sha256:f90515974b39f4dea2f27c0959688621b46d96d5a626cf9c53dbc653a895c05c", + "sha256:fe558371c1bdf3b8fa03e097c523fb9645b8730399c14fe7721ee9c9e2a545d3" ], - "markers": "python_version >= '3.7'", - "version": "==7.2.7" + "markers": "python_version >= '3.8'", + "version": "==7.4.1" }, "exceptiongroup": { "hashes": [ - "sha256:232c37c63e4f682982c8b6459f33a8981039e5fb8756b2074364e5055c498c9e", - "sha256:d484c3090ba2889ae2928419117447a14daf3c1231d5e30d0aae34f354f01785" + "sha256:4bfd3996ac73b41e9b9628b04e079f193850720ea5945fc96a08633c66912f14", + "sha256:91f5c769735f051a4290d52edd0858999b57e5876e9f85937691bd4c9fa3ed68" ], "index": "pypi", - "version": "==1.1.1" + "markers": "python_version >= '3.7'", + "version": "==1.2.0" }, "flake8": { "hashes": [ - "sha256:3833794e27ff64ea4e9cf5d410082a8b97ff1a06c16aa3d2027339cd0f1195c7", - "sha256:c61007e76655af75e6785a931f452915b371dc48f56efd765247c8fe68f2b181" + "sha256:33f96621059e65eec474169085dc92bf26e7b2d47366b70be2f67ab80dc25132", + "sha256:a6dfbb75e03252917f2473ea9653f7cd799c3064e54d4c8140044c5c065f53c3" ], "index": "pypi", - "version": "==6.0.0" + "markers": "python_full_version >= '3.8.1'", + "version": "==7.0.0" + }, + "importlib-metadata": { + "hashes": [ + "sha256:4805911c3a4ec7c3966410053e9ec6a1fecd629117df5adee56dfc9432a1081e", + "sha256:f238736bb06590ae52ac1fab06a3a9ef1d8dce2b7a35b5ab329371d6c8f5d2cc" + ], + "markers": "python_version >= '3.8'", + "version": "==7.0.1" }, "iniconfig": { "hashes": [ @@ -118,35 +120,37 @@ }, "mypy": { "hashes": [ - "sha256:1c4c42c60a8103ead4c1c060ac3cdd3ff01e18fddce6f1016e08939647a0e703", - "sha256:44797d031a41516fcf5cbfa652265bb994e53e51994c1bd649ffcd0c3a7eccbf", - "sha256:473117e310febe632ddf10e745a355714e771ffe534f06db40702775056614c4", - "sha256:4c99c3ecf223cf2952638da9cd82793d8f3c0c5fa8b6ae2b2d9ed1e1ff51ba85", - "sha256:550a8b3a19bb6589679a7c3c31f64312e7ff482a816c96e0cecec9ad3a7564dd", - "sha256:658fe7b674769a0770d4b26cb4d6f005e88a442fe82446f020be8e5f5efb2fae", - "sha256:6e33bb8b2613614a33dff70565f4c803f889ebd2f859466e42b46e1df76018dd", - "sha256:6e42d29e324cdda61daaec2336c42512e59c7c375340bd202efa1fe0f7b8f8ca", - "sha256:74bc9b6e0e79808bf8678d7678b2ae3736ea72d56eede3820bd3849823e7f305", - "sha256:76ec771e2342f1b558c36d49900dfe81d140361dd0d2df6cd71b3db1be155409", - "sha256:7d23370d2a6b7a71dc65d1266f9a34e4cde9e8e21511322415db4b26f46f6b8c", - "sha256:87df44954c31d86df96c8bd6e80dfcd773473e877ac6176a8e29898bfb3501cb", - "sha256:8c5979d0deb27e0f4479bee18ea0f83732a893e81b78e62e2dda3e7e518c92ee", - "sha256:95d8d31a7713510685b05fbb18d6ac287a56c8f6554d88c19e73f724a445448a", - "sha256:a22435632710a4fcf8acf86cbd0d69f68ac389a3892cb23fbad176d1cddaf228", - "sha256:a8763e72d5d9574d45ce5881962bc8e9046bf7b375b0abf031f3e6811732a897", - "sha256:c1eb485cea53f4f5284e5baf92902cd0088b24984f4209e25981cc359d64448d", - "sha256:c5d2cc54175bab47011b09688b418db71403aefad07cbcd62d44010543fc143f", - "sha256:cbc07246253b9e3d7d74c9ff948cd0fd7a71afcc2b77c7f0a59c26e9395cb152", - "sha256:d0b6c62206e04061e27009481cb0ec966f7d6172b5b936f3ead3d74f29fe3dcf", - "sha256:ddae0f39ca146972ff6bb4399f3b2943884a774b8771ea0a8f50e971f5ea5ba8", - "sha256:e1f4d16e296f5135624b34e8fb741eb0eadedca90862405b1f1fde2040b9bd11", - "sha256:e86c2c6852f62f8f2b24cb7a613ebe8e0c7dc1402c61d36a609174f63e0ff017", - "sha256:ebc95f8386314272bbc817026f8ce8f4f0d2ef7ae44f947c4664efac9adec929", - "sha256:f9dca1e257d4cc129517779226753dbefb4f2266c4eaad610fc15c6a7e14283e", - "sha256:faff86aa10c1aa4a10e1a301de160f3d8fc8703b88c7e98de46b531ff1276a9a" + "sha256:028cf9f2cae89e202d7b6593cd98db6759379f17a319b5faf4f9978d7084cdc6", + "sha256:2afecd6354bbfb6e0160f4e4ad9ba6e4e003b767dd80d85516e71f2e955ab50d", + "sha256:2b5b6c721bd4aabaadead3a5e6fa85c11c6c795e0c81a7215776ef8afc66de02", + "sha256:42419861b43e6962a649068a61f4a4839205a3ef525b858377a960b9e2de6e0d", + "sha256:42c6680d256ab35637ef88891c6bd02514ccb7e1122133ac96055ff458f93fc3", + "sha256:485a8942f671120f76afffff70f259e1cd0f0cfe08f81c05d8816d958d4577d3", + "sha256:4c886c6cce2d070bd7df4ec4a05a13ee20c0aa60cb587e8d1265b6c03cf91da3", + "sha256:4e6d97288757e1ddba10dd9549ac27982e3e74a49d8d0179fc14d4365c7add66", + "sha256:4ef4be7baf08a203170f29e89d79064463b7fc7a0908b9d0d5114e8009c3a259", + "sha256:51720c776d148bad2372ca21ca29256ed483aa9a4cdefefcef49006dff2a6835", + "sha256:52825b01f5c4c1c4eb0db253ec09c7aa17e1a7304d247c48b6f3599ef40db8bd", + "sha256:538fd81bb5e430cc1381a443971c0475582ff9f434c16cd46d2c66763ce85d9d", + "sha256:5c1538c38584029352878a0466f03a8ee7547d7bd9f641f57a0f3017a7c905b8", + "sha256:6ff8b244d7085a0b425b56d327b480c3b29cafbd2eff27316a004f9a7391ae07", + "sha256:7178def594014aa6c35a8ff411cf37d682f428b3b5617ca79029d8ae72f5402b", + "sha256:720a5ca70e136b675af3af63db533c1c8c9181314d207568bbe79051f122669e", + "sha256:7f1478736fcebb90f97e40aff11a5f253af890c845ee0c850fe80aa060a267c6", + "sha256:855fe27b80375e5c5878492f0729540db47b186509c98dae341254c8f45f42ae", + "sha256:8963b83d53ee733a6e4196954502b33567ad07dfd74851f32be18eb932fb1cb9", + "sha256:9261ed810972061388918c83c3f5cd46079d875026ba97380f3e3978a72f503d", + "sha256:99b00bc72855812a60d253420d8a2eae839b0afa4938f09f4d2aa9bb4654263a", + "sha256:ab3c84fa13c04aeeeabb2a7f67a25ef5d77ac9d6486ff33ded762ef353aa5592", + "sha256:afe3fe972c645b4632c563d3f3eff1cdca2fa058f730df2b93a35e3b0c538218", + "sha256:d19c413b3c07cbecf1f991e2221746b0d2a9410b59cb3f4fb9557f0365a1a817", + "sha256:df9824ac11deaf007443e7ed2a4a26bebff98d2bc43c6da21b2b64185da011c4", + "sha256:e46f44b54ebddbeedbd3d5b289a893219065ef805d95094d16a0af6630f5d410", + "sha256:f5ac9a4eeb1ec0f1ccdc6f326bcdb464de5f80eb07fb38b5ddd7b0de6bc61e55" ], "index": "pypi", - "version": "==1.3.0" + "markers": "python_version >= '3.8'", + "version": "==1.8.0" }, "mypy-extensions": { "hashes": [ @@ -158,43 +162,52 @@ }, "packaging": { "hashes": [ - "sha256:994793af429502c4ea2ebf6bf664629d07c1a9fe974af92966e4b8d2df7edc61", - "sha256:a392980d2b6cffa644431898be54b0045151319d1e7ec34f0cfed48767dd334f" + "sha256:048fb0e9405036518eaaf48a55953c750c11e1a1b68e0dd1a9d62ed0c092cfc5", + "sha256:8c491190033a9af7e1d931d0b5dacc2ef47509b34dd0de67ed209b5203fc88c7" ], "markers": "python_version >= '3.7'", - "version": "==23.1" + "version": "==23.2" + }, + "platformdirs": { + "hashes": [ + "sha256:0614df2a2f37e1a662acbd8e2b25b92ccf8632929bc6d43467e17fe89c75e068", + "sha256:ef0cc731df711022c174543cb70a9b5bd22e5a9337c8624ef2c2ceb8ddad8768" + ], + "markers": "python_version >= '3.8'", + "version": "==4.2.0" }, "pluggy": { "hashes": [ - "sha256:4224373bacce55f955a878bf9cfa763c1e360858e330072059e10bad68531159", - "sha256:74134bbf457f031a36d68416e1509f34bd5ccc019f0bcc952c7b909d06b37bd3" + "sha256:7db9f7b503d67d1c5b95f59773ebb58a8c1c288129a88665838012cfb07b8981", + "sha256:8c85c2876142a764e5b7548e7d9a0e0ddb46f5185161049a79b7e974454223be" ], - "markers": "python_version >= '3.6'", - "version": "==1.0.0" + "markers": "python_version >= '3.8'", + "version": "==1.4.0" }, "pycodestyle": { "hashes": [ - "sha256:347187bdb476329d98f695c213d7295a846d1152ff4fe9bacb8a9590b8ee7053", - "sha256:8a4eaf0d0495c7395bdab3589ac2db602797d76207242c17d470186815706610" + "sha256:41ba0e7afc9752dfb53ced5489e89f8186be00e599e712660695b7a75ff2663f", + "sha256:44fe31000b2d866f2e41841b18528a505fbd7fef9017b04eff4e2648a0fadc67" ], - "markers": "python_version >= '3.6'", - "version": "==2.10.0" + "markers": "python_version >= '3.8'", + "version": "==2.11.1" }, "pyflakes": { "hashes": [ - "sha256:ec55bf7fe21fff7f1ad2f7da62363d749e2a470500eab1b555334b67aa1ef8cf", - "sha256:ec8b276a6b60bd80defed25add7e439881c19e64850afd9b346283d4165fd0fd" + "sha256:1c61603ff154621fb2a9172037d84dca3500def8c8b630657d1701f026f8af3f", + "sha256:84b5be138a2dfbb40689ca07e2152deb896a65c3a3e24c251c5c62489568074a" ], - "markers": "python_version >= '3.6'", - "version": "==3.0.1" + "markers": "python_version >= '3.8'", + "version": "==3.2.0" }, "pytest": { "hashes": [ - "sha256:3799fa815351fea3a5e96ac7e503a96fa51cc9942c3753cda7651b93c1cfa362", - "sha256:434afafd78b1d78ed0addf160ad2b77a30d35d4bdf8af234fe621919d9ed15e3" + "sha256:267f6563751877d772019b13aacbe4e860d73fe8f651f28112e9ac37de7513ae", + "sha256:3e4f16fe1c0a9dc9d9389161c127c3edc5d810c38d6793042fb81d9f48a59fca" ], "index": "pypi", - "version": "==7.3.1" + "markers": "python_version >= '3.8'", + "version": "==8.0.1" }, "pytest-cov": { "hashes": [ @@ -202,14 +215,24 @@ "sha256:6ba70b9e97e69fcc3fb45bfeab2d0a138fb65c4d0d6a41ef33983ad114be8c3a" ], "index": "pypi", + "markers": "python_version >= '3.7'", "version": "==4.1.0" }, + "pytz": { + "hashes": [ + "sha256:2a29735ea9c18baf14b448846bde5a48030ed267578472d8955cd0e7443a9812", + "sha256:328171f4e3623139da4983451950b28e95ac706e13f3f2630a879749e7a8b319" + ], + "index": "pypi", + "version": "==2024.1" + }, "toml": { "hashes": [ "sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b", "sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f" ], "index": "pypi", + "markers": "python_version >= '2.6' and python_version not in '3.0, 3.1, 3.2, 3.3'", "version": "==0.10.2" }, "tomli": { @@ -217,24 +240,33 @@ "sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc", "sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f" ], - "markers": "python_version >= '3.7'", + "markers": "python_version < '3.11'", "version": "==2.0.1" }, "typing-extensions": { "hashes": [ - "sha256:88a4153d8505aabbb4e13aacb7c486c2b4a33ca3b3f807914a9b4c844c471c26", - "sha256:d91d5919357fe7f681a9f2b5b4cb2a5f1ef0a1e9f59c4d8ff0d3491e05c0ffd5" + "sha256:23478f88c37f27d76ac8aee6c905017a143b0b1b886c3c9f66bc2fd94f9f5783", + "sha256:af72aea155e91adfc61c3ae9e0e342dbc0cba726d6cba4b6c72c1f34e47291cd" ], - "markers": "python_version >= '3.7'", - "version": "==4.6.3" + "markers": "python_version >= '3.8'", + "version": "==4.9.0" }, "yapf": { "hashes": [ - "sha256:4c2b59bd5ffe46f3a7da48df87596877189148226ce267c16e8b44240e51578d", - "sha256:da62bdfea3df3673553351e6246abed26d9fe6780e548a5af9e70f6d2b4f5b9a" + "sha256:4dab8a5ed7134e26d57c1647c7483afb3f136878b579062b786c9ba16b94637b", + "sha256:adc8b5dd02c0143108878c499284205adb258aad6db6634e5b869e7ee2bd548b" ], "index": "pypi", - "version": "==0.33.0" + "markers": "python_version >= '3.7'", + "version": "==0.40.2" + }, + "zipp": { + "hashes": [ + "sha256:0e923e726174922dce09c53c59ad483ff7bbb8e572e00c7f7c46b88556409f31", + "sha256:84e64a1c28cf7e91ed2078bb8cc8c259cb19b76942096c8d7b84947690cabaf0" + ], + "markers": "python_version >= '3.8'", + "version": "==3.17.0" } } } diff --git a/nats/js/api.py b/nats/js/api.py index 4b338712..76000aa9 100644 --- a/nats/js/api.py +++ b/nats/js/api.py @@ -15,6 +15,7 @@ from __future__ import annotations from dataclasses import dataclass, fields, replace +import datetime from enum import Enum from typing import Any, Dict, Optional, TypeVar, List @@ -86,6 +87,58 @@ def _to_nanoseconds(val: Optional[float]) -> Optional[int]: return 0 return int(val * _NANOSECOND) + @staticmethod + def _convert_rfc3339(resp: Dict[str, Any], field: str) -> None: + """Convert a RFC 3339 formatted string into a datetime. + + If the string is None, None is returned. + """ + val = resp.get(field, None) + if val is None: + return None + # Handle Zulu (UTC) + # Until python < 3.11, fromisoformat() do not accept "Z" as a valid + # timezone. See: https://github.com/python/cpython/issues/80010 + if val.endswith("Z"): + offset = "+00:00" + raw_date = val[:-1] + # There MUST be an offset if not Zulu. + # Until python3.11, fromisoformat() only accepts 3 or 6 decimal places for + # fractional seconds. See: https://github.com/python/cpython/issues/95221 + # In order to pad missing microseconds, we need to temporary remove the offset. + # Offset has a fixed sized of 5 characters. + else: + offset = val[-6:] + raw_date = val[:-6] + # Pad missing microseconds by adding "0" until length is 26. + # 26 is the length of a valid RFC3339 string with microseconds precision. + # Since python datetimes do not support nanosecond precision, we need to + # truncate the string if it has more than 6 decimal places for fractional seconds. + if "." in raw_date: + raw_date = raw_date[:26] + length = len(raw_date) + if length < 26: + raw_date += "0" * (26 - length) + # Add offset back + raw_date = raw_date + offset + # Parse string into datetime using fromisoformat + resp[field] = datetime.datetime.fromisoformat(raw_date).astimezone( + datetime.timezone.utc + ) + + @staticmethod + def _to_rfc3339(date: datetime.datetime) -> str: + """Convert a datetime into RFC 3339 formatted string. + + If datetime does not have timezone information, datetime + is assumed to be in UTC timezone. + """ + if date.tzinfo is None: + date = date.replace(tzinfo=datetime.timezone.utc) + elif date.tzinfo != datetime.timezone.utc: + date = date.astimezone(datetime.timezone.utc) + return date.isoformat().replace("+00:00", "Z").replace(".000000", "") + @classmethod def from_response(cls: type[_B], resp: Dict[str, Any]) -> _B: """Read the class instance from a server response. @@ -153,8 +206,7 @@ def as_dict(self) -> Dict[str, object]: class StreamSource(Base): name: str opt_start_seq: Optional[int] = None - # FIXME: Handle time type, omit for now. - # opt_start_time: Optional[str] = None + opt_start_time: Optional[datetime.datetime] = None filter_subject: Optional[str] = None external: Optional[ExternalStream] = None subject_transforms: Optional[List[SubjectTransform]] = None @@ -163,6 +215,7 @@ class StreamSource(Base): def from_response(cls, resp: Dict[str, Any]): cls._convert(resp, 'external', ExternalStream) cls._convert(resp, 'subject_transforms', SubjectTransform) + cls._convert_rfc3339(resp, 'opt_start_time') return super().from_response(resp) def as_dict(self) -> Dict[str, object]: @@ -171,6 +224,8 @@ def as_dict(self) -> Dict[str, object]: result['subject_transform'] = [ tr.as_dict() for tr in self.subject_transforms ] + if self.opt_start_time is not None: + result['opt_start_time'] = self._to_rfc3339(self.opt_start_time) return result @@ -437,7 +492,7 @@ class ConsumerConfig(Base): description: Optional[str] = None deliver_policy: Optional[DeliverPolicy] = DeliverPolicy.ALL opt_start_seq: Optional[int] = None - opt_start_time: Optional[int] = None + opt_start_time: Optional[datetime.datetime] = None ack_policy: Optional[AckPolicy] = AckPolicy.EXPLICIT ack_wait: Optional[float] = None # in seconds max_deliver: Optional[int] = None @@ -476,12 +531,15 @@ def from_response(cls, resp: Dict[str, Any]): cls._convert_nanoseconds(resp, 'ack_wait') cls._convert_nanoseconds(resp, 'idle_heartbeat') cls._convert_nanoseconds(resp, 'inactive_threshold') + cls._convert_rfc3339(resp, 'opt_start_time') if 'backoff' in resp: resp['backoff'] = [val / _NANOSECOND for val in resp['backoff']] return super().from_response(resp) def as_dict(self) -> Dict[str, object]: result = super().as_dict() + if self.opt_start_time is not None: + result['opt_start_time'] = self._to_rfc3339(self.opt_start_time) result['ack_wait'] = self._to_nanoseconds(self.ack_wait) result['idle_heartbeat'] = self._to_nanoseconds(self.idle_heartbeat) result['inactive_threshold'] = self._to_nanoseconds( @@ -496,8 +554,18 @@ def as_dict(self) -> Dict[str, object]: class SequenceInfo(Base): consumer_seq: int stream_seq: int - # FIXME: Do not handle dates for now. - # last_active: Optional[datetime] + last_active: Optional[datetime.datetime] = None + + @classmethod + def from_response(cls, resp: Dict[str, Any]): + cls._convert_rfc3339(resp, 'last_active') + return super().from_response(resp) + + def as_dict(self) -> Dict[str, object]: + result = super().as_dict() + if self.last_active is not None: + result['last_active'] = self._to_rfc3339(self.last_active) + return result @dataclass @@ -508,8 +576,7 @@ class ConsumerInfo(Base): name: str stream_name: str config: ConsumerConfig - # FIXME: Do not handle dates for now. - # created: datetime + created: datetime.datetime delivered: Optional[SequenceInfo] = None ack_floor: Optional[SequenceInfo] = None num_ack_pending: Optional[int] = None @@ -525,8 +592,14 @@ def from_response(cls, resp: Dict[str, Any]): cls._convert(resp, 'ack_floor', SequenceInfo) cls._convert(resp, 'config', ConsumerConfig) cls._convert(resp, 'cluster', ClusterInfo) + cls._convert_rfc3339(resp, 'created') return super().from_response(resp) + def as_dict(self) -> Dict[str, object]: + result = super().as_dict() + result['created'] = self._to_rfc3339(self.created) + return result + @dataclass class AccountLimits(Base): diff --git a/tests/test_js.py b/tests/test_js.py index 0d4d893c..7ce88ccf 100644 --- a/tests/test_js.py +++ b/tests/test_js.py @@ -13,6 +13,7 @@ import tempfile import pytest +import pytz import nats import nats.js.api from nats.aio.msg import Msg @@ -1387,6 +1388,190 @@ async def test_consumer_with_name(self): await nc.close() + @async_test + async def test_consumer_with_opt_start_time_date_only(self): + nc = NATS() + await nc.connect() + jsm = nc.jsm() + await jsm.add_stream(name="ctests", subjects=["a", "b", "c.>"]) + con = await jsm.add_consumer( + "ctests", + opt_start_time=datetime.datetime(1970, 1, 1), + deliver_policy=api.DeliverPolicy.BY_START_TIME, + ) + assert isinstance(con.created, datetime.datetime) + assert con.config.opt_start_time == datetime.datetime( + 1970, 1, 1, tzinfo=datetime.timezone.utc + ) + await nc.close() + + @async_test + async def test_consumer_with_opt_start_time_timestamp(self): + nc = NATS() + await nc.connect() + jsm = nc.jsm() + await jsm.add_stream(name="ctests", subjects=["a", "b", "c.>"]) + con = await jsm.add_consumer( + "ctests", + opt_start_time=datetime.datetime(1970, 1, 1, 1, 1, 1), + deliver_policy=api.DeliverPolicy.BY_START_TIME, + ) + assert isinstance(con.created, datetime.datetime) + assert con.config.opt_start_time == datetime.datetime( + 1970, 1, 1, 1, 1, 1, tzinfo=datetime.timezone.utc + ) + await nc.close() + + @async_test + async def test_consumer_with_opt_start_time_microseconds(self): + nc = NATS() + await nc.connect() + jsm = nc.jsm() + await jsm.add_stream(name="ctests", subjects=["a", "b", "c.>"]) + con = await jsm.add_consumer( + "ctests", + opt_start_time=datetime.datetime( + 1970, 1, 1, 1, 1, 1, microsecond=123456 + ), + deliver_policy=api.DeliverPolicy.BY_START_TIME, + ) + assert isinstance(con.created, datetime.datetime) + assert con.config.opt_start_time == datetime.datetime( + 1970, + 1, + 1, + 1, + 1, + 1, + microsecond=123456, + tzinfo=datetime.timezone.utc + ) + await nc.close() + + @async_test + async def test_consumer_with_opt_start_time_date_tz(self): + nc = NATS() + await nc.connect() + jsm = nc.jsm() + await jsm.add_stream(name="ctests", subjects=["a", "b", "c.>"]) + con = await jsm.add_consumer( + "ctests", + opt_start_time=datetime.datetime( + 1970, 1, 1, tzinfo=pytz.timezone("Europe/Paris") + ), + deliver_policy=api.DeliverPolicy.BY_START_TIME, + ) + assert isinstance(con.created, datetime.datetime) + assert con.config.opt_start_time == datetime.datetime( + 1970, 1, 1, tzinfo=pytz.timezone("Europe/Paris") + ) + await nc.close() + + @async_test + async def test_consumer_with_opt_start_time_timestamp_tz(self): + nc = NATS() + await nc.connect() + jsm = nc.jsm() + await jsm.add_stream(name="ctests", subjects=["a", "b", "c.>"]) + con = await jsm.add_consumer( + "ctests", + opt_start_time=datetime.datetime( + 1970, 1, 1, 1, 1, 1, tzinfo=pytz.timezone("Europe/Paris") + ), + deliver_policy=api.DeliverPolicy.BY_START_TIME, + ) + assert isinstance(con.created, datetime.datetime) + assert con.config.opt_start_time == datetime.datetime( + 1970, 1, 1, 1, 1, 1, tzinfo=pytz.timezone("Europe/Paris") + ) + await nc.close() + + @async_test + async def test_consumer_with_opt_start_time_microseconds_tz(self): + nc = NATS() + await nc.connect() + jsm = nc.jsm() + await jsm.add_stream(name="ctests", subjects=["a", "b", "c.>"]) + con = await jsm.add_consumer( + "ctests", + opt_start_time=datetime.datetime( + 1970, + 1, + 1, + 1, + 1, + 1, + microsecond=123456, + tzinfo=pytz.timezone("Europe/Paris") + ), + deliver_policy=api.DeliverPolicy.BY_START_TIME, + ) + assert isinstance(con.created, datetime.datetime) + assert con.config.opt_start_time == datetime.datetime( + 1970, + 1, + 1, + 1, + 1, + 1, + microsecond=123456, + tzinfo=pytz.timezone("Europe/Paris") + ) + await nc.close() + + def test_parser_consumer_info_with_created_timestamp(self): + for created in [ + "1970-01-01T01:02:03Z", + "1970-01-01T02:02:03+01:00", + "1970-01-01T01:02:03.0Z", + "1970-01-01T01:02:03.00Z", + "1970-01-01T01:02:03.000Z", + "1970-01-01T01:02:03.0000Z", + "1970-01-01T01:02:03.00000Z", + "1970-01-01T01:02:03.000000Z", + "1970-01-01T01:02:03.0000000Z", + "1970-01-01T01:02:03.00000000Z", + "1970-01-01T01:02:03.000000000Z", + "1970-01-01T02:02:03.000000000Z+01:00", + ]: + info = api.ConsumerInfo.from_response({ + "name": "test", + "stream_name": "test", + "config": {}, + "created": created + }) + created = info.created + assert created == datetime.datetime( + 1970, 1, 1, 1, 2, 3, tzinfo=datetime.timezone.utc + ) + for created in [ + "1970-01-01T01:02:03.4Z", + "1970-01-01T01:02:03.4+00:00", + "1970-01-01T01:02:03.40Z", + "1970-01-01T02:02:03.40+01:00", + "1970-01-01T01:02:03.400Z", + "1970-01-01T04:02:03.400+03:00", + "1970-01-01T01:02:03.4000Z", + "1970-01-01T07:22:03.4000+06:20", + "1970-01-01T01:02:03.40000Z", + "1970-01-01T00:02:03.400000-01:00", + "1970-01-01T01:02:03.400000Z", + "1970-01-01T01:02:03.4000000Z", + "1970-01-01T01:02:03.40000000Z", + "1970-01-01T01:02:03.400000000Z", + "1970-01-01T02:02:03.400000000Z+01:00", + ]: + info = api.ConsumerInfo.from_response({ + "name": "test", + "stream_name": "test", + "config": {}, + "created": created + }) + created = info.created + assert created == datetime.datetime( + 1970, 1, 1, 1, 2, 3, 400000, tzinfo=datetime.timezone.utc + ) + @async_test async def test_jsm_stream_info_options(self): nc = NATS()