├── .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 |
10 |
11 |
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 |
6 |
7 |
8 |
9 |
10 |
11 |
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 |
51 |
52 |
53 |
54 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
62 |
63 |
64 |
65 |
66 |
67 |
68 |
69 |
70 |
71 |
72 |
73 |
74 |
75 |
76 |
77 |
78 |
79 |
80 |
81 |
82 |
83 |
84 |
85 |
86 |
87 |
88 |
89 |
90 |
91 |
92 |
93 |
94 |
95 |
96 |
97 |
98 |
99 |
100 |
101 |
102 |
103 |
104 |
105 |
106 |
107 |
108 |
109 |
110 |
111 |
112 | 1536928949454
113 |
114 |
115 | 1536928949454
116 |
117 |
118 |
119 |
120 |
121 |
122 |
123 |
124 |
125 |
126 |
127 |
128 |
129 |
130 |
131 |
132 |
133 |
134 |
135 |
136 |
137 |
138 |
139 |
140 |
141 |
142 |
143 |
144 |
145 |
146 |
147 |
148 |
149 |
150 |
151 |
152 |
153 |
154 |
155 |
156 |
157 |
158 |
159 |
160 |
161 |
162 |
163 |
164 |
165 |
166 |
167 |
168 |
169 |
170 |
171 |
172 |
173 |
174 |
175 |
176 |
177 |
178 |
179 |
180 |
181 |
182 |
183 |
184 |
185 |
186 |
187 |
188 |
189 |
190 |
--------------------------------------------------------------------------------
/zenaton/__init__.py:
--------------------------------------------------------------------------------
1 | __version__ = '0.4.2'
2 | __version_id__ = 402
3 |
--------------------------------------------------------------------------------
/zenaton/abstracts/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/zenaton/zenaton-python/fc9a4ad2e580631480c972e066b8314ca3745ea7/zenaton/abstracts/__init__.py
--------------------------------------------------------------------------------
/zenaton/abstracts/event.py:
--------------------------------------------------------------------------------
1 | """Subclass this to define your custom events"""
2 |
3 |
4 | class Event:
5 | pass
6 |
--------------------------------------------------------------------------------
/zenaton/abstracts/job.py:
--------------------------------------------------------------------------------
1 | import abc
2 |
3 |
4 | class Job:
5 |
6 | @abc.abstractmethod
7 | def handle(self):
8 | pass
9 |
--------------------------------------------------------------------------------
/zenaton/abstracts/task.py:
--------------------------------------------------------------------------------
1 | import abc
2 |
3 | from .job import Job
4 | from ..contexts.task_context import TaskContext
5 |
6 |
7 | class Task(Job):
8 |
9 | @abc.abstractmethod
10 | def handle(self):
11 | pass
12 |
13 | """
14 | (Optional) Implement this method for automatic retrial of task in case of failures.
15 |
16 | :params error Error
17 | :raises Exception when the return type is not falsy or is not positive integer.
18 | :return [int, false, None] the amount of seconds to wait
19 | before automatically retrying this task. Falsy values will avoid
20 | retrial. Other values will raise.
21 | """
22 | def on_error_retry_delay(self, error):
23 | None
24 |
25 | """
26 | :return TaskContext
27 | """
28 | def get_context(self):
29 | return self._context or TaskContext()
30 |
31 | """
32 | Sets a new context if none has been set yet.
33 | This is called from the zenaton agent and will raise if called twice.
34 |
35 | :params context TaskContext
36 | :raises Exception when the context was already set.
37 | """
38 | def set_context(self, context):
39 | if hasattr(self, '_context') and self._context != None:
40 | raise Exception('Context is already set and cannot be mutated.')
41 |
42 | self._context = context
43 |
--------------------------------------------------------------------------------
/zenaton/abstracts/workflow.py:
--------------------------------------------------------------------------------
1 | import abc
2 |
3 | from .job import Job
4 |
5 | from ..contexts.workflow_context import WorkflowContext
6 |
7 | class Workflow(Job):
8 |
9 | @abc.abstractmethod
10 | def handle(self):
11 | pass
12 |
13 | def id(self):
14 | return None
15 |
16 |
17 | """
18 | :return TaskContext
19 | """
20 | def get_context(self):
21 | return self._context or WorkflowContext()
22 |
23 | """
24 | Sets a new context if none has been set yet.
25 | This is called from the zenaton agent and will raise if called twice.
26 |
27 | :context WorkflowContext
28 | :raises Exception when the context was already set.
29 | """
30 | def set_context(self, context):
31 | if hasattr(self, '_context') and self._context != None:
32 | raise Exception('Context is already set and cannot be mutated.')
33 |
34 | self._context = context
35 |
--------------------------------------------------------------------------------
/zenaton/client.py:
--------------------------------------------------------------------------------
1 | from contextlib import contextmanager
2 | import json
3 | import os
4 | import urllib
5 | import uuid
6 |
7 | from .abstracts.workflow import Workflow
8 | from .exceptions import InvalidArgumentError, ExternalError
9 | from .services.http_service import HttpService
10 | from .services.graphql_service import GraphQLService
11 | from .services.properties import Properties
12 | from .services.serializer import Serializer
13 | from .singleton import Singleton
14 | from .workflows.version import Version
15 |
16 |
17 | class Client(metaclass=Singleton):
18 | ZENATON_API_URL = 'https://api.zenaton.com/v1' # Zenaton api url
19 | ZENATON_WORKER_URL = 'http://localhost' # Default worker url
20 | ZENATON_GATEWAY_URL = "https://gateway.zenaton.com/api"; # Zenaton gateway url
21 | DEFAULT_WORKER_PORT = 4001 # Default worker port
22 | WORKER_API_VERSION = 'v_newton' # Default worker api version
23 |
24 | MAX_ID_SIZE = 256 # Limit on length of custom ids
25 |
26 | APP_ENV = 'app_env' # Parameter name for the application environment
27 | APP_ID = 'app_id' # Parameter name for the application ID
28 | API_TOKEN = 'api_token' # Parameter name for the API token
29 |
30 | PROG = 'PYTHON' # The current programming language
31 |
32 | def __init__(self, app_id='', api_token='', app_env=''):
33 | self.app_id = app_id
34 | self.api_token = api_token
35 | self.app_env = app_env
36 | self.graphql = GraphQLService()
37 | self.serializer = Serializer()
38 | self.properties = Properties()
39 |
40 | def __lazy_init__(self, app_id, api_token, app_env):
41 | self.app_id = self.app_id or app_id
42 | self.api_token = self.api_token or api_token
43 | self.app_env = self.app_env or app_env
44 |
45 | """
46 | Gets the gateway url (GraphQL API)
47 | :returns String the gateway url
48 | """
49 | def gateway_url(self):
50 | url = os.environ.get('ZENATON_GATEWAY_URL') or self.ZENATON_GATEWAY_URL
51 | return url
52 |
53 | """
54 | Gets the url for the workers
55 | :param String resource the endpoint for the worker
56 | :param String params url encoded parameters to include in request
57 | :returns String the workers url with parameters
58 | """
59 | def worker_url(self, resource='', params=''):
60 | base_url = os.environ.get('ZENATON_WORKER_URL') or self.ZENATON_WORKER_URL
61 | port = os.environ.get('ZENATON_WORKER_PORT') or self.DEFAULT_WORKER_PORT
62 | url = '{}:{}/api/{}/{}?'.format(base_url, port, self.WORKER_API_VERSION, resource)
63 | return self.add_app_env(url, params)
64 |
65 | """
66 | Gets the url for zenaton api
67 | :param String resource the endpoint for the api
68 | :param String params url encoded parameters to include in request
69 | :returns String the api url with parameters
70 | """
71 | def website_url(self, resource='', params=''):
72 | api_url = os.environ.get('ZENATON_API_URL') or self.ZENATON_API_URL
73 | if not self.api_token:
74 | raise ValueError('Client not initialized to access website: missing an API token.')
75 | url = '{}/{}?{}={}&'.format(api_url, resource, self.API_TOKEN, self.api_token)
76 | return self.add_app_env(url, params)
77 |
78 | """
79 | Start the specified workflow
80 | :params .abstracts.workflow.Workflow flow
81 | """
82 | def start_workflow(self, flow):
83 | query = self.graphql.DISPATCH_WORKFLOW
84 | variables = {
85 | 'input': {
86 | 'intent_id': self.uuid(),
87 | 'environment_name': self.app_env,
88 | 'programming_language': self.PROG,
89 | 'custom_id': self.parse_custom_id_from(flow),
90 | 'name': self.class_name(flow),
91 | 'canonical_name': self.canonical_name(flow),
92 | 'data': self.serializer.encode(self.properties.from_(flow))
93 | }
94 | }
95 | return self.gateway_request(query, variables=variables, data_response_key="dispatchWorkflow")
96 |
97 | def start_task(self, task):
98 | query = self.graphql.DISPATCH_TASK
99 | variables = {
100 | 'input': {
101 | 'intent_id': self.uuid(),
102 | 'environment_name': self.app_env,
103 | 'programming_language': self.PROG,
104 | 'max_processing_time': task.max_processing_time() if hasattr(task, 'max_processing_time') else None,
105 | 'name': self.class_name(task),
106 | 'data': self.serializer.encode(self.properties.from_(task))
107 | }
108 | }
109 | return self.gateway_request(query, variables=variables, data_response_key="dispatchTask")
110 |
111 | def start_scheduled_workflow(self, flow, cron):
112 | query = self.graphql.CREATE_WORKFLOW_SCHEDULE
113 | variables = {
114 | 'input': {
115 | 'intent_id': self.uuid(),
116 | 'environment_name': self.app_env,
117 | 'cron': cron,
118 | 'customId': self.parse_custom_id_from(flow),
119 | 'workflowName': self.class_name(flow),
120 | 'canonicalName': self.canonical_name(flow) or self.class_name(flow),
121 | 'programmingLanguage': self.PROG,
122 | 'properties': self.serializer.encode(self.properties.from_(flow))
123 | }
124 | }
125 | return self.gateway_request(query, variables=variables, data_response_key="createWorkflowSchedule")
126 |
127 | def start_scheduled_task(self, task, cron):
128 | query = self.graphql.CREATE_TASK_SCHEDULE
129 | variables = {
130 | 'input': {
131 | 'intent_id': self.uuid(),
132 | 'environment_name': self.app_env,
133 | 'cron': cron,
134 | 'task_name': self.class_name(task),
135 | 'programming_language': self.PROG,
136 | 'properties': self.serializer.encode(self.properties.from_(task))
137 | }
138 | }
139 | return self.gateway_request(query, variables=variables, data_response_key="createTaskSchedule")
140 |
141 | """
142 | Sends an event to a workflow
143 | :param String workflow_name the class name of the workflow
144 | :param String custom_id the custom ID of the workflow (if any)
145 | :param .abstracts.Event event the event to send
146 | :returns None
147 | """
148 | def send_event(self, workflow_name, custom_id, event):
149 | query = self.graphql.SEND_EVENT
150 | variables = {
151 | 'input': {
152 | 'intent_id': self.uuid(),
153 | 'custom_id': custom_id,
154 | 'environment_name': self.app_env,
155 | 'programming_language': self.PROG,
156 | 'name': type(event).__name__,
157 | 'input': self.serializer.encode(self.properties.from_(event)),
158 | 'workflow_name': workflow_name,
159 | 'data': self.serializer.encode(event)
160 | }
161 | }
162 | return self.gateway_request(query, variables=variables, data_response_key="sendEventToWorkflowByNameAndCustomId")
163 |
164 | """
165 | Finds a workflow
166 | :param String workflow_name the class name of the workflow
167 | :param String custom_id the custom ID of the workflow (if any)
168 | :return .abstracts.workflow.Workflow
169 | """
170 |
171 | def find_workflow(self, workflow, custom_id):
172 | query = self.graphql.FIND_WORKFLOW
173 | variables = {
174 | 'custom_id': custom_id,
175 | 'environment_name': self.app_env,
176 | 'programming_language': self.PROG,
177 | 'name': workflow.__name__
178 | }
179 | res = self.gateway_request(query, variables=variables, data_response_key="findWorkflow", throw_on_error=False)
180 | errors = self.get_graphql_errors(res)
181 | if errors:
182 | if self.contains_not_found_error(errors):
183 | return None
184 | else:
185 | raise ExternalError(errors)
186 |
187 | return self.properties.object_from(
188 | workflow,
189 | self.serializer.decode(res['properties']),
190 | Workflow
191 | )
192 |
193 | """
194 | Stops a workflow
195 | :param .abstracts.workflow.Workflow workflow
196 | :param String custom_id the custom ID of the workflow, if any
197 | :returns None
198 | """
199 | def kill_workflow(self, workflow, custom_id):
200 | query = self.graphql.KILL_WORKFLOW
201 | variables = {
202 | 'input': {
203 | 'intent_id': self.uuid(),
204 | 'environment_name': self.app_env,
205 | 'programming_language': self.PROG,
206 | 'custom_id': custom_id,
207 | 'name': workflow.__name__
208 | }
209 | }
210 | return self.gateway_request(query, variables=variables, data_response_key="killWorkflow")
211 |
212 | """
213 | Pauses a workflow
214 | :param .abstracts.workflow.Workflow flow
215 | :param String custom_id the custom ID of the workflow, if any
216 | :returns None
217 | """
218 | def pause_workflow(self, workflow, custom_id):
219 | query = self.graphql.PAUSE_WORKFLOW
220 | variables = {
221 | 'input': {
222 | 'intent_id': self.uuid(),
223 | 'environment_name': self.app_env,
224 | 'programming_language': self.PROG,
225 | 'custom_id': custom_id,
226 | 'name': workflow.__name__
227 | }
228 | }
229 | return self.gateway_request(query, variables=variables, data_response_key="pauseWorkflow")
230 |
231 | """
232 | Resumes a workflow
233 | :param .abstracts.workflow.Workflow flow
234 | :param String custom_id the custom ID of the workflow, if any
235 | :returns None
236 | """
237 | def resume_workflow(self, workflow, custom_id):
238 | query = self.graphql.RESUME_WORKFLOW
239 | variables = {
240 | 'input': {
241 | 'intent_id': self.uuid(),
242 | 'environment_name': self.app_env,
243 | 'programming_language': self.PROG,
244 | 'custom_id': custom_id,
245 | 'name': workflow.__name__
246 | }
247 | }
248 | return self.gateway_request(query, variables=variables, data_response_key="resumeWorkflow")
249 |
250 | def instance_website_url(self, params=''):
251 | return self.website_url('instances', params)
252 |
253 | def instance_worker_url(self, params=''):
254 | return self.worker_url('instances', params)
255 |
256 | def add_app_env(self, url, params):
257 | app_env = '{}={}&'.format(self.APP_ENV, self.app_env) if self.app_env else ''
258 | app_id = '{}={}&'.format(self.APP_ID, self.app_id) if self.app_id else ''
259 | return '{}{}{}{}'.format(url, app_env, app_id, urllib.parse.quote_plus(params, safe='=&'))
260 |
261 | def gateway_headers(self):
262 | return {'Accept': 'application/json',
263 | 'Content-type': 'application/json',
264 | 'app-id': self.app_id,
265 | 'api-token': self.api_token}
266 |
267 | def parse_custom_id_from(self, flow):
268 | custom_id = flow.id()
269 | if custom_id is not None:
270 | if not isinstance(custom_id, str) and not isinstance(custom_id, int):
271 | raise InvalidArgumentError('Provided ID must be a string or an integer')
272 | custom_id = str(custom_id)
273 | if len(custom_id) > self.MAX_ID_SIZE:
274 | raise InvalidArgumentError('Provided Id must not exceed {} bytes'.format(self.MAX_ID_SIZE))
275 | return custom_id
276 |
277 | def canonical_name(self, flow):
278 | return type(flow).__name__ if isinstance(flow, Version) else None
279 |
280 | def class_name(self, flow):
281 | if issubclass(type(flow), Version):
282 | return type(flow.current_implementation()).__name__
283 | return type(flow).__name__
284 |
285 | def gateway_request(self, query, variables=None, data_response_key=None, throw_on_error=True):
286 | url = self.gateway_url()
287 | headers = self.gateway_headers()
288 | res = self.graphql.request(url, query, variables=variables, headers=headers)
289 |
290 | errors = self.get_graphql_errors(res)
291 | if errors:
292 | if throw_on_error:
293 | raise ExternalError(errors)
294 | else:
295 | return res
296 |
297 | if data_response_key:
298 | return res['data'][data_response_key]
299 | else:
300 | return res['data']
301 |
302 | def contains_not_found_error(self, errors):
303 | not_found_errors = list(filter(lambda error: error.get('type') == 'NOT_FOUND', errors))
304 | return len(not_found_errors) > 0
305 |
306 | def get_graphql_errors(self, response):
307 | errors = response.get('errors')
308 | if isinstance(errors, list) and len(errors) > 0:
309 | for error in errors:
310 | if 'locations' in error:
311 | del error['locations']
312 | return errors
313 |
314 | @contextmanager
315 | def _connect_to_agent(self):
316 | """Display nice error message if connection to agent fails."""
317 | try:
318 | yield
319 | except ConnectionError:
320 | url = os.environ.get('ZENATON_WORKER_URL') or self.ZENATON_WORKER_URL
321 | port = os.environ.get('ZENATON_WORKER_PORT') or self.DEFAULT_WORKER_PORT
322 | raise ConnectionError(
323 | 'Could not connect to Zenaton agent at "{}:{}", make sure it is running and '
324 | 'listening.'.format(url, port))
325 |
326 | def uuid(self):
327 | """Generate a uuidv4"""
328 | return str(uuid.uuid4())
329 |
--------------------------------------------------------------------------------
/zenaton/contexts/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/zenaton/zenaton-python/fc9a4ad2e580631480c972e066b8314ca3745ea7/zenaton/contexts/__init__.py
--------------------------------------------------------------------------------
/zenaton/contexts/task_context.py:
--------------------------------------------------------------------------------
1 | class TaskContext():
2 | """
3 | Represents the current runtime context of a Task.
4 |
5 | The information provided by the context can be useful to alter the
6 | behaviour of the task.
7 |
8 | For example, you can use the retry_index to know if a task has been
9 | automatically retried or not and how many times, and decide to do
10 | something when you did not expect the task to be retried more than X
11 | times.
12 |
13 | You can also use the retry_index in the `on_error_retry_delay` method
14 | of a task in order to implement complex retry strategies.
15 |
16 | Attributes
17 | ----------
18 | id : str
19 | The UUID identifying the current task.
20 |
21 | retry_index : int
22 | The number of times this task has been automatically retried.
23 | This counter is reset if you issue a manual retry from your dashboard
24 | """
25 | def __init__(self, **kwargs):
26 | self.id = kwargs.get('id', None)
27 | self.retry_index = kwargs.get('retry_index', None)
28 |
--------------------------------------------------------------------------------
/zenaton/contexts/workflow_context.py:
--------------------------------------------------------------------------------
1 | class WorkflowContext():
2 | """
3 | Represents the current runtime context of a Workflow.
4 |
5 | Attributes
6 | ----------
7 | id : str
8 | The UUID identifying the current workflow
9 | """
10 | def __init__(self, **kwargs):
11 | self.id = kwargs.get('id', None)
12 |
--------------------------------------------------------------------------------
/zenaton/engine.py:
--------------------------------------------------------------------------------
1 | from .abstracts.task import Task
2 | from .abstracts.workflow import Workflow
3 | from .exceptions import InvalidArgumentError
4 | from .singleton import Singleton
5 |
6 |
7 | class Engine(metaclass=Singleton):
8 | """
9 | Zenaton Engine is a singleton class that stores a reference to the current
10 | client and processor. It then handles job processing either locally or
11 | through the processor with Zenaton workers
12 | To access the instance, call `Zenaton::Engine.instance`
13 | """
14 |
15 | def __init__(self):
16 | from .client import Client
17 | self.client = Client()
18 | self.processor = None
19 |
20 | """
21 | Executes jobs synchronously
22 | @param jobs [Array]
23 | @return [Array, nil] the results if executed locally, or nil
24 | """
25 | def execute(self, jobs):
26 | for job in jobs:
27 | Engine._check_argument(job)
28 | if len(jobs) == 0 or self.processor is None:
29 | return [job.handle() for job in jobs]
30 | return self.processor.process(jobs, True)
31 |
32 | """
33 | Executes schedule jobs synchronously
34 | @param jobs [Array]
35 | @param cron String
36 | @return [Array, nil] the results if executed locally, or nil
37 | """
38 | def schedule(self, jobs, cron):
39 | for job in jobs:
40 | Engine._check_argument(job)
41 |
42 | [self.local_schedule(job, cron) for job in jobs]
43 |
44 | """
45 | Executes jobs asynchronously
46 | @param jobs [Array]
47 | @return nil
48 | """
49 | def dispatch(self, jobs):
50 | map(Engine._check_argument, jobs)
51 | if len(jobs) == 0 or self.processor is None:
52 | return [self.local_dispatch(job) for job in jobs]
53 | if self.processor and len(jobs) > 0:
54 | self.processor.process(jobs, False)
55 |
56 | def local_dispatch(self, job):
57 | if isinstance(job, Workflow):
58 | return self.client.start_workflow(job)
59 | else:
60 | return self.client.start_task(job)
61 |
62 | def local_schedule(self, job, cron):
63 | if isinstance(job, Workflow):
64 | return self.client.start_scheduled_workflow(job, cron)
65 | else:
66 | return self.client.start_scheduled_task(job, cron)
67 |
68 | @staticmethod
69 | def _check_argument(job):
70 | if not Engine.valid_job(job):
71 | raise InvalidArgumentError('You can only execute or dispatch Zenaton Task or Worflow')
72 |
73 | """
74 | Checks if the job is a valid job i.e. it is either a Task or a Workflow
75 | """
76 | @staticmethod
77 | def valid_job(job):
78 | return isinstance(job, (Task, Workflow))
79 |
--------------------------------------------------------------------------------
/zenaton/exceptions.py:
--------------------------------------------------------------------------------
1 | """Zenaton base error class"""
2 | class Error(Exception):
3 | pass
4 |
5 |
6 | """Exception raised when communication with workers failed"""
7 | class InternalError(Exception):
8 | pass
9 |
10 |
11 | """Exception raise when clien code is invalid"""
12 | class ExternalError(Exception):
13 | pass
14 |
15 |
16 | """Exception raised when wrong argument type is provided"""
17 | class InvalidArgumentError(ExternalError):
18 | pass
19 |
20 |
21 | """Exception raised when the workflow is unknown"""
22 | class UnknownWorkflowError(ExternalError):
23 | pass
24 |
--------------------------------------------------------------------------------
/zenaton/parallel.py:
--------------------------------------------------------------------------------
1 | from .engine import Engine
2 |
3 |
4 | class Parallel:
5 | """
6 | Build a collection of jobs to be executed in parallel
7 | :params: [.abstracts.job.Job] items
8 | """
9 |
10 | def __init__(self, *items):
11 | self.items = items
12 |
13 | # Execute synchronous jobs
14 | def execute(self):
15 | return Engine().execute(self.items)
16 |
17 | # Dispatches synchronous jobs
18 | def dispatch(self):
19 | return Engine().dispatch(self.items)
20 |
--------------------------------------------------------------------------------
/zenaton/processor.py:
--------------------------------------------------------------------------------
1 | class Processor:
2 |
3 | def process(self, jobs, boolean):
4 | pass
5 |
--------------------------------------------------------------------------------
/zenaton/query/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/zenaton/zenaton-python/fc9a4ad2e580631480c972e066b8314ca3745ea7/zenaton/query/__init__.py
--------------------------------------------------------------------------------
/zenaton/query/builder.py:
--------------------------------------------------------------------------------
1 | from ..abstracts.workflow import Workflow
2 | from ..exceptions import ExternalError
3 |
4 |
5 | class Builder:
6 | """Wrapper class around the client to interact with workflows by id"""
7 |
8 | def __init__(self, class_):
9 | from ..client import Client
10 | self._check_class(class_)
11 | self.class_ = class_
12 | self.client = Client()
13 |
14 | """
15 | Sets the where_id of the workflow we want to find
16 | :param String or None id the id
17 | :returns Builder the current builder
18 | """
19 | def where_id(self, id):
20 | self.id = id
21 | return self
22 |
23 | """
24 | Finds a workflow
25 | returns Workflow
26 | """
27 | def find(self):
28 | return self.client.find_workflow(self.class_, self.id)
29 |
30 | """
31 | Sends an event to a workflow
32 | :param abstracts.event.Event event the event to send
33 | :returns query.builder the current builder
34 | """
35 | def send_event(self, event):
36 | self.client.send_event(self.class_.__name__, self.id, event)
37 |
38 | """
39 | Kills a workflow
40 | :returns query.builder.Builder the current builder
41 | """
42 | def kill(self):
43 | self.client.kill_workflow(self.class_, self.id)
44 | return self
45 |
46 | """
47 | Pauses a workflow
48 | :returns query.builder.Builder the current builder
49 | """
50 | def pause(self):
51 | self.client.pause_workflow(self.class_, self.id)
52 | return self
53 |
54 | """
55 | Resumes a workflow
56 | :returns query.builder.Builder the current builder
57 | """
58 | def resume(self):
59 | self.client.resume_workflow(self.class_, self.id)
60 | return self
61 |
62 | """
63 | Checks if class_ is subclass of Workflow
64 | :param class class_
65 | """
66 |
67 | def _check_class(self, class_):
68 | msg = '{} should be a subclass of .abstracts.workflow'.format(class_)
69 | if not issubclass(class_, Workflow):
70 | raise ExternalError(msg)
71 |
--------------------------------------------------------------------------------
/zenaton/services/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/zenaton/zenaton-python/fc9a4ad2e580631480c972e066b8314ca3745ea7/zenaton/services/__init__.py
--------------------------------------------------------------------------------
/zenaton/services/graphql_service.py:
--------------------------------------------------------------------------------
1 | from ..exceptions import InternalError
2 | import json
3 | import requests
4 |
5 |
6 | class GraphQLService:
7 |
8 | # Queries
9 | FIND_WORKFLOW = """
10 | query ($custom_id: String!, $environment_name: String!, $programming_language: ProgrammingLanguage!, $name: String!) {
11 | findWorkflow(custom_id: $custom_id, environment_name: $environment_name, programming_language: $programming_language, name: $name) {
12 | id
13 | name
14 | properties
15 | }
16 | }
17 | """
18 |
19 | # Mutations
20 | CREATE_WORKFLOW_SCHEDULE = """
21 | mutation ($input: CreateWorkflowScheduleInput!) {
22 | createWorkflowSchedule(input: $input) {
23 | schedule {
24 | id
25 | name
26 | cron
27 | insertedAt
28 | updatedAt
29 | target {
30 | ... on WorkflowTarget {
31 | name
32 | type
33 | canonicalName
34 | programmingLanguage
35 | properties
36 | }
37 | }
38 | }
39 | }
40 | }
41 | """
42 |
43 | CREATE_TASK_SCHEDULE = """
44 | mutation ($input: CreateTaskScheduleInput!) {
45 | createTaskSchedule(input: $input) {
46 | schedule {
47 | id
48 | name
49 | cron
50 | insertedAt
51 | updatedAt
52 | target {
53 | ... on TaskTarget {
54 | name
55 | type
56 | programmingLanguage
57 | properties
58 | }
59 | }
60 | }
61 | }
62 | }
63 | """
64 |
65 | DISPATCH_TASK = """
66 | mutation dispatchTask($input: DispatchTaskInput!) {
67 | dispatchTask(input: $input) {
68 | task {
69 | intentId
70 | }
71 | }
72 | }
73 | """
74 |
75 | DISPATCH_WORKFLOW = """
76 | mutation dispatchWorkflow($input: DispatchWorkflowInput!) {
77 | dispatchWorkflow(input: $input) {
78 | workflow {
79 | id
80 | }
81 | }
82 | }
83 | """
84 |
85 | KILL_WORKFLOW = """
86 | mutation killWorkflow($input: KillWorkflowInput!) {
87 | killWorkflow(input: $input) {
88 | id
89 | }
90 | }
91 | """
92 |
93 | PAUSE_WORKFLOW = """
94 | mutation pauseWorkflow($input: PauseWorkflowInput!) {
95 | pauseWorkflow(input: $input) {
96 | id
97 | }
98 | }
99 | """
100 |
101 | RESUME_WORKFLOW = """
102 | mutation resumeWorkflow($input: ResumeWorkflowInput!) {
103 | resumeWorkflow(input: $input) {
104 | id
105 | }
106 | }
107 | """
108 |
109 | SEND_EVENT = """
110 | mutation sendEventToWorkflowByNameAndCustomId($input: SendEventToWorkflowByNameAndCustomIdInput!) {
111 | sendEventToWorkflowByNameAndCustomId(input: $input) {
112 | event {
113 | intentId
114 | }
115 | }
116 | }
117 | """
118 |
119 | def request(self, url, query, variables=None, headers={}):
120 | try:
121 | data = {'query': query}
122 | if variables:
123 | data['variables'] = variables
124 | r = requests.request(method='POST', url=url,
125 | headers=headers, data=json.dumps(data))
126 | if r.status_code >= 400:
127 | raise InternalError(r.content)
128 | content = r.json()
129 |
130 | except json.decoder.JSONDecodeError:
131 | raise InternalError
132 | except requests.exceptions.ConnectionError:
133 | raise ConnectionError
134 | return content
135 |
--------------------------------------------------------------------------------
/zenaton/services/http_service.py:
--------------------------------------------------------------------------------
1 | import json
2 |
3 | import requests
4 |
5 | from ..exceptions import InternalError
6 |
7 |
8 | class HttpService:
9 | """Http Call Header"""
10 | HEADER = {'Accept': 'application/json', 'Content-type': 'application/json'}
11 |
12 | """
13 | Generic Http request function
14 |
15 | :param method: 'GET', 'POST' or 'PUT'
16 | :param url: the requested URL
17 | :param headers: the headers
18 | data the request boy, if needed
19 |
20 | :raises InternalError: if status code > 400 or JSON parsing error
21 | :raises ConnectionError: if connection fails
22 |
23 | :returns: the request's response content
24 | """
25 | def request(self, method, url, headers, data=None):
26 | if False:
27 | print('\n')
28 | print(method)
29 | print(headers)
30 | print(url)
31 | print(json.dumps(data))
32 | try:
33 | r = requests.request(method=method, url=url, headers=headers, data=data)
34 | if r.status_code >= 400:
35 | raise InternalError(r.content)
36 | content = r.json()
37 | content['status_code'] = r.status_code
38 | except json.decoder.JSONDecodeError:
39 | raise InternalError
40 | except requests.exceptions.ConnectionError:
41 | raise ConnectionError
42 | return content
43 |
44 | """GET function, calls with the right arguments request()"""
45 | def get(self, url):
46 | return self.request(method='GET', url=url, headers=self.HEADER)
47 |
48 | """POST function, calls with the right arguments request()"""
49 | def post(self, url, data=None):
50 | return self.request(method='POST', url=url, headers=self.HEADER, data=data)
51 |
52 | """PUT function, calls with the right arguments request()"""
53 | def put(self, url, data=None):
54 | return self.request(method='PUT', url=url, headers=self.HEADER, data=data)
--------------------------------------------------------------------------------
/zenaton/services/properties.py:
--------------------------------------------------------------------------------
1 | import json
2 | import datetime
3 |
4 |
5 | class Properties:
6 | SPECIAL_CASES = [
7 | Exception,
8 | datetime.datetime,
9 | datetime.date,
10 | datetime.time,
11 | ]
12 |
13 | def blank_instance(self, class_):
14 |
15 | if isinstance(class_, type(None)) or class_ == 'NoneType':
16 | return None
17 |
18 | class Empty:
19 | pass
20 |
21 | output = Empty()
22 | output.__class__ = class_
23 | return output
24 |
25 | def from_(self, object_):
26 | if not object_:
27 | return None
28 | if self.is_special_case(object_):
29 | return self.from_complex_type(object_)
30 | else:
31 | if hasattr(object_, 'buffer'):
32 | return object_.buffer
33 | if hasattr(object_, 'args'):
34 | return object_.args
35 | try:
36 | return vars(object_)
37 | except TypeError as error:
38 | raise TypeError(
39 | 'Could not get properties from {}: {}'.format(object_, error)) from error
40 |
41 | def set(self, object_, properties):
42 | if properties != (None,) and properties is not None:
43 | for key, value in properties.items():
44 | setattr(object_, key, value)
45 | return object_
46 |
47 | def object_from(self, class_, properties, super_class=None):
48 | object_ = self.blank_instance(class_)
49 | self._check_class(object_, super_class)
50 | self.set(object_, properties)
51 | return object_
52 |
53 | def _check_class(self, object_, super_class):
54 | error_message = 'Error - #{object.class} should be an instance of #{super_class}'
55 | if not isinstance(object_, super_class):
56 | raise Exception(error_message)
57 |
58 | def valid_object(self, object_, super_class):
59 | return not super_class or issubclass(object_, super_class)
60 |
61 | def from_complex_type(self, object_):
62 | if isinstance(object_, datetime.datetime):
63 | return {'year': object_.year, 'month': object_.month, 'day': object_.day, 'hour': object_.hour,
64 | 'minute': object_.minute, 'second': object_.second, 'microsecond': object_.microsecond,
65 | 'tzinfo': object_.tzinfo}
66 | if isinstance(object_, datetime.date):
67 | return {'year': object_.year, 'month': object_.month, 'day': object_.day}
68 | if isinstance(object_, datetime.time):
69 | return {'hour': object_.hour, 'minute': object_.minute, 'second': object_.second,
70 | 'microsecond': object_.microsecond, 'tzinfo': object_.tzinfo}
71 | if isinstance(object_, BaseException):
72 | return {'error_class': object_.__class__.__name__, 'error_args': list(object_.args)}
73 | return json.dumps(object_)
74 |
75 | def set_complex_type(self, object_):
76 | return json.loads(object_)
77 |
78 | def is_special_case(self, object_):
79 | return type(object_) in self.SPECIAL_CASES or isinstance(object_, BaseException)
80 |
--------------------------------------------------------------------------------
/zenaton/services/serializer.py:
--------------------------------------------------------------------------------
1 | import copy
2 | import json
3 | import inspect
4 | import importlib.util
5 |
6 | from zenaton.services.properties import Properties
7 | from zenaton.exceptions import InvalidArgumentError
8 |
9 |
10 | class Serializer:
11 | # this string prefixes ids that are used to identify objects
12 | ID_PREFIX = '@zenaton#'
13 |
14 | KEY_OBJECT = 'o' # JSON key for objects
15 | KEY_OBJECT_NAME = 'n' # JSON key for class name
16 | KEY_OBJECT_PROPERTIES = 'p' # JSON key for object ivars
17 | KEY_ARRAY = 'a' # JSON key for array and hashes
18 | KEY_DATA = 'd' # JSON key for json compatibles types
19 | KEY_STORE = 's' # JSON key for deserialized complex object
20 |
21 | def __init__(self, boot=None, name=None):
22 | self.properties = Properties()
23 | self._encoded = []
24 | self._decoded = []
25 | self.boot = boot
26 | self.name = name
27 |
28 | def encode(self, data):
29 | self._encoded = []
30 | self._decoded = []
31 | value = dict()
32 |
33 | if isinstance(data, dict) and data.get('_context'):
34 | del data['_context']
35 |
36 | if self.__is_basic_type(data):
37 | value[self.KEY_DATA] = data
38 | else:
39 | value[self.KEY_OBJECT] = self.__encode_to_store(data)
40 | value[self.KEY_STORE] = self._encoded
41 | return json.dumps(value, sort_keys=True)
42 |
43 | def decode(self, json_string):
44 | parsed_json = json.loads(json_string)
45 | encoded_json = copy.deepcopy(parsed_json)
46 | self._encoded = encoded_json[self.KEY_STORE]
47 | del encoded_json[self.KEY_STORE]
48 | self._decoded = []
49 | if self.KEY_DATA in parsed_json:
50 | return parsed_json[self.KEY_DATA]
51 | if self.KEY_ARRAY in parsed_json:
52 | return self.__decode_enumerable(parsed_json[self.KEY_ARRAY])
53 | if self.KEY_OBJECT in parsed_json:
54 | id_ = int(parsed_json[self.KEY_OBJECT][len(self.ID_PREFIX):])
55 | return self.__decode_from_store(id_, self._encoded[id_])
56 |
57 | def __encode_value(self, value):
58 | if self.__is_basic_type(value):
59 | return value
60 | else:
61 | return self.__encode_to_store(value)
62 |
63 | def __encode_to_store(self, object_):
64 |
65 | for index, element in enumerate(self._decoded):
66 | if id(element) == id(object_):
67 | return self.__store_id(index)
68 |
69 | id_ = len(self._decoded)
70 | self.insert_at_index(self._decoded, id_, object_)
71 | self.insert_at_index(self._encoded, id_, self.__encode_object_by_type(object_))
72 | return self.__store_id(id_)
73 |
74 | def insert_at_index(self, list_, index, value):
75 | try:
76 | list_[index] = value
77 | except IndexError:
78 | for i in range(0, index - len(list_) + 1):
79 | list_.append(None)
80 | list_[index] = value
81 |
82 | def append_at_index(self, list_, index, value):
83 | if len(list_[index]) >= len(value):
84 | return
85 | try:
86 | list_[index].extend(value)
87 | except (TypeError, IndexError, AttributeError):
88 | for i in range(0, index - len(list_) + 1):
89 | list_.append(None)
90 | list_[index] = value
91 |
92 | def update_at_index(self, list_, index, value):
93 | if len(list_[index]) >= len(value):
94 | return
95 | try:
96 | list_[index].update(value)
97 | except (TypeError, IndexError, AttributeError):
98 | for i in range(0, index - len(list_) + 1):
99 | list_.append(None)
100 | list_[index] = value
101 |
102 | def __encode_object_by_type(self, object_):
103 | if isinstance(object_, list):
104 | return self.__encode_list(object_)
105 | if isinstance(object_, dict):
106 | return self.__encode_dict(object_)
107 | return self.__encode_object(object_)
108 |
109 | def __encode_object(self, object_):
110 | return {
111 | self.KEY_OBJECT_NAME: type(object_).__name__,
112 | self.KEY_OBJECT_PROPERTIES: self.__encode_legacy_dict(self.properties.from_(object_))
113 | }
114 |
115 | def __encode_list(self, list_):
116 | return {
117 | self.KEY_ARRAY: [self.__encode_value(element) for element in list_]
118 | }
119 |
120 | def __encode_dict(self, dict_):
121 | return {
122 | self.KEY_ARRAY: {key: self.__encode_value(value) for key, value in dict_.items()}
123 | }
124 |
125 | def __encode_legacy_dict(self, dict_):
126 | return {key: self.__encode_value(value) for key, value in dict_.items()}
127 |
128 | def __decode_element(self, value):
129 | if self.__is_store_id(value):
130 | id_ = int(value[len(self.ID_PREFIX):])
131 | encoded = self._encoded[id_]
132 | return self.__decode_from_store(id_, encoded)
133 | elif isinstance(value, list):
134 | return self.__decode_legacy_list(value)
135 | elif isinstance(value, dict):
136 | return self.__decode_legacy_dict(value)
137 | else:
138 | return value
139 |
140 | def __decode_enumerable(self, enumerable):
141 | if isinstance(enumerable, list):
142 | return self.__decode_legacy_list(enumerable)
143 | if isinstance(enumerable, dict):
144 | return self.__decode_legacy_dict(enumerable)
145 | raise InvalidArgumentError('Unknown type')
146 |
147 | def __decode_legacy_list(self, list_):
148 | return [self.__decode_element(element) for element in list_]
149 |
150 | def __decode_legacy_dict(self, dict_):
151 | ret = {key: self.__decode_element(value) for key, value in dict_.items()}
152 | return ret
153 |
154 | def __decode_list(self, id_, list_):
155 | self.insert_at_index(self._decoded, id_, [])
156 | decoded_list = [self.__decode_element(element) for element in list_]
157 | self.append_at_index(self._decoded, id_, decoded_list)
158 | return self._decoded[id_]
159 |
160 | def __decode_dict(self, id_, dict_):
161 | self.insert_at_index(self._decoded, id_, dict())
162 | decoded_dict = {key: self.__decode_element(value) for key, value in dict_.items()}
163 | self.update_at_index(self._decoded, id_, decoded_dict)
164 | return self._decoded[id_]
165 |
166 | def __decode_from_store(self, id_, encoded):
167 | if len(self._decoded) >= id_ + 1 and self._decoded[id_] is not None:
168 | decoded = self._decoded[id_]
169 | return decoded
170 | else:
171 | encoded_value = encoded.get(self.KEY_ARRAY, None)
172 | if isinstance(encoded_value, list):
173 | return self.__decode_list(id_, encoded_value)
174 | if isinstance(encoded_value, dict):
175 | return self.__decode_dict(id_, encoded_value)
176 | return self.__decoded_object(id_, encoded)
177 |
178 | def __decoded_object(self, id_, encoded_object):
179 | if len(self._decoded) >= id_ + 1 and self._decoded[id_] is not None:
180 | return self._decoded[id_]
181 | try:
182 | object_class = self.import_class(self.name, encoded_object[self.KEY_OBJECT_NAME])
183 | object_ = self.properties.blank_instance(object_class)
184 | self.insert_at_index(self._decoded, id_, object_)
185 | properties = self.__decode_legacy_dict(encoded_object.get(self.KEY_OBJECT_PROPERTIES, None))
186 | self.properties.set(object_, properties)
187 | return object_
188 | except TypeError:
189 | properties = self.__decode_legacy_dict(encoded_object.get(self.KEY_OBJECT_PROPERTIES, None))
190 | object_ = object_class(**properties)
191 | self.insert_at_index(self._decoded, id_, object_)
192 | return object_
193 | except KeyError:
194 | return None
195 |
196 | def __is_store_id(self, string_):
197 | try:
198 | suffix = int(string_[len(self.ID_PREFIX):])
199 | except (TypeError, ValueError):
200 | return False
201 | return isinstance(string_, str) and \
202 | string_.startswith(self.ID_PREFIX) and \
203 | suffix <= len(self._encoded)
204 |
205 | def __store_id(self, id):
206 | return '{}{}'.format(self.ID_PREFIX, id)
207 |
208 | def __is_basic_type(self, data):
209 | return (isinstance(data, str) or
210 | isinstance(data, int) or
211 | isinstance(data, float) or
212 | isinstance(data, bool) or data is None)
213 |
214 | def import_class(self, workflow_name, class_name):
215 | spec = importlib.util.spec_from_file_location('boot', self.boot)
216 | boot = importlib.util.module_from_spec(spec)
217 | spec.loader.exec_module(boot)
218 | workflow_class = getattr(boot, workflow_name)
219 | workflow_module = inspect.getmodule(workflow_class)
220 | return getattr(workflow_module, class_name)
221 |
--------------------------------------------------------------------------------
/zenaton/singleton.py:
--------------------------------------------------------------------------------
1 | class Singleton(type):
2 | _instances = {}
3 |
4 | def __call__(cls, *args, **kwargs):
5 | if cls not in cls._instances:
6 | cls._instances[cls] = super(Singleton, cls).__call__(*args, **kwargs)
7 | elif (args or kwargs) and hasattr(cls._instances[cls], '__lazy_init__'):
8 | cls._instances[cls].__lazy_init__(*args, **kwargs)
9 | return cls._instances[cls]
10 |
--------------------------------------------------------------------------------
/zenaton/tasks/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/zenaton/zenaton-python/fc9a4ad2e580631480c972e066b8314ca3745ea7/zenaton/tasks/__init__.py
--------------------------------------------------------------------------------
/zenaton/tasks/wait.py:
--------------------------------------------------------------------------------
1 | import inspect
2 |
3 | from ..abstracts.event import Event
4 | from ..abstracts.task import Task
5 | from ..exceptions import ExternalError
6 | from ..traits.with_timestamp import WithTimestamp
7 | from ..traits.zenatonable import Zenatonable
8 |
9 |
10 | class Wait(Task, Zenatonable, WithTimestamp):
11 |
12 | def __init__(self, event=None):
13 | super(Wait, self).__init__()
14 | if not self.valid_param(event):
15 | raise ExternalError(self.error)
16 | if event:
17 | self.event = event.__name__
18 |
19 | @property
20 | def error(self):
21 | return '{}: Invalid parameter - argument must be a zenaton.abstracts.event.Event subclass'.format(
22 | self.__class__.__name__)
23 |
24 | def handle(self):
25 | pass
26 |
27 | def valid_param(self, event):
28 | return not event or isinstance(event, str) or self.event_class(event)
29 |
30 | def event_class(self, event):
31 | return inspect.isclass(event) and issubclass(event, Event)
32 |
--------------------------------------------------------------------------------
/zenaton/traits/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/zenaton/zenaton-python/fc9a4ad2e580631480c972e066b8314ca3745ea7/zenaton/traits/__init__.py
--------------------------------------------------------------------------------
/zenaton/traits/with_duration.py:
--------------------------------------------------------------------------------
1 | import copy
2 | import datetime
3 | import calendar
4 |
5 | import pytz
6 |
7 |
8 | class WithDuration:
9 |
10 | def get_duration(self):
11 | if not hasattr(self, 'buffer'):
12 | return 0
13 | now, now_dup = self.__init_now_then()
14 | for (time_unit, time_value) in self.buffer.items():
15 | now_dup = self.__apply_duration(time_unit, time_value, now_dup)
16 | return self.__diff_in_seconds(now, now_dup)
17 |
18 | def seconds(self, value):
19 | self.__push('seconds', value)
20 | return self
21 |
22 | def minutes(self, value):
23 | self.__push('minutes', value)
24 | return self
25 |
26 | def hours(self, value):
27 | self.__push('hours', value)
28 | return self
29 |
30 | def days(self, value):
31 | self.__push('days', value)
32 | return self
33 |
34 | def weeks(self, value):
35 | self.__push('weeks', value)
36 | return self
37 |
38 | # Inspired by https://stackoverflow.com/a/50301887
39 | def months_to_days(self, months):
40 |
41 | now = datetime.datetime.today().date() + datetime.timedelta(seconds=self.get_duration())
42 | months_count = now.month + months
43 |
44 | year = now.year + int(months_count / 13)
45 |
46 | month = (months_count % 12)
47 | if month == 0:
48 | month = 12
49 |
50 | day = now.day
51 | last_day_of_month = calendar.monthrange(year, month)[1]
52 | if day > last_day_of_month:
53 | day = last_day_of_month
54 |
55 | new_date = datetime.date(year, month, day)
56 |
57 | return (new_date - now).days
58 |
59 | def months(self, value):
60 | days = self.months_to_days(value)
61 | self.__push('days', days)
62 | return self
63 |
64 | def years_to_days(self, years):
65 | return self.months_to_days(years * 12)
66 |
67 | def years(self, value):
68 | days = self.years_to_days(value)
69 | self.__push('days', days)
70 | return self
71 |
72 | def __init_now_then(self):
73 | tz = self.timezone if self.timezone else 'UTC'
74 | now = datetime.datetime.now(pytz.timezone(tz))
75 | return now, copy.deepcopy(now)
76 |
77 | def __push(self, key, value=1):
78 | if not hasattr(self, 'buffer'):
79 | self.buffer = {}
80 | if not self.buffer.get(key):
81 | self.buffer[key] = value
82 | else:
83 | self.buffer[key] += value
84 |
85 | def __apply_duration(self, time_unit, time_value, time):
86 | args = {str(time_unit): int(time_value)}
87 | return time + datetime.timedelta(**args)
88 |
89 | def __diff_in_seconds(self, before, after):
90 | return (after - before) / datetime.timedelta(seconds=1)
91 |
--------------------------------------------------------------------------------
/zenaton/traits/with_timestamp.py:
--------------------------------------------------------------------------------
1 | import datetime
2 | import itertools
3 |
4 | import pytz
5 |
6 | from .with_duration import WithDuration
7 | from ..exceptions import ExternalError, InternalError
8 |
9 |
10 | class WithTimestamp(WithDuration):
11 | WEEKDAYS = ('monday', 'tuesday', 'wednesday', 'thursday', 'friday', 'saturday', 'sunday')
12 | MODE_AT = 'AT' # When specifying a time
13 | MODE_WEEK_DAY = 'WEEK_DAY' # When specifying a day of the week
14 | MODE_MONTH_DAY = 'MONTH_DAY' # When specifying a day of the month
15 | MODE_TIMESTAMP = 'TIMESTAMP' # When specifying a unix timestamp
16 |
17 | timezone = 'UTC'
18 |
19 | HAS_DAY = False
20 |
21 | def get_timetamp_or_duration(self):
22 | if getattr(self, 'buffer', None) is None:
23 | return [None, None]
24 | now, now_dup = self._WithDuration__init_now_then()
25 | self.mode = None
26 | for (time_unit, time_value) in sorted(self.buffer.items(), key=lambda kv: kv[0]):
27 | now_dup = self.__apply(time_unit, time_value, now, now_dup)
28 | if self.mode is None:
29 | return [None, self._WithDuration__diff_in_seconds(now, now_dup)]
30 | if self.mode == self.MODE_TIMESTAMP:
31 | return [int(now_dup), None]
32 | return [int(now_dup.timestamp()), None]
33 |
34 | def timestamp(self, value):
35 | self._WithDuration__push('timestamp', value)
36 | return self
37 |
38 | def at(self, value):
39 | self._WithDuration__push('at', value)
40 | return self
41 |
42 | def day_of_month(self, value):
43 | self._WithDuration__push('day_of_month', value)
44 | return self
45 |
46 | def monday(self, value=1):
47 | self.HAS_DAY = True
48 | self._WithDuration__push('monday', value)
49 | return self
50 |
51 | def tuesday(self, value=1):
52 | self.HAS_DAY = True
53 | self._WithDuration__push('tuesday', value)
54 | return self
55 |
56 | def wednesday(self, value=1):
57 | self.HAS_DAY = True
58 | self._WithDuration__push('wednesday', value)
59 | return self
60 |
61 | def thursday(self, value=1):
62 | self.HAS_DAY = True
63 | self._WithDuration__push('thursday', value)
64 | return self
65 |
66 | def friday(self, value=1):
67 | self.HAS_DAY = True
68 | self._WithDuration__push('friday', value)
69 | return self
70 |
71 | def saturday(self, value=1):
72 | self.HAS_DAY = True
73 | self._WithDuration__push('saturday', value)
74 | return self
75 |
76 | def sunday(self, value=1):
77 | self.HAS_DAY = True
78 | self._WithDuration__push('sunday', value)
79 | return self
80 |
81 | def __apply(self, method, value, now, now_dup):
82 | if method in self.WEEKDAYS:
83 | return self.__weekday(value, method, now_dup)
84 | elif method == 'timestamp':
85 | return self.__timestamp(value)
86 | elif method == 'at':
87 | return self.__at(value, now, now_dup)
88 | elif method == 'day_of_month':
89 | return self.__day_of_month(value, now, now_dup)
90 | else:
91 | return self._WithDuration__apply_duration(method, value, now)
92 |
93 | def __weekday(self, value, day, now_dup):
94 | self.__set_mode(self.MODE_WEEK_DAY)
95 | for _ in itertools.repeat(None, value):
96 | now_dup = self.__next_weekday(now_dup, day)
97 | return now_dup
98 |
99 | # https://stackoverflow.com/questions/6558535/
100 | def __next_weekday(self, d, weekday):
101 | weekday = self.WEEKDAYS.index(weekday)
102 | days_ahead = weekday - d.weekday()
103 | if days_ahead <= 0: # Target day already happened this week
104 | days_ahead += 7
105 | return d + datetime.timedelta(days_ahead)
106 |
107 | def __timestamp(self, timestamp):
108 | self.__set_mode(self.MODE_TIMESTAMP)
109 | return timestamp
110 |
111 | def __at(self, time, now, now_dup):
112 | self.__set_mode(self.MODE_AT)
113 | hour, minute, second = time.split(':')
114 | hour, minute, second = int(hour), int(minute), int(second)
115 | now_dup = now_dup.replace(hour=hour, minute=minute, second=second)
116 | if now > now_dup:
117 | now_dup += self.__delay()
118 | elif self.HAS_DAY:
119 | now_dup -= datetime.timedelta(weeks=1)
120 | return now_dup
121 |
122 | def __delay(self):
123 | if self.mode == self.MODE_AT:
124 | return datetime.timedelta(days=1)
125 | elif self.mode == self.MODE_WEEK_DAY:
126 | return datetime.timedelta(weeks=1)
127 | elif self.mode == self.MODE_MONTH_DAY:
128 | days = self.months_to_days(1)
129 | return datetime.timedelta(days=days)
130 | else:
131 | raise InternalError('Unknown mode: {}'.format(self.mode))
132 |
133 | def __day_of_month(self, day, now, now_dup):
134 | if not 1 <= day <= 31:
135 | raise ValueError('Day should be in 1..31')
136 | self.__set_mode(self.MODE_MONTH_DAY)
137 | try:
138 | now_dup = now_dup.replace(day=day)
139 | except ValueError:
140 | now_dup = now_dup.replace(month=now_dup.month + 1)
141 | self.__set_mode(None)
142 | return self.__day_of_month(day, now, now_dup)
143 | if now >= now_dup:
144 | if now_dup.month > 11:
145 | now_dup = now_dup.replace(month=1, year=now_dup.year + 1)
146 | else:
147 | now_dup = now_dup.replace(month=now_dup.month + 1)
148 | return now_dup
149 |
150 | def __set_mode(self, mode):
151 | if not hasattr(self, 'mode') or self.mode is None:
152 | self.mode = mode
153 | return
154 | if mode == self.mode or self.__is_timestamp_mode_set(mode):
155 | raise ExternalError('Incompatible definition in Wait methods')
156 | else:
157 | self.mode = self.MODE_AT
158 |
159 | def __is_timestamp_mode_set(self, mode):
160 | return (self.mode is not None and self.MODE_TIMESTAMP == mode) or self.mode == self.MODE_TIMESTAMP
161 |
162 | def set_timezone(self, timezone):
163 | if not self.__is_valid_timezone(timezone):
164 | raise ExternalError('Unknown timezone')
165 | self.__class__.timezone = timezone
166 | return self
167 |
168 | def __is_valid_timezone(self, timezone):
169 | return timezone in pytz.all_timezones
170 |
--------------------------------------------------------------------------------
/zenaton/traits/zenatonable.py:
--------------------------------------------------------------------------------
1 | from ..engine import Engine
2 | from ..query.builder import Builder
3 | from zenaton.exceptions import InvalidArgumentError
4 |
5 |
6 | class Zenatonable:
7 | """
8 | Sends self as the single job to be dispatched
9 | to the engine and returns the result
10 | """
11 | def dispatch(self):
12 | return Engine().dispatch([self])
13 |
14 | """
15 | Sends self as the single job to be scheduled
16 | to the engine and returns the result
17 | """
18 | def schedule(self, cron):
19 | if not isinstance(cron, str) or cron == "":
20 | raise InvalidArgumentError("Param passed to 'schedule' function must be a non empty cron string")
21 |
22 | return Engine().schedule([self], cron)
23 |
24 | """
25 | Sends self as the single job to be executed
26 | to the engine and returns the result
27 | """
28 | def execute(self):
29 | return Engine().execute([self])[0]
30 |
31 | """
32 | Search for workflows to interact with.
33 | For available methods, see .query.builder.Builder
34 | :param String id ID for a given worflow
35 | :returns .query.builder.Builder a query builder object
36 | """
37 |
38 | @classmethod
39 | def where_id(cls, workflow_id):
40 | return Builder(cls).where_id(workflow_id)
41 |
--------------------------------------------------------------------------------
/zenaton/workflows/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/zenaton/zenaton-python/fc9a4ad2e580631480c972e066b8314ca3745ea7/zenaton/workflows/__init__.py
--------------------------------------------------------------------------------
/zenaton/workflows/version.py:
--------------------------------------------------------------------------------
1 | import abc
2 |
3 | from ..abstracts.workflow import Workflow
4 | from ..exceptions import ExternalError
5 | from ..traits.zenatonable import Zenatonable
6 |
7 |
8 | class Version(Workflow, Zenatonable):
9 |
10 | def __init__(self, *args):
11 | if args:
12 | self.args = args
13 |
14 | @abc.abstractmethod
15 | def versions(self):
16 | raise NotImplementedError("Please override the `versions' method in your subclass")
17 |
18 | """Calls handle on the current implementation"""
19 |
20 | def handle(self):
21 | self.current_implementation().handle()
22 |
23 | """
24 | Get the current implementation class
25 | returns class
26 | """
27 |
28 | def current(self):
29 | return self.__get_versions()[-1]
30 |
31 | """
32 | Get the first implementation class
33 | returns class
34 | """
35 |
36 | def initial(self):
37 | return self.__get_versions()[0]
38 |
39 | """
40 | Returns an instance of the current implementation
41 | :returns .abstracts.workflow.Workflow
42 | """
43 |
44 | def current_implementation(self):
45 | if hasattr(self, 'args'):
46 | return self.current()(self.args)
47 | else:
48 | return self.current()()
49 |
50 | def __get_versions(self):
51 | if not type(self.versions()) == list:
52 | raise ExternalError
53 | if not len(self.versions()) > 0:
54 | raise ExternalError
55 | for version in self.versions():
56 | if not issubclass(version, Workflow):
57 | raise ExternalError
58 | return self.versions()
59 |
--------------------------------------------------------------------------------