@@ -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" |
@@ -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" | |||
} | |||
} | |||
} |
@@ -0,0 +1,21 @@ | |||
#!/usr/bin/env python3 | |||
# Kosmorro - Compute The Next Ephemerides | |||
# Copyright (C) 2019 Jérôme Deuchnord <jerome@deuchnord.fr> | |||
# | |||
# 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 <https://www.gnu.org/licenses/>. | |||
from .version import MAJOR_VERSION, MINOR_VERSION, PATCH_VERSION, VERSION | |||
from .ephemerides import get_ephemerides | |||
from .events import get_events |
@@ -0,0 +1,117 @@ | |||
#!/usr/bin/env python3 | |||
# Kosmorro - Compute The Next Ephemerides | |||
# Copyright (C) 2019 Jérôme Deuchnord <jerome@deuchnord.fr> | |||
# | |||
# 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 <https://www.gnu.org/licenses/>. | |||
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)) |
@@ -0,0 +1,223 @@ | |||
#!/usr/bin/env python3 | |||
# Kosmorro - Compute The Next Ephemerides | |||
# Copyright (C) 2019 Jérôme Deuchnord <jerome@deuchnord.fr> | |||
# | |||
# 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 <https://www.gnu.org/licenses/>. | |||
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 '<Object type=%s name=%s />' % (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 '<Event type=%s objects=%s start=%s end=%s details=%s />' % (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 |
@@ -0,0 +1,28 @@ | |||
#!/usr/bin/env python3 | |||
# Kosmorro - Compute The Next Ephemerides | |||
# Copyright (C) 2019 Jérôme Deuchnord <jerome@deuchnord.fr> | |||
# | |||
# 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 <https://www.gnu.org/licenses/>. | |||
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))) |
@@ -0,0 +1,41 @@ | |||
#!/usr/bin/env python3 | |||
# Kosmorro - Compute The Next Ephemerides | |||
# Copyright (C) 2019 Jérôme Deuchnord <jerome@deuchnord.fr> | |||
# | |||
# 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 <https://www.gnu.org/licenses/>. | |||
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 |
@@ -0,0 +1,163 @@ | |||
#!/usr/bin/env python3 | |||
# Kosmorro - Compute The Next Ephemerides | |||
# Copyright (C) 2019 Jérôme Deuchnord <jerome@deuchnord.fr> | |||
# | |||
# 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 <https://www.gnu.org/licenses/>. | |||
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 |
@@ -0,0 +1,224 @@ | |||
#!/usr/bin/env python3 | |||
# Kosmorro - Compute The Next Ephemerides | |||
# Copyright (C) 2019 Jérôme Deuchnord <jerome@deuchnord.fr> | |||
# | |||
# 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 <https://www.gnu.org/licenses/>. | |||
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)) | |||
[<Event type=CONJUNCTION objects=[<Object type=planet name=Mercury />, <Object type=planet name=Neptune />] 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) | |||
[<Event type=CONJUNCTION objects=[<Object type=planet name=Mercury />, <Object type=planet name=Neptune />] 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) | |||
[<Event type=CONJUNCTION objects=[<Object type=planet name=Mercury />, <Object type=planet name=Neptune />] 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 |
@@ -0,0 +1,40 @@ | |||
#!/usr/bin/env python3 | |||
# Kosmorro - Compute The Next Ephemerides | |||
# Copyright (C) 2019 Jérôme Deuchnord <jerome@deuchnord.fr> | |||
# | |||
# 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 <https://www.gnu.org/licenses/>. | |||
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 |
@@ -0,0 +1,46 @@ | |||
#!/usr/bin/env python3 | |||
# Kosmorro - Compute The Next Ephemerides | |||
# Copyright (C) 2019 Jérôme Deuchnord <jerome@deuchnord.fr> | |||
# | |||
# 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 <https://www.gnu.org/licenses/>. | |||
""" | |||
Kosmorrolib's versioning follows the `Semantic Versioning <https://semver.org/spec/v2.0.0.html>`_ 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. | |||
""" |
@@ -0,0 +1,6 @@ | |||
import doctest | |||
from kosmorrolib import * | |||
for module in [events]: | |||
doctest.testmod(module) |