From a649f3e1abfc8d16efdf5994d0ea8cb632a131b0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=B4me=20Deuchnord?= Date: Wed, 24 Feb 2021 12:26:44 +0100 Subject: [PATCH] Init repository, extract Kosmorrolib --- Pipfile | 19 + Pipfile.lock | 330 ++++++++++++++++++ kosmorrolib/__init__.py | 21 ++ .../__pycache__/dateutil.cpython-39.pyc | Bin 0 -> 522 bytes kosmorrolib/core.py | 117 +++++++ kosmorrolib/data.py | 223 ++++++++++++ kosmorrolib/dateutil.py | 28 ++ kosmorrolib/enum.py | 41 +++ kosmorrolib/ephemerides.py | 163 +++++++++ kosmorrolib/events.py | 224 ++++++++++++ kosmorrolib/exceptions.py | 40 +++ kosmorrolib/version.py | 46 +++ tests.py | 6 + 13 files changed, 1258 insertions(+) create mode 100644 Pipfile create mode 100644 Pipfile.lock create mode 100644 kosmorrolib/__init__.py create mode 100644 kosmorrolib/__pycache__/dateutil.cpython-39.pyc create mode 100644 kosmorrolib/core.py create mode 100644 kosmorrolib/data.py create mode 100644 kosmorrolib/dateutil.py create mode 100644 kosmorrolib/enum.py create mode 100644 kosmorrolib/ephemerides.py create mode 100644 kosmorrolib/events.py create mode 100644 kosmorrolib/exceptions.py create mode 100644 kosmorrolib/version.py create mode 100644 tests.py diff --git a/Pipfile b/Pipfile new file mode 100644 index 0000000..17a706b --- /dev/null +++ b/Pipfile @@ -0,0 +1,19 @@ +[[source]] +name = "pypi" +url = "https://pypi.org/simple" +verify_ssl = true + +[dev-packages] +pylintfileheader = "*" +pylint = "*" +babel = "*" +unittest-data-provider = "*" +coveralls = "*" + +[packages] +skyfield = ">=1.32.0,<2.0.0" +numpy = ">=1.17.0,<2.0.0" +python-dateutil = "*" + +[requires] +python_version = "3" diff --git a/Pipfile.lock b/Pipfile.lock new file mode 100644 index 0000000..2267729 --- /dev/null +++ b/Pipfile.lock @@ -0,0 +1,330 @@ +{ + "_meta": { + "hash": { + "sha256": "05fc0ad542c6b81f02f9f1d6c4dbf8d2743ab07678c5c42aa3f7739c9aee7e36" + }, + "pipfile-spec": 6, + "requires": { + "python_version": "3" + }, + "sources": [ + { + "name": "pypi", + "url": "https://pypi.org/simple", + "verify_ssl": true + } + ] + }, + "default": { + "certifi": { + "hashes": [ + "sha256:1a4995114262bffbc2413b159f2a1a480c969de6e6eb13ee966d470af86af59c", + "sha256:719a74fb9e33b9bd44cc7f3a8d94bc35e4049deebe19ba7d8e108280cfd59830" + ], + "version": "==2020.12.5" + }, + "jplephem": { + "hashes": [ + "sha256:e0017de1a45015b247faa8d15ebf35d2014abe949135f3703a3252adb96e43b1" + ], + "version": "==2.15" + }, + "numpy": { + "hashes": [ + "sha256:032be656d89bbf786d743fee11d01ef318b0781281241997558fa7950028dd29", + "sha256:104f5e90b143dbf298361a99ac1af4cf59131218a045ebf4ee5990b83cff5fab", + "sha256:125a0e10ddd99a874fd357bfa1b636cd58deb78ba4a30b5ddb09f645c3512e04", + "sha256:12e4ba5c6420917571f1a5becc9338abbde71dd811ce40b37ba62dec7b39af6d", + "sha256:13adf545732bb23a796914fe5f891a12bd74cf3d2986eed7b7eba2941eea1590", + "sha256:2d7e27442599104ee08f4faed56bb87c55f8b10a5494ac2ead5c98a4b289e61f", + "sha256:3bc63486a870294683980d76ec1e3efc786295ae00128f9ea38e2c6e74d5a60a", + "sha256:3d3087e24e354c18fb35c454026af3ed8997cfd4997765266897c68d724e4845", + "sha256:4ed8e96dc146e12c1c5cdd6fb9fd0757f2ba66048bf94c5126b7efebd12d0090", + "sha256:60759ab15c94dd0e1ed88241fd4fa3312db4e91d2c8f5a2d4cf3863fad83d65b", + "sha256:65410c7f4398a0047eea5cca9b74009ea61178efd78d1be9847fac1d6716ec1e", + "sha256:66b467adfcf628f66ea4ac6430ded0614f5cc06ba530d09571ea404789064adc", + "sha256:7199109fa46277be503393be9250b983f325880766f847885607d9b13848f257", + "sha256:72251e43ac426ff98ea802a931922c79b8d7596480300eb9f1b1e45e0543571e", + "sha256:89e5336f2bec0c726ac7e7cdae181b325a9c0ee24e604704ed830d241c5e47ff", + "sha256:89f937b13b8dd17b0099c7c2e22066883c86ca1575a975f754babc8fbf8d69a9", + "sha256:9c94cab5054bad82a70b2e77741271790304651d584e2cdfe2041488e753863b", + "sha256:9eb551d122fadca7774b97db8a112b77231dcccda8e91a5bc99e79890797175e", + "sha256:a1d7995d1023335e67fb070b2fae6f5968f5be3802b15ad6d79d81ecaa014fe0", + "sha256:ae61f02b84a0211abb56462a3b6cd1e7ec39d466d3160eb4e1da8bf6717cdbeb", + "sha256:b9410c0b6fed4a22554f072a86c361e417f0258838957b78bd063bde2c7f841f", + "sha256:c26287dfc888cf1e65181f39ea75e11f42ffc4f4529e5bd19add57ad458996e2", + "sha256:c91ec9569facd4757ade0888371eced2ecf49e7982ce5634cc2cf4e7331a4b14", + "sha256:ecb5b74c702358cdc21268ff4c37f7466357871f53a30e6f84c686952bef16a9" + ], + "index": "pypi", + "version": "==1.20.1" + }, + "python-dateutil": { + "hashes": [ + "sha256:73ebfe9dbf22e832286dafa60473e4cd239f8592f699aa5adaf10050e6e1823c", + "sha256:75bb3f31ea686f1197762692a9ee6a7550b59fc6ca3a1f4b5d7e32fb98e2da2a" + ], + "index": "pypi", + "version": "==2.8.1" + }, + "sgp4": { + "hashes": [ + "sha256:0864ba1062418b1b3e23e38e90032dd2c6be1f09b66c5026a18f8171548720a9", + "sha256:1601f590950cd34ffc54c8c2811a228632d394c1e28a74054373638f7ead4801", + "sha256:2a75446b7299217f7e66002677226dab4e41b1ad62ce7fb238c0947071655899", + "sha256:2ad7a1c6dde04591a847c85397151e2adb458d889838a9e10177055bf373ec4a", + "sha256:2c21fc1f47882136883390497f92afd387d79c35f23a534b652ae44e02da0518", + "sha256:318309f4efa05361a64498ec0ac8e42a288439efe9cce6f823366c792d645e5c", + "sha256:39acc3f6a5efb9ccf76e2d3fbe4b56c9c0de10db0d5d4cfdf421b0969bbaa1e6", + "sha256:41bcb23f9fe020fa41d1729eebdff97144a6c884260ce50065e9201c45a989f8", + "sha256:71db20e277b639c05287302521e7c56fd9f77d081bb7a5f251294c18af188e43", + "sha256:7ea354472687dd54a40c0199238e6d22e4b86ab4d5eb356353be8559816f2754", + "sha256:89b42139eefd741724b6e41b80510cdbd76b0bda8f85d6cb37564051d119c384", + "sha256:93edb356869cd0c4c8b715523d26555c69bf81cebad23e5a31abdd5623e8819b", + "sha256:967ac911e1ffbd795df97e89c17f0c07fb2c782ae16f02b2bed2f9ad834cc7f9", + "sha256:b59509b488c0674c29806c181238a4d4868797122fb8e6681f7a3e5540a25403", + "sha256:c76a8db1df422a8473a680fcfb3c6b4cfa0318cf0e7fac9ea1b898f898139b8f", + "sha256:d33c185606eeeb7067ab3626eccdfdc4ea74346093fae646541ce8998c861540", + "sha256:de3ddc1a8a01a8b9f67008d2da85376d59aebac464a54477e11f459119de5308", + "sha256:df87ca4b6eac69b7ee2a6c0f7a3a29d9eda9785e21767e320d768f14170dd693", + "sha256:e303c82a3fc51a73cf0f69ee3215b25c299d84fb766e522a71136ad6f9d3a6c0", + "sha256:e576d9f4721d6380d6a7b9a9ca4dbc863ba9bd6f9242b4df12c0f68b50282f45", + "sha256:f45d0a205fb18919ff83ca449d483e6306721365d5ea52d63f86d91b6ca967c0", + "sha256:f5e6787d59683bfa5c9e1b88210d7bafc36b2fd8799438f30e1effa7da76764c", + "sha256:f7fc5b55497d79fe638fe203bc634332cea3f466d3f08910a956f97477cdecbb", + "sha256:fec4b597c4cc3dd330c9fd049c2d88ccea16aac474a57343082fa17fe1f84a05" + ], + "version": "==2.17" + }, + "six": { + "hashes": [ + "sha256:30639c035cdb23534cd4aa2dd52c3bf48f06e5f4a941509c8bafd8ce11080259", + "sha256:8b74bedcbbbaca38ff6d7491d76f2b06b3592611af620f8426e82dddb04a5ced" + ], + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2'", + "version": "==1.15.0" + }, + "skyfield": { + "hashes": [ + "sha256:64d2716187b94ccb587ec6db46ecb252fb14ecc3b32ef37ce6e90683bb5956cb" + ], + "index": "pypi", + "version": "==1.37" + } + }, + "develop": { + "astroid": { + "hashes": [ + "sha256:87ae7f2398b8a0ae5638ddecf9987f081b756e0e9fc071aeebdca525671fc4dc", + "sha256:b31c92f545517dcc452f284bc9c044050862fbe6d93d2b3de4a215a6b384bf0d" + ], + "markers": "python_version >= '3.6'", + "version": "==2.5.0" + }, + "babel": { + "hashes": [ + "sha256:9d35c22fcc79893c3ecc85ac4a56cde1ecf3f19c540bba0922308a6c06ca6fa5", + "sha256:da031ab54472314f210b0adcff1588ee5d1d1d0ba4dbd07b94dba82bde791e05" + ], + "index": "pypi", + "version": "==2.9.0" + }, + "certifi": { + "hashes": [ + "sha256:1a4995114262bffbc2413b159f2a1a480c969de6e6eb13ee966d470af86af59c", + "sha256:719a74fb9e33b9bd44cc7f3a8d94bc35e4049deebe19ba7d8e108280cfd59830" + ], + "version": "==2020.12.5" + }, + "chardet": { + "hashes": [ + "sha256:0d6f53a15db4120f2b08c94f11e7d93d2c911ee118b6b30a04ec3ee8310179fa", + "sha256:f864054d66fd9118f2e67044ac8981a54775ec5b67aed0441892edb553d21da5" + ], + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'", + "version": "==4.0.0" + }, + "coverage": { + "hashes": [ + "sha256:03ed2a641e412e42cc35c244508cf186015c217f0e4d496bf6d7078ebe837ae7", + "sha256:04b14e45d6a8e159c9767ae57ecb34563ad93440fc1b26516a89ceb5b33c1ad5", + "sha256:0cdde51bfcf6b6bd862ee9be324521ec619b20590787d1655d005c3fb175005f", + "sha256:0f48fc7dc82ee14aeaedb986e175a429d24129b7eada1b7e94a864e4f0644dde", + "sha256:107d327071061fd4f4a2587d14c389a27e4e5c93c7cba5f1f59987181903902f", + "sha256:1375bb8b88cb050a2d4e0da901001347a44302aeadb8ceb4b6e5aa373b8ea68f", + "sha256:14a9f1887591684fb59fdba8feef7123a0da2424b0652e1b58dd5b9a7bb1188c", + "sha256:16baa799ec09cc0dcb43a10680573269d407c159325972dd7114ee7649e56c66", + "sha256:1b811662ecf72eb2d08872731636aee6559cae21862c36f74703be727b45df90", + "sha256:1ccae21a076d3d5f471700f6d30eb486da1626c380b23c70ae32ab823e453337", + "sha256:2f2cf7a42d4b7654c9a67b9d091ec24374f7c58794858bff632a2039cb15984d", + "sha256:322549b880b2d746a7672bf6ff9ed3f895e9c9f108b714e7360292aa5c5d7cf4", + "sha256:32ab83016c24c5cf3db2943286b85b0a172dae08c58d0f53875235219b676409", + "sha256:3fe50f1cac369b02d34ad904dfe0771acc483f82a1b54c5e93632916ba847b37", + "sha256:4a780807e80479f281d47ee4af2eb2df3e4ccf4723484f77da0bb49d027e40a1", + "sha256:4a8eb7785bd23565b542b01fb39115a975fefb4a82f23d407503eee2c0106247", + "sha256:5bee3970617b3d74759b2d2df2f6a327d372f9732f9ccbf03fa591b5f7581e39", + "sha256:60a3307a84ec60578accd35d7f0c71a3a971430ed7eca6567399d2b50ef37b8c", + "sha256:6625e52b6f346a283c3d563d1fd8bae8956daafc64bb5bbd2b8f8a07608e3994", + "sha256:66a5aae8233d766a877c5ef293ec5ab9520929c2578fd2069308a98b7374ea8c", + "sha256:68fb816a5dd901c6aff352ce49e2a0ffadacdf9b6fae282a69e7a16a02dad5fb", + "sha256:6b588b5cf51dc0fd1c9e19f622457cc74b7d26fe295432e434525f1c0fae02bc", + "sha256:6c4d7165a4e8f41eca6b990c12ee7f44fef3932fac48ca32cecb3a1b2223c21f", + "sha256:6d2e262e5e8da6fa56e774fb8e2643417351427604c2b177f8e8c5f75fc928ca", + "sha256:6d9c88b787638a451f41f97446a1c9fd416e669b4d9717ae4615bd29de1ac135", + "sha256:755c56beeacac6a24c8e1074f89f34f4373abce8b662470d3aa719ae304931f3", + "sha256:7e40d3f8eb472c1509b12ac2a7e24158ec352fc8567b77ab02c0db053927e339", + "sha256:812eaf4939ef2284d29653bcfee9665f11f013724f07258928f849a2306ea9f9", + "sha256:84df004223fd0550d0ea7a37882e5c889f3c6d45535c639ce9802293b39cd5c9", + "sha256:859f0add98707b182b4867359e12bde806b82483fb12a9ae868a77880fc3b7af", + "sha256:87c4b38288f71acd2106f5d94f575bc2136ea2887fdb5dfe18003c881fa6b370", + "sha256:89fc12c6371bf963809abc46cced4a01ca4f99cba17be5e7d416ed7ef1245d19", + "sha256:9564ac7eb1652c3701ac691ca72934dd3009997c81266807aef924012df2f4b3", + "sha256:9754a5c265f991317de2bac0c70a746efc2b695cf4d49f5d2cddeac36544fb44", + "sha256:a565f48c4aae72d1d3d3f8e8fb7218f5609c964e9c6f68604608e5958b9c60c3", + "sha256:a636160680c6e526b84f85d304e2f0bb4e94f8284dd765a1911de9a40450b10a", + "sha256:a839e25f07e428a87d17d857d9935dd743130e77ff46524abb992b962eb2076c", + "sha256:b62046592b44263fa7570f1117d372ae3f310222af1fc1407416f037fb3af21b", + "sha256:b7f7421841f8db443855d2854e25914a79a1ff48ae92f70d0a5c2f8907ab98c9", + "sha256:ba7ca81b6d60a9f7a0b4b4e175dcc38e8fef4992673d9d6e6879fd6de00dd9b8", + "sha256:bb32ca14b4d04e172c541c69eec5f385f9a075b38fb22d765d8b0ce3af3a0c22", + "sha256:c0ff1c1b4d13e2240821ef23c1efb1f009207cb3f56e16986f713c2b0e7cd37f", + "sha256:c669b440ce46ae3abe9b2d44a913b5fd86bb19eb14a8701e88e3918902ecd345", + "sha256:c67734cff78383a1f23ceba3b3239c7deefc62ac2b05fa6a47bcd565771e5880", + "sha256:c6809ebcbf6c1049002b9ac09c127ae43929042ec1f1dbd8bb1615f7cd9f70a0", + "sha256:cd601187476c6bed26a0398353212684c427e10a903aeafa6da40c63309d438b", + "sha256:ebfa374067af240d079ef97b8064478f3bf71038b78b017eb6ec93ede1b6bcec", + "sha256:fbb17c0d0822684b7d6c09915677a32319f16ff1115df5ec05bdcaaee40b35f3", + "sha256:fff1f3a586246110f34dc762098b5afd2de88de507559e63553d7da643053786" + ], + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4' and python_version < '4'", + "version": "==5.4" + }, + "coveralls": { + "hashes": [ + "sha256:5399c0565ab822a70a477f7031f6c88a9dd196b3de2877b3facb43b51bd13434", + "sha256:f8384968c57dee4b7133ae701ecdad88e85e30597d496dcba0d7fbb470dca41f" + ], + "index": "pypi", + "version": "==3.0.0" + }, + "docopt": { + "hashes": [ + "sha256:49b3a825280bd66b3aa83585ef59c4a8c82f2c8a522dbe754a8bc8d08c85c491" + ], + "version": "==0.6.2" + }, + "idna": { + "hashes": [ + "sha256:b307872f855b18632ce0c21c5e45be78c0ea7ae4c15c828c20788b26921eb3f6", + "sha256:b97d804b1e9b523befed77c48dacec60e6dcb0b5391d57af6a65a312a90648c0" + ], + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", + "version": "==2.10" + }, + "isort": { + "hashes": [ + "sha256:c729845434366216d320e936b8ad6f9d681aab72dc7cbc2d51bedc3582f3ad1e", + "sha256:fff4f0c04e1825522ce6949973e83110a6e907750cd92d128b0d14aaaadbffdc" + ], + "markers": "python_version >= '3.6' and python_version < '4'", + "version": "==5.7.0" + }, + "lazy-object-proxy": { + "hashes": [ + "sha256:1d33d6f789697f401b75ce08e73b1de567b947740f768376631079290118ad39", + "sha256:2f2de8f8ac0be3e40d17730e0600619d35c78c13a099ea91ef7fb4ad944ce694", + "sha256:3782931963dc89e0e9a0ae4348b44762e868ea280e4f8c233b537852a8996ab9", + "sha256:37d9c34b96cca6787fe014aeb651217944a967a5b165e2cacb6b858d2997ab84", + "sha256:38c3865bd220bd983fcaa9aa11462619e84a71233bafd9c880f7b1cb753ca7fa", + "sha256:429c4d1862f3fc37cd56304d880f2eae5bd0da83bdef889f3bd66458aac49128", + "sha256:522b7c94b524389f4a4094c4bf04c2b02228454ddd17c1a9b2801fac1d754871", + "sha256:57fb5c5504ddd45ed420b5b6461a78f58cbb0c1b0cbd9cd5a43ad30a4a3ee4d0", + "sha256:5944a9b95e97de1980c65f03b79b356f30a43de48682b8bdd90aa5089f0ec1f4", + "sha256:6f4e5e68b7af950ed7fdb594b3f19a0014a3ace0fedb86acb896e140ffb24302", + "sha256:71a1ef23f22fa8437974b2d60fedb947c99a957ad625f83f43fd3de70f77f458", + "sha256:8a44e9901c0555f95ac401377032f6e6af66d8fc1fbfad77a7a8b1a826e0b93c", + "sha256:b6577f15d5516d7d209c1a8cde23062c0f10625f19e8dc9fb59268859778d7d7", + "sha256:c8fe2d6ff0ff583784039d0255ea7da076efd08507f2be6f68583b0da32e3afb", + "sha256:cadfa2c2cf54d35d13dc8d231253b7985b97d629ab9ca6e7d672c35539d38163", + "sha256:cd1bdace1a8762534e9a36c073cd54e97d517a17d69a17985961265be6d22847", + "sha256:ddbdcd10eb999d7ab292677f588b658372aadb9a52790f82484a37127a390108", + "sha256:e7273c64bccfd9310e9601b8f4511d84730239516bada26a0c9846c9697617ef", + "sha256:e7428977763150b4cf83255625a80a23dfdc94d43be7791ce90799d446b4e26f", + "sha256:e960e8be509e8d6d618300a6c189555c24efde63e85acaf0b14b2cd1ac743315", + "sha256:ecb5dd5990cec6e7f5c9c1124a37cb2c710c6d69b0c1a5c4aa4b35eba0ada068", + "sha256:ef3f5e288aa57b73b034ce9c1f1ac753d968f9069cd0742d1d69c698a0167166", + "sha256:fa5b2dee0e231fa4ad117be114251bdfe6afe39213bd629d43deb117b6a6c40a", + "sha256:fa7fb7973c622b9e725bee1db569d2c2ee64d2f9a089201c5e8185d482c7352d" + ], + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'", + "version": "==1.5.2" + }, + "mccabe": { + "hashes": [ + "sha256:ab8a6258860da4b6677da4bd2fe5dc2c659cff31b3ee4f7f5d64e79735b80d42", + "sha256:dd8d182285a0fe56bace7f45b5e7d1a6ebcbf524e8f3bd87eb0f125271b8831f" + ], + "version": "==0.6.1" + }, + "pylint": { + "hashes": [ + "sha256:81ce108f6342421169ea039ff1f528208c99d2e5a9c4ca95cfc5291be6dfd982", + "sha256:a251b238db462b71d25948f940568bb5b3ae0e37dbaa05e10523f54f83e6cc7e" + ], + "index": "pypi", + "version": "==2.7.1" + }, + "pylintfileheader": { + "hashes": [ + "sha256:7871193691484210268d467dc12d88ac5b3ba7eb7dec6239e24075797185a3b2", + "sha256:a23f143b0fb4d65f984ffd824731d6e41f2840e26a5752a90df93f4454b5ccd1" + ], + "index": "pypi", + "version": "==0.3.0" + }, + "pytz": { + "hashes": [ + "sha256:83a4a90894bf38e243cf052c8b58f381bfe9a7a483f6a9cab140bc7f702ac4da", + "sha256:eb10ce3e7736052ed3623d49975ce333bcd712c7bb19a58b9e2089d4057d0798" + ], + "version": "==2021.1" + }, + "requests": { + "hashes": [ + "sha256:27973dd4a904a4f13b263a19c866c13b92a39ed1c964655f025f3f8d3d75b804", + "sha256:c210084e36a42ae6b9219e00e48287def368a26d03a048ddad7bfee44f75871e" + ], + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'", + "version": "==2.25.1" + }, + "toml": { + "hashes": [ + "sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b", + "sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f" + ], + "markers": "python_version >= '2.6' and python_version not in '3.0, 3.1, 3.2'", + "version": "==0.10.2" + }, + "unittest-data-provider": { + "hashes": [ + "sha256:86bc7fb6608c2570aeedadea346fe3034afc940807dd7519e95e5dbc899ac2be" + ], + "index": "pypi", + "version": "==1.0.1" + }, + "urllib3": { + "hashes": [ + "sha256:1b465e494e3e0d8939b50680403e3aedaa2bc434b7d5af64dfd3c958d7f5ae80", + "sha256:de3eedaad74a2683334e282005cd8d7f22f4d55fa690a2a1020a416cb0a47e73" + ], + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4' and python_version < '4'", + "version": "==1.26.3" + }, + "wrapt": { + "hashes": [ + "sha256:b62ffa81fb85f4332a4f609cab4ac40709470da05643a082ec1eb88e6d9b97d7" + ], + "version": "==1.12.1" + } + } +} diff --git a/kosmorrolib/__init__.py b/kosmorrolib/__init__.py new file mode 100644 index 0000000..648d1f4 --- /dev/null +++ b/kosmorrolib/__init__.py @@ -0,0 +1,21 @@ +#!/usr/bin/env python3 + +# Kosmorro - Compute The Next Ephemerides +# Copyright (C) 2019 Jérôme Deuchnord +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . + +from .version import MAJOR_VERSION, MINOR_VERSION, PATCH_VERSION, VERSION +from .ephemerides import get_ephemerides +from .events import get_events diff --git a/kosmorrolib/__pycache__/dateutil.cpython-39.pyc b/kosmorrolib/__pycache__/dateutil.cpython-39.pyc new file mode 100644 index 0000000000000000000000000000000000000000..4c8bfd3de6a06b519317df1423149761985baffe GIT binary patch literal 522 zcmY*V%}N6?5S}Fa*FsfLi*Fzc;@+i*;Jp`bd#URtrMum1N|Hq^?Mbh_c=iQ+65kXWeX<`RCO3G@1p)_78jz5KBsq~Zrv!K; zKrq2Ldt*G{;Y*kYoZdkie_;ir%q6_A!d6na#N$%6!u34kqOrvo6@gc{(5i`Dx_GLU zL{c(ezYDYm-pVZ=GeTHV4js{F7A!$dHsqPCSsPiX>6)#eW-G?&hSb4UL4FmSZ3tQm z#wl^(Om*5IiR`j!rwSRh%QVz}uJ!{R>ul*_Ef!5t3fC{ppZ_$_-i?kt#0YuMV7F4B z?|0A#c=LUg&6E^bE$}-cdsKRE7T6}6DqVw5GwEiC@ZY yYeQ^n5(FR#IieBON61q$M$;78zVoWJ+t;m^Jvc<2OGZ-ht`XPAm>FMs!hQgUTz^&o literal 0 HcmV?d00001 diff --git a/kosmorrolib/core.py b/kosmorrolib/core.py new file mode 100644 index 0000000..6a949a5 --- /dev/null +++ b/kosmorrolib/core.py @@ -0,0 +1,117 @@ +#!/usr/bin/env python3 + +# Kosmorro - Compute The Next Ephemerides +# Copyright (C) 2019 Jérôme Deuchnord +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . + +import os +import re +from shutil import rmtree +from pathlib import Path + +from datetime import date +from dateutil.relativedelta import relativedelta + +from skyfield.api import Loader +from skyfield.timelib import Time +from skyfield.nutationlib import iau2000b + +CACHE_FOLDER = str(Path.home()) + '/.kosmorro-cache' + +class Environment: + def __init__(self): + self._vars = {} + + def __set__(self, key, value): + self._vars[key] = value + + def __getattr__(self, key): + return self._vars[key] if key in self._vars else None + + def __str__(self): + return self._vars.__str__() + + def __len__(self): + return len(self._vars) + +def get_env() -> Environment: + environment = Environment() + + for var in os.environ: + if not re.search('^KOSMORRO_', var): + continue + + [_, env] = var.split('_', 1) + environment.__set__(env.lower(), os.getenv(var)) + + return environment + +def get_loader(): + return Loader(CACHE_FOLDER) + + +def get_timescale(): + return get_loader().timescale() + + +def get_skf_objects(): + return get_loader()('de421.bsp') + + +def get_iau2000b(time: Time): + return iau2000b(time.tt) + + +def clear_cache(): + rmtree(CACHE_FOLDER) + + +def flatten_list(the_list: list): + new_list = [] + for item in the_list: + if isinstance(item, list): + for item2 in flatten_list(item): + new_list.append(item2) + continue + + new_list.append(item) + + return new_list + + +def get_date(date_arg: str) -> date: + if re.match(r'^\d{4}-\d{2}-\d{2}$', date_arg): + try: + return date.fromisoformat(date_arg) + except ValueError as error: + raise ValueError(_('The date {date} is not valid: {error}').format(date=date_arg, + error=error.args[0])) from error + elif re.match(r'^([+-])(([0-9]+)y)?[ ]?(([0-9]+)m)?[ ]?(([0-9]+)d)?$', date_arg): + def get_offset(date_arg: str, signifier: str): + if re.search(r'([0-9]+)' + signifier, date_arg): + return abs(int(re.search(r'[+-]?([0-9]+)' + signifier, date_arg).group(0)[:-1])) + return 0 + + days = get_offset(date_arg, 'd') + months = get_offset(date_arg, 'm') + years = get_offset(date_arg, 'y') + + if date_arg[0] == '+': + return date.today() + relativedelta(days=days, months=months, years=years) + return date.today() - relativedelta(days=days, months=months, years=years) + + else: + error_msg = 'The date {date} does not match the required YYYY-MM-DD format or the offset format.' + raise ValueError(error_msg.format(date=date_arg)) diff --git a/kosmorrolib/data.py b/kosmorrolib/data.py new file mode 100644 index 0000000..91e9d7b --- /dev/null +++ b/kosmorrolib/data.py @@ -0,0 +1,223 @@ +#!/usr/bin/env python3 + +# Kosmorro - Compute The Next Ephemerides +# Copyright (C) 2019 Jérôme Deuchnord +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . + +from abc import ABC, abstractmethod +from typing import Union +from datetime import datetime + +from numpy import pi, arcsin + +from skyfield.api import Topos, Time +from skyfield.vectorlib import VectorSum as SkfPlanet + +from .core import get_skf_objects +from .enum import MoonPhaseType, EventType + + +class Serializable(ABC): + @abstractmethod + def serialize(self) -> dict: + pass + + +class MoonPhase(Serializable): + def __init__(self, phase_type: MoonPhaseType, time: datetime = None, next_phase_date: datetime = None): + self.phase_type = phase_type + self.time = time + self.next_phase_date = next_phase_date + + def get_next_phase(self): + if self.phase_type in [MoonPhaseType.NEW_MOON, MoonPhaseType.WAXING_CRESCENT]: + return MoonPhaseType.FIRST_QUARTER + if self.phase_type in [MoonPhaseType.FIRST_QUARTER, MoonPhaseType.WAXING_GIBBOUS]: + return MoonPhaseType.FULL_MOON + if self.phase_type in [MoonPhaseType.FULL_MOON, MoonPhaseType.WANING_GIBBOUS]: + return MoonPhaseType.LAST_QUARTER + + return MoonPhaseType.NEW_MOON + + def serialize(self) -> dict: + return { + 'phase': self.phase_type.name, + 'time': self.time.isoformat() if self.time is not None else None, + 'next': { + 'phase': self.get_next_phase().name, + 'time': self.next_phase_date.isoformat() + } + } + + +class Object(Serializable): + """ + An astronomical object. + """ + + def __init__(self, + name: str, + skyfield_name: str, + radius: float = None): + """ + Initialize an astronomical object + + :param str name: the official name of the object (may be internationalized) + :param str skyfield_name: the internal name of the object in Skyfield library + :param float radius: the radius (in km) of the object + :param AsterEphemerides ephemerides: the ephemerides associated to the object + """ + self.name = name + self.skyfield_name = skyfield_name + self.radius = radius + + def __repr__(self): + return '' % (self.get_type(), self.name) + + def get_skyfield_object(self) -> SkfPlanet: + return get_skf_objects()[self.skyfield_name] + + @abstractmethod + def get_type(self) -> str: + pass + + def get_apparent_radius(self, time: Time, from_place) -> float: + """ + Calculate the apparent radius, in degrees, of the object from the given place at a given time. + :param time: + :param from_place: + :return: + """ + if self.radius is None: + raise ValueError('Missing radius for %s object' % self.name) + + return 360 / pi * arcsin(self.radius / from_place.at(time).observe(self.get_skyfield_object()).distance().km) + + def serialize(self) -> dict: + return { + 'name': self.name, + 'type': self.get_type(), + 'radius': self.radius, + } + + +class Star(Object): + def get_type(self) -> str: + return 'star' + + +class Planet(Object): + def get_type(self) -> str: + return 'planet' + + +class DwarfPlanet(Planet): + def get_type(self) -> str: + return 'dwarf_planet' + + +class Satellite(Object): + def get_type(self) -> str: + return 'satellite' + + +class Event(Serializable): + def __init__(self, event_type: EventType, objects: [Object], start_time: datetime, + end_time: Union[datetime, None] = None, details: str = None): + self.event_type = event_type + self.objects = objects + self.start_time = start_time + self.end_time = end_time + self.details = details + + def __repr__(self): + return '' % (self.event_type.name, + self.objects, + self.start_time, + self.end_time, + self.details) + + def get_description(self, show_details: bool = True) -> str: + description = self.event_type.value % self._get_objects_name() + if show_details and self.details is not None: + description += ' ({:s})'.format(self.details) + return description + + def _get_objects_name(self): + if len(self.objects) == 1: + return self.objects[0].name + + return tuple(object.name for object in self.objects) + + def serialize(self) -> dict: + return { + 'objects': [object.serialize() for object in self.objects], + 'EventType': self.event_type.name, + 'starts_at': self.start_time.isoformat(), + 'ends_at': self.end_time.isoformat() if self.end_time is not None else None, + 'details': self.details + } + + +class AsterEphemerides(Serializable): + def __init__(self, + rise_time: Union[datetime, None], + culmination_time: Union[datetime, None], + set_time: Union[datetime, None], + aster: Object): + self.rise_time = rise_time + self.culmination_time = culmination_time + self.set_time = set_time + self.object = aster + + def serialize(self) -> dict: + return { + 'object': self.object.serialize(), + 'rise_time': self.rise_time.isoformat() if self.rise_time is not None else None, + 'culmination_time': self.culmination_time.isoformat() if self.culmination_time is not None else None, + 'set_time': self.set_time.isoformat() if self.set_time is not None else None + } + + +EARTH = Planet('Earth', 'EARTH') + +ASTERS = [Star('Sun', 'SUN', radius=696342), + Satellite('Moon', 'MOON', radius=1737.4), + Planet('Mercury', 'MERCURY', radius=2439.7), + Planet('Venus', 'VENUS', radius=6051.8), + Planet('Mars', 'MARS', radius=3396.2), + Planet('Jupiter', 'JUPITER BARYCENTER', radius=71492), + Planet('Saturn', 'SATURN BARYCENTER', radius=60268), + Planet('Uranus', 'URANUS BARYCENTER', radius=25559), + Planet('Neptune', 'NEPTUNE BARYCENTER', radius=24764), + Planet('Pluto', 'PLUTO BARYCENTER', radius=1185)] + + +class Position: + def __init__(self, latitude: float, longitude: float, aster: Object): + self.latitude = latitude + self.longitude = longitude + self.aster = aster + self._topos = None + + def get_planet_topos(self) -> Topos: + if self.aster is None: + raise TypeError('Observation planet must be set.') + + if self._topos is None: + self._topos = self.aster.get_skyfield_object() + Topos(latitude_degrees=self.latitude, + longitude_degrees=self.longitude) + + return self._topos diff --git a/kosmorrolib/dateutil.py b/kosmorrolib/dateutil.py new file mode 100644 index 0000000..6ff64d4 --- /dev/null +++ b/kosmorrolib/dateutil.py @@ -0,0 +1,28 @@ +#!/usr/bin/env python3 + +# Kosmorro - Compute The Next Ephemerides +# Copyright (C) 2019 Jérôme Deuchnord +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . + +from datetime import datetime, timezone, timedelta + + +def translate_to_timezone(date: datetime, to_tz: int, from_tz: int = None): + if from_tz is not None: + source_tz = timezone(timedelta(hours=from_tz)) + else: + source_tz = timezone.utc + + return date.replace(tzinfo=source_tz).astimezone(tz=timezone(timedelta(hours=to_tz))) diff --git a/kosmorrolib/enum.py b/kosmorrolib/enum.py new file mode 100644 index 0000000..29b185f --- /dev/null +++ b/kosmorrolib/enum.py @@ -0,0 +1,41 @@ +#!/usr/bin/env python3 + +# Kosmorro - Compute The Next Ephemerides +# Copyright (C) 2019 Jérôme Deuchnord +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . + +from enum import Enum, auto + + +class MoonPhaseType(Enum): + """An enumeration of moon phases.""" + NEW_MOON = 1 + WAXING_CRESCENT = 2 + FIRST_QUARTER = 3 + WAXING_GIBBOUS = 4 + FULL_MOON = 5 + WANING_GIBBOUS = 6 + LAST_QUARTER = 7 + WANING_CRESCENT = 8 + + +class EventType(Enum): + """An enumeration for the supported event types.""" + OPPOSITION = 1 + CONJUNCTION = 2 + OCCULTATION = 3 + MAXIMAL_ELONGATION = 4 + MOON_PERIGEE = 5 + MOON_APOGEE = 6 diff --git a/kosmorrolib/ephemerides.py b/kosmorrolib/ephemerides.py new file mode 100644 index 0000000..08c34b0 --- /dev/null +++ b/kosmorrolib/ephemerides.py @@ -0,0 +1,163 @@ +#!/usr/bin/env python3 + +# Kosmorro - Compute The Next Ephemerides +# Copyright (C) 2019 Jérôme Deuchnord +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . + +import datetime +from typing import Union + +from skyfield.searchlib import find_discrete, find_maxima +from skyfield.timelib import Time +from skyfield.constants import tau +from skyfield.errors import EphemerisRangeError + +from .data import Position, AsterEphemerides, MoonPhase, Object, ASTERS +from .dateutil import translate_to_timezone +from .core import get_skf_objects, get_timescale, get_iau2000b +from .enum import MoonPhaseType +from .exceptions import OutOfRangeDateError + +RISEN_ANGLE = -0.8333 + + +def _get_skyfield_to_moon_phase(times: [Time], vals: [int], now: Time) -> Union[MoonPhaseType, None]: + tomorrow = get_timescale().utc(now.utc_datetime().year, now.utc_datetime().month, now.utc_datetime().day + 1) + + phases = list(MoonPhaseType) + current_phase = None + current_phase_time = None + next_phase_time = None + i = 0 + + if len(times) == 0: + return None + + for i, time in enumerate(times): + if now.utc_iso() <= time.utc_iso(): + if vals[i] in [0, 2, 4, 6]: + if time.utc_datetime() < tomorrow.utc_datetime(): + current_phase_time = time + current_phase = phases[vals[i]] + else: + i -= 1 + current_phase_time = None + current_phase = phases[vals[i]] + else: + current_phase = phases[vals[i]] + + break + + for j in range(i + 1, len(times)): + if vals[j] in [0, 2, 4, 6]: + next_phase_time = times[j] + break + + return MoonPhaseType(current_phase, + current_phase_time.utc_datetime() if current_phase_time is not None else None, + next_phase_time.utc_datetime() if next_phase_time is not None else None) + + +def get_moon_phase(compute_date: datetime.date, timezone: int = 0) -> MoonPhaseType: + earth = get_skf_objects()['earth'] + moon = get_skf_objects()['moon'] + sun = get_skf_objects()['sun'] + + def moon_phase_at(time: Time): + time._nutation_angles = get_iau2000b(time) + current_earth = earth.at(time) + _, mlon, _ = current_earth.observe(moon).apparent().ecliptic_latlon('date') + _, slon, _ = current_earth.observe(sun).apparent().ecliptic_latlon('date') + return (((mlon.radians - slon.radians) // (tau / 8)) % 8).astype(int) + + moon_phase_at.rough_period = 7.0 # one lunar phase per week + + today = get_timescale().utc(compute_date.year, compute_date.month, compute_date.day) + time1 = get_timescale().utc(compute_date.year, compute_date.month, compute_date.day - 10) + time2 = get_timescale().utc(compute_date.year, compute_date.month, compute_date.day + 10) + + try: + times, phase = find_discrete(time1, time2, moon_phase_at) + except EphemerisRangeError as error: + start = translate_to_timezone(error.start_time.utc_datetime(), timezone) + end = translate_to_timezone(error.end_time.utc_datetime(), timezone) + + start = datetime.date(start.year, start.month, start.day) + datetime.timedelta(days=12) + end = datetime.date(end.year, end.month, end.day) - datetime.timedelta(days=12) + + raise OutOfRangeDateError(start, end) from error + + return _get_skyfield_to_moon_phase(times, phase, today) + + +def get_ephemerides(date: datetime.date, position: Position, timezone: int = 0) -> [AsterEphemerides]: + ephemerides = [] + + def get_angle(for_aster: Object): + def fun(time: Time) -> float: + return position.get_planet_topos().at(time).observe(for_aster.get_skyfield_object()).apparent().altaz()[0]\ + .degrees + fun.rough_period = 1.0 + return fun + + def is_risen(for_aster: Object): + def fun(time: Time) -> bool: + return get_angle(for_aster)(time) > RISEN_ANGLE + fun.rough_period = 0.5 + return fun + + start_time = get_timescale().utc(date.year, date.month, date.day, -timezone) + end_time = get_timescale().utc(date.year, date.month, date.day, 23 - timezone, 59, 59) + + try: + for aster in ASTERS: + rise_times, arr = find_discrete(start_time, end_time, is_risen(aster)) + try: + culmination_time, _ = find_maxima(start_time, end_time, f=get_angle(aster), epsilon=1./3600/24, num=12) + culmination_time = culmination_time[0] if len(culmination_time) > 0 else None + except ValueError: + culmination_time = None + + if len(rise_times) == 2: + rise_time = rise_times[0 if arr[0] else 1] + set_time = rise_times[1 if not arr[1] else 0] + else: + rise_time = rise_times[0] if arr[0] else None + set_time = rise_times[0] if not arr[0] else None + + # Convert the Time instances to Python datetime objects + if rise_time is not None: + rise_time = translate_to_timezone(rise_time.utc_datetime().replace(microsecond=0), + to_tz=timezone) + + if culmination_time is not None: + culmination_time = translate_to_timezone(culmination_time.utc_datetime().replace(microsecond=0), + to_tz=timezone) + + if set_time is not None: + set_time = translate_to_timezone(set_time.utc_datetime().replace(microsecond=0), + to_tz=timezone) + + ephemerides.append(AsterEphemerides(rise_time, culmination_time, set_time, aster=aster)) + except EphemerisRangeError as error: + start = translate_to_timezone(error.start_time.utc_datetime(), timezone) + end = translate_to_timezone(error.end_time.utc_datetime(), timezone) + + start = datetime.date(start.year, start.month, start.day + 1) + end = datetime.date(end.year, end.month, end.day - 1) + + raise OutOfRangeDateError(start, end) from error + + return ephemerides diff --git a/kosmorrolib/events.py b/kosmorrolib/events.py new file mode 100644 index 0000000..7aa5657 --- /dev/null +++ b/kosmorrolib/events.py @@ -0,0 +1,224 @@ +#!/usr/bin/env python3 + +# Kosmorro - Compute The Next Ephemerides +# Copyright (C) 2019 Jérôme Deuchnord +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . + +from datetime import date as date_type + +from skyfield.errors import EphemerisRangeError +from skyfield.timelib import Time +from skyfield.searchlib import find_discrete, find_maxima, find_minima +from numpy import pi + +from .data import Event, Star, Planet, ASTERS +from .dateutil import translate_to_timezone +from .enum import EventType +from .exceptions import OutOfRangeDateError +from .core import get_timescale, get_skf_objects, flatten_list + + +def _search_conjunction(start_time: Time, end_time: Time, timezone: int) -> [Event]: + earth = get_skf_objects()['earth'] + aster1 = None + aster2 = None + + def is_in_conjunction(time: Time): + earth_pos = earth.at(time) + _, aster1_lon, _ = earth_pos.observe(aster1.get_skyfield_object()).apparent().ecliptic_latlon() + _, aster2_lon, _ = earth_pos.observe(aster2.get_skyfield_object()).apparent().ecliptic_latlon() + + return ((aster1_lon.radians - aster2_lon.radians) / pi % 2.0).astype('int8') == 0 + + is_in_conjunction.rough_period = 60.0 + + computed = [] + conjunctions = [] + + for aster1 in ASTERS: + # Ignore the Sun + if isinstance(aster1, Star): + continue + + for aster2 in ASTERS: + if isinstance(aster2, Star) or aster2 == aster1 or aster2 in computed: + continue + + times, is_conjs = find_discrete(start_time, end_time, is_in_conjunction) + + for i, time in enumerate(times): + if is_conjs[i]: + aster1_pos = (aster1.get_skyfield_object() - earth).at(time) + aster2_pos = (aster2.get_skyfield_object() - earth).at(time) + distance = aster1_pos.separation_from(aster2_pos).degrees + + if distance - aster2.get_apparent_radius(time, earth) < aster1.get_apparent_radius(time, earth): + occulting_aster = [aster1, + aster2] if aster1_pos.distance().km < aster2_pos.distance().km else [aster2, + aster1] + + conjunctions.append(Event(EventType.OCCULTATION, occulting_aster, + translate_to_timezone(time.utc_datetime(), timezone))) + else: + conjunctions.append(Event(EventType.CONJUNCTION, [aster1, aster2], + translate_to_timezone(time.utc_datetime(), timezone))) + + computed.append(aster1) + + return conjunctions + + +def _search_oppositions(start_time: Time, end_time: Time, timezone: int) -> [Event]: + earth = get_skf_objects()['earth'] + sun = get_skf_objects()['sun'] + aster = None + + def is_oppositing(time: Time) -> [bool]: + earth_pos = earth.at(time) + sun_pos = earth_pos.observe(sun).apparent() # Never do this without eyes protection! + aster_pos = earth_pos.observe(get_skf_objects()[aster.skyfield_name]).apparent() + _, lon1, _ = sun_pos.ecliptic_latlon() + _, lon2, _ = aster_pos.ecliptic_latlon() + return (lon1.degrees - lon2.degrees) > 180 + + is_oppositing.rough_period = 1.0 + events = [] + + for aster in ASTERS: + if not isinstance(aster, Planet) or aster.skyfield_name in ['MERCURY', 'VENUS']: + continue + + times, _ = find_discrete(start_time, end_time, is_oppositing) + for time in times: + events.append(Event(EventType.OPPOSITION, [aster], translate_to_timezone(time.utc_datetime(), timezone))) + + return events + + +def _search_maximal_elongations(start_time: Time, end_time: Time, timezone: int) -> [Event]: + earth = get_skf_objects()['earth'] + sun = get_skf_objects()['sun'] + aster = None + + def get_elongation(time: Time): + sun_pos = (sun - earth).at(time) + aster_pos = (aster.get_skyfield_object() - earth).at(time) + separation = sun_pos.separation_from(aster_pos) + return separation.degrees + + get_elongation.rough_period = 1.0 + + events = [] + + for aster in ASTERS: + if aster.skyfield_name not in ['MERCURY', 'VENUS']: + continue + + times, elongations = find_maxima(start_time, end_time, f=get_elongation, epsilon=1./24/3600, num=12) + + for i, time in enumerate(times): + elongation = elongations[i] + events.append(Event(EventType.MAXIMAL_ELONGATION, + [aster], + translate_to_timezone(time.utc_datetime(), timezone), + details='{:.3n}°'.format(elongation))) + + return events + + +def _get_moon_distance(): + earth = get_skf_objects()['earth'] + moon = get_skf_objects()['moon'] + + def get_distance(time: Time): + earth_pos = earth.at(time) + moon_pos = earth_pos.observe(moon).apparent() + + return moon_pos.distance().au + + get_distance.rough_period = 1.0 + + return get_distance + + +def _search_moon_apogee(start_time: Time, end_time: Time, timezone: int) -> [Event]: + moon = ASTERS[1] + events = [] + + times, _ = find_maxima(start_time, end_time, f=_get_moon_distance(), epsilon=1./24/60) + + for time in times: + events.append(Event(EventType.MOON_APOGEE, [moon], translate_to_timezone(time.utc_datetime(), timezone))) + + return events + + +def _search_moon_perigee(start_time: Time, end_time: Time, timezone: int) -> [Event]: + moon = ASTERS[1] + events = [] + + times, _ = find_minima(start_time, end_time, f=_get_moon_distance(), epsilon=1./24/60) + + for time in times: + events.append(Event(EventType.MOON_PERIGEE, [moon], translate_to_timezone(time.utc_datetime(), timezone))) + + return events + + +def get_events(date: date_type, timezone: int = 0) -> [Event]: + """Calculate and return a list of events for the given date, adjusted to the given timezone if any. + + Find events that happen on April 4th, 2020 (show hours in UTC): + + >>> get_events(date_type(2020, 4, 4)) + [, ] start=2020-04-04 01:14:39.063308+00:00 end=None details=None />] + + Find events that happen on April 4th, 2020 (show timezones in UTC+2): + + >>> get_events(date_type(2020, 4, 4), 2) + [, ] start=2020-04-04 03:14:39.063267+02:00 end=None details=None />] + + Find events that happen on April 3rd, 2020 (show timezones in UTC-2): + + >>> get_events(date_type(2020, 4, 3), -2) + [, ] start=2020-04-03 23:14:39.063388-02:00 end=None details=None />] + + :param date: the date for which the events must be calculated + :param timezone: the timezone to adapt the results to. If not given, defaults to 0. + :return: a list of events found for the given date. + """ + + start_time = get_timescale().utc(date.year, date.month, date.day, -timezone) + end_time = get_timescale().utc(date.year, date.month, date.day + 1, -timezone) + + try: + found_events = [] + + for fun in [_search_oppositions, + _search_conjunction, + _search_maximal_elongations, + _search_moon_apogee, + _search_moon_perigee]: + found_events.append(fun(start_time, end_time, timezone)) + + return sorted(flatten_list(found_events), key=lambda event: event.start_time) + except EphemerisRangeError as error: + start_date = translate_to_timezone(error.start_time.utc_datetime(), timezone) + end_date = translate_to_timezone(error.end_time.utc_datetime(), timezone) + + start_date = date_type(start_date.year, start_date.month, start_date.day) + end_date = date_type(end_date.year, end_date.month, end_date.day) + + raise OutOfRangeDateError(start_date, end_date) from error diff --git a/kosmorrolib/exceptions.py b/kosmorrolib/exceptions.py new file mode 100644 index 0000000..e6effdd --- /dev/null +++ b/kosmorrolib/exceptions.py @@ -0,0 +1,40 @@ +#!/usr/bin/env python3 + +# Kosmorro - Compute The Next Ephemerides +# Copyright (C) 2019 Jérôme Deuchnord +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . + +from datetime import date + + +class UnavailableFeatureError(RuntimeError): + def __init__(self, msg: str): + super().__init__() + self.msg = msg + + +class OutOfRangeDateError(RuntimeError): + def __init__(self, min_date: date, max_date: date): + super().__init__() + self.min_date = min_date + self.max_date = max_date + self.msg = 'The date must be between %s and %s' % (min_date.strftime('%Y-%m-%d'), + max_date.strftime('%Y-%m-%d')) + + +class CompileError(RuntimeError): + def __init__(self, msg): + super().__init__() + self.msg = msg diff --git a/kosmorrolib/version.py b/kosmorrolib/version.py new file mode 100644 index 0000000..30c07e3 --- /dev/null +++ b/kosmorrolib/version.py @@ -0,0 +1,46 @@ +#!/usr/bin/env python3 + +# Kosmorro - Compute The Next Ephemerides +# Copyright (C) 2019 Jérôme Deuchnord +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . + +""" +Kosmorrolib's versioning follows the `Semantic Versioning `_ standard, +meaning that: + +* the versions always follow the X.Y.Z format, where X, Y and Z are natural numbers. +* the major version (X) never changes unless a change breaks compatibility (any breaking compatibility change in the + same major version is considered as a bug) +* the minor version (Y) never changes unless new features are introduced +* the patch version (Z) never changes unless there are bug fixes +""" + +MAJOR_VERSION = 0 +"""The major version of the library""" + +MINOR_VERSION = 9 +"""The minor version of the library""" + +PATCH_VERSION = 0 +"""The patch version of the library""" + +VERSION = '%d.%d.%d' % (MAJOR_VERSION, MINOR_VERSION, PATCH_VERSION) +""" +The library version in a readable for human beings format. +Useful for instance, if you want to display it to the end user. + +If you need to check the version in your program, you should prefer using the MAJOR_VERSION, MINOR_MINOR_VERSION and +PATCH_VERSION constants instead. +""" diff --git a/tests.py b/tests.py new file mode 100644 index 0000000..5ed410f --- /dev/null +++ b/tests.py @@ -0,0 +1,6 @@ +import doctest + +from kosmorrolib import * + +for module in [events]: + doctest.testmod(module)