├── .gitignore ├── .readthedocs.yml ├── LICENSE ├── README.md ├── docs ├── README.txt ├── make.bat ├── make_html.bat ├── requirements.txt └── source │ ├── License.rst │ ├── Modules.rst │ ├── conf.py │ ├── getting_started.rst │ └── index.rst ├── launchlibrary ├── __init__.py ├── api.py ├── async_models.py ├── constants.py ├── exceptions.py ├── models.py ├── network.py └── utils.py ├── requirements.txt └── setup.py /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | *.egg-info/ 24 | .installed.cfg 25 | *.egg 26 | MANIFEST 27 | 28 | # PyInstaller 29 | # Usually these files are written by a python script from a template 30 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 31 | *.manifest 32 | *.spec 33 | 34 | # Installer logs 35 | pip-log.txt 36 | pip-delete-this-directory.txt 37 | 38 | # Unit test / coverage reports 39 | htmlcov/ 40 | .tox/ 41 | .coverage 42 | .coverage.* 43 | .cache 44 | nosetests.xml 45 | coverage.xml 46 | *.cover 47 | .hypothesis/ 48 | .pytest_cache/ 49 | 50 | # Translations 51 | *.mo 52 | *.pot 53 | 54 | # Django stuff: 55 | *.log 56 | local_settings.py 57 | db.sqlite3 58 | 59 | # Flask stuff: 60 | instance/ 61 | .webassets-cache 62 | 63 | # Scrapy stuff: 64 | .scrapy 65 | 66 | # Sphinx documentation 67 | docs/_build/ 68 | 69 | # PyBuilder 70 | target/ 71 | 72 | # Jupyter Notebook 73 | .ipynb_checkpoints 74 | 75 | # pyenv 76 | .python-version 77 | 78 | # celery beat schedule file 79 | celerybeat-schedule 80 | 81 | # SageMath parsed files 82 | *.sage.py 83 | 84 | # Environments 85 | .env 86 | .venv 87 | env/ 88 | venv/ 89 | ENV/ 90 | env.bak/ 91 | venv.bak/ 92 | 93 | # Spyder project settings 94 | .spyderproject 95 | .spyproject 96 | 97 | # Rope project settings 98 | .ropeproject 99 | 100 | # mkdocs documentation 101 | /site 102 | 103 | # mypy 104 | .mypy_cache/ 105 | 106 | # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and WebStorm 107 | # Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 108 | 109 | # User-specific stuff 110 | .idea/**/workspace.xml 111 | .idea/**/tasks.xml 112 | .idea/**/usage.statistics.xml 113 | .idea/**/dictionaries 114 | .idea/**/shelf 115 | 116 | # Generated files 117 | .idea/**/contentModel.xml 118 | 119 | # Sensitive or high-churn files 120 | .idea/**/dataSources/ 121 | .idea/**/dataSources.ids 122 | .idea/**/dataSources.local.xml 123 | .idea/**/sqlDataSources.xml 124 | .idea/**/dynamic.xml 125 | .idea/**/uiDesigner.xml 126 | .idea/**/dbnavigator.xml 127 | 128 | # Gradle 129 | .idea/**/gradle.xml 130 | .idea/**/libraries 131 | 132 | # CMake 133 | cmake-build-*/ 134 | 135 | # Mongo Explorer plugin 136 | .idea/**/mongoSettings.xml 137 | 138 | # File-based project format 139 | *.iws 140 | 141 | # IntelliJ 142 | out/ 143 | 144 | # mpeltonen/sbt-idea plugin 145 | .idea_modules/ 146 | 147 | # JIRA plugin 148 | atlassian-ide-plugin.xml 149 | 150 | # Cursive Clojure plugin 151 | .idea/replstate.xml 152 | 153 | # Crashlytics plugin (for Android Studio and IntelliJ) 154 | com_crashlytics_export_strings.xml 155 | crashlytics.properties 156 | crashlytics-build.properties 157 | fabric.properties 158 | 159 | # Editor-based Rest Client 160 | .idea/httpRequests 161 | 162 | .idea/* 163 | 164 | tests.py 165 | buildandupload.bat 166 | launchlibrary/models.py___jb_tmp___ 167 | -------------------------------------------------------------------------------- /.readthedocs.yml: -------------------------------------------------------------------------------- 1 | # .readthedocs.yml 2 | # Read the Docs configuration file 3 | # See https://docs.readthedocs.io/en/stable/config-file/v2.html for details 4 | 5 | # Required 6 | version: 2 7 | 8 | # Build documentation in the docs/ directory with Sphinx 9 | sphinx: 10 | configuration: docs/source/conf.py 11 | 12 | # Build documentation with MkDocs 13 | #mkdocs: 14 | # configuration: mkdocs.yml 15 | 16 | # Optionally build your docs in additional formats such as PDF 17 | formats: 18 | - pdf 19 | 20 | # Optionally set the version of Python and requirements required to build your docs 21 | python: 22 | version: 3.7 23 | install: 24 | - requirements: docs/requirements.txt -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # python-launch-library 2 | A simple python wrapper for the Launch Library web API. Can also be used asynchronously too. 3 | 4 | [![PyPI Version](https://img.shields.io/pypi/v/python-launch-library.svg)](https://pypi.org/project/python-launch-library/) [![Documentation Status](https://readthedocs.org/projects/python-launch-library/badge/?version=latest)](https://python-launch-library.readthedocs.io/en/latest/?badge=latest) 5 | 6 | ## Important: New projects should NOT use this library. Instead, generate client stubs from the [OpenAPI definitions](https://ll.thespacedevs.com/2.1.0/swagger?format=openapi) using [openapi-generator](https://github.com/OpenAPITools/openapi-generator). 7 | 8 | Available models: `Agency, Launch, Pad, Location, Rocket` 9 | 10 | The usage of the API is simple. 11 | 12 | ### Usage 13 | 14 | ##### Proper documentation is available in [Read The Docs](https://python-launch-library.readthedocs.io/en/latest/). 15 | 16 | To install, simply use pip: ```pip install python-launch-library``` 17 | 18 | ```python 19 | # First, import the library 20 | import launchlibrary as ll 21 | 22 | # Then, initialize an API object 23 | api = ll.Api() 24 | 25 | # And fetch whichever models you'd like. 26 | 27 | # Either by an explicit API call 28 | next_5_go_launches = api.fetch_launch(next=5, status=1) 29 | 30 | # Or by one of the simpler methods available for some models. 31 | next_5_go_launches = api.next_launches(5) 32 | 33 | # Now, you can utilize whatever data you'd like. Data from the API is processed recursively, so if a Launch object 34 | # contains a Location object, you'll have models for both. 35 | launch_loc = next_5_go_launches[0].location 36 | 37 | # Some properties, like status, are only represented by ID. You can call the appropriate methods to get a proper object from that ID 38 | launch_status = next_5_go_launches[0].get_status() 39 | 40 | # It's now possible to also use the regular API names as well as pythonic names. 41 | vid_urls = next_5_go_launches[0].vid_urls 42 | vid_urls_2 = next_5_go_launches[0].vidURLs 43 | ``` 44 | 45 | ### Changelog 46 | 47 | Since version `1.0.1`, the library is versioned according to semantic versioning rules. 48 | 49 | ### Important: New projects should NOT use this library. Instead, use the OpenAPI definitions provided in https://ll.thespacedevs.com/2.0.0/swagger 50 | 51 | * 2.0.0 - Update to LL2, aka `https://thespacedevs.com/llapi`. There are some data changes, so this is a breaking change. 52 | 53 | **This is a quickly hacked together attempt to add some compatibility to LL2 for current users, as there's no point in spending time creating a wrapper when OpenAPI definitions exist.** 54 | 55 | Breaking Changes: 56 | * The AgencyType, LaunchStatus, and RocketFamily models have been eliminated. 57 | * All instances, except in `Launch` of `info_urls` have been changed to `info_url` (this is compliant with the new API). 58 | * Many parameters might be unavailable as many names were changed. 59 | * Modelizations might not work as well as many names were changed. 60 | 61 | * 1.0.3, 1.0.4, 1.0.5, 1.0.6 - Fixed some bugs in the async variant 62 | 63 | * 1.0.2 - Added an exception hierarchy. All exceptions now inherit from LlException 64 | 65 | * 1.0.1 - Improved caching. 66 | 67 | * 1.0 - Changed all fetch calls to be through the Api object. This is a breaking change. 68 | 69 | 70 | 71 | ```python 72 | # Porting guide 73 | 74 | import launchlibrary as ll 75 | api = ll.Api() 76 | 77 | # Before 78 | next_launch = ll.Launch.next(api, 1) 79 | 80 | # After 81 | next_launch = api.next_launches(1) 82 | ``` 83 | 84 | 85 | ### Todo 86 | - [x] Tidy up the repository 87 | - [x] Add exceptions to handle server timeout 88 | - [x] Handle nested models (i.e. a Pad model inside a Location model inside a Launch model) 89 | - [x] Handle times with the datetime class 90 | - [x] Package properly and upload to PyPI 91 | - [x] Add more abstraction methods for the api calls (open to suggestions) 92 | - [x] Add magic method comparisons (open to suggestions) 93 | - [x] Asynchronous operation 94 | - [x] Add aliases for actual API names with getattr 95 | - [ ] Add tests 96 | - [ ] Your suggestion here 97 | 98 | 99 | 100 | 101 | Feel free to open issues and pull requests! I usually check Github daily. 102 | 103 | -------------------------------------------------------------------------------- /docs/README.txt: -------------------------------------------------------------------------------- 1 | To make the docs yourself, you'll need to install the sphinx_rtd_theme module as well as the sphinx module. 2 | Use in this folder: 3 | 4 | pip install -r requirements.txt -------------------------------------------------------------------------------- /docs/make.bat: -------------------------------------------------------------------------------- 1 | @ECHO OFF 2 | 3 | pushd %~dp0 4 | 5 | REM Command file for Sphinx documentation 6 | 7 | if "%SPHINXBUILD%" == "" ( 8 | set SPHINXBUILD=sphinx-build 9 | ) 10 | set SOURCEDIR=source 11 | set BUILDDIR=build 12 | set SPHINXPROJ=python-launch-library 13 | 14 | if "%1" == "" goto help 15 | 16 | %SPHINXBUILD% >NUL 2>NUL 17 | if errorlevel 9009 ( 18 | echo. 19 | echo.The 'sphinx-build' command was not found. Make sure you have Sphinx 20 | echo.installed, then set the SPHINXBUILD environment variable to point 21 | echo.to the full path of the 'sphinx-build' executable. Alternatively you 22 | echo.may add the Sphinx directory to PATH. 23 | echo. 24 | echo.If you don't have Sphinx installed, grab it from 25 | echo.http://sphinx-doc.org/ 26 | exit /b 1 27 | ) 28 | 29 | %SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% 30 | goto end 31 | 32 | :help 33 | %SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% 34 | 35 | :end 36 | popd 37 | -------------------------------------------------------------------------------- /docs/make_html.bat: -------------------------------------------------------------------------------- 1 | make html 2 | -------------------------------------------------------------------------------- /docs/requirements.txt: -------------------------------------------------------------------------------- 1 | sphinx 2 | sphinx-autodoc-typehints==1.5.2 3 | sphinx_rtd_theme 4 | requests 5 | python-dateutil 6 | aiohttp 7 | unidecode 8 | async_lru -------------------------------------------------------------------------------- /docs/source/License.rst: -------------------------------------------------------------------------------- 1 | Apache 2 License 2 | ================ 3 | 4 | Apache License 5 | Version 2.0, January 2004 6 | http://www.apache.org/licenses/ 7 | 8 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 9 | 10 | 1. Definitions. 11 | 12 | "License" shall mean the terms and conditions for use, reproduction, 13 | and distribution as defined by Sections 1 through 9 of this document. 14 | 15 | "Licensor" shall mean the copyright owner or entity authorized by 16 | the copyright owner that is granting the License. 17 | 18 | "Legal Entity" shall mean the union of the acting entity and all 19 | other entities that control, are controlled by, or are under common 20 | control with that entity. For the purposes of this definition, 21 | "control" means (i) the power, direct or indirect, to cause the 22 | direction or management of such entity, whether by contract or 23 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 24 | outstanding shares, or (iii) beneficial ownership of such entity. 25 | 26 | "You" (or "Your") shall mean an individual or Legal Entity 27 | exercising permissions granted by this License. 28 | 29 | "Source" form shall mean the preferred form for making modifications, 30 | including but not limited to software source code, documentation 31 | source, and configuration files. 32 | 33 | "Object" form shall mean any form resulting from mechanical 34 | transformation or translation of a Source form, including but 35 | not limited to compiled object code, generated documentation, 36 | and conversions to other media types. 37 | 38 | "Work" shall mean the work of authorship, whether in Source or 39 | Object form, made available under the License, as indicated by a 40 | copyright notice that is included in or attached to the work 41 | (an example is provided in the Appendix below). 42 | 43 | "Derivative Works" shall mean any work, whether in Source or Object 44 | form, that is based on (or derived from) the Work and for which the 45 | editorial revisions, annotations, elaborations, or other modifications 46 | represent, as a whole, an original work of authorship. For the purposes 47 | of this License, Derivative Works shall not include works that remain 48 | separable from, or merely link (or bind by name) to the interfaces of, 49 | the Work and Derivative Works thereof. 50 | 51 | "Contribution" shall mean any work of authorship, including 52 | the original version of the Work and any modifications or additions 53 | to that Work or Derivative Works thereof, that is intentionally 54 | submitted to Licensor for inclusion in the Work by the copyright owner 55 | or by an individual or Legal Entity authorized to submit on behalf of 56 | the copyright owner. For the purposes of this definition, "submitted" 57 | means any form of electronic, verbal, or written communication sent 58 | to the Licensor or its representatives, including but not limited to 59 | communication on electronic mailing lists, source code control systems, 60 | and issue tracking systems that are managed by, or on behalf of, the 61 | Licensor for the purpose of discussing and improving the Work, but 62 | excluding communication that is conspicuously marked or otherwise 63 | designated in writing by the copyright owner as "Not a Contribution." 64 | 65 | "Contributor" shall mean Licensor and any individual or Legal Entity 66 | on behalf of whom a Contribution has been received by Licensor and 67 | subsequently incorporated within the Work. 68 | 69 | 2. Grant of Copyright License. Subject to the terms and conditions of 70 | this License, each Contributor hereby grants to You a perpetual, 71 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 72 | copyright license to reproduce, prepare Derivative Works of, 73 | publicly display, publicly perform, sublicense, and distribute the 74 | Work and such Derivative Works in Source or Object form. 75 | 76 | 3. Grant of Patent License. Subject to the terms and conditions of 77 | this License, each Contributor hereby grants to You a perpetual, 78 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 79 | (except as stated in this section) patent license to make, have made, 80 | use, offer to sell, sell, import, and otherwise transfer the Work, 81 | where such license applies only to those patent claims licensable 82 | by such Contributor that are necessarily infringed by their 83 | Contribution(s) alone or by combination of their Contribution(s) 84 | with the Work to which such Contribution(s) was submitted. If You 85 | institute patent litigation against any entity (including a 86 | cross-claim or counterclaim in a lawsuit) alleging that the Work 87 | or a Contribution incorporated within the Work constitutes direct 88 | or contributory patent infringement, then any patent licenses 89 | granted to You under this License for that Work shall terminate 90 | as of the date such litigation is filed. 91 | 92 | 4. Redistribution. You may reproduce and distribute copies of the 93 | Work or Derivative Works thereof in any medium, with or without 94 | modifications, and in Source or Object form, provided that You 95 | meet the following conditions: 96 | 97 | (a) You must give any other recipients of the Work or 98 | Derivative Works a copy of this License; and 99 | 100 | (b) You must cause any modified files to carry prominent notices 101 | stating that You changed the files; and 102 | 103 | (c) You must retain, in the Source form of any Derivative Works 104 | that You distribute, all copyright, patent, trademark, and 105 | attribution notices from the Source form of the Work, 106 | excluding those notices that do not pertain to any part of 107 | the Derivative Works; and 108 | 109 | (d) If the Work includes a "NOTICE" text file as part of its 110 | distribution, then any Derivative Works that You distribute must 111 | include a readable copy of the attribution notices contained 112 | within such NOTICE file, excluding those notices that do not 113 | pertain to any part of the Derivative Works, in at least one 114 | of the following places: within a NOTICE text file distributed 115 | as part of the Derivative Works; within the Source form or 116 | documentation, if provided along with the Derivative Works; or, 117 | within a display generated by the Derivative Works, if and 118 | wherever such third-party notices normally appear. The contents 119 | of the NOTICE file are for informational purposes only and 120 | do not modify the License. You may add Your own attribution 121 | notices within Derivative Works that You distribute, alongside 122 | or as an addendum to the NOTICE text from the Work, provided 123 | that such additional attribution notices cannot be construed 124 | as modifying the License. 125 | 126 | You may add Your own copyright statement to Your modifications and 127 | may provide additional or different license terms and conditions 128 | for use, reproduction, or distribution of Your modifications, or 129 | for any such Derivative Works as a whole, provided Your use, 130 | reproduction, and distribution of the Work otherwise complies with 131 | the conditions stated in this License. 132 | 133 | 5. Submission of Contributions. Unless You explicitly state otherwise, 134 | any Contribution intentionally submitted for inclusion in the Work 135 | by You to the Licensor shall be under the terms and conditions of 136 | this License, without any additional terms or conditions. 137 | Notwithstanding the above, nothing herein shall supersede or modify 138 | the terms of any separate license agreement you may have executed 139 | with Licensor regarding such Contributions. 140 | 141 | 6. Trademarks. This License does not grant permission to use the trade 142 | names, trademarks, service marks, or product names of the Licensor, 143 | except as required for reasonable and customary use in describing the 144 | origin of the Work and reproducing the content of the NOTICE file. 145 | 146 | 7. Disclaimer of Warranty. Unless required by applicable law or 147 | agreed to in writing, Licensor provides the Work (and each 148 | Contributor provides its Contributions) on an "AS IS" BASIS, 149 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 150 | implied, including, without limitation, any warranties or conditions 151 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 152 | PARTICULAR PURPOSE. You are solely responsible for determining the 153 | appropriateness of using or redistributing the Work and assume any 154 | risks associated with Your exercise of permissions under this License. 155 | 156 | 8. Limitation of Liability. In no event and under no legal theory, 157 | whether in tort (including negligence), contract, or otherwise, 158 | unless required by applicable law (such as deliberate and grossly 159 | negligent acts) or agreed to in writing, shall any Contributor be 160 | liable to You for damages, including any direct, indirect, special, 161 | incidental, or consequential damages of any character arising as a 162 | result of this License or out of the use or inability to use the 163 | Work (including but not limited to damages for loss of goodwill, 164 | work stoppage, computer failure or malfunction, or any and all 165 | other commercial damages or losses), even if such Contributor 166 | has been advised of the possibility of such damages. 167 | 168 | 9. Accepting Warranty or Additional Liability. While redistributing 169 | the Work or Derivative Works thereof, You may choose to offer, 170 | and charge a fee for, acceptance of support, warranty, indemnity, 171 | or other liability obligations and/or rights consistent with this 172 | License. However, in accepting such obligations, You may act only 173 | on Your own behalf and on Your sole responsibility, not on behalf 174 | of any other Contributor, and only if You agree to indemnify, 175 | defend, and hold each Contributor harmless for any liability 176 | incurred by, or claims asserted against, such Contributor by reason 177 | of your accepting any such warranty or additional liability. 178 | 179 | END OF TERMS AND CONDITIONS 180 | -------------------------------------------------------------------------------- /docs/source/Modules.rst: -------------------------------------------------------------------------------- 1 | Module Documentation 2 | ==================== 3 | 4 | Api 5 | --- 6 | 7 | The Api class exposes the main interface for this library. A fetch method is provided for all models. 8 | 9 | The fetch method will get the data, turn it into python objects recursively, and do a few other nice things like adding python date-time objects. 10 | 11 | .. autoclass:: launchlibrary.Api 12 | :members: 13 | 14 | .. automethod:: __init__ 15 | 16 | Models 17 | ------ 18 | 19 | As this library is based on the **launchlibrary** API, you can find a lot of info on `their website `_ . 20 | Note that the wrapper only uses the `detailed` mode. 21 | 22 | Additionally, the parameters of every model can be accessed post-creation by using `model.param_names`. 23 | 24 | .. automodule:: launchlibrary.models 25 | :members: 26 | :undoc-members: 27 | :show-inheritance: 28 | 29 | Asynchronous Models 30 | ------------------- 31 | 32 | The library also supports asynchronous operation. To receive proper coroutines, just prepend Async to the name of the class. 33 | 34 | .. automodule:: launchlibrary.async_models 35 | :members: 36 | :undoc-members: 37 | :show-inheritance: 38 | 39 | Exceptions 40 | ---------- 41 | 42 | The library attempts not to leak any exceptions except the regular Python ones, like ValueError and KeyError. 43 | 44 | .. automodule:: launchlibrary.exceptions 45 | :members: 46 | :undoc-members: 47 | :show-inheritance: 48 | -------------------------------------------------------------------------------- /docs/source/conf.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 3 | # Configuration file for the Sphinx documentation builder. 4 | # 5 | # This file does only contain a selection of the most common options. For a 6 | # full list see the documentation: 7 | # http://www.sphinx-doc.org/en/master/config 8 | 9 | # -- Path setup -------------------------------------------------------------- 10 | 11 | # If extensions (or modules to document with autodoc) are in another directory, 12 | # add these directories to sys.path here. If the directory is relative to the 13 | # documentation root, use os.path.abspath to make it absolute, like shown here. 14 | # 15 | import os 16 | import sys 17 | 18 | sys.path.insert(0, os.path.abspath('../../')) 19 | 20 | # -- Project information ----------------------------------------------------- 21 | 22 | project = 'python-launch-library' 23 | copyright = '2020, Plutoberth' 24 | author = 'Plutoberth' 25 | 26 | # The short X.Y version 27 | version = '' 28 | # The full version, including alpha/beta/rc tags 29 | release = '0.3' 30 | 31 | # -- General configuration --------------------------------------------------- 32 | 33 | # If your documentation needs a minimal Sphinx version, state it here. 34 | # 35 | # needs_sphinx = '1.0' 36 | 37 | # Add any Sphinx extension module names here, as strings. They can be 38 | # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom 39 | # ones. 40 | extensions = [ 41 | 'sphinx.ext.autodoc', 42 | 'sphinx_autodoc_typehints', 43 | 'sphinx.ext.viewcode', 44 | ] 45 | 46 | # Change autodoc order to by source 47 | autodoc_member_order = "bysource" 48 | 49 | # Add any paths that contain templates here, relative to this directory. 50 | templates_path = ['_templates'] 51 | 52 | # The suffix(es) of source filenames. 53 | # You can specify multiple suffix as a list of string: 54 | # 55 | # source_suffix = ['.rst', '.md'] 56 | source_suffix = '.rst' 57 | 58 | # The master toctree document. 59 | master_doc = 'index' 60 | 61 | # The language for content autogenerated by Sphinx. Refer to documentation 62 | # for a list of supported languages. 63 | # 64 | # This is also used if you do content translation via gettext catalogs. 65 | # Usually you set "language" from the command line for these cases. 66 | language = None 67 | 68 | # List of patterns, relative to source directory, that match files and 69 | # directories to ignore when looking for source files. 70 | # This pattern also affects html_static_path and html_extra_path . 71 | exclude_patterns = [] 72 | 73 | # The name of the Pygments (syntax highlighting) style to use. 74 | pygments_style = 'sphinx' 75 | 76 | # -- Options for HTML output ------------------------------------------------- 77 | 78 | # The theme to use for HTML and HTML Help pages. See the documentation for 79 | # a list of builtin themes. 80 | # 81 | html_theme = 'sphinx_rtd_theme' 82 | 83 | # Theme options are theme-specific and customize the look and feel of a theme 84 | # further. For a list of options available for each theme, see the 85 | # documentation. 86 | # 87 | # html_theme_options = {} 88 | 89 | # Add any paths that contain custom static files (such as style sheets) here, 90 | # relative to this directory. They are copied after the builtin static files, 91 | # so a file named "default.css" will overwrite the builtin "default.css". 92 | html_static_path = ['_static'] 93 | 94 | # Custom sidebar templates, must be a dictionary that maps document names 95 | # to template names. 96 | # 97 | # The default sidebars (for documents that don't match any pattern) are 98 | # defined by theme itself. Builtin themes are using these templates by 99 | # default: ``['localtoc.html', 'relations.html', 'sourcelink.html', 100 | # 'searchbox.html']``. 101 | # 102 | # html_sidebars = {} 103 | 104 | 105 | # -- Options for HTMLHelp output --------------------------------------------- 106 | 107 | # Output file base name for HTML help builder. 108 | htmlhelp_basename = 'python-launch-librarydoc' 109 | 110 | # -- Options for LaTeX output ------------------------------------------------ 111 | 112 | latex_elements = { 113 | # The paper size ('letterpaper' or 'a4paper'). 114 | # 115 | # 'papersize': 'letterpaper', 116 | 117 | # The font size ('10pt', '11pt' or '12pt'). 118 | # 119 | # 'pointsize': '10pt', 120 | 121 | # Additional stuff for the LaTeX preamble. 122 | # 123 | # 'preamble': '', 124 | 125 | # Latex figure (float) alignment 126 | # 127 | # 'figure_align': 'htbp', 128 | } 129 | 130 | # Grouping the document tree into LaTeX files. List of tuples 131 | # (source start file, target name, title, 132 | # author, documentclass [howto, manual, or own class]). 133 | latex_documents = [ 134 | (master_doc, 'python-launch-library.tex', 'python-launch-library Documentation', 135 | 'Plutoberth', 'manual'), 136 | ] 137 | 138 | # -- Options for manual page output ------------------------------------------ 139 | 140 | # One entry per manual page. List of tuples 141 | # (source start file, name, description, authors, manual section). 142 | man_pages = [ 143 | (master_doc, 'python-launch-library', 'python-launch-library Documentation', 144 | [author], 1) 145 | ] 146 | 147 | # -- Options for Texinfo output ---------------------------------------------- 148 | 149 | # Grouping the document tree into Texinfo files. List of tuples 150 | # (source start file, target name, title, author, 151 | # dir menu entry, description, category) 152 | texinfo_documents = [ 153 | (master_doc, 'python-launch-library', 'python-launch-library Documentation', 154 | author, 'python-launch-library', 'One line description of project.', 155 | 'Miscellaneous'), 156 | ] 157 | 158 | # -- Extension configuration ------------------------------------------------- 159 | -------------------------------------------------------------------------------- /docs/source/getting_started.rst: -------------------------------------------------------------------------------- 1 | Getting Started 2 | =============== 3 | 4 | Installation 5 | ------------ 6 | 7 | The python-launch-library module only supports Python 3.6 and above. To install it, just use: 8 | 9 | .. code:: 10 | 11 | pip install python-launch-library 12 | 13 | 14 | Usage 15 | ----- 16 | 17 | Usage of the wrapper is simple. 18 | 19 | .. code:: py3 20 | 21 | # First, import the library 22 | import launchlibrary as ll 23 | 24 | # Then, initialize an API object 25 | api = ll.Api() 26 | 27 | # And fetch whichever models you'd like. 28 | 29 | # Either by an explicit API call 30 | next_5_go_launches = api.fetch_launch(next=5, status=1) 31 | 32 | # Or by one of the simpler methods available for some models. 33 | next_5_go_launches = api.next_launches(5) 34 | 35 | # Now, you can utilize whatever data you'd like. Data from the API is processed recursively, so if a Launch object 36 | # contains a Location object, you'll have models for both. 37 | launch_loc = next_5_go_launches[0].location 38 | 39 | # Some properties, like status, are only represented by ID. You can call the appropriate methods to get a proper object from that ID 40 | launch_status = next_5_go_launches[0].get_status() 41 | 42 | # It's now possible to also use the regular API names as well as pythonic names. 43 | vid_urls = next_5_go_launches[0].vid_urls 44 | vid_urls_2 = next_5_go_launches[0].vidURLs 45 | 46 | Asynchronous Usage 47 | ------------------ 48 | 49 | .. code:: py 50 | 51 | import launchlibrary as ll 52 | 53 | api = ll.Api() 54 | 55 | async def foo(): 56 | # Use the api as usual, but with an async prefix for api functions. 57 | next_5_go_launches = await api.async_next_launches(5) 58 | status = await next_5_go_launches[0].get_status() 59 | 60 | 61 | 62 | 63 | 64 | -------------------------------------------------------------------------------- /docs/source/index.rst: -------------------------------------------------------------------------------- 1 | Welcome to python-launch-library's documentation! 2 | ================================================= 3 | 4 | Welcome to python-launch-library's documentation! This is a fairly simple library built to easily interact with the launchlibrary.net API. 5 | 6 | .. toctree:: 7 | :maxdepth: 2 8 | 9 | 10 | getting_started 11 | Modules 12 | License 13 | 14 | First, visit the getting started page, and then feel free to proceed to the Modules page to learn about the rest of the wrapper. 15 | 16 | About Launch Library 17 | -------------------- 18 | Launch Library is a free to use website that exposes a rich API with a plethora of data about future (and past) launches. 19 | 20 | Check them out `here `_! -------------------------------------------------------------------------------- /launchlibrary/__init__.py: -------------------------------------------------------------------------------- 1 | # Copyright 2020 Nir Harel 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | from .api import Api 16 | from .models import * 17 | from .exceptions import * 18 | from .utils import * 19 | from .async_models import * 20 | 21 | -------------------------------------------------------------------------------- /launchlibrary/api.py: -------------------------------------------------------------------------------- 1 | # Copyright 2020 Nir Harel 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | from .async_models import * 16 | from .constants import * 17 | from typing import List 18 | 19 | 20 | class Api: 21 | def __init__(self, api_url: str = DEFAULT_LL_URL, version: str = DEFAULT_VERSION, unicode: bool = True): 22 | """ 23 | The API class for the launchlibrary module. 24 | 25 | :param api_url: The URL of the launchlibrary website. 26 | :param version: Version of the api 27 | :param unicode: Set to False to convert unicode characters to ASCII using unidecode. 28 | """ 29 | 30 | # These probably shouldn't be changed unless the site changed its address. The wrapper may not work as well 31 | # with a different version than the default one. 32 | url = "/".join([api_url, version]) 33 | self.network = Network(url, "detailed") 34 | 35 | global DO_UNIDECODE 36 | # I know that this is super hacky, but it'll work for almost all users. 37 | # Fix it and submit a PR if you care. 38 | DO_UNIDECODE = unicode 39 | 40 | def fetch_agency(self, **kwargs): 41 | """Fetch from the Agency endpoint""" 42 | return Agency.fetch(self.network, **kwargs) 43 | 44 | def fetch_launch(self, **kwargs): 45 | """Fetch from the Launch endpoint""" 46 | return Launch.fetch(self.network, **kwargs) 47 | 48 | def next_launches(self, num: int) -> List[UpcomingLaunch]: 49 | """ 50 | Get the next {num} launches. 51 | 52 | :param num: a number for the number of launches 53 | """ 54 | return UpcomingLaunch.next(self.network, num) 55 | 56 | def fetch_pad(self, **kwargs): 57 | """Fetch from the Pad endpoint""" 58 | return Pad.fetch(self.network, **kwargs) 59 | 60 | def fetch_location(self, **kwargs): 61 | """Fetch from the Location endpoint""" 62 | return Location.fetch(self.network, **kwargs) 63 | 64 | def fetch_rocket(self, **kwargs): 65 | """Fetch from the Rocket endpoint""" 66 | return Rocket.fetch(self.network, **kwargs) 67 | 68 | # Async fetchers 69 | 70 | async def async_fetch_agency(self, **kwargs): 71 | """Fetch from the Agency endpoint""" 72 | return await AsyncAgency.fetch(self.network, **kwargs) 73 | 74 | async def async_fetch_launch(self, **kwargs): 75 | """Fetch from the Launch endpoint""" 76 | return await AsyncLaunch.fetch(self.network, **kwargs) 77 | 78 | async def async_next_launches(self, num: int) -> List[AsyncUpcomingLaunch]: 79 | """ 80 | Get the next {num} launches. 81 | 82 | :param num: a number for the number of launches 83 | """ 84 | return await AsyncUpcomingLaunch.next(self.network, num) 85 | 86 | async def async_fetch_pad(self, **kwargs): 87 | """Fetch from the Pad endpoint""" 88 | return await AsyncPad.fetch(self.network, **kwargs) 89 | 90 | async def async_fetch_location(self, **kwargs): 91 | """Fetch from the Location endpoint""" 92 | return await AsyncLocation.fetch(self.network, **kwargs) 93 | 94 | async def async_fetch_rocket(self, **kwargs): 95 | """Fetch from the Rocket endpoint""" 96 | return await AsyncRocket.fetch(self.network, **kwargs) 97 | -------------------------------------------------------------------------------- /launchlibrary/async_models.py: -------------------------------------------------------------------------------- 1 | from launchlibrary.models import * 2 | from async_lru import alru_cache 3 | from .network import Network 4 | 5 | 6 | class BaseAsync(BaseModel): 7 | def __init__(self, network: Network, param_translations: dict, proper_name: str): 8 | # Gotta use explicit params for it to work correctly 9 | super().__init__(network, param_translations, proper_name) 10 | 11 | @classmethod 12 | async def fetch(cls, network: Network, **kwargs): 13 | """ 14 | The fetch method implements fetch with an async HTTP GET function. 15 | 16 | :param network: A network instance 17 | :param kwargs: args for the api call 18 | :return: objects based on BaseAsync 19 | """ 20 | 21 | kwargs = utils.sanitize_input(kwargs) 22 | 23 | json_object = await network.async_send_message(cls._endpoint_name, kwargs) 24 | 25 | classes = cls._create_classes(network, json_object) 26 | return classes 27 | 28 | 29 | # All async models should be based on this, and all functions that use fetch should be reimplemented 30 | class AsyncAgency(Agency, BaseAsync): 31 | """A class representing an async agency object.""" 32 | 33 | 34 | class AsyncLaunch(Launch, BaseAsync): 35 | """A class representing an async launch object.""" 36 | 37 | 38 | class AsyncUpcomingLaunch(UpcomingLaunch, BaseAsync): 39 | """A class representing an upcoming launch object.""" 40 | 41 | @classmethod 42 | async def next(cls, network: Network, num: int): 43 | """ 44 | Get the next {num} launches. 45 | 46 | :param network: A network instance 47 | 48 | :param num: a number for the number of launches 49 | """ 50 | return await cls.fetch(network) 51 | 52 | 53 | class AsyncPad(Pad, BaseAsync): 54 | """A class representing an async pad object.""" 55 | pass 56 | 57 | 58 | class AsyncLocation(Location, BaseAsync): 59 | """A class representing an async Location object.""" 60 | pass 61 | 62 | 63 | class AsyncRocket(Rocket, BaseAsync): 64 | """A class representing an async rocket.""" 65 | 66 | @staticmethod 67 | @alru_cache() 68 | async def _get_pads_for_id(network: Network, pads: str): 69 | return await AsyncPad.fetch(network, id=pads) 70 | 71 | async def get_pads(self) -> List[AsyncPad]: 72 | """Returns Pad type objects of the pads the rocket uses.""" 73 | pad_objs = [] 74 | if self.default_pads: 75 | pad_objs = await AsyncRocket._get_pads_for_id(self.network, self.default_pads) 76 | 77 | return pad_objs 78 | -------------------------------------------------------------------------------- /launchlibrary/constants.py: -------------------------------------------------------------------------------- 1 | DEFAULT_LL_URL = "https://ll.thespacedevs.com" 2 | DEFAULT_VERSION = "2.0.0" 3 | DEFAULT_API_URL = "/".join([DEFAULT_LL_URL, DEFAULT_VERSION]) 4 | -------------------------------------------------------------------------------- /launchlibrary/exceptions.py: -------------------------------------------------------------------------------- 1 | # Copyright 2020 Nir Harel 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | 16 | class LlException(Exception): 17 | """The base class for all exceptions emitted by the library""" 18 | def __init__(self, message: str = "There was an unspecified exception regarding the LaunchLibrary wrapper."): 19 | super().__init__(message) 20 | 21 | 22 | class ApiException(LlException): 23 | """An exception related to the API's response""" 24 | def __init__(self, message: str = "There was an unknown issue with the API. Please reevaluate your call."): 25 | super().__init__(message) 26 | 27 | 28 | class NetworkException(LlException): 29 | """Some network failure that's unrelated to the request, like a dropped connection""" 30 | 31 | def __init__(self, message: str): 32 | super().__init__(message) 33 | 34 | 35 | class TimeoutException(LlException): 36 | """All timeout failures, both during the initial connection and subsequent messages""" 37 | 38 | def __init__(self, message: str = ""): 39 | super().__init__(message) 40 | -------------------------------------------------------------------------------- /launchlibrary/models.py: -------------------------------------------------------------------------------- 1 | # Copyright 2020 Nir Harel 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | from unidecode import unidecode 16 | from dateutil import parser 17 | from dateutil import relativedelta 18 | from functools import lru_cache 19 | import datetime 20 | from typing import List 21 | from launchlibrary import utils 22 | from .network import Network 23 | 24 | # Set default dt to the beginning of next month 25 | DEFAULT_DT = datetime.datetime.now().replace(day=1, hour=0, minute=0, second=0, microsecond=0) \ 26 | + relativedelta.relativedelta(months=1) 27 | 28 | DO_UNIDECODE = False 29 | 30 | 31 | class BaseModel: 32 | """The base model class all models should inherit from. Provides fetch and other utility functionalities. 33 | 34 | :_endpoint_name: The endpoint to use in the api 35 | :_nested_name: The name of that will appear in nested results. "Agencies" and the such. 36 | 37 | ========= =========== 38 | Operation Description 39 | --------- ----------- 40 | x == y Checks if both objects are of the same type and have the same id. 41 | ========= =========== 42 | """ 43 | 44 | _endpoint_name = "" 45 | _nested_name = "" 46 | 47 | def __init__(self, network: Network, param_translations: dict, proper_name: str): 48 | """ 49 | All launchlibrary models should inherit from this class. Contains utility and fetch functions. 50 | 51 | :param network: An instance of the Api class. 52 | :param param_translations: Translations from API names to pythonic names. 53 | :param proper_name: The proper name for use in __repr__ 54 | """ 55 | 56 | self.network = network 57 | # param translations serves both for pythonic translations and default param values 58 | self._param_translations = param_translations 59 | self.proper_name = proper_name 60 | self.param_names = self._param_translations.values() 61 | 62 | @classmethod 63 | def fetch(cls, network: Network, **kwargs) -> list: 64 | """ 65 | Initializes a class, or even a list of them from the api using the needed params. 66 | 67 | :param network: An instance of the network class 68 | :param kwargs: Arguments to include in the GET request 69 | """ 70 | 71 | kwargs = utils.sanitize_input(kwargs) 72 | 73 | json_object = network.send_message(cls._endpoint_name, kwargs) 74 | 75 | classes = cls._create_classes(network, json_object) 76 | 77 | return classes 78 | 79 | @classmethod 80 | def init_from_json(cls, network: Network, json_object: dict): 81 | """ 82 | Initializes a class from a json object. Only single classes. 83 | 84 | :param network: launchlibrary.Network 85 | :param json_object: An object containing the "entry" we want to init. 86 | :return: cls 87 | """ 88 | cls_init = cls(network) 89 | cls_init._set_params_json(json_object) 90 | cls_init._postprocess() 91 | return cls_init 92 | 93 | @classmethod 94 | def _create_classes(cls, network: Network, json_object) -> list: 95 | """ 96 | Creates the required classes from the json object. 97 | 98 | :param network: 99 | :param json_object: 100 | :return: 101 | """ 102 | 103 | classes = [] 104 | for entry in json_object.get("results", []): 105 | cls_init = cls(network) 106 | cls_init._set_params_json(entry) 107 | cls_init._postprocess() 108 | classes.append(cls_init) 109 | 110 | return classes 111 | 112 | def _set_params_json(self, json_object: dict): 113 | """Sets the parameters of a class from an object (raw data, not inside "agencies" for example)""" 114 | self._modelize(json_object) 115 | for api_name, pythonic_name in self._param_translations.items(): 116 | data = json_object.get(api_name, None) 117 | # If the data is a string, and the unicode option is set to false 118 | if isinstance(data, str) and DO_UNIDECODE: 119 | data = unidecode(data) 120 | 121 | setattr(self, pythonic_name, data) 122 | 123 | def _modelize(self, json_object): 124 | """Recursively goes over the json object, looking for any compatible models. It's recursive in an indirect 125 | way (through set_params_json).""" 126 | 127 | for key, val in json_object.items(): 128 | if key in MODEL_LIST_PLURAL.keys(): 129 | if val and isinstance(val, list): 130 | if len(val) > 0: 131 | json_object[key] = [MODEL_LIST_PLURAL[key].init_from_json(self.network, r) for r in val] 132 | elif key in MODEL_LIST_SINGULAR.keys(): # if it is a singular 133 | if val and isinstance(val, dict): 134 | if len(val) > 0: 135 | json_object[key] = MODEL_LIST_SINGULAR[key].init_from_json(self.network, val) 136 | 137 | def _postprocess(self): 138 | """Optional method. May be used for model specific operations (like purging times).""" 139 | pass 140 | 141 | def _get_all_params(self) -> dict: 142 | return {k: getattr(self, k, None) for k in self.param_names} 143 | 144 | def __repr__(self) -> str: 145 | subclass_name = self.proper_name 146 | variables = ",".join("{}={}".format(k, v) for k, v in self._get_all_params().items()) 147 | return "{}({})".format(subclass_name, variables) 148 | 149 | def __eq__(self, other): 150 | return hash(self) == hash(other) 151 | 152 | def __hash__(self): 153 | return hash(getattr(self, "id", None)) + hash(type(self)) 154 | 155 | def __getattr__(self, item): 156 | """ 157 | This function will allow the user to use the standard, non pythonic api names for the fields returned by the api 158 | It's possible that the user will want to use API names like infoURLs, instead of info_urls. We can allow this 159 | using our param_translation dictionaries. 160 | """ 161 | if item in self._param_translations: 162 | return getattr(self, self._param_translations[item], None) 163 | 164 | 165 | class Agency(BaseModel): 166 | """A class representing an agency object.""" 167 | 168 | _nested_name = "agencies" 169 | _endpoint_name = "agency" 170 | 171 | def __init__(self, network: Network): 172 | param_translations = {'id': 'id', 'name': 'name', 'abbrev': 'abbrev', 'type': 'type', 173 | 'description': 'description', 'country_code': 'country_code', 'wiki_url': 'wiki_url', 174 | 'info_url': 'info_url', 'changed': 'changed'} 175 | 176 | self.id = None 177 | self.name = None 178 | self.abbrev = None 179 | self.type = None 180 | self.description = None 181 | self.country_code = None 182 | self.wiki_url = None 183 | self.info_url = None 184 | self.changed = None 185 | 186 | proper_name = self.__class__.__name__ 187 | 188 | super().__init__(network, param_translations, proper_name) 189 | 190 | 191 | class Launch(BaseModel): 192 | """A class representing a launch object. 193 | 194 | You may use the **'windowstart'**, **'windowend'**, and **'net'** params to access datetime objects of the times. 195 | They'll be 'None' if the conversion fails. 196 | 197 | The comparison magic methods that are implemented essentially compare the dates of the two objects. 198 | 199 | ========= =========== 200 | Operation Description 201 | --------- ----------- 202 | x < y Checks if launch y occurs before launch x. 203 | x > y Checks if launch x occurs before launch y. 204 | ========= ===========""" 205 | 206 | _nested_name = "launches" 207 | _endpoint_name = "launch" 208 | 209 | def __init__(self, network: Network): 210 | self.datetime_conversions = {} 211 | param_translations = {'id': 'id', 'name': 'name', 'tbddate': 'tbddate', 'tbdtime': 'tbdtime', 212 | 'status': 'status', 'inhold': 'inhold', 'window_start': 'windowstart', 213 | 'window_end': 'windowend', 214 | 'net': 'net', 'infoURLs': 'info_urls', 'vidURLs': 'vid_urls', 215 | 'holdreason': 'holdreason', 'failreason': 'failreason', 'probability': 'probability', 216 | 'hashtag': 'hashtag', 'lsp': 'agency', 'changed': 'changed', 'pad': 'pad', 217 | 'rocket': 'rocket', 'missions': 'missions'} 218 | 219 | self.id = None 220 | self.name = None 221 | self.tbddate = None 222 | self.tbdtime = None 223 | self.status = None 224 | self.inhold = None 225 | self.windowstart = None 226 | self.windowend = None 227 | self.net = None 228 | self.info_urls = None 229 | self.vid_urls = None 230 | self.holdreason = None 231 | self.failreason = None 232 | self.probability = None 233 | self.hashtag = None 234 | self._lsp = None 235 | self.changed = None 236 | self.pad = None 237 | self.rocket = None 238 | self.missions = None 239 | 240 | proper_name = self.__class__.__name__ 241 | 242 | super().__init__(network, param_translations, proper_name) 243 | 244 | def _postprocess(self): 245 | """Changes times to the datetime format.""" 246 | for time_name in ["windowstart", "windowend", "net"]: 247 | try: 248 | # Will need to modify this if we ever implement modes other than detailed 249 | setattr(self, time_name, parser.parse(getattr(self, time_name, ""))) 250 | except (ValueError, TypeError): 251 | # The string might not contain a date, so we'll need to handle it with an empty datetime object. 252 | setattr(self, time_name, None) 253 | 254 | def __lt__(self, other: "Launch") -> bool: 255 | return self.net < other.net 256 | 257 | def __gt__(self, other: "Launch") -> bool: 258 | return self.net > other.net 259 | 260 | 261 | class UpcomingLaunch(Launch): 262 | _nested_name = "launches" 263 | _endpoint_name = "launch/upcoming" 264 | 265 | def __init__(self, network: Network): 266 | super().__init__(network) 267 | 268 | @classmethod 269 | def next(cls, network: Network, num: int) -> List["UpcomingLaunch"]: 270 | """ 271 | A simple abstraction method to get the next {num} launches. 272 | 273 | :param network: An instance of launchlibrary.Api 274 | 275 | :param num: a number for the number of launches 276 | """ 277 | return cls.fetch(network, next=num, status=1) 278 | 279 | 280 | class Pad(BaseModel): 281 | """A class representing a pad object.""" 282 | 283 | _nested_name = "pads" 284 | _endpoint_name = "pad" 285 | 286 | def __init__(self, network: Network): 287 | param_translations = {'id': 'id', 'name': 'name', 'latitude': 'latitude', 288 | 'longitude': 'longitude', 'map_url': 'map_url', 'retired': 'retired', 289 | 'total_launch_count': 'total_launch_count', 'agency_id': 'agency_id', 290 | 'wiki_url': 'wiki_url', 'info_url': 'info_url', 291 | 'location': 'location', 'map_image': 'map_image'} 292 | 293 | self.id = None 294 | self.name = None 295 | self.latitude = None 296 | self.longitude = None 297 | self.map_url = None 298 | self.wiki_url = None 299 | self.info_url = None 300 | self.agency_id = None 301 | self.total_launch_count = None 302 | self.location = None 303 | self.map_image = None 304 | 305 | proper_name = self.__class__.__name__ 306 | 307 | super().__init__(network, param_translations, proper_name) 308 | 309 | 310 | class Location(BaseModel): 311 | """A class representing a location object.""" 312 | 313 | _nested_name = "locations" 314 | _endpoint_name = "location" 315 | 316 | def __init__(self, network: Network): 317 | param_translations = {'id': 'id', 'name': 'name', 'country_code': 'country_code', 318 | 'total_launch_count': 'total_launch_count', 'total_landing_count': 'total_landing_count', 319 | 'pads': 'pads'} 320 | # pads might be included w/ launch endpoint 321 | 322 | self.id = None 323 | self.name = None 324 | self.country_code = None 325 | self.total_launch_count = None 326 | self.total_landing_count = None 327 | self.pads = None 328 | 329 | proper_name = self.__class__.__name__ 330 | 331 | super().__init__(network, param_translations, proper_name) 332 | 333 | 334 | class Rocket(BaseModel): 335 | """A class representing a rocket object.""" 336 | 337 | _nested_name = "rockets" 338 | _endpoint_name = "config/launcher" 339 | 340 | def __init__(self, network: Network): 341 | param_translations = {'id': 'id', 'name': 'name', 'defaultPads': 'default_pads', 'family': 'family', 342 | 'wiki_url': 'wiki_url', 'info_url': 'info_url', 'image_url': 'image_url', 343 | } 344 | 345 | self.id = None 346 | self.name = None 347 | self.default_pads = None 348 | self.family = None 349 | self.wiki_url = None 350 | self.info_url = None 351 | self.image_url = None 352 | 353 | proper_name = self.__class__.__name__ 354 | 355 | super().__init__(network, param_translations, proper_name) 356 | 357 | @staticmethod 358 | @lru_cache() 359 | def _get_pads_for_id(network: Network, pads: str): 360 | return Pad.fetch(network, id=pads) 361 | 362 | def get_pads(self) -> List[Pad]: 363 | """Returns Pad type objects of the pads the rocket uses.""" 364 | pad_objs = [] 365 | if self.default_pads: 366 | pad_objs = Rocket._get_pads_for_id(self.network, self.default_pads) 367 | 368 | return pad_objs 369 | 370 | 371 | # putting it at the end to load the classes first 372 | MODEL_LIST_PLURAL = {"launch_service_providers": Agency, "pads": Pad, "locations": Location 373 | , "rockets": Rocket, "launcher_list": Rocket} 374 | MODEL_LIST_SINGULAR = {"launch_service_provider": Agency, "manufacturer": "Agency", "pad": Pad, 375 | "location": Location, "rocket": Rocket, "lsp": Agency} 376 | -------------------------------------------------------------------------------- /launchlibrary/network.py: -------------------------------------------------------------------------------- 1 | # Copyright 2020 Nir Harel 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | import asyncio 15 | 16 | import aiohttp 17 | import aiohttp.web 18 | import requests 19 | from .constants import * 20 | from launchlibrary import exceptions as ll_exceptions 21 | 22 | 23 | class Network: 24 | def __init__(self, url=DEFAULT_API_URL, mode="detailed"): 25 | self.url = url 26 | self.mode = mode 27 | self.sess = aiohttp.ClientSession(raise_for_status=True) 28 | 29 | def _get_url(self, endpoint, data: dict) -> str: 30 | """ 31 | Parse the data as GET parameters and return it as a proper request url. 32 | 33 | :param data: A dictionary containing values for the api call. 34 | :return: A proper GET param string 35 | """ 36 | params = "?mode={}&".format(self.mode) + "&".join(["{}={}".format(k, v) for k, v in data.items()]) 37 | return "/".join([self.url, endpoint]) + params 38 | 39 | def send_message(self, endpoint: str, data: dict) -> dict: 40 | """ 41 | Send synchronous messages 42 | 43 | :param endpoint: The api endpoint 44 | :param data: A dict containing data for the request 45 | :return: response dict. 46 | """ 47 | request_url = self._get_url(endpoint, data) 48 | try: 49 | resp = requests.get(request_url) 50 | resp.raise_for_status() 51 | resp_dict = resp.json() 52 | 53 | # Don't leak implementation details 54 | except requests.exceptions.Timeout as e: 55 | raise ll_exceptions.TimeoutException(str(e)) 56 | except requests.exceptions.HTTPError as e: 57 | raise ll_exceptions.ApiException(str(e)) 58 | except requests.exceptions.RequestException as e: 59 | raise ll_exceptions.NetworkException(str(e)) 60 | 61 | return resp_dict # Returns a json style object of the response. 62 | 63 | async def async_send_message(self, endpoint: str, data: dict): 64 | """ 65 | Send asynchronous messages 66 | 67 | :param endpoint: The api endpoint 68 | :param data: A dict containing data for the request 69 | :return: response dict. 70 | """ 71 | request_url = self._get_url(endpoint, data) 72 | try: 73 | async with self.sess.get(request_url) as resp: 74 | resp_dict = await resp.json() 75 | 76 | # Don't leak implementation details 77 | except asyncio.TimeoutError as e: 78 | raise ll_exceptions.TimeoutException(str(e)) 79 | except aiohttp.web.HTTPClientError as e: 80 | raise ll_exceptions.ApiException(str(e)) 81 | except aiohttp.ClientError as e: 82 | raise ll_exceptions.NetworkException(str(e)) 83 | 84 | return resp_dict # Returns a json style object of the response. 85 | 86 | # For lru_cache. We're not hashing the sess because it doesn't affect responses 87 | def __hash__(self): 88 | return hash((self.url, self.mode)) 89 | -------------------------------------------------------------------------------- /launchlibrary/utils.py: -------------------------------------------------------------------------------- 1 | # Copyright 2020 Nir Harel 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | """Contains simple utility functions for use within the wrapper.""" 16 | 17 | ILLEGAL_CHARS = '&=/\\' 18 | 19 | 20 | def sanitize_input(args: dict) -> dict: 21 | """ 22 | Gets a dictionary for url params and makes sure it doesn't contain any illegal keywords. 23 | :param args: 24 | :return: 25 | """ 26 | if "mode" in args: 27 | del args["mode"] # the mode should always be detailed 28 | 29 | trans = str.maketrans(ILLEGAL_CHARS, ' ' * len(ILLEGAL_CHARS)) 30 | 31 | for k, v in args.copy().items(): 32 | if isinstance(v, str): # we only need to verify v because k will never be entered by a user 33 | args[k] = v.translate(trans) 34 | 35 | return args 36 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | requests 2 | python-dateutil 3 | aiohttp 4 | unidecode 5 | async_lru -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | import setuptools 2 | 3 | with open("README.md", "r") as fh: 4 | long_description = fh.read() 5 | 6 | # python setup.py sdist 7 | # twine upload dist/* 8 | 9 | setuptools.setup( 10 | name="python-launch-library", 11 | version="2.0.3", 12 | author="Nir Harel", 13 | author_email="nir@nirharel.dev", 14 | description="A wrapper for the launchlibrary.net API", 15 | long_description=long_description, 16 | long_description_content_type="text/markdown", 17 | url="https://github.com/Plutoberth/python-launch-library", 18 | packages=setuptools.find_packages(exclude=".gitignore"), 19 | classifiers=[ 20 | "Programming Language :: Python :: 3", 21 | "License :: OSI Approved :: Apache Software License", 22 | "Operating System :: OS Independent", 23 | "Development Status :: 4 - Beta", 24 | "Programming Language :: Python :: 3.6", 25 | "Programming Language :: Python :: 3.7", 26 | "Programming Language :: Python :: 3.8" 27 | ], 28 | install_requires=[ 29 | "requests>=2.22,<3", 30 | "python-dateutil>=2.8,<3", 31 | "aiohttp>=3.6,<4", 32 | "unidecode>=1,<2", 33 | "async_lru>=1.0.2,<2" 34 | ], 35 | python_requires='>=3.6' 36 | ) 37 | --------------------------------------------------------------------------------