├── .circleci └── config.yml ├── .github ├── ISSUE_TEMPLATE │ ├── bug_report.md │ └── feature_request.md └── PULL_REQUEST_TEMPLATE.md ├── .gitignore ├── CHANGELOG.md ├── CODE_OF_CONDUCT.md ├── LICENSE.txt ├── MANIFEST.in ├── README.md ├── __init__.py ├── setup.py ├── tests ├── __init__.py ├── abstracts │ ├── __init__.py │ ├── test_job.py │ ├── test_task.py │ └── test_workflow.py ├── conftest.py ├── fixtures │ ├── __init__.py │ ├── fixture_builder.py │ ├── fixture_client.py │ ├── fixture_engine.py │ ├── fixture_event.py │ ├── fixture_properties.py │ ├── fixture_serializer.py │ ├── fixture_task.py │ ├── fixture_wait.py │ ├── fixture_workflow.py │ └── fixtures.py ├── query │ ├── __init__.py │ └── test_builder.py ├── requirements-test.txt.py ├── services │ ├── __init__.py │ ├── test_properties.py │ └── test_serializer.py ├── tasks │ ├── __init__.py │ └── test_wait.py ├── test_client.py ├── test_engine.py ├── test_singleton.py ├── test_versioning.py ├── traits │ ├── __init__.py │ ├── test_with_duration.py │ └── test_with_timestamp.py └── utils.py └── zenaton ├── .idea ├── core.iml ├── misc.xml ├── modules.xml └── workspace.xml ├── __init__.py ├── abstracts ├── __init__.py ├── event.py ├── job.py ├── task.py └── workflow.py ├── client.py ├── contexts ├── __init__.py ├── task_context.py └── workflow_context.py ├── engine.py ├── exceptions.py ├── parallel.py ├── processor.py ├── query ├── __init__.py └── builder.py ├── services ├── __init__.py ├── graphql_service.py ├── http_service.py ├── properties.py └── serializer.py ├── singleton.py ├── tasks ├── __init__.py └── wait.py ├── traits ├── __init__.py ├── with_duration.py ├── with_timestamp.py └── zenatonable.py └── workflows ├── __init__.py └── version.py /.circleci/config.yml: -------------------------------------------------------------------------------- 1 | version: 2.1 2 | jobs: 3 | build: 4 | docker: 5 | - image: python:3 6 | steps: 7 | - checkout 8 | - run: 9 | name: Install dependencies 10 | command: pip install .[test] 11 | - run: 12 | name: Run tests 13 | command: pytest 14 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | 5 | --- 6 | 7 | **Describe the bug** 8 | A clear and concise description of what the bug is. 9 | 10 | **To Reproduce** 11 | Steps to reproduce the behavior: 12 | 1. Go to '...' 13 | 2. Click on '....' 14 | 3. Scroll down to '....' 15 | 4. See error 16 | 17 | **Expected behavior** 18 | A clear and concise description of what you expected to happen. 19 | 20 | **Screenshots** 21 | If applicable, add screenshots to help explain your problem. 22 | 23 | **Desktop (please complete the following information):** 24 | - OS: [e.g. iOS] 25 | - Browser [e.g. chrome, safari] 26 | - Version [e.g. 22] 27 | 28 | **Additional context** 29 | Add any other context about the problem here. 30 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | 5 | --- 6 | 7 | **Is your feature request related to a problem? Please describe.** 8 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 9 | 10 | **Describe the solution you'd like** 11 | A clear and concise description of what you want to happen. 12 | 13 | **Describe alternatives you've considered** 14 | A clear and concise description of any alternative solutions or features you've considered. 15 | 16 | **Additional context** 17 | Add any other context or screenshots about the feature request here. 18 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | ### Summary 2 | 3 | 4 | 5 | ### Safety check 6 | 7 | | Q | A 8 | | ------------------------- | --- 9 | | Type | bugfix/enhancement 10 | | Related issues | #... 11 | | Code covered with tests? | yes/no 12 | | Changelog updated? | yes/no 13 | | Documentation updated? | yes/no 14 | 15 | ### Screenshots / Video 16 | 17 | 18 | 19 | ### Deployment instructions 20 | 21 | 29 | 30 | ### Implementation details 31 | 32 | 36 | -------------------------------------------------------------------------------- /.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 | .nox/ 42 | .coverage 43 | .coverage.* 44 | .cache 45 | nosetests.xml 46 | coverage.xml 47 | *.cover 48 | .hypothesis/ 49 | .pytest_cache/ 50 | 51 | # Translations 52 | *.mo 53 | *.pot 54 | 55 | # Django stuff: 56 | *.log 57 | local_settings.py 58 | db.sqlite3 59 | 60 | # Flask stuff: 61 | instance/ 62 | .webassets-cache 63 | 64 | # Scrapy stuff: 65 | .scrapy 66 | 67 | # Sphinx documentation 68 | docs/_build/ 69 | 70 | # PyBuilder 71 | target/ 72 | 73 | # Jupyter Notebook 74 | .ipynb_checkpoints 75 | 76 | # IPython 77 | profile_default/ 78 | ipython_config.py 79 | 80 | # pyenv 81 | .python-version 82 | 83 | # celery beat schedule file 84 | celerybeat-schedule 85 | 86 | # SageMath parsed files 87 | *.sage.py 88 | 89 | # Environments 90 | .env 91 | .venv 92 | env/ 93 | venv/ 94 | ENV/ 95 | env.bak/ 96 | venv.bak/ 97 | 98 | # Spyder project settings 99 | .spyderproject 100 | .spyproject 101 | 102 | # Rope project settings 103 | .ropeproject 104 | 105 | # mkdocs documentation 106 | /site 107 | 108 | # mypy 109 | .mypy_cache/ 110 | .dmypy.json 111 | dmypy.json 112 | 113 | .idea 114 | 115 | *.pyc 116 | 117 | *.out 118 | *.err 119 | *.log 120 | 121 | 122 | 123 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## [0.4.2] - 2019-10-03 4 | 5 | ### Added 6 | 7 | - Added `custom_id` argument for workflow schedule. 8 | - Dispatch of tasks and workflows are now done using the API instead of a local agent. 9 | - Pause, Resume and Kill workflows are now done using the API instead of a local agent. 10 | - Send event to workflow is now done using the API instead of a local agent. 11 | - Find workflow is now done using the API instead of a local agent. 12 | 13 | ## [0.4.1] - 2019-09-25 14 | 15 | ### Added 16 | 17 | - Added a `intent_id` property when dispatching workflows and tasks, sending events to workflows, and 18 | pausing/resuming/killing workflows. 19 | - Execution context for tasks and workflows 20 | - Optional `on_error_retry_delay` method handling task failures and specifying 21 | how many seconds to wait before retrying. 22 | 23 | ## [0.4.0] - 2019-08-26 24 | 25 | ### Added 26 | 27 | - Added a `intent_id` property when dispatching workflows and tasks, sending events to workflows, and 28 | pausing/resuming/killing workflows. 29 | 30 | - Added scheduling: `schedule(cron)` 31 | 32 | ## [0.3.4] - 2019-07-01 33 | 34 | ### Added 35 | 36 | - Run tests in a continuous integration flow. 37 | - No need for credentials when this lib is running in a Zenaton agent except if dispatching a 38 | sub-job. 39 | 40 | ## [0.3.3] - 2019-06-25 41 | 42 | ## Fixed 43 | 44 | - Fix a typo in client.py that prevents correct executions of versions 45 | 46 | ## [0.3.2] - 2019-06-21 47 | 48 | ## Fixed 49 | 50 | - Calling `day_of_month` on a wait task now waits for to wait for the next day having the requested day number, even if that means waiting for next month. (i.e calling Wait().day_of_month(31) on February, 2nd will wait for March, 31st) 51 | - Fixed Wait task behavior in some edge cases 52 | - Encodes HTTP params before sending request 53 | 54 | ### Added 55 | 56 | - Added `event_data` property when sending event. 57 | 58 | ## [0.3.1] - 2019-04-26 59 | 60 | ## Fixed 61 | 62 | - Fixed `MANIFEST.in` file not included files required by `setup.py`. 63 | 64 | ## [0.3.0] - 2019-03-25 65 | 66 | ### Added 67 | 68 | - Calling `dispatch` on tasks now allows to process tasks asynchronously 69 | 70 | ### Fixed 71 | 72 | Fixed Wait task behavior in some edge cases 73 | Encodes HTTP params before sending request 74 | 75 | ## [0.2.5] - 2018/10/17 76 | 77 | Object Serialization (including circular structures) 78 | 79 | ## [0.2.4] - 2018/09/26 80 | 81 | Enhanced WithDuration & WithTimestamp classes 82 | 83 | ## [0.2.3] - 2018/09/21 84 | 85 | Minor enhancements (including the workflow find() method) 86 | 87 | ## [0.2.2] - 2018/09/19 88 | 89 | New version scheme management 90 | 91 | ## [0.2.1] - 2018/09/17 92 | 93 | Reorganized modules 94 | 95 | ## [0.2.0] - 2018/09/14 96 | 97 | Full rewriting of the package 98 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as 6 | contributors and maintainers pledge to making participation in our project and 7 | our community a harassment-free experience for everyone, regardless of age, body 8 | size, disability, ethnicity, gender identity and expression, level of experience, 9 | nationality, personal appearance, race, religion, or sexual identity and 10 | orientation. 11 | 12 | ## Our Standards 13 | 14 | Examples of behavior that contributes to creating a positive environment 15 | include: 16 | 17 | * Using welcoming and inclusive language 18 | * Being respectful of differing viewpoints and experiences 19 | * Gracefully accepting constructive criticism 20 | * Focusing on what is best for the community 21 | * Showing empathy towards other community members 22 | 23 | Examples of unacceptable behavior by participants include: 24 | 25 | * The use of sexualized language or imagery and unwelcome sexual attention or 26 | advances 27 | * Trolling, insulting/derogatory comments, and personal or political attacks 28 | * Public or private harassment 29 | * Publishing others' private information, such as a physical or electronic 30 | address, without explicit permission 31 | * Other conduct which could reasonably be considered inappropriate in a 32 | professional setting 33 | 34 | ## Our Responsibilities 35 | 36 | Project maintainers are responsible for clarifying the standards of acceptable 37 | behavior and are expected to take appropriate and fair corrective action in 38 | response to any instances of unacceptable behavior. 39 | 40 | Project maintainers have the right and responsibility to remove, edit, or 41 | reject comments, commits, code, wiki edits, issues, and other contributions 42 | that are not aligned to this Code of Conduct, or to ban temporarily or 43 | permanently any contributor for other behaviors that they deem inappropriate, 44 | threatening, offensive, or harmful. 45 | 46 | ## Scope 47 | 48 | This Code of Conduct applies both within project spaces and in public spaces 49 | when an individual is representing the project or its community. Examples of 50 | representing a project or community include using an official project e-mail 51 | address, posting via an official social media account, or acting as an appointed 52 | representative at an online or offline event. Representation of a project may be 53 | further defined and clarified by project maintainers. 54 | 55 | ## Enforcement 56 | 57 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 58 | reported by contacting the project team at "yann@zenaton.com". All 59 | complaints will be reviewed and investigated and will result in a response that 60 | is deemed necessary and appropriate to the circumstances. The project team is 61 | obligated to maintain confidentiality with regard to the reporter of an incident. 62 | Further details of specific enforcement policies may be posted separately. 63 | 64 | Project maintainers who do not follow or enforce the Code of Conduct in good 65 | faith may face temporary or permanent repercussions as determined by other 66 | members of the project's leadership. 67 | 68 | ## Attribution 69 | 70 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, 71 | available at [http://contributor-covenant.org/version/1/4][version] 72 | 73 | [homepage]: http://contributor-covenant.org 74 | [version]: http://contributor-covenant.org/version/1/4/ -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2018 Zenaton 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include *.txt README.md CHANGELOG.md 2 | recursive-include docs *.txt 3 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | > ⚠️ This repository is abandoned. 2 | 3 |

4 | 5 | 6 |
7 | Easy Asynchronous Jobs Manager for Developers
8 | 9 | Explore the docs » 10 |
11 | Website 12 | · 13 | Examples in Python 14 | · 15 | Tutorial in Python 16 |

17 | 18 | 19 | # Zenaton library for Python 20 | 21 | [Zenaton](https://zenaton.com) helps developers to easily run, monitor and orchestrate background jobs on your workers without managing a queuing system. In addition to this, a monitoring dashboard shows you in real-time tasks executions and helps you to handle errors. 22 | 23 | The Zenaton library for Python lets you code and launch tasks using Zenaton platform, as well as write workflows as code. You can sign up for an account on [Zenaton](https://zenaton.com) and go through the [tutorial in python](https://app.zenaton.com/tutorial/python). 24 | 25 | ## Requirements 26 | 27 | This package has been tested with Python 3.5. 28 | 29 | ## Python Documentation 30 | 31 | You can find all details on [Zenaton's website](https://zenaton.com/documentation/python/getting-started). 32 | 33 |
34 | Table of contents 35 | 36 | 37 | 38 | 39 | - [Zenaton library for Python](#zenaton-library-for-python) 40 | - [Requirements](#requirements) 41 | - [Python Documentation](#python-documentation) 42 | - [Getting started](#getting-started) 43 | - [Installation](#installation) 44 | - [Install the Zenaton Agent](#install-the-zenaton-agent) 45 | - [Install the library](#install-the-library) 46 | - [Framework integration](#framework-integration) 47 | - [Quick start](#quick-start) 48 | - [Client Initialization](#client-initialization) 49 | - [Executing a background job](#executing-a-background-job) 50 | - [Orchestrating background jobs](#orchestrating-background-jobs) 51 | - [Using workflows](#using-workflows) 52 | - [Getting help](#getting-help) 53 | - [Theorical Examples](#theorical-examples) 54 | - [Real-life Examples](#real-life-examples) 55 | - [Contributing](#contributing) 56 | - [License](#license) 57 | - [Code of Conduct](#code-of-conduct) 58 | 59 | 60 | 61 |
62 | 63 | ## Getting started 64 | 65 | ### Installation 66 | 67 | #### Install the Zenaton Agent 68 | 69 | To install the Zenaton agent, run the following command: 70 | 71 | ```sh 72 | curl https://install.zenaton.com/ | sh 73 | ``` 74 | 75 | Then, you need your agent to listen to your application. 76 | To do this, you need your **Application ID** and **API Token**. 77 | You can find both on [your Zenaton account](https://app.zenaton.com/api). 78 | 79 | ```sh 80 | zenaton listen --app_id=YourApplicationId --api_token=YourApiToken --app_env=YourApplicationEnv --boot=boot.py 81 | ``` 82 | 83 | #### Install the library 84 | 85 | To add the latest version of the library to your project, run the following command: 86 | 87 | ```python 88 | pip install zenaton 89 | ``` 90 | 91 | #### Framework integration 92 | 93 | If you are using **Django**, please refer to our dedicated documentation to get started: 94 | 95 | - [Getting started with Django](https://zenaton.com/documentation/python/agents#django) 96 | 97 | 98 | ### Quick start 99 | 100 | #### Client Initialization 101 | 102 | To start, you need to initialize the client. To do this, you need your **Application ID** and **API Token**. 103 | You can find both on [your Zenaton account](https://app.zenaton.com/api). 104 | 105 | Then, initialize your Zenaton client: 106 | 107 | ```python 108 | 109 | from zenaton.client import Client 110 | 111 | Client(your_app_id, your_api_token, your_app_env) 112 | ``` 113 | 114 | #### Executing a background job 115 | 116 | A background job in Zenaton is a class implementing the `Zenaton.abstracts.task.Task` interface. 117 | 118 | Let's start by implementing a first task printing something, and returning a value: 119 | 120 | ```python 121 | import random 122 | 123 | from zenaton.abstracts.task import Task 124 | from zenaton.traits.zenatonable import Zenatonable 125 | 126 | class HelloWorldTask(Task, Zenatonable): 127 | 128 | def handle(self): 129 | print('Hello World\n') 130 | return random.randint (0, 1) 131 | ``` 132 | 133 | Now, when you want to run this task as a background job, you need to do the following: 134 | 135 | ```python 136 | HelloWorldTask().dispatch() 137 | ``` 138 | 139 | That's all you need to get started. With this, you can run many background jobs. 140 | However, the real power of Zenaton is to be able to orchestrate these jobs. The next section will introduce you to job orchestration. 141 | 142 | ### Orchestrating background jobs 143 | 144 | Job orchestration is what allows you to write complex business workflows in a simple way. 145 | You can execute jobs sequentially, in parallel, conditionally based on the result of a previous job, 146 | and you can even use loops to repeat some tasks. 147 | 148 | We wrote about some use-cases of job orchestration, you can take a look at [these articles](https://medium.com/zenaton/tagged/python) 149 | to see how people use job orchestration. 150 | 151 | #### Using workflows 152 | 153 | A workflow in Zenaton is a class implementing the `Zenaton.abstracts.workflow.Workflow` interface. 154 | 155 | We will implement a very simple workflow: 156 | 157 | First, it will execute the `HelloWorld` task. 158 | The result of the first task will be used to make a condition using an `if` statement. 159 | When the returned value will be greater than `0`, we will execute a second task named `FinalTask`. 160 | Otherwise, we won't do anything else. 161 | 162 | One important thing to remember is that your workflow implementation **must** be idempotent. 163 | You can read more about that in our [documentation](https://zenaton.com/documentation/python/workflow-basics/#implementation). 164 | 165 | The implementation looks like this: 166 | 167 | ```python 168 | from tasks.hello_world_task import HelloWorldTask 169 | from tasks.final_task import FinalTask 170 | 171 | from zenaton.abstracts.workflow import Workflow 172 | from zenaton.traits.zenatonable import Zenatonable 173 | 174 | class MyFirstWorkflow(Workflow, Zenatonable): 175 | 176 | def handle(self): 177 | 178 | n = HelloWorldTask().execute() 179 | 180 | if n > 0: 181 | FinalTask().execute() 182 | ``` 183 | 184 | Now that your workflow is implemented, you can execute it by calling the `dispatch` method: 185 | 186 | ```python 187 | MyFirstWorkflow().dispatch() 188 | ``` 189 | 190 | If you really want to run this example, you will need to implement the `FinalTask` task. 191 | 192 | There are many more features usable in workflows in order to get the orchestration done right. You can learn more 193 | in our [documentation](https://zenaton.com/documentation/python/workflow-basics/#implementation). 194 | 195 | ## Getting help 196 | 197 | **Need help**? Feel free to contact us by chat on [Zenaton](https://zenaton.com/). 198 | 199 | **Found a bug?** You can open a [GitHub issue](https://github.com/zenaton/zenaton-python/issues). 200 | 201 | ### Theorical Examples 202 | 203 | [Python examples repo](https://github.com/zenaton/examples-python) 204 | 205 | ### Real-life Examples 206 | 207 | __Triggering An Email After 3 Days of Cold Weather__ ([Medium Article](https://medium.com/zenaton/triggering-an-email-after-3-days-of-cold-weather-f7bed6f2df16), [Source Code](https://github.com/zenaton/articles-python/tree/master/triggering-an-email-after-3-days-of-cold-weather)) 208 | 209 | ## Contributing 210 | 211 | Bug reports and pull requests are welcome on GitHub [here](https://github.com/zenaton/zenaton-Python). This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the [Contributor Covenant](http://contributor-covenant.org) code of conduct. 212 | 213 | ### Testing 214 | 215 | To test your changes before sending a pull request, first install the tests requirements: 216 | 217 | ```sh 218 | pip install '.[test]' 219 | ``` 220 | 221 | Then run PyTest: 222 | 223 | ```sh 224 | pytest 225 | ``` 226 | 227 | ## License 228 | 229 | The package is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT). 230 | 231 | ## Code of Conduct 232 | 233 | Everyone interacting in the zenaton-Python project’s codebases, issue trackers, chat rooms and mailing lists is expected to follow the [code of conduct](https://github.com/zenaton/zenaton-Python/blob/master/CODE_OF_CONDUCT.md). 234 | -------------------------------------------------------------------------------- /__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zenaton/zenaton-python/fc9a4ad2e580631480c972e066b8314ca3745ea7/__init__.py -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | """ 2 | Zenaton client library 3 | """ 4 | from __future__ import absolute_import, print_function, unicode_literals 5 | 6 | import os 7 | from io import open 8 | 9 | from setuptools import find_packages, setup 10 | 11 | import zenaton 12 | 13 | 14 | def read(filename): 15 | path = os.path.join(os.path.dirname(__file__), filename) 16 | with open(path, encoding='utf-8') as f: 17 | return f.read() 18 | 19 | TEST_DEPS = [ 20 | 'freezegun', 21 | 'pytest', 22 | 'pytest_mock', 23 | 'python-dotenv', 24 | ] 25 | 26 | setup( 27 | name='zenaton', 28 | version=zenaton.__version__, 29 | author='Zenaton', 30 | author_email='yann@zenaton.com', 31 | description='Zenaton client library', 32 | long_description=read('README.md') + '\n' + read('CHANGELOG.md'), 33 | long_description_content_type='text/markdown', 34 | keywords=['workflow engine', 'workflows', 'orchestration', 'event-driven architecture', 'queuing systems', 'orchestration engine', 'background jobs', 'hosted queues', 'queues', 'jobs', 'asynchronous tasks'], 35 | url='https://zenaton.com/', 36 | license='Apache License, Version 2.0', 37 | classifiers=[ 38 | 'Development Status :: 4 - Beta', 39 | 'Intended Audience :: Developers', 40 | 'Intended Audience :: System Administrators', 41 | 'License :: OSI Approved :: Apache Software License', 42 | 'Programming Language :: Python :: 3.5', 43 | 'Topic :: Software Development :: Libraries', 44 | ], 45 | packages=find_packages(), 46 | include_package_data=True, 47 | python_requires='>=3', 48 | install_requires=[ 49 | 'requests', 50 | 'pytz' 51 | ], 52 | tests_require=TEST_DEPS, 53 | extras_require={'test': TEST_DEPS}, 54 | zip_safe=False 55 | ) 56 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zenaton/zenaton-python/fc9a4ad2e580631480c972e066b8314ca3745ea7/tests/__init__.py -------------------------------------------------------------------------------- /tests/abstracts/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zenaton/zenaton-python/fc9a4ad2e580631480c972e066b8314ca3745ea7/tests/abstracts/__init__.py -------------------------------------------------------------------------------- /tests/abstracts/test_job.py: -------------------------------------------------------------------------------- 1 | from zenaton.abstracts.job import Job 2 | 3 | 4 | def test_has_handle(): 5 | assert hasattr(Job(), 'handle') 6 | -------------------------------------------------------------------------------- /tests/abstracts/test_task.py: -------------------------------------------------------------------------------- 1 | from zenaton.abstracts.task import Task 2 | 3 | 4 | def test_has_handle(): 5 | assert hasattr(Task(), 'handle') 6 | -------------------------------------------------------------------------------- /tests/abstracts/test_workflow.py: -------------------------------------------------------------------------------- 1 | from zenaton.abstracts.workflow import Workflow 2 | 3 | 4 | def test_has_handle(): 5 | assert hasattr(Workflow(), 'handle') 6 | -------------------------------------------------------------------------------- /tests/conftest.py: -------------------------------------------------------------------------------- 1 | from .fixtures.fixtures import dummy_object 2 | from .fixtures.fixture_client import client 3 | from .fixtures.fixture_task import task0, task1, task2, task3 4 | from .fixtures.fixture_workflow import sequential_workflow, version_workflow 5 | from .fixtures.fixture_event import my_event 6 | from .fixtures.fixture_engine import engine 7 | from .fixtures.fixture_wait import wait, wait_event 8 | from .fixtures.fixture_serializer import serializer 9 | from .fixtures.fixture_properties import properties 10 | -------------------------------------------------------------------------------- /tests/fixtures/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zenaton/zenaton-python/fc9a4ad2e580631480c972e066b8314ca3745ea7/tests/fixtures/__init__.py -------------------------------------------------------------------------------- /tests/fixtures/fixture_builder.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zenaton/zenaton-python/fc9a4ad2e580631480c972e066b8314ca3745ea7/tests/fixtures/fixture_builder.py -------------------------------------------------------------------------------- /tests/fixtures/fixture_client.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | import pytest 4 | from dotenv import load_dotenv 5 | 6 | from zenaton.client import Client 7 | 8 | # LOADING CONFIG FROM .env file 9 | load_dotenv() 10 | app_id = os.getenv('ZENATON_APP_ID') 11 | api_token = os.getenv('ZENATON_API_TOKEN') 12 | app_env = os.getenv('ZENATON_APP_ENV') 13 | 14 | 15 | @pytest.fixture 16 | def client(): 17 | return Client(app_id, api_token, app_env) 18 | -------------------------------------------------------------------------------- /tests/fixtures/fixture_engine.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from zenaton.engine import Engine 4 | from zenaton.client import Client 5 | 6 | 7 | import os 8 | 9 | import pytest 10 | from dotenv import load_dotenv 11 | 12 | from zenaton.client import Client 13 | 14 | # LOADING CONFIG FROM .env file 15 | load_dotenv() 16 | app_id = os.getenv('ZENATON_APP_ID') 17 | api_token = os.getenv('ZENATON_API_TOKEN') 18 | app_env = os.getenv('ZENATON_APP_ENV') 19 | 20 | @pytest.fixture 21 | def engine(): 22 | Client(app_id, api_token, app_env) 23 | return Engine() 24 | -------------------------------------------------------------------------------- /tests/fixtures/fixture_event.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from zenaton.abstracts.event import Event 4 | 5 | 6 | class MyEvent(Event): 7 | pass 8 | 9 | 10 | @pytest.fixture 11 | def my_event(): 12 | return MyEvent() 13 | -------------------------------------------------------------------------------- /tests/fixtures/fixture_properties.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from zenaton.services.properties import Properties 4 | 5 | 6 | @pytest.fixture 7 | def properties(): 8 | return Properties() 9 | -------------------------------------------------------------------------------- /tests/fixtures/fixture_serializer.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from zenaton.services.serializer import Serializer 4 | 5 | 6 | @pytest.fixture 7 | def serializer(): 8 | return Serializer() 9 | -------------------------------------------------------------------------------- /tests/fixtures/fixture_task.py: -------------------------------------------------------------------------------- 1 | import time 2 | 3 | import pytest 4 | 5 | from zenaton.abstracts.task import Task 6 | from zenaton.traits.zenatonable import Zenatonable 7 | 8 | 9 | class Task0(Task, Zenatonable): 10 | 11 | def handle(self): 12 | print('Task 0 starts') 13 | time.sleep(3) 14 | print('Task 0 ends') 15 | return 0 16 | 17 | 18 | @pytest.fixture 19 | def task0(): 20 | return Task0() 21 | 22 | 23 | class Task1(Task, Zenatonable): 24 | 25 | def handle(self): 26 | print('Task 1 starts') 27 | time.sleep(5) 28 | print('Task 1 ends') 29 | return 1 30 | 31 | 32 | @pytest.fixture 33 | def task1(): 34 | return Task1() 35 | 36 | 37 | class Task2(Task, Zenatonable): 38 | 39 | def handle(self): 40 | print('Task 2 starts') 41 | time.sleep(7) 42 | print('Task 2 ends') 43 | return 2 44 | 45 | 46 | @pytest.fixture 47 | def task2(): 48 | return Task2() 49 | 50 | 51 | class Task3(Task, Zenatonable): 52 | 53 | def handle(self): 54 | print('Task 3 starts') 55 | time.sleep(9) 56 | print('Task 3 ends') 57 | return 3 58 | 59 | 60 | @pytest.fixture 61 | def task3(): 62 | return Task3() 63 | -------------------------------------------------------------------------------- /tests/fixtures/fixture_wait.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from zenaton.tasks.wait import Wait 4 | from .fixture_event import MyEvent 5 | 6 | 7 | @pytest.fixture 8 | def wait(): 9 | wait = Wait() 10 | wait.set_timezone('Europe/Paris') 11 | return wait 12 | 13 | 14 | @pytest.fixture 15 | def wait_event(): 16 | return Wait(MyEvent) 17 | -------------------------------------------------------------------------------- /tests/fixtures/fixture_workflow.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from .fixture_task import task0, task1, task2, task3 4 | from .fixture_event import MyEvent 5 | 6 | from zenaton.abstracts.workflow import Workflow 7 | from zenaton.traits.zenatonable import Zenatonable 8 | from zenaton.workflows.version import Version 9 | 10 | 11 | class SequentialWorkflow(Workflow, Zenatonable): 12 | 13 | def __init__(self): 14 | self.id_ = 'MyTestWorkflowId' 15 | 16 | def handle(self): 17 | 18 | a = task0.execute() 19 | 20 | if a > 0: 21 | task1.execute() 22 | else: 23 | task2.execute() 24 | 25 | task3.execute() 26 | 27 | def id(self): 28 | return self.id_ 29 | 30 | def set_id(self, id_): 31 | self.id_ = id_ 32 | 33 | def on_event(self, event): 34 | return isinstance(event, MyEvent) 35 | 36 | 37 | @pytest.fixture 38 | def sequential_workflow(): 39 | return SequentialWorkflow() 40 | 41 | 42 | class VersionWorkflow(Version): 43 | 44 | def versions(self): 45 | return [SequentialWorkflow, ] 46 | 47 | 48 | @pytest.fixture 49 | def version_workflow(): 50 | return VersionWorkflow() 51 | -------------------------------------------------------------------------------- /tests/fixtures/fixtures.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | 4 | class DummyClass: 5 | pass 6 | 7 | 8 | @pytest.fixture 9 | def dummy_object(): 10 | return DummyClass() 11 | -------------------------------------------------------------------------------- /tests/query/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zenaton/zenaton-python/fc9a4ad2e580631480c972e066b8314ca3745ea7/tests/query/__init__.py -------------------------------------------------------------------------------- /tests/query/test_builder.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zenaton/zenaton-python/fc9a4ad2e580631480c972e066b8314ca3745ea7/tests/query/test_builder.py -------------------------------------------------------------------------------- /tests/requirements-test.txt.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zenaton/zenaton-python/fc9a4ad2e580631480c972e066b8314ca3745ea7/tests/requirements-test.txt.py -------------------------------------------------------------------------------- /tests/services/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zenaton/zenaton-python/fc9a4ad2e580631480c972e066b8314ca3745ea7/tests/services/__init__.py -------------------------------------------------------------------------------- /tests/services/test_properties.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | 4 | class CustomClass(object): 5 | def __init__(self, value): 6 | self.value = value 7 | 8 | 9 | @pytest.mark.usefixtures("properties") 10 | def test_from_args(properties): 11 | props = properties.from_(CustomClass(5)) 12 | assert props == {'value': 5} 13 | 14 | 15 | @pytest.mark.usefixtures("properties") 16 | def test_from_args(properties): 17 | with pytest.raises(TypeError, match=r'Could not get properties from 5: .*'): 18 | properties.from_(5) 19 | -------------------------------------------------------------------------------- /tests/services/test_serializer.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | import json 3 | import datetime 4 | 5 | 6 | @pytest.mark.usefixtures("serializer") 7 | def test_str(serializer): 8 | str_ = 'foo bar' 9 | assert '{"d": "foo bar", "s": []}' == serializer.encode(str_) 10 | assert serializer.decode('{"d": "foo bar", "s": []}') == str_ 11 | 12 | empty_str_ = '' 13 | assert '{"d": "", "s": []}' == serializer.encode(empty_str_) 14 | assert serializer.decode('{"d": "", "s": []}') == empty_str_ 15 | 16 | 17 | @pytest.mark.usefixtures("serializer") 18 | def test_int(serializer): 19 | int_ = 10 20 | assert '{"d": 10, "s": []}' == serializer.encode(int_) 21 | assert serializer.decode('{"d": 10, "s": []}') == int_ 22 | 23 | neg_int_ = -10 24 | assert '{"d": -10, "s": []}' == serializer.encode(neg_int_) 25 | assert serializer.decode('{"d": -10, "s": []}') == neg_int_ 26 | 27 | zero_int_ = 0 28 | assert '{"d": 0, "s": []}' == serializer.encode(zero_int_) 29 | assert serializer.decode('{"d": 0, "s": []}') == zero_int_ 30 | 31 | 32 | @pytest.mark.usefixtures("serializer") 33 | def test_float(serializer): 34 | float_ = 10.0 35 | assert '{"d": 10.0, "s": []}' == serializer.encode(float_) 36 | assert serializer.decode('{"d": 10.0, "s": []}') == float_ 37 | 38 | neg_float_ = -10.0 39 | assert '{"d": -10.0, "s": []}' == serializer.encode(neg_float_) 40 | assert serializer.decode('{"d": -10.0, "s": []}') == neg_float_ 41 | 42 | zero_float_ = 0.0 43 | assert '{"d": 0.0, "s": []}' == serializer.encode(zero_float_) 44 | assert serializer.decode('{"d": 0.0, "s": []}') == zero_float_ 45 | 46 | 47 | @pytest.mark.usefixtures("serializer") 48 | def test_bool(serializer): 49 | true = True 50 | assert '{"d": true, "s": []}' == serializer.encode(true) 51 | assert serializer.decode('{"d": true, "s": []}') == true 52 | 53 | false = False 54 | assert '{"d": false, "s": []}' == serializer.encode(false) 55 | assert serializer.decode('{"d": false, "s": []}') == false 56 | 57 | 58 | @pytest.mark.usefixtures("serializer") 59 | def test_none(serializer): 60 | none = None 61 | assert '{"d": null, "s": []}' == serializer.encode(none) 62 | assert serializer.decode('{"d": null, "s": []}') == none 63 | 64 | 65 | datetime_ = datetime.datetime(2018, 10, 16, 16, 53, 29, 164069) 66 | 67 | 68 | @pytest.mark.usefixtures("serializer") 69 | def test_datetime(serializer): 70 | assert '{"o": "@zenaton#0",' \ 71 | ' "s": [{"n": "datetime",' \ 72 | ' "p": {"day": 16, "hour": 16, "microsecond": 164069, "minute": 53, "month": 10, "second": 29, "tzinfo": null, "year": 2018}}]}' == serializer.encode( 73 | datetime_) 74 | 75 | 76 | @pytest.mark.usefixtures("serializer") 77 | def test_date(serializer): 78 | date_ = datetime_.date() 79 | assert '{"o": "@zenaton#0", "s": [{"n": "date", "p": {"day": 16, "month": 10, "year": 2018}}]}' == serializer.encode( 80 | date_) 81 | 82 | 83 | @pytest.mark.usefixtures("serializer") 84 | def test_time(serializer): 85 | time_ = datetime_.time() 86 | assert '{"o": "@zenaton#0",' \ 87 | ' "s": [{"n": "time", "p": {"hour": 16, "microsecond": 164069, "minute": 53, "second": 29, "tzinfo": null}}]}' == serializer.encode( 88 | time_) 89 | 90 | 91 | @pytest.mark.usefixtures("serializer") 92 | def test_dict(serializer): 93 | dict_ = {'foo': 'bar'} 94 | assert '{"o": "@zenaton#0", "s": [{"a": {"foo": "bar"}}]}' == serializer.encode(dict_) 95 | assert serializer.decode('{"o": "@zenaton#0", "s": [{"a": {"foo": "bar"}}]}') == dict_ 96 | 97 | empty_dict_ = dict() 98 | assert '{"o": "@zenaton#0", "s": [{"a": {}}]}' == serializer.encode(empty_dict_) 99 | assert serializer.decode('{"o": "@zenaton#0", "s": [{"a": {}}]}') == empty_dict_ 100 | 101 | 102 | @pytest.mark.usefixtures("serializer") 103 | def test_list(serializer): 104 | list_ = ['foo', 'bar', 1, 10] 105 | assert '{"o": "@zenaton#0", "s": [{"a": ["foo", "bar", 1, 10]}]}' == serializer.encode(list_) 106 | assert serializer.decode('{"o": "@zenaton#0", "s": [{"a": ["foo", "bar", 1, 10]}]}') == list_ 107 | 108 | empty_list_ = [] 109 | assert '{"o": "@zenaton#0", "s": [{"a": []}]}' == serializer.encode(empty_list_) 110 | assert serializer.decode('{"o": "@zenaton#0", "s": [{"a": []}]}') == empty_list_ 111 | 112 | 113 | dict0 = {} 114 | dict1 = {'dict0': dict0} 115 | dict0['dict1'] = dict1 116 | 117 | 118 | @pytest.mark.usefixtures("serializer") 119 | def test_circular_dict(serializer): 120 | assert '{"o": "@zenaton#0",' \ 121 | ' "s": [{"a": {"dict1": "@zenaton#1"}}, {"a": {"dict0": "@zenaton#0"}}]}' == serializer.encode(dict0) 122 | decoded = serializer.decode('{"o": "@zenaton#0",' 123 | ' "s": [{"a": {"dict1": "@zenaton#1"}}, {"a": {"dict0": "@zenaton#0"}}]}') 124 | assert id(decoded) == id(decoded['dict1']['dict0']) 125 | assert id(decoded['dict1']) == id(decoded['dict1']['dict0']['dict1']) 126 | 127 | assert '{"o": "@zenaton#0",' \ 128 | ' "s": [{"a": {"dict0": "@zenaton#1"}}, {"a": {"dict1": "@zenaton#0"}}]}' == serializer.encode(dict1) 129 | decoded = serializer.decode('{"o": "@zenaton#0",' 130 | ' "s": [{"a": {"dict0": "@zenaton#1"}}, {"a": {"dict1": "@zenaton#0"}}]}') 131 | assert id(decoded) == id(decoded['dict0']['dict1']) 132 | assert id(decoded['dict0']) == id(decoded['dict0']['dict1']['dict0']) 133 | 134 | 135 | list0 = ['foo'] 136 | list1 = ['bar'] 137 | list0.append(list1) 138 | list1.append(list0) 139 | 140 | 141 | @pytest.mark.usefixtures("serializer") 142 | def test_circular_list(serializer): 143 | assert '{"o": "@zenaton#0",' \ 144 | ' "s": [{"a": ["foo", "@zenaton#1"]}, {"a": ["bar", "@zenaton#0"]}]}' == serializer.encode(list0) 145 | decoded = serializer.decode('{"o": "@zenaton#0",' 146 | ' "s": [{"a": ["foo", "@zenaton#1"]}, {"a": ["bar", "@zenaton#0"]}]}') 147 | assert id(decoded) == id(decoded[1][1]) 148 | assert id(decoded[1]) == id(decoded[1][1][1]) 149 | 150 | assert '{"o": "@zenaton#0",' \ 151 | ' "s": [{"a": ["bar", "@zenaton#1"]}, {"a": ["foo", "@zenaton#0"]}]}' == serializer.encode(list1) 152 | decoded = serializer.decode('{"o": "@zenaton#0",' 153 | ' "s": [{"a": ["bar", "@zenaton#1"]}, {"a": ["foo", "@zenaton#0"]}]}') 154 | assert id(decoded) == id(decoded[1][1]) 155 | assert id(decoded[1]) == id(decoded[1][1][1]) 156 | 157 | 158 | class Container: 159 | pass 160 | 161 | 162 | container = Container() 163 | container.dict0 = dict0 164 | container.dict1 = dict1 165 | 166 | 167 | @pytest.mark.usefixtures("serializer") 168 | def test_container(serializer): 169 | assert ('{"o": "@zenaton#0",' \ 170 | ' "s": [{"n": "Container",' \ 171 | ' "p": {"dict0": "@zenaton#2", "dict1": "@zenaton#1"}},' \ 172 | ' {"a": {"dict0": "@zenaton#2"}},' \ 173 | ' {"a": {"dict1": "@zenaton#1"}}]}' == serializer.encode(container) or 174 | '{"o": "@zenaton#0",' \ 175 | ' "s": [{"n": "Container",' \ 176 | ' "p": {"dict0": "@zenaton#1", "dict1": "@zenaton#2"}},' \ 177 | ' {"a": {"dict1": "@zenaton#2"}},' \ 178 | ' {"a": {"dict0": "@zenaton#1"}}]}' == serializer.encode(container)) 179 | 180 | 181 | container0 = Container() 182 | container1 = Container() 183 | container0.teammate = container1 184 | container1.teammate = container0 185 | 186 | 187 | @pytest.mark.usefixtures("serializer") 188 | def test_circular_container(serializer): 189 | assert '{"o": "@zenaton#0",' \ 190 | ' "s": [{"n": "Container",' \ 191 | ' "p": {"teammate": "@zenaton#1"}},' \ 192 | ' {"n": "Container",' \ 193 | ' "p": {"teammate": "@zenaton#0"}}]}' == serializer.encode(container0) 194 | -------------------------------------------------------------------------------- /tests/tasks/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zenaton/zenaton-python/fc9a4ad2e580631480c972e066b8314ca3745ea7/tests/tasks/__init__.py -------------------------------------------------------------------------------- /tests/tasks/test_wait.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from zenaton.tasks.wait import Wait 4 | from zenaton.exceptions import ExternalError 5 | from ..fixtures.fixture_event import MyEvent 6 | 7 | 8 | def test_init(): 9 | with pytest.raises(ExternalError): 10 | Wait(1) 11 | 12 | 13 | @pytest.mark.usefixtures("wait") 14 | def test_has_handle(wait): 15 | assert hasattr(wait, 'handle') 16 | 17 | 18 | @pytest.mark.usefixtures("wait", "wait_event", "my_event") 19 | def test_valid_param(wait, my_event): 20 | assert not wait.valid_param(1) 21 | assert wait.valid_param(None) 22 | assert wait.valid_param("foo") 23 | assert wait.valid_param(MyEvent) 24 | 25 | 26 | @pytest.mark.usefixtures("wait", "my_event") 27 | def test_event_class(wait, my_event): 28 | assert not wait.event_class("foo") 29 | assert wait.event_class(MyEvent) 30 | -------------------------------------------------------------------------------- /tests/test_client.py: -------------------------------------------------------------------------------- 1 | import os 2 | import pytest 3 | 4 | from zenaton.exceptions import InvalidArgumentError 5 | from zenaton.client import Client 6 | from zenaton.singleton import Singleton 7 | 8 | from .utils import validate_url 9 | 10 | 11 | @pytest.mark.usefixtures("client") 12 | def test_url_functions(client): 13 | assert validate_url(client.worker_url()) 14 | assert validate_url(client.gateway_url()) 15 | assert validate_url(client.instance_worker_url()) 16 | 17 | 18 | @pytest.mark.usefixtures("client", "sequential_workflow") 19 | def test_class_name(client, sequential_workflow): 20 | assert type(client.class_name(sequential_workflow)) == str 21 | 22 | 23 | @pytest.mark.usefixtures("client", "sequential_workflow", "version_workflow") 24 | def test_canonical_name(client, sequential_workflow, version_workflow): 25 | assert client.canonical_name(sequential_workflow) is None 26 | assert type(client.canonical_name(version_workflow)) == str 27 | 28 | 29 | @pytest.mark.usefixtures("client", "sequential_workflow") 30 | def test_parse_custom_id_from(client, sequential_workflow): 31 | assert type(client.parse_custom_id_from(sequential_workflow)) == str 32 | sequential_workflow.set_id(0) 33 | assert type(client.parse_custom_id_from(sequential_workflow)) == str 34 | sequential_workflow.set_id({}) 35 | with pytest.raises(InvalidArgumentError): 36 | client.parse_custom_id_from(sequential_workflow) 37 | with pytest.raises(InvalidArgumentError): 38 | sequential_workflow.set_id('A' * (Client.MAX_ID_SIZE + 1)) 39 | client.parse_custom_id_from(sequential_workflow) 40 | 41 | 42 | @pytest.mark.usefixtures("client", "sequential_workflow") 43 | @pytest.mark.skipif(not os.getenv('ZENATON_API_TOKEN'), reason="requires an API token") 44 | def test_workflow_lifecycle(client, sequential_workflow): 45 | response = client.start_workflow(sequential_workflow) 46 | assert response['status_code'] == 201 47 | response = client.pause_workflow(type(sequential_workflow), sequential_workflow.id()) 48 | assert response['status_code'] == 200 49 | response = client.resume_workflow(type(sequential_workflow), sequential_workflow.id()) 50 | assert response['status_code'] == 200 51 | worfklow = client.find_workflow(type(sequential_workflow), sequential_workflow.id()) 52 | assert type(worfklow) == type(sequential_workflow) 53 | assert worfklow.id() == sequential_workflow.id() 54 | response = client.kill_workflow(type(sequential_workflow), sequential_workflow.id()) 55 | assert response['status_code'] == 200 56 | 57 | 58 | @pytest.mark.usefixtures("client", "sequential_workflow", "my_event") 59 | @pytest.mark.skipif(not os.getenv('ZENATON_API_TOKEN'), reason="requires an API token") 60 | def test_event_workflow_lifecycle(client, sequential_workflow, my_event): 61 | sequential_workflow.set_id('MyEventWorkflow') 62 | response = client.start_workflow(sequential_workflow) 63 | assert response['status_code'] == 201 64 | response = client.send_event(type(sequential_workflow).__name__, sequential_workflow.id(), my_event) 65 | assert response['status_code'] == 200 66 | response = client.kill_workflow(type(sequential_workflow), sequential_workflow.id()) 67 | assert response['status_code'] == 200 68 | 69 | 70 | @pytest.mark.usefixtures("client") 71 | def test_url_params_encoding(client): 72 | client.app_env = 'prod' 73 | assert client.add_app_env('', 'workflow_id').endswith('workflow_id') 74 | assert client.add_app_env('', 'yann+1@zenaton.com').endswith('&yann%2B1%40zenaton.com') 75 | 76 | 77 | def test_lazy_init(): 78 | Singleton._instances.clear() 79 | 80 | # Start with a client without the API token. 81 | client = Client() 82 | # OK to contact the worker. 83 | assert validate_url(client.worker_url()) 84 | # Not OK to contac the Zenaton API. 85 | with pytest.raises(ValueError, match=r'.*API token.*'): 86 | client.website_url() 87 | 88 | # Create another client with the API token later. 89 | client2 = Client('my-app-id', 'my-api-token', 'my-app-env') 90 | # It's actually a singleton. 91 | assert client == client2 92 | assert validate_url(client2.worker_url()) 93 | website_url = client2.website_url() 94 | assert validate_url(website_url) 95 | assert 'my-api-token' in website_url 96 | -------------------------------------------------------------------------------- /tests/test_engine.py: -------------------------------------------------------------------------------- 1 | import os 2 | import pytest 3 | import pytest_mock 4 | 5 | from zenaton.exceptions import InvalidArgumentError 6 | 7 | 8 | class FakeTask: 9 | pass 10 | 11 | 12 | @pytest.mark.usefixtures("engine") 13 | def test_check_argument(engine): 14 | with pytest.raises(InvalidArgumentError): 15 | engine.execute([FakeTask()]) 16 | 17 | 18 | @pytest.mark.usefixtures("engine", "task0", "sequential_workflow") 19 | def test_valid_job(engine, sequential_workflow, task0): 20 | assert not engine.valid_job(FakeTask()) 21 | assert engine.valid_job(sequential_workflow) 22 | assert engine.valid_job(task0) 23 | 24 | 25 | @pytest.mark.usefixtures("client", "engine", "task0") 26 | @pytest.mark.skipif(not os.getenv('ZENATON_API_TOKEN'), reason="requires an API token") 27 | def test_dispatch_task(client, engine, task0, mocker): 28 | mocker.spy(client, "start_task") 29 | task0.dispatch() 30 | assert client.start_task.call_count == 1 31 | -------------------------------------------------------------------------------- /tests/test_singleton.py: -------------------------------------------------------------------------------- 1 | from zenaton.singleton import Singleton 2 | 3 | 4 | class SingleClass(metaclass=Singleton): 5 | def __init__(self): 6 | self.foo = 0 7 | 8 | 9 | def test_unicity(): 10 | SingleClass().foo = 1 11 | assert SingleClass().foo == 1 12 | -------------------------------------------------------------------------------- /tests/test_versioning.py: -------------------------------------------------------------------------------- 1 | from distutils.version import LooseVersion 2 | 3 | 4 | def test_version(): 5 | import zenaton 6 | assert zenaton.__version__ 7 | assert LooseVersion(zenaton.__version__) -------------------------------------------------------------------------------- /tests/traits/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zenaton/zenaton-python/fc9a4ad2e580631480c972e066b8314ca3745ea7/tests/traits/__init__.py -------------------------------------------------------------------------------- /tests/traits/test_with_duration.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | import datetime 3 | 4 | 5 | @pytest.mark.usefixtures("wait") 6 | def test_get_duration(wait): 7 | 8 | def assert_duration(): 9 | assert wait.get_duration() == duration 10 | 11 | assert wait.get_duration() == 0 12 | wait.seconds(20) 13 | duration = 20.0 14 | assert_duration() 15 | wait.seconds(20) 16 | duration += 20 17 | assert wait.get_duration() == duration 18 | wait.hours(10) 19 | duration += (10 * 3600) 20 | assert wait.get_duration() == duration 21 | wait.days(1) 22 | duration += (24 * 3600) 23 | assert wait.get_duration() == duration 24 | wait.weeks(1) 25 | duration += (7 * 24 * 3600) 26 | assert wait.get_duration() == duration 27 | wait.months(1) 28 | duration28 = duration + (24 * 3600 * 28) 29 | duration31 = duration + (24 * 3600 * 31) 30 | duration += (24 * 3600 * 30) 31 | assert duration28 <= wait.get_duration() <= duration31 32 | wait.years(1) 33 | duration_31_leap_year = duration31 + (24 * 3600 * 366) 34 | duration_28_short_year = duration28 + (24 * 3600 * 365) 35 | assert duration_28_short_year <= wait.get_duration() <= duration_31_leap_year 36 | 37 | 38 | @pytest.mark.usefixtures("wait") 39 | def test_time_functions(wait): 40 | for name in ['seconds', 'minutes', 'hours', 'days', 'weeks']: 41 | getattr(wait, name)(1) 42 | assert wait.buffer[name] == 1 43 | getattr(wait, name)(1) 44 | assert wait.buffer[name] == 2 45 | wait.buffer = {} 46 | wait.months(1) 47 | assert 28 <= wait.buffer['days'] <= 31 48 | wait.months(1) 49 | assert 59 <= wait.buffer['days'] <= 62 50 | wait.buffer = {} 51 | wait.years(1) 52 | assert wait.buffer['days'] in [365, 366] 53 | wait.years(1) 54 | assert wait.buffer['days'] in [730, 731] 55 | 56 | 57 | @pytest.mark.usefixtures("wait") 58 | def test_add_months(wait): 59 | days = wait.months_to_days(1) 60 | assert 28 <= days <= 31 61 | days = wait.months_to_days(2) 62 | assert 56 <= days <= 62 63 | assert wait.months_to_days(12) in [365, 366] 64 | 65 | 66 | @pytest.mark.usefixtures("wait") 67 | def test_add_years(wait): 68 | days = wait.years_to_days(1) 69 | assert days in [365, 366] 70 | 71 | 72 | @pytest.mark.usefixtures("wait") 73 | def test_init_now_then(wait): 74 | now, now_dup = wait._WithDuration__init_now_then() 75 | assert type(now) is datetime.datetime 76 | assert type(now_dup) is datetime.datetime 77 | 78 | 79 | @pytest.mark.usefixtures("wait") 80 | def test_push(wait): 81 | wait.buffer = {} 82 | wait.seconds(10) 83 | assert wait.buffer['seconds'] == 10 84 | wait.seconds(10) 85 | assert wait.buffer['seconds'] == 20 86 | wait.hours(20) 87 | assert wait.buffer['hours'] == 20 88 | 89 | 90 | @pytest.mark.usefixtures("wait") 91 | def test_apply_duration(wait): 92 | time = datetime.datetime.now() 93 | assert (time + datetime.timedelta(seconds=10)) == wait._WithDuration__apply_duration('seconds', 10, time) 94 | 95 | 96 | @pytest.mark.usefixtures("wait") 97 | def test_diff_in_seconds(wait): 98 | now = datetime.datetime.now() 99 | now_plus_10 = now + datetime.timedelta(seconds=10) 100 | assert wait._WithDuration__diff_in_seconds(now, now_plus_10) == 10 101 | -------------------------------------------------------------------------------- /tests/traits/test_with_timestamp.py: -------------------------------------------------------------------------------- 1 | import copy 2 | import datetime 3 | import pytest 4 | 5 | from freezegun import freeze_time 6 | import pytz 7 | 8 | from zenaton.exceptions import ExternalError 9 | from zenaton.tasks.wait import Wait 10 | from zenaton.traits.with_timestamp import WithTimestamp 11 | 12 | 13 | @freeze_time("2017-02-03 12:00:00") 14 | @pytest.mark.usefixtures("wait") 15 | def test_get_timetamp_or_duration_day_of_month_overflow_pos(wait): 16 | now = datetime.datetime.now() 17 | with pytest.raises(ValueError): 18 | wait.day_of_month(32) 19 | wait.get_timetamp_or_duration() 20 | 21 | 22 | @freeze_time("2017-02-03 12:00:00") 23 | @pytest.mark.usefixtures("wait") 24 | def test_get_timetamp_or_duration_day_of_month_overflow_zero(wait): 25 | now = datetime.datetime.now() 26 | with pytest.raises(ValueError): 27 | wait.day_of_month(0) 28 | wait.get_timetamp_or_duration() 29 | 30 | 31 | @freeze_time("2017-02-03 12:00:00") 32 | @pytest.mark.usefixtures("wait") 33 | def test_get_timetamp_or_duration_day_of_month_overflow_neg(wait): 34 | now = datetime.datetime.now() 35 | with pytest.raises(ValueError): 36 | wait.day_of_month(-1) 37 | wait.get_timetamp_or_duration() 38 | 39 | 40 | @freeze_time("2017-02-03 12:00:00") 41 | @pytest.mark.usefixtures("wait") 42 | def test_get_timetamp_or_duration_day_of_month_end_of_month_no_skip(wait): 43 | wait.day_of_month(28) 44 | assert wait.get_timetamp_or_duration()[0] == 1488283200 # i.e: "2017-02-28 12:00:00" 45 | 46 | 47 | @freeze_time("2017-02-03 12:00:00") 48 | @pytest.mark.usefixtures("wait") 49 | def test_get_timetamp_or_duration_day_of_month_end_of_month_next(wait): 50 | wait.day_of_month(1) 51 | assert wait.get_timetamp_or_duration()[0] == 1488369600 # i.e: "2017-03-01 12:00:00" 52 | 53 | 54 | @freeze_time("2017-02-03 12:00:00") 55 | @pytest.mark.usefixtures("wait") 56 | def test_get_timetamp_or_duration_day_of_month_end_of_month_skip(wait): 57 | wait.day_of_month(31) 58 | assert wait.get_timetamp_or_duration()[0] == 1490961600 # i.e: "2017-03-31 12:00:00" 59 | 60 | 61 | @pytest.mark.usefixtures("wait") 62 | @freeze_time("2017-02-03 12:01:02") # It's a Friday. 63 | def test_get_timetamp_or_duration_next_weekday(wait): 64 | wait2 = copy.deepcopy(wait) 65 | now = datetime.datetime.now(pytz.timezone('Europe/Paris')) 66 | now_day = (now.weekday() + 1) % 7 67 | weekday = WithTimestamp.WEEKDAYS[now.weekday()] 68 | getattr(wait2, weekday)() 69 | assert wait2.get_timetamp_or_duration()[0] - int(now.timestamp()) == (7 * 24 * 60 * 60) 70 | for i in range(1, 6): 71 | new_wait = copy.deepcopy(wait) 72 | getattr(new_wait, new_wait.WEEKDAYS[now_day])(i) 73 | assert new_wait.get_timetamp_or_duration()[0] - int(now.timestamp()) == (24 * 60 * 60) * (1 + (7 * (i - 1))) 74 | 75 | 76 | @pytest.mark.usefixtures("wait") 77 | @freeze_time("2017-02-03 11:01:02") # It's a Friday, and 11:01 UTC is 12:01 in Paris that day. 78 | def test_get_timetamp_or_duration_weekday(wait): 79 | wait2 = copy.deepcopy(wait) 80 | now = datetime.datetime.now(pytz.timezone('Europe/Paris')) 81 | wait.friday().at("13:01:02") 82 | assert wait.get_timetamp_or_duration()[0] - int(now.timestamp()) == (60 * 60) 83 | wait2.friday().at("11:01:02") 84 | assert wait2.get_timetamp_or_duration()[0] - int(now.timestamp()) == (7 * 24 * 60 * 60) - (60 * 60) 85 | 86 | 87 | @pytest.mark.usefixtures("wait") 88 | @freeze_time("2017-02-03 11:01:02") 89 | def test_get_timetamp_or_duration_day_of_month(wait): 90 | wait2 = copy.deepcopy(wait) 91 | wait3 = copy.deepcopy(wait) 92 | now = datetime.datetime.now(pytz.timezone('Europe/Paris')) 93 | wait.day_of_month(now.day + 1) 94 | assert wait.get_timetamp_or_duration()[0] - int(now.timestamp()) == 24 * 60 * 60 95 | wait2.day_of_month(now.day - 1) 96 | wait_duration = wait2.get_timetamp_or_duration()[0] - int(now.timestamp()) 97 | assert 27 * 24 * 60 * 60 <= wait_duration <= 30 * 24 * 60 * 60 98 | wait3.day_of_month(now.day) 99 | wait_duration = wait3.get_timetamp_or_duration()[0] - int(now.timestamp()) 100 | assert 28 * 24 * 60 * 60 <= wait_duration <= 31 * 24 * 60 * 60 101 | 102 | 103 | @pytest.mark.usefixtures("wait") 104 | @freeze_time("2017-02-03 11:01:02") # 11:01 UTC is actually 12:01 Paris time that day. 105 | def test_get_timetamp_or_duration_day_of_month_at(wait): 106 | wait2 = copy.deepcopy(wait) 107 | now = datetime.datetime.now(pytz.timezone('Europe/Paris')) 108 | wait.day_of_month(3).at("13:01:02") 109 | assert wait.get_timetamp_or_duration()[0] - int(now.timestamp()) == 60 * 60 110 | wait2.day_of_month(3).at("11:01:02") 111 | assert 28 * 24 * 60 * 60 - 60 * 60 == wait2.get_timetamp_or_duration()[0] - int(now.timestamp()) 112 | 113 | 114 | @pytest.mark.usefixtures("wait") 115 | @freeze_time("2017-02-03 11:01:02") # 11:01 UTC is actually 12:01 Paris time that day. 116 | def test_get_timetamp_or_duration_at(wait): 117 | now = datetime.datetime.now(pytz.timezone('Europe/Paris')) 118 | wait.set_timezone('Europe/Paris') 119 | wait.at('13:01:02') 120 | assert wait.get_timetamp_or_duration()[0] - int(now.timestamp()) == 60 * 60 121 | 122 | 123 | @pytest.mark.usefixtures("wait") 124 | def test_get_timetamp_or_duration_timestamp(wait): 125 | now_timestamp = int(datetime.datetime.now().timestamp()) 126 | wait.timestamp(now_timestamp + 10.0) 127 | assert wait.get_timetamp_or_duration()[0] - now_timestamp == 10 128 | 129 | 130 | @pytest.mark.usefixtures("wait") 131 | def test_get_timetamp_push(wait): 132 | for name in ['timestamp', 'at', 'day_of_month', 'monday', 'tuesday', 'wednesday', 'thursday', 'friday', 'saturday', 133 | 'sunday']: 134 | getattr(wait, name)(1) 135 | assert wait.buffer.get(name, None) 136 | 137 | 138 | @pytest.mark.usefixtures("wait") 139 | def test_set_mode(wait): 140 | wait._WithTimestamp__set_mode(wait.MODE_WEEK_DAY) 141 | assert wait.mode == wait.MODE_WEEK_DAY 142 | with pytest.raises(ExternalError): 143 | wait._WithTimestamp__set_mode(wait.MODE_WEEK_DAY) 144 | wait._WithTimestamp__set_mode(wait.MODE_MONTH_DAY) 145 | assert wait.mode == wait.MODE_AT 146 | 147 | 148 | @pytest.mark.usefixtures("wait") 149 | def test_is_timestamp_mode_set(wait): 150 | wait.mode = wait.MODE_WEEK_DAY 151 | assert not wait._WithTimestamp__is_timestamp_mode_set(wait.MODE_WEEK_DAY) 152 | assert wait._WithTimestamp__is_timestamp_mode_set(wait.MODE_TIMESTAMP) 153 | assert not wait._WithTimestamp__is_timestamp_mode_set(wait.MODE_WEEK_DAY) 154 | 155 | 156 | @pytest.mark.usefixtures("wait") 157 | def test_set_timezone(wait): 158 | wait.set_timezone('Africa/Libreville') 159 | assert wait.timezone == 'Africa/Libreville' 160 | new_wait = Wait() 161 | assert new_wait.timezone == 'Africa/Libreville' 162 | with pytest.raises(ExternalError): 163 | wait.set_timezone('Africa/Leconi') 164 | 165 | 166 | @pytest.mark.usefixtures("wait") 167 | def test_valid_timezone(wait): 168 | assert wait._WithTimestamp__is_valid_timezone('Africa/Libreville') 169 | assert not wait._WithTimestamp__is_valid_timezone('Africa/Mvengué') 170 | -------------------------------------------------------------------------------- /tests/utils.py: -------------------------------------------------------------------------------- 1 | import re 2 | 3 | 4 | def validate_url(url): 5 | regex = re.compile( 6 | r'^(?:http|ftp)s?://' # http:// or https:// 7 | r'(?:(?:[A-Z0-9](?:[A-Z0-9-]{0,61}[A-Z0-9])?\.)+(?:[A-Z]{2,6}\.?|[A-Z0-9-]{2,}\.?)|' # domain... 8 | r'localhost|' # localhost... 9 | r'\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})' # ...or ip 10 | r'(?::\d+)?' # optional port 11 | r'(?:/?|[/?]\S+)$', re.IGNORECASE) 12 | 13 | return re.match(regex, url) is not None 14 | -------------------------------------------------------------------------------- /zenaton/.idea/core.iml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 12 | -------------------------------------------------------------------------------- /zenaton/.idea/misc.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /zenaton/.idea/modules.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /zenaton/.idea/workspace.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 55 | 56 | 57 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 |