├── .coveragerc ├── .github └── workflows │ ├── publish.yaml │ ├── release.yaml │ └── test.yaml ├── .gitignore ├── .pre-commit-config.yaml ├── .python-version ├── .releaserc.json ├── LICENSE ├── README.md ├── docs ├── assets │ └── nitric-logo.svg ├── index.html └── nitric │ ├── api │ ├── const.html │ ├── documents.html │ ├── events.html │ ├── index.html │ ├── queues.html │ ├── secrets.html │ └── storage.html │ ├── application.html │ ├── config │ ├── default_settings.html │ └── index.html │ ├── exception.html │ ├── faas.html │ ├── index.html │ ├── proto │ ├── index.html │ ├── nitric │ │ ├── deploy │ │ │ ├── index.html │ │ │ └── v1 │ │ │ │ └── index.html │ │ ├── document │ │ │ ├── index.html │ │ │ └── v1 │ │ │ │ └── index.html │ │ ├── error │ │ │ ├── index.html │ │ │ └── v1 │ │ │ │ └── index.html │ │ ├── event │ │ │ ├── index.html │ │ │ └── v1 │ │ │ │ └── index.html │ │ ├── faas │ │ │ ├── index.html │ │ │ └── v1 │ │ │ │ └── index.html │ │ ├── index.html │ │ ├── queue │ │ │ ├── index.html │ │ │ └── v1 │ │ │ │ └── index.html │ │ ├── resource │ │ │ ├── index.html │ │ │ └── v1 │ │ │ │ └── index.html │ │ ├── secret │ │ │ ├── index.html │ │ │ └── v1 │ │ │ │ └── index.html │ │ └── storage │ │ │ ├── index.html │ │ │ └── v1 │ │ │ └── index.html │ └── validate │ │ └── index.html │ ├── resources │ ├── apis.html │ ├── base.html │ ├── buckets.html │ ├── collections.html │ ├── index.html │ ├── queues.html │ ├── schedules.html │ ├── secrets.html │ └── topics.html │ └── utils.html ├── makefile ├── mypy.ini ├── nitric ├── __init__.py ├── application.py ├── bidi.py ├── channel.py ├── config │ └── __init__.py ├── context.py ├── exception.py ├── proto │ ├── __init__.py │ ├── apis │ │ ├── __init__.py │ │ └── v1 │ │ │ └── __init__.py │ ├── batch │ │ ├── __init__.py │ │ └── v1 │ │ │ └── __init__.py │ ├── deployments │ │ ├── __init__.py │ │ └── v1 │ │ │ └── __init__.py │ ├── http │ │ ├── __init__.py │ │ └── v1 │ │ │ └── __init__.py │ ├── kvstore │ │ ├── __init__.py │ │ └── v1 │ │ │ └── __init__.py │ ├── queues │ │ ├── __init__.py │ │ └── v1 │ │ │ └── __init__.py │ ├── resources │ │ ├── __init__.py │ │ └── v1 │ │ │ └── __init__.py │ ├── schedules │ │ ├── __init__.py │ │ └── v1 │ │ │ └── __init__.py │ ├── secrets │ │ ├── __init__.py │ │ └── v1 │ │ │ └── __init__.py │ ├── sql │ │ ├── __init__.py │ │ └── v1 │ │ │ └── __init__.py │ ├── storage │ │ ├── __init__.py │ │ └── v1 │ │ │ └── __init__.py │ ├── topics │ │ ├── __init__.py │ │ └── v1 │ │ │ └── __init__.py │ └── websockets │ │ ├── __init__.py │ │ └── v1 │ │ └── __init__.py ├── py.typed ├── resources │ ├── __init__.py │ ├── apis.py │ ├── buckets.py │ ├── job.py │ ├── kv.py │ ├── queues.py │ ├── resource.py │ ├── schedules.py │ ├── secrets.py │ ├── sql.py │ ├── topics.py │ └── websockets.py └── utils.py ├── pyproject.toml ├── setup.py ├── tests ├── __init__.py ├── resources │ ├── __init__.py │ ├── test_apis.py │ ├── test_buckets.py │ ├── test_kv.py │ ├── test_queues.py │ ├── test_schedules.py │ ├── test_secrets.py │ ├── test_sql.py │ ├── test_topics.py │ └── test_websockets.py ├── test__utils.py ├── test_application.py └── test_exception.py ├── tools └── apache-2.tmpl └── tox.ini /.coveragerc: -------------------------------------------------------------------------------- 1 | [run] 2 | omit = 3 | ./nitric/proto/* -------------------------------------------------------------------------------- /.github/workflows/publish.yaml: -------------------------------------------------------------------------------- 1 | name: Publish release to Pypi 2 | 3 | on: 4 | push: 5 | # run only against tags 6 | tags: 7 | - '*' 8 | 9 | jobs: 10 | publish: 11 | runs-on: ubuntu-latest 12 | steps: 13 | - name: Checkout 14 | uses: actions/checkout@v2.3.1 15 | with: 16 | fetch-depth: 0 # needed to retrieve most recent tag 17 | - name: Set up Python '3.11' 18 | uses: actions/setup-python@v2 19 | with: 20 | python-version: '3.11' 21 | - name: Build 22 | run: make build 23 | - name: Publish to PyPI 24 | uses: pypa/gh-action-pypi-publish@master 25 | with: 26 | password: ${{ secrets.PYPI_API_TOKEN }} -------------------------------------------------------------------------------- /.github/workflows/release.yaml: -------------------------------------------------------------------------------- 1 | name: Production Release 2 | on: 3 | push: 4 | branches: 5 | - main 6 | jobs: 7 | release: 8 | name: semantic-release 9 | runs-on: ubuntu-20.04 10 | outputs: 11 | new-release-published: ${{ steps.semantic-release.outputs.new_release_published }} 12 | new-release-version: ${{ steps.semantic-release.outputs.new_release_version }} 13 | steps: 14 | - uses: actions/checkout@v4 15 | with: 16 | fetch-depth: 0 17 | persist-credentials: false 18 | - id: semantic-release 19 | uses: cycjimmy/semantic-release-action@v4 20 | env: 21 | GITHUB_TOKEN: ${{ secrets.NITRIC_BOT_TOKEN }} 22 | -------------------------------------------------------------------------------- /.github/workflows/test.yaml: -------------------------------------------------------------------------------- 1 | name: Tests 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | pull_request: 8 | 9 | jobs: 10 | test: 11 | runs-on: ubuntu-latest 12 | strategy: 13 | matrix: 14 | python-version: ['3.11'] 15 | 16 | steps: 17 | - name: Checkout 18 | uses: actions/checkout@v2.3.1 19 | with: 20 | fetch-depth: 0 # needed to retrieve most recent tag 21 | - name: Set up Python ${{ matrix.python-version }} 22 | uses: actions/setup-python@v2 23 | with: 24 | python-version: ${{ matrix.python-version }} 25 | - name: Install 26 | run: make install 27 | - name: Check generated sources 28 | run: | 29 | make grpc-client 30 | git add . 31 | git diff --cached --quiet 32 | - name: Run Tox 33 | # Run tox using the version of Python in `PATH` 34 | run: tox -e py 35 | - name: Upload coverage to Codecov 36 | continue-on-error: true 37 | uses: codecov/codecov-action@v4.0.1 38 | with: 39 | fail_ci_if_error: true 40 | token: ${{ secrets.CODECOV_TOKEN }} 41 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .idea/ 2 | .nitric/ 3 | nitric.yaml 4 | nitric.*.yaml 5 | /proto/ 6 | /nitric/proto/KeyValue 7 | 8 | # Byte-compiled / optimized / DLL files 9 | __pycache__/ 10 | *.py[cod] 11 | *$py.class 12 | 13 | # C extensions 14 | *.so 15 | 16 | # Distribution / packaging 17 | .Python 18 | build/ 19 | develop-eggs/ 20 | dist/ 21 | downloads/ 22 | eggs/ 23 | .eggs/ 24 | lib/ 25 | lib64/ 26 | parts/ 27 | sdist/ 28 | var/ 29 | wheels/ 30 | pip-wheel-metadata/ 31 | share/python-wheels/ 32 | *.egg-info/ 33 | .installed.cfg 34 | *.egg 35 | MANIFEST 36 | 37 | # PyInstaller 38 | # Usually these files are written by a python script from a template 39 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 40 | *.manifest 41 | *.spec 42 | 43 | # Installer logs 44 | pip-log.txt 45 | pip-delete-this-directory.txt 46 | 47 | # Unit test / coverage reports 48 | htmlcov/ 49 | .tox/ 50 | .nox/ 51 | .coverage 52 | .coverage.* 53 | .cache 54 | nosetests.xml 55 | coverage.xml 56 | *.cover 57 | *.py,cover 58 | .hypothesis/ 59 | .pytest_cache/ 60 | cover/ 61 | 62 | # Translations 63 | *.mo 64 | *.pot 65 | 66 | # Django stuff: 67 | *.log 68 | local_settings.py 69 | db.sqlite3 70 | db.sqlite3-journal 71 | 72 | # Flask stuff: 73 | instance/ 74 | .webassets-cache 75 | 76 | # Scrapy stuff: 77 | .scrapy 78 | 79 | # Sphinx documentation 80 | docs/_build/ 81 | 82 | # PyBuilder 83 | .pybuilder/ 84 | target/ 85 | 86 | # Jupyter Notebook 87 | .ipynb_checkpoints 88 | 89 | # IPython 90 | profile_default/ 91 | ipython_config.py 92 | 93 | # pyenv 94 | # For a library or package, you might want to ignore these files since the code is 95 | # intended to run in multiple environments; otherwise, check them in: 96 | # .python-version 97 | 98 | # pipenv 99 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 100 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 101 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 102 | # install all needed dependencies. 103 | #Pipfile.lock 104 | 105 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 106 | __pypackages__/ 107 | 108 | # Celery stuff 109 | celerybeat-schedule 110 | celerybeat.pid 111 | 112 | # SageMath parsed files 113 | *.sage.py 114 | 115 | # Environments 116 | .env 117 | .venv 118 | env/ 119 | venv/ 120 | ENV/ 121 | env.bak/ 122 | venv.bak/ 123 | 124 | # Spyder project settings 125 | .spyderproject 126 | .spyproject 127 | 128 | # Rope project settings 129 | .ropeproject 130 | 131 | # mkdocs documentation 132 | /site 133 | 134 | # mypy 135 | .mypy_cache/ 136 | .dmypy.json 137 | dmypy.json 138 | 139 | # Pyre type checker 140 | .pyre/ 141 | 142 | # pytype static type analyzer 143 | .pytype/ 144 | 145 | # Cython debug symbols 146 | cython_debug/ 147 | 148 | 149 | .vscode/ 150 | 151 | contracts/ 152 | 153 | testproj/ 154 | -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | repos: 2 | - repo: https://github.com/ambv/black 3 | rev: stable 4 | hooks: 5 | - id: black 6 | exclude: ^(nitric/proto)/ 7 | - repo: https://github.com/pycqa/flake8 8 | rev: 3.7.9 9 | hooks: 10 | - id: flake8 11 | exclude: ^(venv|tests|build|dist|nitric/proto)/ 12 | - repo: https://github.com/pycqa/pydocstyle 13 | rev: 6.0.0 14 | hooks: 15 | - id: pydocstyle 16 | args: 17 | - --ignore=D100, D105, D203, D212, D415 18 | exclude: ^(venv|tests|build|dist|nitric/proto)/ -------------------------------------------------------------------------------- /.python-version: -------------------------------------------------------------------------------- 1 | 3.11.0 2 | -------------------------------------------------------------------------------- /.releaserc.json: -------------------------------------------------------------------------------- 1 | { 2 | "branches": ["main"], 3 | "plugins": [ 4 | "@semantic-release/commit-analyzer", 5 | "@semantic-release/release-notes-generator", 6 | "@semantic-release/github" 7 | ] 8 | } 9 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | 3 | Nitric Logo 4 | 5 |

6 | 7 |

8 | Build nitric applications with Python 9 |

10 | 11 |

12 | Build Status 13 | 14 | Codecov 15 | 16 | 17 | Version 18 | 19 | 20 | Downloads/week 21 | 22 | Discord 23 |

24 | 25 | The Python SDK supports the use of the [Nitric](https://nitric.io) framework with Python 3.11+. For more information check out the main [Nitric repo](https://github.com/nitrictech/nitric). 26 | 27 | Python SDKs provide an infrastructure-from-code style that lets you define resources in code. You can also write the functions that support the logic behind APIs, subscribers and schedules. 28 | 29 | You can request the type of access you need to resources such as publishing for topics, without dealing directly with IAM or policy documents. 30 | 31 | - Reference Documentation: https://nitric.io/docs/reference/python 32 | - Guides: https://nitric.io/docs/guides/python 33 | 34 | ## Usage 35 | 36 | ### Starting a new project 37 | 38 | Install the [Nitric CLI](https://nitric.io/docs/getting-started/installation), then generate your project: 39 | 40 | ```bash 41 | nitric new hello-world py-starter 42 | ``` 43 | 44 | ### Add to an existing project 45 | 46 | First of all, you need to install the library: 47 | 48 | **pip** 49 | 50 | ```bash 51 | pip3 install nitric 52 | ``` 53 | 54 | **pipenv** 55 | 56 | ``` 57 | pipenv install nitric 58 | ``` 59 | 60 | Then you're able to import the library and create cloud resources: 61 | 62 | ```python 63 | from nitric.resources import api, bucket 64 | from nitric.application import Nitric 65 | from nitric.context import HttpContext 66 | 67 | publicApi = api("public") 68 | uploads = bucket("uploads").allow("write") 69 | 70 | @publicApi.get("/upload") 71 | async def upload(ctx: HttpContext): 72 | photo = uploads.file("images/photo.jpg") 73 | 74 | url = await photo.upload_url() 75 | 76 | ctx.res.body = {"url": url} 77 | 78 | Nitric.run() 79 | ``` 80 | 81 | ## Learn more 82 | 83 | Learn more by checking out the [Nitric documentation](https://nitric.io/docs). 84 | 85 | ## Get in touch: 86 | 87 | - Join us on [Discord](https://nitric.io/chat) 88 | 89 | - Ask questions in [GitHub discussions](https://github.com/nitrictech/nitric/discussions) 90 | 91 | - Find us on [Twitter](https://twitter.com/nitric_io) 92 | 93 | - Send us an [email](mailto:maintainers@nitric.io) 94 | -------------------------------------------------------------------------------- /docs/assets/nitric-logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /docs/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 |

If you are not redirected, click here.

7 | 8 | -------------------------------------------------------------------------------- /docs/nitric/api/const.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | nitric.api.const API documentation 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 |
20 |
21 |
22 |

Module nitric.api.const

23 |
24 |
25 |
26 | 27 | Expand source code 28 | 29 |
#
30 | # Copyright (c) 2021 Nitric Technologies Pty Ltd.
31 | #
32 | # This file is part of Nitric Python 3 SDK.
33 | # See https://github.com/nitrictech/python-sdk for further info.
34 | #
35 | # Licensed under the Apache License, Version 2.0 (the "License");
36 | # you may not use this file except in compliance with the License.
37 | # You may obtain a copy of the License at
38 | #
39 | #     http://www.apache.org/licenses/LICENSE-2.0
40 | #
41 | # Unless required by applicable law or agreed to in writing, software
42 | # distributed under the License is distributed on an "AS IS" BASIS,
43 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
44 | # See the License for the specific language governing permissions and
45 | # limitations under the License.
46 | #
47 | 
48 | # The maximum number of parent collections a sub-collection can have.
49 | # This is implemented in the Membrane, but reinforced here for immediate exceptions without a server connection.
50 | MAX_SUB_COLLECTION_DEPTH = 1
51 |
52 |
53 |
54 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
62 | 75 |
76 | 79 | 80 | -------------------------------------------------------------------------------- /docs/nitric/config/default_settings.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | nitric.config.default_settings API documentation 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 |
20 |
21 |
22 |

Module nitric.config.default_settings

23 |
24 |
25 |
26 | 27 | Expand source code 28 | 29 |
#
30 | # Copyright (c) 2021 Nitric Technologies Pty Ltd.
31 | #
32 | # This file is part of Nitric Python 3 SDK.
33 | # See https://github.com/nitrictech/python-sdk for further info.
34 | #
35 | # Licensed under the Apache License, Version 2.0 (the "License");
36 | # you may not use this file except in compliance with the License.
37 | # You may obtain a copy of the License at
38 | #
39 | #     http://www.apache.org/licenses/LICENSE-2.0
40 | #
41 | # Unless required by applicable law or agreed to in writing, software
42 | # distributed under the License is distributed on an "AS IS" BASIS,
43 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
44 | # See the License for the specific language governing permissions and
45 | # limitations under the License.
46 | #
47 | import os
48 | 
49 | # Provides a set of default settings that env vars can replace
50 | # Nitric Membrane Service Address
51 | SERVICE_BIND = os.environ.get('SERVICE_ADDRESS', '127.0.0.1:50051');
52 |
53 |
54 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
62 |
63 | 76 |
77 | 80 | 81 | -------------------------------------------------------------------------------- /docs/nitric/proto/nitric/deploy/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | nitric.proto.nitric.deploy API documentation 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 |
20 |
21 |
22 |

Module nitric.proto.nitric.deploy

23 |
24 |
25 |
26 | 27 | Expand source code 28 | 29 |
#
30 | # Copyright (c) 2021 Nitric Technologies Pty Ltd.
31 | #
32 | # This file is part of Nitric Python 3 SDK.
33 | # See https://github.com/nitrictech/python-sdk for further info.
34 | #
35 | # Licensed under the Apache License, Version 2.0 (the "License");
36 | # you may not use this file except in compliance with the License.
37 | # You may obtain a copy of the License at
38 | #
39 | #     http://www.apache.org/licenses/LICENSE-2.0
40 | #
41 | # Unless required by applicable law or agreed to in writing, software
42 | # distributed under the License is distributed on an "AS IS" BASIS,
43 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
44 | # See the License for the specific language governing permissions and
45 | # limitations under the License.
46 | #
47 |
48 |
49 |
50 |

Sub-modules

51 |
52 |
nitric.proto.nitric.deploy.v1
53 |
54 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
62 |
63 |
64 |
65 | 83 |
84 | 87 | 88 | -------------------------------------------------------------------------------- /docs/nitric/proto/nitric/document/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | nitric.proto.nitric.document API documentation 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 |
20 |
21 |
22 |

Module nitric.proto.nitric.document

23 |
24 |
25 |
26 | 27 | Expand source code 28 | 29 |
#
30 | # Copyright (c) 2021 Nitric Technologies Pty Ltd.
31 | #
32 | # This file is part of Nitric Python 3 SDK.
33 | # See https://github.com/nitrictech/python-sdk for further info.
34 | #
35 | # Licensed under the Apache License, Version 2.0 (the "License");
36 | # you may not use this file except in compliance with the License.
37 | # You may obtain a copy of the License at
38 | #
39 | #     http://www.apache.org/licenses/LICENSE-2.0
40 | #
41 | # Unless required by applicable law or agreed to in writing, software
42 | # distributed under the License is distributed on an "AS IS" BASIS,
43 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
44 | # See the License for the specific language governing permissions and
45 | # limitations under the License.
46 | #
47 |
48 |
49 |
50 |

Sub-modules

51 |
52 |
nitric.proto.nitric.document.v1
53 |
54 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
62 |
63 |
64 |
65 | 83 |
84 | 87 | 88 | -------------------------------------------------------------------------------- /docs/nitric/proto/nitric/error/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | nitric.proto.nitric.error API documentation 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 |
20 |
21 |
22 |

Module nitric.proto.nitric.error

23 |
24 |
25 |
26 | 27 | Expand source code 28 | 29 |
#
30 | # Copyright (c) 2021 Nitric Technologies Pty Ltd.
31 | #
32 | # This file is part of Nitric Python 3 SDK.
33 | # See https://github.com/nitrictech/python-sdk for further info.
34 | #
35 | # Licensed under the Apache License, Version 2.0 (the "License");
36 | # you may not use this file except in compliance with the License.
37 | # You may obtain a copy of the License at
38 | #
39 | #     http://www.apache.org/licenses/LICENSE-2.0
40 | #
41 | # Unless required by applicable law or agreed to in writing, software
42 | # distributed under the License is distributed on an "AS IS" BASIS,
43 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
44 | # See the License for the specific language governing permissions and
45 | # limitations under the License.
46 | #
47 |
48 |
49 |
50 |

Sub-modules

51 |
52 |
nitric.proto.nitric.error.v1
53 |
54 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
62 |
63 |
64 |
65 | 83 |
84 | 87 | 88 | -------------------------------------------------------------------------------- /docs/nitric/proto/nitric/event/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | nitric.proto.nitric.event API documentation 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 |
20 |
21 |
22 |

Module nitric.proto.nitric.event

23 |
24 |
25 |
26 | 27 | Expand source code 28 | 29 |
#
30 | # Copyright (c) 2021 Nitric Technologies Pty Ltd.
31 | #
32 | # This file is part of Nitric Python 3 SDK.
33 | # See https://github.com/nitrictech/python-sdk for further info.
34 | #
35 | # Licensed under the Apache License, Version 2.0 (the "License");
36 | # you may not use this file except in compliance with the License.
37 | # You may obtain a copy of the License at
38 | #
39 | #     http://www.apache.org/licenses/LICENSE-2.0
40 | #
41 | # Unless required by applicable law or agreed to in writing, software
42 | # distributed under the License is distributed on an "AS IS" BASIS,
43 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
44 | # See the License for the specific language governing permissions and
45 | # limitations under the License.
46 | #
47 |
48 |
49 |
50 |

Sub-modules

51 |
52 |
nitric.proto.nitric.event.v1
53 |
54 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
62 |
63 |
64 |
65 | 83 |
84 | 87 | 88 | -------------------------------------------------------------------------------- /docs/nitric/proto/nitric/faas/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | nitric.proto.nitric.faas API documentation 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 |
20 |
21 |
22 |

Module nitric.proto.nitric.faas

23 |
24 |
25 |
26 | 27 | Expand source code 28 | 29 |
#
30 | # Copyright (c) 2021 Nitric Technologies Pty Ltd.
31 | #
32 | # This file is part of Nitric Python 3 SDK.
33 | # See https://github.com/nitrictech/python-sdk for further info.
34 | #
35 | # Licensed under the Apache License, Version 2.0 (the "License");
36 | # you may not use this file except in compliance with the License.
37 | # You may obtain a copy of the License at
38 | #
39 | #     http://www.apache.org/licenses/LICENSE-2.0
40 | #
41 | # Unless required by applicable law or agreed to in writing, software
42 | # distributed under the License is distributed on an "AS IS" BASIS,
43 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
44 | # See the License for the specific language governing permissions and
45 | # limitations under the License.
46 | #
47 |
48 |
49 |
50 |

Sub-modules

51 |
52 |
nitric.proto.nitric.faas.v1
53 |
54 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
62 |
63 |
64 |
65 | 83 |
84 | 87 | 88 | -------------------------------------------------------------------------------- /docs/nitric/proto/nitric/queue/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | nitric.proto.nitric.queue API documentation 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 |
20 |
21 |
22 |

Module nitric.proto.nitric.queue

23 |
24 |
25 |
26 | 27 | Expand source code 28 | 29 |
#
30 | # Copyright (c) 2021 Nitric Technologies Pty Ltd.
31 | #
32 | # This file is part of Nitric Python 3 SDK.
33 | # See https://github.com/nitrictech/python-sdk for further info.
34 | #
35 | # Licensed under the Apache License, Version 2.0 (the "License");
36 | # you may not use this file except in compliance with the License.
37 | # You may obtain a copy of the License at
38 | #
39 | #     http://www.apache.org/licenses/LICENSE-2.0
40 | #
41 | # Unless required by applicable law or agreed to in writing, software
42 | # distributed under the License is distributed on an "AS IS" BASIS,
43 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
44 | # See the License for the specific language governing permissions and
45 | # limitations under the License.
46 | #
47 |
48 |
49 |
50 |

Sub-modules

51 |
52 |
nitric.proto.nitric.queue.v1
53 |
54 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
62 |
63 |
64 |
65 | 83 |
84 | 87 | 88 | -------------------------------------------------------------------------------- /docs/nitric/proto/nitric/resource/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | nitric.proto.nitric.resource API documentation 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 |
20 |
21 |
22 |

Module nitric.proto.nitric.resource

23 |
24 |
25 |
26 | 27 | Expand source code 28 | 29 |
#
30 | # Copyright (c) 2021 Nitric Technologies Pty Ltd.
31 | #
32 | # This file is part of Nitric Python 3 SDK.
33 | # See https://github.com/nitrictech/python-sdk for further info.
34 | #
35 | # Licensed under the Apache License, Version 2.0 (the "License");
36 | # you may not use this file except in compliance with the License.
37 | # You may obtain a copy of the License at
38 | #
39 | #     http://www.apache.org/licenses/LICENSE-2.0
40 | #
41 | # Unless required by applicable law or agreed to in writing, software
42 | # distributed under the License is distributed on an "AS IS" BASIS,
43 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
44 | # See the License for the specific language governing permissions and
45 | # limitations under the License.
46 | #
47 |
48 |
49 |
50 |

Sub-modules

51 |
52 |
nitric.proto.nitric.resource.v1
53 |
54 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
62 |
63 |
64 |
65 | 83 |
84 | 87 | 88 | -------------------------------------------------------------------------------- /docs/nitric/proto/nitric/secret/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | nitric.proto.nitric.secret API documentation 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 |
20 |
21 |
22 |

Module nitric.proto.nitric.secret

23 |
24 |
25 |
26 | 27 | Expand source code 28 | 29 |
#
30 | # Copyright (c) 2021 Nitric Technologies Pty Ltd.
31 | #
32 | # This file is part of Nitric Python 3 SDK.
33 | # See https://github.com/nitrictech/python-sdk for further info.
34 | #
35 | # Licensed under the Apache License, Version 2.0 (the "License");
36 | # you may not use this file except in compliance with the License.
37 | # You may obtain a copy of the License at
38 | #
39 | #     http://www.apache.org/licenses/LICENSE-2.0
40 | #
41 | # Unless required by applicable law or agreed to in writing, software
42 | # distributed under the License is distributed on an "AS IS" BASIS,
43 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
44 | # See the License for the specific language governing permissions and
45 | # limitations under the License.
46 | #
47 |
48 |
49 |
50 |

Sub-modules

51 |
52 |
nitric.proto.nitric.secret.v1
53 |
54 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
62 |
63 |
64 |
65 | 83 |
84 | 87 | 88 | -------------------------------------------------------------------------------- /docs/nitric/proto/nitric/storage/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | nitric.proto.nitric.storage API documentation 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 |
20 |
21 |
22 |

Module nitric.proto.nitric.storage

23 |
24 |
25 |
26 | 27 | Expand source code 28 | 29 |
#
30 | # Copyright (c) 2021 Nitric Technologies Pty Ltd.
31 | #
32 | # This file is part of Nitric Python 3 SDK.
33 | # See https://github.com/nitrictech/python-sdk for further info.
34 | #
35 | # Licensed under the Apache License, Version 2.0 (the "License");
36 | # you may not use this file except in compliance with the License.
37 | # You may obtain a copy of the License at
38 | #
39 | #     http://www.apache.org/licenses/LICENSE-2.0
40 | #
41 | # Unless required by applicable law or agreed to in writing, software
42 | # distributed under the License is distributed on an "AS IS" BASIS,
43 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
44 | # See the License for the specific language governing permissions and
45 | # limitations under the License.
46 | #
47 |
48 |
49 |
50 |

Sub-modules

51 |
52 |
nitric.proto.nitric.storage.v1
53 |
54 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
62 |
63 |
64 |
65 | 83 |
84 | 87 | 88 | -------------------------------------------------------------------------------- /makefile: -------------------------------------------------------------------------------- 1 | install: 2 | @echo Installing Project Dependencies 3 | @python3 -m pip install -e .[dev] 4 | @pre-commit install 5 | @rm -rf ./.tox 6 | 7 | .PHONY: docs clean license 8 | 9 | docs: 10 | @echo Generating SDK Documentation 11 | @rm -rf docs/nitric 12 | @pdoc3 -f --html -o docs nitric 13 | 14 | clean: 15 | @echo Cleaning Build Artefacts 16 | @rm -rf ./.eggs 17 | @rm -rf ./build 18 | @rm -rf ./dist 19 | 20 | test: 21 | @echo Running Tox tests 22 | @tox -e py 23 | 24 | NITRIC_VERSION := 1.14.0 25 | 26 | download-local: 27 | @rm -r ./proto/nitric 28 | @mkdir ./proto/nitric 29 | @cp -r ${NITRIC_CORE_HOME}/nitric/proto ./proto/nitric 30 | 31 | download: 32 | @mkdir -p ./proto/ 33 | @curl -L https://github.com/nitrictech/nitric/releases/download/v${NITRIC_VERSION}/proto.tgz -o ./proto/nitric.tgz 34 | @cd ./proto && tar xvzf nitric.tgz 35 | @cd ../ 36 | @rm ./proto/nitric.tgz 37 | 38 | OUTPUT="." 39 | CONTRACTS="./proto" 40 | 41 | grpc-client: install download generate-proto 42 | 43 | generate-proto: 44 | @echo Generating Proto Sources 45 | @ rm -rf $(OUTPUT)/nitric/proto 46 | @echo $(OUTPUT) 47 | @mkdir -p $(OUTPUT) 48 | @python3 -m grpc_tools.protoc -I $(CONTRACTS) --python_betterproto_out=$(OUTPUT) ./$(CONTRACTS)/nitric/proto/*/*/*.proto 49 | @rm ./__init__.py 50 | 51 | license: 52 | @echo Applying Apache 2 header to source files 53 | @licenseheaders -t tools/apache-2.tmpl -o "Nitric Technologies Pty Ltd" -y 2021 -n "Nitric Python 3 SDK" -u "https://github.com/nitrictech/python-sdk" -d nitric 54 | @licenseheaders -t tools/apache-2.tmpl -o "Nitric Technologies Pty Ltd" -y 2021 -n "Nitric Python 3 SDK" -u "https://github.com/nitrictech/python-sdk" -d tests 55 | @licenseheaders -t tools/apache-2.tmpl -o "Nitric Technologies Pty Ltd" -y 2021 -n "Nitric Python 3 SDK" -u "https://github.com/nitrictech/python-sdk" -d tools 56 | 57 | build: clean grpc-client license docs 58 | @echo Building sdist and wheel 59 | @python3 setup.py sdist bdist_wheel 60 | 61 | distribute: build 62 | @echo Uploading to pypi 63 | @python3 -m twine upload --repository pypi dist/* 64 | 65 | distribute-test: build 66 | @echo Uploading to testpypi 67 | @python3 -m twine upload --repository testpypi dist/* -------------------------------------------------------------------------------- /mypy.ini: -------------------------------------------------------------------------------- 1 | [mypy] 2 | 3 | 4 | [mypy-nitric] 5 | disallow_untyped_defs = True -------------------------------------------------------------------------------- /nitric/__init__.py: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright (c) 2021 Nitric Technologies Pty Ltd. 3 | # 4 | # This file is part of Nitric Python 3 SDK. 5 | # See https://github.com/nitrictech/python-sdk for further info. 6 | # 7 | # Licensed under the Apache License, Version 2.0 (the "License"); 8 | # you may not use this file except in compliance with the License. 9 | # You may obtain a copy of the License at 10 | # 11 | # http://www.apache.org/licenses/LICENSE-2.0 12 | # 13 | # Unless required by applicable law or agreed to in writing, software 14 | # distributed under the License is distributed on an "AS IS" BASIS, 15 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 16 | # See the License for the specific language governing permissions and 17 | # limitations under the License. 18 | # 19 | """Nitric Python SDK API Documentation. See: https://nitric.io/docs?lang=python for full framework documentation.""" 20 | -------------------------------------------------------------------------------- /nitric/application.py: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright (c) 2021 Nitric Technologies Pty Ltd. 3 | # 4 | # This file is part of Nitric Python 3 SDK. 5 | # See https://github.com/nitrictech/python-sdk for further info. 6 | # 7 | # Licensed under the Apache License, Version 2.0 (the "License"); 8 | # you may not use this file except in compliance with the License. 9 | # You may obtain a copy of the License at 10 | # 11 | # http://www.apache.org/licenses/LICENSE-2.0 12 | # 13 | # Unless required by applicable law or agreed to in writing, software 14 | # distributed under the License is distributed on an "AS IS" BASIS, 15 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 16 | # See the License for the specific language governing permissions and 17 | # limitations under the License. 18 | # 19 | import asyncio 20 | from typing import Any, Dict, List, Type, TypeVar 21 | 22 | from nitric.context import FunctionServer 23 | from nitric.exception import NitricUnavailableException 24 | 25 | BT = TypeVar("BT") 26 | 27 | 28 | class Nitric: 29 | """Represents a nitric app.""" 30 | 31 | _has_run = False 32 | 33 | _workers: List[FunctionServer] = [] 34 | _cache: Dict[str, Dict[str, Any]] = { 35 | "api": {}, 36 | "bucket": {}, 37 | "topic": {}, 38 | "secret": {}, 39 | "queue": {}, 40 | "collection": {}, 41 | "websocket": {}, 42 | "keyvaluestore": {}, 43 | "oidcsecuritydefinition": {}, 44 | "sql": {}, 45 | "job": {}, 46 | "jobdefinition": {}, 47 | } 48 | 49 | @classmethod 50 | def _register_worker(cls, srv: FunctionServer): 51 | """Register a worker for this application.""" 52 | cls._workers.append(srv) 53 | 54 | @classmethod 55 | def _create_resource(cls, resource: Type[BT], name: str, *args: Any, **kwargs: Any) -> BT: 56 | try: 57 | resource_type = resource.__name__.lower() 58 | cached_resources = cls._cache.get(resource_type) 59 | if cached_resources is None or cached_resources.get(name) is None: 60 | cls._cache[resource_type][name] = resource.make(name, *args, **kwargs) # type: ignore 61 | 62 | return cls._cache[resource_type][name] 63 | except ConnectionRefusedError as cre: 64 | raise NitricUnavailableException( 65 | "The nitric server may not be running or the host/port is inaccessible" 66 | ) from cre 67 | 68 | @classmethod 69 | def has_run(cls) -> bool: 70 | """ 71 | Check if the Nitric application has been started. 72 | 73 | Returns: 74 | bool: True if the Nitric application has been started, False otherwise. 75 | """ 76 | return cls._has_run 77 | 78 | @classmethod 79 | def run(cls) -> None: 80 | """ 81 | Start the nitric application. 82 | 83 | This will execute in an existing event loop if there is one, otherwise it will attempt to create its own. 84 | """ 85 | if cls._has_run: 86 | print("The Nitric application has already been started, Nitric.run() should only be called once.") 87 | cls._has_run = True 88 | try: 89 | try: 90 | loop = asyncio.get_running_loop() 91 | except RuntimeError: 92 | loop = asyncio.get_event_loop() 93 | 94 | loop.run_until_complete(asyncio.gather(*[wkr.start() for wkr in cls._workers])) 95 | except KeyboardInterrupt: 96 | 97 | print("\nexiting") 98 | except ConnectionRefusedError as cre: 99 | raise NitricUnavailableException( 100 | 'If you\'re running locally use "nitric start" or "nitric run" to start your application' 101 | ) from cre 102 | -------------------------------------------------------------------------------- /nitric/bidi.py: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright (c) 2021 Nitric Technologies Pty Ltd. 3 | # 4 | # This file is part of Nitric Python 3 SDK. 5 | # See https://github.com/nitrictech/python-sdk for further info. 6 | # 7 | # Licensed under the Apache License, Version 2.0 (the "License"); 8 | # you may not use this file except in compliance with the License. 9 | # You may obtain a copy of the License at 10 | # 11 | # http://www.apache.org/licenses/LICENSE-2.0 12 | # 13 | # Unless required by applicable law or agreed to in writing, software 14 | # distributed under the License is distributed on an "AS IS" BASIS, 15 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 16 | # See the License for the specific language governing permissions and 17 | # limitations under the License. 18 | # 19 | import asyncio 20 | from typing import Generic, List, TypeVar 21 | 22 | T = TypeVar("T") 23 | 24 | 25 | class AsyncNotifierList(Generic[T]): 26 | """An async iterable that notifies when new items are added.""" 27 | 28 | def __init__(self): 29 | """Create a new AsyncNotifierList.""" 30 | self.items: List[T] = [] # type: ignore 31 | self.new_item_event: asyncio.Event = asyncio.Event() # type: ignore 32 | 33 | async def add_item(self, item: T) -> None: 34 | """Add a new item to the list.""" 35 | self.items.append(item) 36 | self.new_item_event.set() 37 | 38 | def __aiter__(self): 39 | return self 40 | 41 | async def __anext__(self): 42 | while not self.items: 43 | await self.new_item_event.wait() # Wait for an item to be added 44 | item = self.items.pop(0) 45 | if not self.items: 46 | self.new_item_event.clear() # Reset the event if there are no more items 47 | return item 48 | -------------------------------------------------------------------------------- /nitric/channel.py: -------------------------------------------------------------------------------- 1 | import atexit 2 | import re 3 | from urllib.parse import urlparse 4 | from grpclib.client import Channel 5 | 6 | from nitric.application import Nitric 7 | from nitric.config import settings 8 | from nitric.exception import NitricNotRunningException 9 | 10 | 11 | def format_url(url: str): 12 | """Add the default http scheme prefix to urls without one.""" 13 | if not re.match("^((?:http|ftp|https):)?//", url.lower()): 14 | return "http://{0}".format(url) 15 | return url 16 | 17 | 18 | class ChannelManager: 19 | """A singleton class to manage the gRPC channel.""" 20 | 21 | channel = None 22 | 23 | @classmethod 24 | def get_channel(cls) -> Channel: 25 | """Return the channel instance.""" 26 | 27 | if cls.channel is None: 28 | cls._create_channel() 29 | return cls.channel # type: ignore 30 | 31 | @classmethod 32 | def _create_channel(cls): 33 | """Create a new channel instance.""" 34 | 35 | channel_url = urlparse(format_url(settings.SERVICE_ADDRESS)) 36 | cls.channel = Channel(host=channel_url.hostname, port=channel_url.port) 37 | atexit.register(cls._close_channel) 38 | 39 | @classmethod 40 | def _close_channel(cls): 41 | """Close the channel instance.""" 42 | 43 | if cls.channel is not None: 44 | cls.channel.close() 45 | cls.channel = None 46 | 47 | # If the program exits without calling Nitric.run(), it may have been a mistake. 48 | if not Nitric.has_run(): 49 | print( 50 | "WARNING: The Nitric application was not started. " 51 | "If you intended to start the application, call Nitric.run() before exiting." 52 | ) 53 | -------------------------------------------------------------------------------- /nitric/config/__init__.py: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright (c) 2021 Nitric Technologies Pty Ltd. 3 | # 4 | # This file is part of Nitric Python 3 SDK. 5 | # See https://github.com/nitrictech/python-sdk for further info. 6 | # 7 | # Licensed under the Apache License, Version 2.0 (the "License"); 8 | # you may not use this file except in compliance with the License. 9 | # You may obtain a copy of the License at 10 | # 11 | # http://www.apache.org/licenses/LICENSE-2.0 12 | # 13 | # Unless required by applicable law or agreed to in writing, software 14 | # distributed under the License is distributed on an "AS IS" BASIS, 15 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 16 | # See the License for the specific language governing permissions and 17 | # limitations under the License. 18 | # 19 | """Nitric SDK Configuration Settings.""" 20 | 21 | import os 22 | 23 | 24 | class Settings: 25 | """Nitric default and env settings helper class.""" 26 | 27 | def __init__(self): 28 | """Construct a new Nitric settings helper object.""" 29 | self.SERVICE_ADDRESS = os.environ.get("SERVICE_ADDRESS", "127.0.0.1:50051") 30 | 31 | 32 | settings = Settings() 33 | -------------------------------------------------------------------------------- /nitric/exception.py: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright (c) 2021 Nitric Technologies Pty Ltd. 3 | # 4 | # This file is part of Nitric Python 3 SDK. 5 | # See https://github.com/nitrictech/python-sdk for further info. 6 | # 7 | # Licensed under the Apache License, Version 2.0 (the "License"); 8 | # you may not use this file except in compliance with the License. 9 | # You may obtain a copy of the License at 10 | # 11 | # http://www.apache.org/licenses/LICENSE-2.0 12 | # 13 | # Unless required by applicable law or agreed to in writing, software 14 | # distributed under the License is distributed on an "AS IS" BASIS, 15 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 16 | # See the License for the specific language governing permissions and 17 | # limitations under the License. 18 | # 19 | from typing import Callable, Optional, Union 20 | 21 | from grpclib import GRPCError 22 | 23 | 24 | class NitricServiceException(Exception): 25 | """Base exception for all errors returned by Nitric API methods.""" 26 | 27 | pass 28 | 29 | 30 | class AbortedException(NitricServiceException): 31 | """The operation was aborted, typically due to a concurrency issue such as a transaction abort.""" 32 | 33 | pass 34 | 35 | 36 | class AlreadyExistsException(NitricServiceException): 37 | """The entity that a client attempted to create (e.g., file or directory) already exists.""" 38 | 39 | pass 40 | 41 | 42 | class CancelledException(NitricServiceException): 43 | """The operation was cancelled, typically by the caller.""" 44 | 45 | pass 46 | 47 | 48 | class DataLossException(NitricServiceException): 49 | """Unrecoverable data loss or corruption.""" 50 | 51 | pass 52 | 53 | 54 | class DeadlineExceededException(NitricServiceException): 55 | """The deadline expired before the operation could complete.""" 56 | 57 | pass 58 | 59 | 60 | class FailedPreconditionException(NitricServiceException): 61 | """ 62 | The operation was rejected because the system is not in a state required for the operation's execution. 63 | 64 | For example, the document collection to be deleted is not empty. 65 | """ 66 | 67 | pass 68 | 69 | 70 | class InternalException(NitricServiceException): 71 | """Internal errors.""" 72 | 73 | pass 74 | 75 | 76 | class InvalidArgumentException(NitricServiceException): 77 | """ 78 | The client specified an invalid argument. 79 | 80 | Note that this differs from FAILED_PRECONDITION. INVALID_ARGUMENT indicates arguments that are problematic 81 | regardless of the state of the system (e.g., a malformed file name). 82 | """ 83 | 84 | pass 85 | 86 | 87 | class OutOfRangeException(NitricServiceException): 88 | """ 89 | The operation was attempted past the valid range. 90 | 91 | E.g. reading past the end of a file. 92 | """ 93 | 94 | pass 95 | 96 | 97 | class NotFoundException(NitricServiceException): 98 | """Some requested entity was not found.""" 99 | 100 | pass 101 | 102 | 103 | class PermissionDeniedException(NitricServiceException): 104 | """The caller does not have permission to execute the specified operation.""" 105 | 106 | pass 107 | 108 | 109 | class ResourceExhaustedException(NitricServiceException): 110 | """Some resource has been exhausted, perhaps a per-user quota, or perhaps the entire file system is out of space.""" 111 | 112 | pass 113 | 114 | 115 | class UnauthenticatedException(NitricServiceException): 116 | """The request does not have valid authentication credentials for the operation.""" 117 | 118 | pass 119 | 120 | 121 | class UnavailableException(NitricServiceException): 122 | """ 123 | The service is currently unavailable. 124 | 125 | This is most likely a transient condition, which can be corrected by retrying with a backoff. 126 | """ 127 | 128 | pass 129 | 130 | 131 | class UnimplementedException(NitricServiceException): 132 | """ 133 | The operation is not implemented or is not supported/enabled in this service. 134 | 135 | May appear when using an older version of the Membrane with a newer SDK. 136 | """ 137 | 138 | pass 139 | 140 | 141 | class UnknownException(NitricServiceException): 142 | """Unknown error.""" 143 | 144 | pass 145 | 146 | 147 | class NitricResourceException(Exception): 148 | """Illegal nitric resource creation.""" 149 | 150 | pass 151 | 152 | 153 | class NitricUnavailableException(Exception): 154 | """Unable to connect to a nitric server.""" 155 | 156 | def __init__(self, message: str): 157 | super().__init__("Unable to connect to nitric server." + (" " + message if message else "")) 158 | 159 | 160 | class NitricNotRunningException(Exception): 161 | """The Nitric application wasn't started before the program exited.""" 162 | 163 | def __init__(self): 164 | super().__init__("The Nitric application was not started, call Nitric.run() to start the application.") 165 | 166 | 167 | def exception_from_grpc_error(error: GRPCError): 168 | """Translate a gRPC error to a nitric api exception.""" 169 | return exception_from_grpc_code(error.status.value, error.message or "") 170 | 171 | 172 | def exception_from_grpc_code(code: int, message: Optional[str] = None): 173 | """ 174 | Return a new instance of the appropriate exception for the given status code. 175 | 176 | If an unknown or unexpected status code value is provided an UnknownException will be returned. 177 | """ 178 | if code not in _exception_code_map: 179 | return UnknownException() 180 | 181 | return _exception_code_map[code](message) 182 | 183 | 184 | _zero_code_exception: Callable[[Union[str, None]], Exception] = lambda message: Exception( 185 | "Error returned with status 0, which is a success status" 186 | ) 187 | 188 | # Map of gRPC status codes to the appropriate exception class. 189 | _exception_code_map = { 190 | 0: _zero_code_exception, 191 | 1: CancelledException, 192 | 2: UnknownException, 193 | 3: InvalidArgumentException, 194 | 4: DeadlineExceededException, 195 | 5: NotFoundException, 196 | 6: AlreadyExistsException, 197 | 7: PermissionDeniedException, 198 | 8: ResourceExhaustedException, 199 | 9: FailedPreconditionException, 200 | 10: AbortedException, 201 | 11: OutOfRangeException, 202 | 12: UnimplementedException, 203 | 13: InternalException, 204 | 14: UnavailableException, 205 | 15: DataLossException, 206 | 16: UnauthenticatedException, 207 | } 208 | -------------------------------------------------------------------------------- /nitric/proto/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nitrictech/python-sdk/f10852bc2505062893b31bafcbabd16c844e6cb4/nitric/proto/__init__.py -------------------------------------------------------------------------------- /nitric/proto/apis/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nitrictech/python-sdk/f10852bc2505062893b31bafcbabd16c844e6cb4/nitric/proto/apis/__init__.py -------------------------------------------------------------------------------- /nitric/proto/batch/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nitrictech/python-sdk/f10852bc2505062893b31bafcbabd16c844e6cb4/nitric/proto/batch/__init__.py -------------------------------------------------------------------------------- /nitric/proto/batch/v1/__init__.py: -------------------------------------------------------------------------------- 1 | # Generated by the protocol buffer compiler. DO NOT EDIT! 2 | # sources: nitric/proto/batch/v1/batch.proto 3 | # plugin: python-betterproto 4 | # This file has been @generated 5 | 6 | from dataclasses import dataclass 7 | from typing import ( 8 | TYPE_CHECKING, 9 | AsyncIterable, 10 | AsyncIterator, 11 | Dict, 12 | Iterable, 13 | Optional, 14 | Union, 15 | ) 16 | 17 | import betterproto 18 | import betterproto.lib.google.protobuf as betterproto_lib_google_protobuf 19 | import grpclib 20 | from betterproto.grpc.grpclib_server import ServiceBase 21 | 22 | 23 | if TYPE_CHECKING: 24 | import grpclib.server 25 | from betterproto.grpc.grpclib_client import MetadataLike 26 | from grpclib.metadata import Deadline 27 | 28 | 29 | @dataclass(eq=False, repr=False) 30 | class ClientMessage(betterproto.Message): 31 | id: str = betterproto.string_field(1) 32 | """globally unique ID of the request/response pair""" 33 | 34 | registration_request: "RegistrationRequest" = betterproto.message_field( 35 | 2, group="content" 36 | ) 37 | """Register a handler for a job""" 38 | 39 | job_response: "JobResponse" = betterproto.message_field(3, group="content") 40 | """Handle a job submission""" 41 | 42 | 43 | @dataclass(eq=False, repr=False) 44 | class JobRequest(betterproto.Message): 45 | job_name: str = betterproto.string_field(1) 46 | data: "JobData" = betterproto.message_field(2) 47 | 48 | 49 | @dataclass(eq=False, repr=False) 50 | class JobData(betterproto.Message): 51 | struct: "betterproto_lib_google_protobuf.Struct" = betterproto.message_field( 52 | 1, group="data" 53 | ) 54 | 55 | 56 | @dataclass(eq=False, repr=False) 57 | class JobResponse(betterproto.Message): 58 | success: bool = betterproto.bool_field(1) 59 | """Mark if the job was successfully processed""" 60 | 61 | 62 | @dataclass(eq=False, repr=False) 63 | class RegistrationRequest(betterproto.Message): 64 | job_name: str = betterproto.string_field(1) 65 | requirements: "JobResourceRequirements" = betterproto.message_field(2) 66 | """Register with default requirements""" 67 | 68 | 69 | @dataclass(eq=False, repr=False) 70 | class RegistrationResponse(betterproto.Message): 71 | pass 72 | 73 | 74 | @dataclass(eq=False, repr=False) 75 | class JobResourceRequirements(betterproto.Message): 76 | cpus: float = betterproto.float_field(1) 77 | """The number of CPUs to allocate for the job""" 78 | 79 | memory: int = betterproto.int64_field(2) 80 | """The amount of memory to allocate for the job""" 81 | 82 | gpus: int = betterproto.int64_field(3) 83 | """The number of GPUs to allocate for the job""" 84 | 85 | 86 | @dataclass(eq=False, repr=False) 87 | class ServerMessage(betterproto.Message): 88 | """ 89 | ServerMessage is the message sent from the nitric server to the service 90 | """ 91 | 92 | id: str = betterproto.string_field(1) 93 | """globally unique ID of the request/response pair""" 94 | 95 | registration_response: "RegistrationResponse" = betterproto.message_field( 96 | 2, group="content" 97 | ) 98 | """ 99 | 100 | """ 101 | 102 | job_request: "JobRequest" = betterproto.message_field(3, group="content") 103 | """Request to a job handler""" 104 | 105 | 106 | @dataclass(eq=False, repr=False) 107 | class JobSubmitRequest(betterproto.Message): 108 | job_name: str = betterproto.string_field(1) 109 | """The name of the job that should handle the data""" 110 | 111 | data: "JobData" = betterproto.message_field(2) 112 | """The data to be processed by the job""" 113 | 114 | 115 | @dataclass(eq=False, repr=False) 116 | class JobSubmitResponse(betterproto.Message): 117 | pass 118 | 119 | 120 | class JobStub(betterproto.ServiceStub): 121 | async def handle_job( 122 | self, 123 | client_message_iterator: Union[ 124 | AsyncIterable["ClientMessage"], Iterable["ClientMessage"] 125 | ], 126 | *, 127 | timeout: Optional[float] = None, 128 | deadline: Optional["Deadline"] = None, 129 | metadata: Optional["MetadataLike"] = None 130 | ) -> AsyncIterator["ServerMessage"]: 131 | async for response in self._stream_stream( 132 | "/nitric.proto.batch.v1.Job/HandleJob", 133 | client_message_iterator, 134 | ClientMessage, 135 | ServerMessage, 136 | timeout=timeout, 137 | deadline=deadline, 138 | metadata=metadata, 139 | ): 140 | yield response 141 | 142 | 143 | class BatchStub(betterproto.ServiceStub): 144 | async def submit_job( 145 | self, 146 | job_submit_request: "JobSubmitRequest", 147 | *, 148 | timeout: Optional[float] = None, 149 | deadline: Optional["Deadline"] = None, 150 | metadata: Optional["MetadataLike"] = None 151 | ) -> "JobSubmitResponse": 152 | return await self._unary_unary( 153 | "/nitric.proto.batch.v1.Batch/SubmitJob", 154 | job_submit_request, 155 | JobSubmitResponse, 156 | timeout=timeout, 157 | deadline=deadline, 158 | metadata=metadata, 159 | ) 160 | 161 | 162 | class JobBase(ServiceBase): 163 | async def handle_job( 164 | self, client_message_iterator: AsyncIterator["ClientMessage"] 165 | ) -> AsyncIterator["ServerMessage"]: 166 | raise grpclib.GRPCError(grpclib.const.Status.UNIMPLEMENTED) 167 | yield ServerMessage() 168 | 169 | async def __rpc_handle_job( 170 | self, stream: "grpclib.server.Stream[ClientMessage, ServerMessage]" 171 | ) -> None: 172 | request = stream.__aiter__() 173 | await self._call_rpc_handler_server_stream( 174 | self.handle_job, 175 | stream, 176 | request, 177 | ) 178 | 179 | def __mapping__(self) -> Dict[str, grpclib.const.Handler]: 180 | return { 181 | "/nitric.proto.batch.v1.Job/HandleJob": grpclib.const.Handler( 182 | self.__rpc_handle_job, 183 | grpclib.const.Cardinality.STREAM_STREAM, 184 | ClientMessage, 185 | ServerMessage, 186 | ), 187 | } 188 | 189 | 190 | class BatchBase(ServiceBase): 191 | async def submit_job( 192 | self, job_submit_request: "JobSubmitRequest" 193 | ) -> "JobSubmitResponse": 194 | raise grpclib.GRPCError(grpclib.const.Status.UNIMPLEMENTED) 195 | 196 | async def __rpc_submit_job( 197 | self, stream: "grpclib.server.Stream[JobSubmitRequest, JobSubmitResponse]" 198 | ) -> None: 199 | request = await stream.recv_message() 200 | response = await self.submit_job(request) 201 | await stream.send_message(response) 202 | 203 | def __mapping__(self) -> Dict[str, grpclib.const.Handler]: 204 | return { 205 | "/nitric.proto.batch.v1.Batch/SubmitJob": grpclib.const.Handler( 206 | self.__rpc_submit_job, 207 | grpclib.const.Cardinality.UNARY_UNARY, 208 | JobSubmitRequest, 209 | JobSubmitResponse, 210 | ), 211 | } 212 | -------------------------------------------------------------------------------- /nitric/proto/deployments/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nitrictech/python-sdk/f10852bc2505062893b31bafcbabd16c844e6cb4/nitric/proto/deployments/__init__.py -------------------------------------------------------------------------------- /nitric/proto/http/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nitrictech/python-sdk/f10852bc2505062893b31bafcbabd16c844e6cb4/nitric/proto/http/__init__.py -------------------------------------------------------------------------------- /nitric/proto/http/v1/__init__.py: -------------------------------------------------------------------------------- 1 | # Generated by the protocol buffer compiler. DO NOT EDIT! 2 | # sources: nitric/proto/http/v1/http.proto 3 | # plugin: python-betterproto 4 | # This file has been @generated 5 | 6 | from dataclasses import dataclass 7 | from typing import ( 8 | TYPE_CHECKING, 9 | AsyncIterable, 10 | AsyncIterator, 11 | Dict, 12 | Iterable, 13 | Optional, 14 | Union, 15 | ) 16 | 17 | import betterproto 18 | import grpclib 19 | from betterproto.grpc.grpclib_server import ServiceBase 20 | 21 | 22 | if TYPE_CHECKING: 23 | import grpclib.server 24 | from betterproto.grpc.grpclib_client import MetadataLike 25 | from grpclib.metadata import Deadline 26 | 27 | 28 | @dataclass(eq=False, repr=False) 29 | class ClientMessage(betterproto.Message): 30 | request: "HttpProxyRequest" = betterproto.message_field(1) 31 | """Details of the HTTP server to proxy""" 32 | 33 | 34 | @dataclass(eq=False, repr=False) 35 | class ServerMessage(betterproto.Message): 36 | pass 37 | 38 | 39 | @dataclass(eq=False, repr=False) 40 | class HttpProxyRequest(betterproto.Message): 41 | host: str = betterproto.string_field(1) 42 | """The address the server can be accessed on""" 43 | 44 | 45 | class HttpStub(betterproto.ServiceStub): 46 | async def proxy( 47 | self, 48 | client_message_iterator: Union[ 49 | AsyncIterable["ClientMessage"], Iterable["ClientMessage"] 50 | ], 51 | *, 52 | timeout: Optional[float] = None, 53 | deadline: Optional["Deadline"] = None, 54 | metadata: Optional["MetadataLike"] = None 55 | ) -> AsyncIterator["ServerMessage"]: 56 | async for response in self._stream_stream( 57 | "/nitric.proto.http.v1.Http/Proxy", 58 | client_message_iterator, 59 | ClientMessage, 60 | ServerMessage, 61 | timeout=timeout, 62 | deadline=deadline, 63 | metadata=metadata, 64 | ): 65 | yield response 66 | 67 | 68 | class HttpBase(ServiceBase): 69 | async def proxy( 70 | self, client_message_iterator: AsyncIterator["ClientMessage"] 71 | ) -> AsyncIterator["ServerMessage"]: 72 | raise grpclib.GRPCError(grpclib.const.Status.UNIMPLEMENTED) 73 | yield ServerMessage() 74 | 75 | async def __rpc_proxy( 76 | self, stream: "grpclib.server.Stream[ClientMessage, ServerMessage]" 77 | ) -> None: 78 | request = stream.__aiter__() 79 | await self._call_rpc_handler_server_stream( 80 | self.proxy, 81 | stream, 82 | request, 83 | ) 84 | 85 | def __mapping__(self) -> Dict[str, grpclib.const.Handler]: 86 | return { 87 | "/nitric.proto.http.v1.Http/Proxy": grpclib.const.Handler( 88 | self.__rpc_proxy, 89 | grpclib.const.Cardinality.STREAM_STREAM, 90 | ClientMessage, 91 | ServerMessage, 92 | ), 93 | } 94 | -------------------------------------------------------------------------------- /nitric/proto/kvstore/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nitrictech/python-sdk/f10852bc2505062893b31bafcbabd16c844e6cb4/nitric/proto/kvstore/__init__.py -------------------------------------------------------------------------------- /nitric/proto/queues/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nitrictech/python-sdk/f10852bc2505062893b31bafcbabd16c844e6cb4/nitric/proto/queues/__init__.py -------------------------------------------------------------------------------- /nitric/proto/resources/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nitrictech/python-sdk/f10852bc2505062893b31bafcbabd16c844e6cb4/nitric/proto/resources/__init__.py -------------------------------------------------------------------------------- /nitric/proto/schedules/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nitrictech/python-sdk/f10852bc2505062893b31bafcbabd16c844e6cb4/nitric/proto/schedules/__init__.py -------------------------------------------------------------------------------- /nitric/proto/schedules/v1/__init__.py: -------------------------------------------------------------------------------- 1 | # Generated by the protocol buffer compiler. DO NOT EDIT! 2 | # sources: nitric/proto/schedules/v1/schedules.proto 3 | # plugin: python-betterproto 4 | # This file has been @generated 5 | 6 | from dataclasses import dataclass 7 | from typing import ( 8 | TYPE_CHECKING, 9 | AsyncIterable, 10 | AsyncIterator, 11 | Dict, 12 | Iterable, 13 | Optional, 14 | Union, 15 | ) 16 | 17 | import betterproto 18 | import grpclib 19 | from betterproto.grpc.grpclib_server import ServiceBase 20 | 21 | 22 | if TYPE_CHECKING: 23 | import grpclib.server 24 | from betterproto.grpc.grpclib_client import MetadataLike 25 | from grpclib.metadata import Deadline 26 | 27 | 28 | @dataclass(eq=False, repr=False) 29 | class ClientMessage(betterproto.Message): 30 | """ClientMessages are sent from the service to the nitric server""" 31 | 32 | id: str = betterproto.string_field(1) 33 | """globally unique ID of the request/response pair""" 34 | 35 | registration_request: "RegistrationRequest" = betterproto.message_field( 36 | 2, group="content" 37 | ) 38 | """Register new a schedule""" 39 | 40 | interval_response: "IntervalResponse" = betterproto.message_field( 41 | 3, group="content" 42 | ) 43 | """ 44 | Response to a schedule interval (i.e. response from callback function) 45 | """ 46 | 47 | 48 | @dataclass(eq=False, repr=False) 49 | class IntervalRequest(betterproto.Message): 50 | schedule_name: str = betterproto.string_field(1) 51 | 52 | 53 | @dataclass(eq=False, repr=False) 54 | class ServerMessage(betterproto.Message): 55 | """ServerMessages are sent from the nitric server to the service""" 56 | 57 | id: str = betterproto.string_field(1) 58 | """globally unique ID of the request/response pair""" 59 | 60 | registration_response: "RegistrationResponse" = betterproto.message_field( 61 | 2, group="content" 62 | ) 63 | """Response to a schedule subscription request""" 64 | 65 | interval_request: "IntervalRequest" = betterproto.message_field(3, group="content") 66 | """A schedule interval trigger request (i.e. call the callback)""" 67 | 68 | 69 | @dataclass(eq=False, repr=False) 70 | class RegistrationRequest(betterproto.Message): 71 | schedule_name: str = betterproto.string_field(1) 72 | every: "ScheduleEvery" = betterproto.message_field(10, group="cadence") 73 | cron: "ScheduleCron" = betterproto.message_field(11, group="cadence") 74 | 75 | 76 | @dataclass(eq=False, repr=False) 77 | class ScheduleEvery(betterproto.Message): 78 | rate: str = betterproto.string_field(1) 79 | 80 | 81 | @dataclass(eq=False, repr=False) 82 | class ScheduleCron(betterproto.Message): 83 | expression: str = betterproto.string_field(1) 84 | 85 | 86 | @dataclass(eq=False, repr=False) 87 | class RegistrationResponse(betterproto.Message): 88 | pass 89 | 90 | 91 | @dataclass(eq=False, repr=False) 92 | class IntervalResponse(betterproto.Message): 93 | pass 94 | 95 | 96 | class SchedulesStub(betterproto.ServiceStub): 97 | async def schedule( 98 | self, 99 | client_message_iterator: Union[ 100 | AsyncIterable["ClientMessage"], Iterable["ClientMessage"] 101 | ], 102 | *, 103 | timeout: Optional[float] = None, 104 | deadline: Optional["Deadline"] = None, 105 | metadata: Optional["MetadataLike"] = None 106 | ) -> AsyncIterator["ServerMessage"]: 107 | async for response in self._stream_stream( 108 | "/nitric.proto.schedules.v1.Schedules/Schedule", 109 | client_message_iterator, 110 | ClientMessage, 111 | ServerMessage, 112 | timeout=timeout, 113 | deadline=deadline, 114 | metadata=metadata, 115 | ): 116 | yield response 117 | 118 | 119 | class SchedulesBase(ServiceBase): 120 | async def schedule( 121 | self, client_message_iterator: AsyncIterator["ClientMessage"] 122 | ) -> AsyncIterator["ServerMessage"]: 123 | raise grpclib.GRPCError(grpclib.const.Status.UNIMPLEMENTED) 124 | yield ServerMessage() 125 | 126 | async def __rpc_schedule( 127 | self, stream: "grpclib.server.Stream[ClientMessage, ServerMessage]" 128 | ) -> None: 129 | request = stream.__aiter__() 130 | await self._call_rpc_handler_server_stream( 131 | self.schedule, 132 | stream, 133 | request, 134 | ) 135 | 136 | def __mapping__(self) -> Dict[str, grpclib.const.Handler]: 137 | return { 138 | "/nitric.proto.schedules.v1.Schedules/Schedule": grpclib.const.Handler( 139 | self.__rpc_schedule, 140 | grpclib.const.Cardinality.STREAM_STREAM, 141 | ClientMessage, 142 | ServerMessage, 143 | ), 144 | } 145 | -------------------------------------------------------------------------------- /nitric/proto/secrets/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nitrictech/python-sdk/f10852bc2505062893b31bafcbabd16c844e6cb4/nitric/proto/secrets/__init__.py -------------------------------------------------------------------------------- /nitric/proto/secrets/v1/__init__.py: -------------------------------------------------------------------------------- 1 | # Generated by the protocol buffer compiler. DO NOT EDIT! 2 | # sources: nitric/proto/secrets/v1/secrets.proto 3 | # plugin: python-betterproto 4 | # This file has been @generated 5 | 6 | from dataclasses import dataclass 7 | from typing import ( 8 | TYPE_CHECKING, 9 | Dict, 10 | Optional, 11 | ) 12 | 13 | import betterproto 14 | import grpclib 15 | from betterproto.grpc.grpclib_server import ServiceBase 16 | 17 | 18 | if TYPE_CHECKING: 19 | import grpclib.server 20 | from betterproto.grpc.grpclib_client import MetadataLike 21 | from grpclib.metadata import Deadline 22 | 23 | 24 | @dataclass(eq=False, repr=False) 25 | class SecretPutRequest(betterproto.Message): 26 | """Request to put a secret to a Secret Store""" 27 | 28 | secret: "Secret" = betterproto.message_field(1) 29 | """The Secret to put to the Secret store""" 30 | 31 | value: bytes = betterproto.bytes_field(2) 32 | """The value to assign to that secret""" 33 | 34 | 35 | @dataclass(eq=False, repr=False) 36 | class SecretPutResponse(betterproto.Message): 37 | """Result from putting the secret to a Secret Store""" 38 | 39 | secret_version: "SecretVersion" = betterproto.message_field(1) 40 | """The id of the secret""" 41 | 42 | 43 | @dataclass(eq=False, repr=False) 44 | class SecretAccessRequest(betterproto.Message): 45 | """Request to get a secret from a Secret Store""" 46 | 47 | secret_version: "SecretVersion" = betterproto.message_field(1) 48 | """The id of the secret""" 49 | 50 | 51 | @dataclass(eq=False, repr=False) 52 | class SecretAccessResponse(betterproto.Message): 53 | """The secret response""" 54 | 55 | secret_version: "SecretVersion" = betterproto.message_field(1) 56 | """The version of the secret that was requested""" 57 | 58 | value: bytes = betterproto.bytes_field(2) 59 | """The value of the secret""" 60 | 61 | 62 | @dataclass(eq=False, repr=False) 63 | class Secret(betterproto.Message): 64 | """The secret container""" 65 | 66 | name: str = betterproto.string_field(1) 67 | """The secret name""" 68 | 69 | 70 | @dataclass(eq=False, repr=False) 71 | class SecretVersion(betterproto.Message): 72 | """A version of a secret""" 73 | 74 | secret: "Secret" = betterproto.message_field(1) 75 | """Reference to the secret container""" 76 | 77 | version: str = betterproto.string_field(2) 78 | """The secret version""" 79 | 80 | 81 | class SecretManagerStub(betterproto.ServiceStub): 82 | async def put( 83 | self, 84 | secret_put_request: "SecretPutRequest", 85 | *, 86 | timeout: Optional[float] = None, 87 | deadline: Optional["Deadline"] = None, 88 | metadata: Optional["MetadataLike"] = None 89 | ) -> "SecretPutResponse": 90 | return await self._unary_unary( 91 | "/nitric.proto.secrets.v1.SecretManager/Put", 92 | secret_put_request, 93 | SecretPutResponse, 94 | timeout=timeout, 95 | deadline=deadline, 96 | metadata=metadata, 97 | ) 98 | 99 | async def access( 100 | self, 101 | secret_access_request: "SecretAccessRequest", 102 | *, 103 | timeout: Optional[float] = None, 104 | deadline: Optional["Deadline"] = None, 105 | metadata: Optional["MetadataLike"] = None 106 | ) -> "SecretAccessResponse": 107 | return await self._unary_unary( 108 | "/nitric.proto.secrets.v1.SecretManager/Access", 109 | secret_access_request, 110 | SecretAccessResponse, 111 | timeout=timeout, 112 | deadline=deadline, 113 | metadata=metadata, 114 | ) 115 | 116 | 117 | class SecretManagerBase(ServiceBase): 118 | async def put(self, secret_put_request: "SecretPutRequest") -> "SecretPutResponse": 119 | raise grpclib.GRPCError(grpclib.const.Status.UNIMPLEMENTED) 120 | 121 | async def access( 122 | self, secret_access_request: "SecretAccessRequest" 123 | ) -> "SecretAccessResponse": 124 | raise grpclib.GRPCError(grpclib.const.Status.UNIMPLEMENTED) 125 | 126 | async def __rpc_put( 127 | self, stream: "grpclib.server.Stream[SecretPutRequest, SecretPutResponse]" 128 | ) -> None: 129 | request = await stream.recv_message() 130 | response = await self.put(request) 131 | await stream.send_message(response) 132 | 133 | async def __rpc_access( 134 | self, stream: "grpclib.server.Stream[SecretAccessRequest, SecretAccessResponse]" 135 | ) -> None: 136 | request = await stream.recv_message() 137 | response = await self.access(request) 138 | await stream.send_message(response) 139 | 140 | def __mapping__(self) -> Dict[str, grpclib.const.Handler]: 141 | return { 142 | "/nitric.proto.secrets.v1.SecretManager/Put": grpclib.const.Handler( 143 | self.__rpc_put, 144 | grpclib.const.Cardinality.UNARY_UNARY, 145 | SecretPutRequest, 146 | SecretPutResponse, 147 | ), 148 | "/nitric.proto.secrets.v1.SecretManager/Access": grpclib.const.Handler( 149 | self.__rpc_access, 150 | grpclib.const.Cardinality.UNARY_UNARY, 151 | SecretAccessRequest, 152 | SecretAccessResponse, 153 | ), 154 | } 155 | -------------------------------------------------------------------------------- /nitric/proto/sql/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nitrictech/python-sdk/f10852bc2505062893b31bafcbabd16c844e6cb4/nitric/proto/sql/__init__.py -------------------------------------------------------------------------------- /nitric/proto/sql/v1/__init__.py: -------------------------------------------------------------------------------- 1 | # Generated by the protocol buffer compiler. DO NOT EDIT! 2 | # sources: nitric/proto/sql/v1/sql.proto 3 | # plugin: python-betterproto 4 | # This file has been @generated 5 | 6 | from dataclasses import dataclass 7 | from typing import ( 8 | TYPE_CHECKING, 9 | Dict, 10 | Optional, 11 | ) 12 | 13 | import betterproto 14 | import grpclib 15 | from betterproto.grpc.grpclib_server import ServiceBase 16 | 17 | 18 | if TYPE_CHECKING: 19 | import grpclib.server 20 | from betterproto.grpc.grpclib_client import MetadataLike 21 | from grpclib.metadata import Deadline 22 | 23 | 24 | @dataclass(eq=False, repr=False) 25 | class SqlConnectionStringRequest(betterproto.Message): 26 | database_name: str = betterproto.string_field(1) 27 | """The name of the database to retrieve the connection string for""" 28 | 29 | 30 | @dataclass(eq=False, repr=False) 31 | class SqlConnectionStringResponse(betterproto.Message): 32 | connection_string: str = betterproto.string_field(1) 33 | """The connection string for the database""" 34 | 35 | 36 | class SqlStub(betterproto.ServiceStub): 37 | async def connection_string( 38 | self, 39 | sql_connection_string_request: "SqlConnectionStringRequest", 40 | *, 41 | timeout: Optional[float] = None, 42 | deadline: Optional["Deadline"] = None, 43 | metadata: Optional["MetadataLike"] = None 44 | ) -> "SqlConnectionStringResponse": 45 | return await self._unary_unary( 46 | "/nitric.proto.sql.v1.Sql/ConnectionString", 47 | sql_connection_string_request, 48 | SqlConnectionStringResponse, 49 | timeout=timeout, 50 | deadline=deadline, 51 | metadata=metadata, 52 | ) 53 | 54 | 55 | class SqlBase(ServiceBase): 56 | async def connection_string( 57 | self, sql_connection_string_request: "SqlConnectionStringRequest" 58 | ) -> "SqlConnectionStringResponse": 59 | raise grpclib.GRPCError(grpclib.const.Status.UNIMPLEMENTED) 60 | 61 | async def __rpc_connection_string( 62 | self, 63 | stream: "grpclib.server.Stream[SqlConnectionStringRequest, SqlConnectionStringResponse]", 64 | ) -> None: 65 | request = await stream.recv_message() 66 | response = await self.connection_string(request) 67 | await stream.send_message(response) 68 | 69 | def __mapping__(self) -> Dict[str, grpclib.const.Handler]: 70 | return { 71 | "/nitric.proto.sql.v1.Sql/ConnectionString": grpclib.const.Handler( 72 | self.__rpc_connection_string, 73 | grpclib.const.Cardinality.UNARY_UNARY, 74 | SqlConnectionStringRequest, 75 | SqlConnectionStringResponse, 76 | ), 77 | } 78 | -------------------------------------------------------------------------------- /nitric/proto/storage/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nitrictech/python-sdk/f10852bc2505062893b31bafcbabd16c844e6cb4/nitric/proto/storage/__init__.py -------------------------------------------------------------------------------- /nitric/proto/topics/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nitrictech/python-sdk/f10852bc2505062893b31bafcbabd16c844e6cb4/nitric/proto/topics/__init__.py -------------------------------------------------------------------------------- /nitric/proto/topics/v1/__init__.py: -------------------------------------------------------------------------------- 1 | # Generated by the protocol buffer compiler. DO NOT EDIT! 2 | # sources: nitric/proto/topics/v1/topics.proto 3 | # plugin: python-betterproto 4 | # This file has been @generated 5 | 6 | from dataclasses import dataclass 7 | from datetime import timedelta 8 | from typing import ( 9 | TYPE_CHECKING, 10 | AsyncIterable, 11 | AsyncIterator, 12 | Dict, 13 | Iterable, 14 | Optional, 15 | Union, 16 | ) 17 | 18 | import betterproto 19 | import betterproto.lib.google.protobuf as betterproto_lib_google_protobuf 20 | import grpclib 21 | from betterproto.grpc.grpclib_server import ServiceBase 22 | 23 | 24 | if TYPE_CHECKING: 25 | import grpclib.server 26 | from betterproto.grpc.grpclib_client import MetadataLike 27 | from grpclib.metadata import Deadline 28 | 29 | 30 | @dataclass(eq=False, repr=False) 31 | class ClientMessage(betterproto.Message): 32 | """ 33 | ClientMessage is the message sent from the service to the nitric server 34 | """ 35 | 36 | id: str = betterproto.string_field(1) 37 | """globally unique ID of the request/response pair""" 38 | 39 | registration_request: "RegistrationRequest" = betterproto.message_field( 40 | 2, group="content" 41 | ) 42 | """Register a subscription to a topic""" 43 | 44 | message_response: "MessageResponse" = betterproto.message_field(3, group="content") 45 | """Handle a message received from a topic""" 46 | 47 | 48 | @dataclass(eq=False, repr=False) 49 | class MessageRequest(betterproto.Message): 50 | topic_name: str = betterproto.string_field(1) 51 | message: "TopicMessage" = betterproto.message_field(2) 52 | """Message Type""" 53 | 54 | 55 | @dataclass(eq=False, repr=False) 56 | class MessageResponse(betterproto.Message): 57 | success: bool = betterproto.bool_field(1) 58 | 59 | 60 | @dataclass(eq=False, repr=False) 61 | class ServerMessage(betterproto.Message): 62 | """ 63 | ServerMessage is the message sent from the nitric server to the service 64 | """ 65 | 66 | id: str = betterproto.string_field(1) 67 | """globally unique ID of the request/response pair""" 68 | 69 | registration_response: "RegistrationResponse" = betterproto.message_field( 70 | 2, group="content" 71 | ) 72 | """Response to a topic subscription request""" 73 | 74 | message_request: "MessageRequest" = betterproto.message_field(3, group="content") 75 | """Response to a topic message request""" 76 | 77 | 78 | @dataclass(eq=False, repr=False) 79 | class RegistrationRequest(betterproto.Message): 80 | topic_name: str = betterproto.string_field(1) 81 | 82 | 83 | @dataclass(eq=False, repr=False) 84 | class RegistrationResponse(betterproto.Message): 85 | pass 86 | 87 | 88 | @dataclass(eq=False, repr=False) 89 | class TopicMessage(betterproto.Message): 90 | struct_payload: "betterproto_lib_google_protobuf.Struct" = ( 91 | betterproto.message_field(1, group="content") 92 | ) 93 | 94 | 95 | @dataclass(eq=False, repr=False) 96 | class TopicPublishRequest(betterproto.Message): 97 | """Request to publish a message to a topic""" 98 | 99 | topic_name: str = betterproto.string_field(1) 100 | """The name of the topic to publish the topic to""" 101 | 102 | message: "TopicMessage" = betterproto.message_field(2) 103 | """The message to be published""" 104 | 105 | delay: timedelta = betterproto.message_field(3) 106 | """An optional delay specified in seconds (minimum 10 seconds)""" 107 | 108 | 109 | @dataclass(eq=False, repr=False) 110 | class TopicPublishResponse(betterproto.Message): 111 | """Result of publishing an topic""" 112 | 113 | pass 114 | 115 | 116 | class TopicsStub(betterproto.ServiceStub): 117 | async def publish( 118 | self, 119 | topic_publish_request: "TopicPublishRequest", 120 | *, 121 | timeout: Optional[float] = None, 122 | deadline: Optional["Deadline"] = None, 123 | metadata: Optional["MetadataLike"] = None 124 | ) -> "TopicPublishResponse": 125 | return await self._unary_unary( 126 | "/nitric.proto.topics.v1.Topics/Publish", 127 | topic_publish_request, 128 | TopicPublishResponse, 129 | timeout=timeout, 130 | deadline=deadline, 131 | metadata=metadata, 132 | ) 133 | 134 | 135 | class SubscriberStub(betterproto.ServiceStub): 136 | async def subscribe( 137 | self, 138 | client_message_iterator: Union[ 139 | AsyncIterable["ClientMessage"], Iterable["ClientMessage"] 140 | ], 141 | *, 142 | timeout: Optional[float] = None, 143 | deadline: Optional["Deadline"] = None, 144 | metadata: Optional["MetadataLike"] = None 145 | ) -> AsyncIterator["ServerMessage"]: 146 | async for response in self._stream_stream( 147 | "/nitric.proto.topics.v1.Subscriber/Subscribe", 148 | client_message_iterator, 149 | ClientMessage, 150 | ServerMessage, 151 | timeout=timeout, 152 | deadline=deadline, 153 | metadata=metadata, 154 | ): 155 | yield response 156 | 157 | 158 | class TopicsBase(ServiceBase): 159 | async def publish( 160 | self, topic_publish_request: "TopicPublishRequest" 161 | ) -> "TopicPublishResponse": 162 | raise grpclib.GRPCError(grpclib.const.Status.UNIMPLEMENTED) 163 | 164 | async def __rpc_publish( 165 | self, stream: "grpclib.server.Stream[TopicPublishRequest, TopicPublishResponse]" 166 | ) -> None: 167 | request = await stream.recv_message() 168 | response = await self.publish(request) 169 | await stream.send_message(response) 170 | 171 | def __mapping__(self) -> Dict[str, grpclib.const.Handler]: 172 | return { 173 | "/nitric.proto.topics.v1.Topics/Publish": grpclib.const.Handler( 174 | self.__rpc_publish, 175 | grpclib.const.Cardinality.UNARY_UNARY, 176 | TopicPublishRequest, 177 | TopicPublishResponse, 178 | ), 179 | } 180 | 181 | 182 | class SubscriberBase(ServiceBase): 183 | async def subscribe( 184 | self, client_message_iterator: AsyncIterator["ClientMessage"] 185 | ) -> AsyncIterator["ServerMessage"]: 186 | raise grpclib.GRPCError(grpclib.const.Status.UNIMPLEMENTED) 187 | yield ServerMessage() 188 | 189 | async def __rpc_subscribe( 190 | self, stream: "grpclib.server.Stream[ClientMessage, ServerMessage]" 191 | ) -> None: 192 | request = stream.__aiter__() 193 | await self._call_rpc_handler_server_stream( 194 | self.subscribe, 195 | stream, 196 | request, 197 | ) 198 | 199 | def __mapping__(self) -> Dict[str, grpclib.const.Handler]: 200 | return { 201 | "/nitric.proto.topics.v1.Subscriber/Subscribe": grpclib.const.Handler( 202 | self.__rpc_subscribe, 203 | grpclib.const.Cardinality.STREAM_STREAM, 204 | ClientMessage, 205 | ServerMessage, 206 | ), 207 | } 208 | -------------------------------------------------------------------------------- /nitric/proto/websockets/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nitrictech/python-sdk/f10852bc2505062893b31bafcbabd16c844e6cb4/nitric/proto/websockets/__init__.py -------------------------------------------------------------------------------- /nitric/py.typed: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nitrictech/python-sdk/f10852bc2505062893b31bafcbabd16c844e6cb4/nitric/py.typed -------------------------------------------------------------------------------- /nitric/resources/__init__.py: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright (c) 2021 Nitric Technologies Pty Ltd. 3 | # 4 | # This file is part of Nitric Python 3 SDK. 5 | # See https://github.com/nitrictech/python-sdk for further info. 6 | # 7 | # Licensed under the Apache License, Version 2.0 (the "License"); 8 | # you may not use this file except in compliance with the License. 9 | # You may obtain a copy of the License at 10 | # 11 | # http://www.apache.org/licenses/LICENSE-2.0 12 | # 13 | # Unless required by applicable law or agreed to in writing, software 14 | # distributed under the License is distributed on an "AS IS" BASIS, 15 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 16 | # See the License for the specific language governing permissions and 17 | # limitations under the License. 18 | # 19 | """Nitric Python SDK API Documentation. See: https://nitric.io/docs?lang=python for full framework documentation.""" 20 | 21 | from nitric.resources.apis import Api, api, ApiOptions, ApiDetails, JwtSecurityDefinition, oidc_rule 22 | from nitric.resources.buckets import Bucket, bucket, BucketNotificationContext, FileNotificationContext 23 | from nitric.resources.kv import KeyValueStoreRef, kv 24 | from nitric.resources.schedules import ScheduleServer, schedule 25 | from nitric.resources.secrets import Secret, secret 26 | from nitric.resources.topics import Topic, topic 27 | from nitric.resources.websockets import Websocket, websocket 28 | from nitric.resources.queues import Queue, queue 29 | from nitric.resources.sql import Sql, sql 30 | from nitric.resources.job import job, Job 31 | 32 | __all__ = [ 33 | "api", 34 | "Api", 35 | "ApiOptions", 36 | "ApiDetails", 37 | "JwtSecurityDefinition", 38 | "bucket", 39 | "Bucket", 40 | "BucketNotificationContext", 41 | "FileNotificationContext", 42 | "kv", 43 | "KeyValueStoreRef", 44 | "job", 45 | "Job", 46 | "oidc_rule", 47 | "queue", 48 | "Queue", 49 | "ScheduleServer", 50 | "schedule", 51 | "secret", 52 | "Secret", 53 | "sql", 54 | "Sql", 55 | "topic", 56 | "Topic", 57 | "websocket", 58 | "Websocket", 59 | ] 60 | -------------------------------------------------------------------------------- /nitric/resources/job.py: -------------------------------------------------------------------------------- 1 | from nitric.resources.resource import SecureResource 2 | from nitric.application import Nitric 3 | from nitric.proto.resources.v1 import ( 4 | Action, 5 | JobResource, 6 | ResourceDeclareRequest, 7 | ResourceIdentifier, 8 | ResourceType, 9 | ) 10 | from nitric.context import JobContext 11 | import logging 12 | import betterproto 13 | from nitric.proto.batch.v1 import ( 14 | BatchStub, 15 | JobSubmitRequest, 16 | JobData, 17 | JobStub, 18 | RegistrationRequest, 19 | ClientMessage, 20 | JobResponse as ProtoJobResponse, 21 | JobResourceRequirements, 22 | ) 23 | from nitric.exception import exception_from_grpc_error 24 | from grpclib import GRPCError 25 | from grpclib.client import Channel 26 | from typing import Callable, Any, Optional, Literal, List 27 | from nitric.context import FunctionServer, Handler 28 | from nitric.channel import ChannelManager 29 | from nitric.bidi import AsyncNotifierList 30 | from nitric.utils import struct_from_dict 31 | import grpclib 32 | 33 | 34 | JobPermission = Literal["submit"] 35 | JobHandle = Handler[JobContext] 36 | 37 | 38 | class JobHandler(FunctionServer): 39 | """Function worker for Jobs.""" 40 | 41 | _handler: JobHandle 42 | _registration_request: RegistrationRequest 43 | _responses: AsyncNotifierList[ClientMessage] 44 | 45 | def __init__( 46 | self, 47 | job_name: str, 48 | handler: JobHandle, 49 | cpus: float | None = None, 50 | memory: int | None = None, 51 | gpus: int | None = None, 52 | ): 53 | """Construct a new JobHandler.""" 54 | self._handler = handler 55 | self._responses = AsyncNotifierList() 56 | self._registration_request = RegistrationRequest( 57 | job_name=job_name, 58 | requirements=JobResourceRequirements( 59 | cpus=cpus if cpus is not None else 0, 60 | memory=memory if memory is not None else 0, 61 | gpus=gpus if gpus is not None else 0, 62 | ), 63 | ) 64 | 65 | async def _message_request_iterator(self): 66 | # Register with the server 67 | yield ClientMessage(registration_request=self._registration_request) 68 | # wait for any responses for the server and send them 69 | async for response in self._responses: 70 | yield response 71 | 72 | async def start(self) -> None: 73 | """Register this job handler and listen for tasks.""" 74 | channel = ChannelManager.get_channel() 75 | server = JobStub(channel=channel) 76 | 77 | try: 78 | async for server_msg in server.handle_job(self._message_request_iterator()): 79 | msg_type, _ = betterproto.which_one_of(server_msg, "content") 80 | 81 | if msg_type == "registration_response": 82 | continue 83 | if msg_type == "job_request": 84 | ctx = JobContext._from_request(server_msg) 85 | 86 | response: ClientMessage 87 | try: 88 | resp_ctx = await self._handler(ctx) 89 | if resp_ctx is None: 90 | resp_ctx = ctx 91 | 92 | response = ClientMessage( 93 | id=server_msg.id, 94 | job_response=ProtoJobResponse(success=ctx.res.success), 95 | ) 96 | except Exception as e: # pylint: disable=broad-except 97 | logging.exception("An unhandled error occurred in a job event handler: %s", e) 98 | response = ClientMessage(id=server_msg.id, job_response=ProtoJobResponse(success=False)) 99 | await self._responses.add_item(response) 100 | except grpclib.exceptions.GRPCError as e: 101 | print(f"Stream terminated: {e.message}") 102 | except grpclib.exceptions.StreamTerminatedError: 103 | print("Stream from membrane closed.") 104 | finally: 105 | print("Closing client stream") 106 | channel.close() 107 | 108 | 109 | class JobRef: 110 | """A reference to a deployed job, used to interact with the job at runtime.""" 111 | 112 | _channel: Channel 113 | _stub: BatchStub 114 | name: str 115 | 116 | def __init__(self, name: str) -> None: 117 | """Construct a reference to a deployed Job.""" 118 | self._channel: Channel = ChannelManager.get_channel() 119 | self._stub = BatchStub(channel=self._channel) 120 | self.name = name 121 | 122 | def __del__(self) -> None: 123 | # close the channel when this client is destroyed 124 | if self._channel is not None: 125 | self._channel.close() 126 | 127 | async def submit(self, data: dict[str, Any]) -> None: 128 | """Submit a new execution for this job definition.""" 129 | await self._stub.submit_job( 130 | job_submit_request=JobSubmitRequest(job_name=self.name, data=JobData(struct=struct_from_dict(data))) 131 | ) 132 | 133 | 134 | class Job(SecureResource): 135 | """A Job Definition.""" 136 | 137 | name: str 138 | 139 | def __init__(self, name: str): 140 | """Job definition constructor.""" 141 | super().__init__(name) 142 | self.name = name 143 | 144 | async def _register(self) -> None: 145 | try: 146 | await self._resources_stub.declare( 147 | resource_declare_request=ResourceDeclareRequest( 148 | id=_to_resource_identifier(self), 149 | job=JobResource(), 150 | ) 151 | ) 152 | 153 | except GRPCError as grpc_err: 154 | raise exception_from_grpc_error(grpc_err) from grpc_err 155 | 156 | def _perms_to_actions(self, *args: JobPermission) -> List[Action]: 157 | _permMap: dict[JobPermission, List[Action]] = {"submit": [Action.JobSubmit]} 158 | 159 | return [action for perm in args for action in _permMap[perm]] 160 | 161 | def allow(self, perm: JobPermission, *args: JobPermission) -> JobRef: 162 | """Request the specified permissions to this resource.""" 163 | str_args = [perm] + [str(permission) for permission in args] 164 | self._register_policy(*str_args) 165 | 166 | return JobRef(self.name) 167 | 168 | def _to_resource_id(self) -> ResourceIdentifier: 169 | return ResourceIdentifier(name=self.name, type=ResourceType.Job) 170 | 171 | def __call__( 172 | self, cpus: Optional[float] = None, memory: Optional[int] = None, gpus: Optional[int] = None 173 | ) -> Callable[[JobHandle], None]: 174 | """Define the handler for this job definition.""" 175 | 176 | def decorator(function: JobHandle) -> None: 177 | wrkr = JobHandler(self.name, function, cpus, memory, gpus) 178 | Nitric._register_worker(wrkr) 179 | 180 | return decorator 181 | 182 | 183 | def _to_resource_identifier(b: Job) -> ResourceIdentifier: 184 | return ResourceIdentifier(name=b.name, type=ResourceType.Job) 185 | 186 | 187 | def job(name: str) -> Job: 188 | """ 189 | Create and register a job. 190 | 191 | If a job has already been registered with the same name, the original reference will be reused. 192 | """ 193 | # type ignored because the create call are treated as protected. 194 | return Nitric._create_resource(Job, name) # type: ignore pylint: disable=protected-access 195 | -------------------------------------------------------------------------------- /nitric/resources/kv.py: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright (c) 2021 Nitric Technologies Pty Ltd. 3 | # 4 | # This file is part of Nitric Python 3 SDK. 5 | # See https://github.com/nitrictech/python-sdk for further info. 6 | # 7 | # Licensed under the Apache License, Version 2.0 (the "License"); 8 | # you may not use this file except in compliance with the License. 9 | # You may obtain a copy of the License at 10 | # 11 | # http://www.apache.org/licenses/LICENSE-2.0 12 | # 13 | # Unless required by applicable law or agreed to in writing, software 14 | # distributed under the License is distributed on an "AS IS" BASIS, 15 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 16 | # See the License for the specific language governing permissions and 17 | # limitations under the License. 18 | # 19 | from __future__ import annotations 20 | 21 | from typing import Any, List, Literal, AsyncIterator, Optional 22 | 23 | from grpclib import GRPCError 24 | from grpclib.client import Channel 25 | 26 | from nitric.application import Nitric 27 | from nitric.exception import exception_from_grpc_error 28 | from nitric.proto.kvstore.v1 import ( 29 | Store, 30 | KvStoreScanKeysRequest, 31 | KvStoreDeleteKeyRequest, 32 | KvStoreGetValueRequest, 33 | KvStoreSetValueRequest, 34 | KvStoreStub, 35 | ValueRef, 36 | ) 37 | from nitric.proto.resources.v1 import ( 38 | Action, 39 | KeyValueStoreResource, 40 | ResourceDeclareRequest, 41 | ResourceIdentifier, 42 | ResourceType, 43 | ) 44 | from nitric.resources.resource import SecureResource 45 | from nitric.utils import dict_from_struct, struct_from_dict 46 | from nitric.channel import ChannelManager 47 | 48 | 49 | class KeyValueStoreRef: 50 | """A reference to a deployed key value store, used to interact with the key value store at runtime.""" 51 | 52 | _kv_stub: KvStoreStub 53 | _channel: Channel 54 | name: str 55 | 56 | def __init__(self, name: str): 57 | """Construct a reference to a deployed key value store.""" 58 | self._channel: Channel = ChannelManager.get_channel() 59 | self._kv_stub = KvStoreStub(channel=self._channel) 60 | self.name = name 61 | 62 | def __del__(self): 63 | # close the channel when this client is destroyed 64 | if self._channel is not None: 65 | self._channel.close() 66 | 67 | async def set(self, key: str, value: dict[str, Any]) -> None: 68 | """Set a key and value in the key value store.""" 69 | ref = ValueRef(store=self.name, key=key) 70 | 71 | req = KvStoreSetValueRequest(ref=ref, content=struct_from_dict(value)) 72 | 73 | try: 74 | await self._kv_stub.set_value(kv_store_set_value_request=req) 75 | except GRPCError as grpc_err: 76 | raise exception_from_grpc_error(grpc_err) from grpc_err 77 | 78 | async def get(self, key: str) -> dict[str, Any]: 79 | """Return a value from the key value store.""" 80 | ref = ValueRef(store=self.name, key=key) 81 | 82 | req = KvStoreGetValueRequest(ref=ref) 83 | 84 | try: 85 | resp = await self._kv_stub.get_value(kv_store_get_value_request=req) 86 | 87 | return dict_from_struct(resp.value.content) 88 | except GRPCError as grpc_err: 89 | raise exception_from_grpc_error(grpc_err) from grpc_err 90 | 91 | async def keys(self, prefix: Optional[str] = "") -> AsyncIterator[str]: 92 | """Return a list of keys from the key value store.""" 93 | if prefix is None: 94 | prefix = "" 95 | 96 | req = KvStoreScanKeysRequest( 97 | store=Store(name=self.name), 98 | prefix=prefix, 99 | ) 100 | 101 | try: 102 | response_iterator = self._kv_stub.scan_keys(kv_store_scan_keys_request=req) 103 | async for item in response_iterator: 104 | yield item.key 105 | except GRPCError as grpc_err: 106 | raise exception_from_grpc_error(grpc_err) from grpc_err 107 | 108 | return 109 | 110 | async def delete(self, key: str) -> None: 111 | """Delete a key from the key value store.""" 112 | ref = ValueRef(store=self.name, key=key) 113 | 114 | req = KvStoreDeleteKeyRequest(ref=ref) 115 | 116 | try: 117 | await self._kv_stub.delete_key(kv_store_delete_key_request=req) 118 | except GRPCError as grpc_err: 119 | raise exception_from_grpc_error(grpc_err) from grpc_err 120 | 121 | 122 | KVPermission = Literal["get", "set", "delete"] 123 | 124 | 125 | class KeyValueStore(SecureResource): 126 | """A key value store resource.""" 127 | 128 | async def _register(self) -> None: 129 | try: 130 | await self._resources_stub.declare( 131 | resource_declare_request=ResourceDeclareRequest( 132 | id=self._to_resource_id(), key_value_store=KeyValueStoreResource() 133 | ) 134 | ) 135 | except GRPCError as grpc_err: 136 | raise exception_from_grpc_error(grpc_err) from grpc_err 137 | 138 | def _to_resource_id(self) -> ResourceIdentifier: 139 | return ResourceIdentifier(name=self.name, type=ResourceType.KeyValueStore) 140 | 141 | def _perms_to_actions(self, *args: KVPermission) -> List[Action]: 142 | permission_actions_map: dict[KVPermission, List[Action]] = { 143 | "get": [Action.KeyValueStoreRead], 144 | "set": [Action.KeyValueStoreWrite], 145 | "delete": [Action.KeyValueStoreDelete], 146 | } 147 | 148 | return [action for perm in args for action in permission_actions_map[perm]] 149 | 150 | def allow(self, perm: KVPermission, *args: KVPermission) -> KeyValueStoreRef: 151 | """Request the required permissions for this collection.""" 152 | # Ensure registration of the resource is complete before requesting permissions. 153 | str_args = [str(perm)] + [str(permission) for permission in args] 154 | self._register_policy(*str_args) 155 | 156 | return KeyValueStoreRef(self.name) 157 | 158 | 159 | def kv(name: str) -> KeyValueStore: 160 | """ 161 | Create and register a key value store. 162 | 163 | If a key value store has already been registered with the same name, the original reference will be reused. 164 | """ 165 | return Nitric._create_resource(KeyValueStore, name) # type: ignore pylint: disable=protected-access 166 | -------------------------------------------------------------------------------- /nitric/resources/resource.py: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright (c) 2021 Nitric Technologies Pty Ltd. 3 | # 4 | # This file is part of Nitric Python 3 SDK. 5 | # See https://github.com/nitrictech/python-sdk for further info. 6 | # 7 | # Licensed under the Apache License, Version 2.0 (the "License"); 8 | # you may not use this file except in compliance with the License. 9 | # You may obtain a copy of the License at 10 | # 11 | # http://www.apache.org/licenses/LICENSE-2.0 12 | # 13 | # Unless required by applicable law or agreed to in writing, software 14 | # distributed under the License is distributed on an "AS IS" BASIS, 15 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 16 | # See the License for the specific language governing permissions and 17 | # limitations under the License. 18 | # 19 | from __future__ import annotations 20 | 21 | import asyncio 22 | from abc import ABC, abstractmethod 23 | from asyncio import Task 24 | from typing import Any, List, Optional, Sequence, Type, TypeVar 25 | 26 | from grpclib import GRPCError 27 | 28 | from nitric.exception import NitricResourceException, exception_from_grpc_error 29 | from nitric.proto.resources.v1 import ( 30 | Action, 31 | PolicyResource, 32 | ResourceDeclareRequest, 33 | ResourceIdentifier, 34 | ResourcesStub, 35 | ResourceType, 36 | ) 37 | from nitric.channel import ChannelManager 38 | 39 | T = TypeVar("T", bound="Resource") 40 | 41 | 42 | class Resource(ABC): 43 | """A base resource class with common functionality.""" 44 | 45 | name: str 46 | 47 | def __init__(self, name: str): 48 | """Construct a new resource.""" 49 | self.name = name 50 | self._reg: Optional[Task[Any]] = None # type: ignore 51 | self._channel = ChannelManager.get_channel() 52 | self._resources_stub = ResourcesStub(channel=self._channel) 53 | 54 | @abstractmethod 55 | async def _register(self) -> None: 56 | pass 57 | 58 | @classmethod 59 | def make(cls: Type[T], name: str, *args: Sequence[Any], **kwargs: dict[str, Any]) -> T: 60 | """ 61 | Create and register the resource. 62 | 63 | The registration process for resources async, so this method should be used instead of __init__. 64 | """ 65 | r = cls(name, *args, **kwargs) # type: ignore 66 | try: 67 | loop = asyncio.get_running_loop() 68 | r._reg = loop.create_task(r._register()) 69 | except RuntimeError: 70 | loop = asyncio.get_event_loop() 71 | loop.run_until_complete(r._register()) 72 | 73 | return r 74 | 75 | 76 | class SecureResource(Resource): 77 | """A secure base resource class.""" 78 | 79 | @abstractmethod 80 | def _to_resource_id(self) -> ResourceIdentifier: 81 | pass 82 | 83 | @abstractmethod 84 | def _perms_to_actions(self, *args: Any) -> List[Action]: 85 | pass 86 | 87 | async def _register_policy_async(self, *args: str) -> None: 88 | policy = PolicyResource( 89 | principals=[ResourceIdentifier(type=ResourceType.Service)], 90 | actions=self._perms_to_actions(*args), 91 | resources=[self._to_resource_id()], 92 | ) 93 | try: 94 | await self._resources_stub.declare( 95 | resource_declare_request=ResourceDeclareRequest( 96 | id=ResourceIdentifier(type=ResourceType.Policy), policy=policy 97 | ) 98 | ) 99 | except GRPCError as grpc_err: 100 | raise exception_from_grpc_error(grpc_err) from grpc_err 101 | 102 | def _register_policy(self, *args: str) -> None: 103 | try: 104 | loop = asyncio.get_event_loop() 105 | loop.run_until_complete(self._register_policy_async(*args)) 106 | except RuntimeError: 107 | raise NitricResourceException( 108 | "Nitric resources cannot be declared at runtime e.g. within the scope of a runtime function. \ 109 | Move resource declarations to the top level of scripts so that they can be safely provisioned" 110 | ) from None 111 | -------------------------------------------------------------------------------- /nitric/resources/schedules.py: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright (c) 2021 Nitric Technologies Pty Ltd. 3 | # 4 | # This file is part of Nitric Python 3 SDK. 5 | # See https://github.com/nitrictech/python-sdk for further info. 6 | # 7 | # Licensed under the Apache License, Version 2.0 (the "License"); 8 | # you may not use this file except in compliance with the License. 9 | # You may obtain a copy of the License at 10 | # 11 | # http://www.apache.org/licenses/LICENSE-2.0 12 | # 13 | # Unless required by applicable law or agreed to in writing, software 14 | # distributed under the License is distributed on an "AS IS" BASIS, 15 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 16 | # See the License for the specific language governing permissions and 17 | # limitations under the License. 18 | # 19 | from __future__ import annotations 20 | 21 | import logging 22 | from datetime import timedelta 23 | from enum import Enum 24 | from typing import Callable, List 25 | 26 | import betterproto 27 | import grpclib.exceptions 28 | 29 | from nitric.application import Nitric 30 | from nitric.bidi import AsyncNotifierList 31 | from nitric.context import FunctionServer, IntervalContext, IntervalHandler 32 | from nitric.proto.schedules.v1 import ( 33 | ClientMessage, 34 | IntervalResponse, 35 | RegistrationRequest, 36 | ScheduleCron, 37 | ScheduleEvery, 38 | SchedulesStub, 39 | ) 40 | from nitric.channel import ChannelManager 41 | 42 | 43 | class ScheduleServer(FunctionServer): 44 | """A schedule for running functions on a cadence.""" 45 | 46 | description: str 47 | 48 | handler: IntervalHandler 49 | _registration_request: RegistrationRequest 50 | _responses: AsyncNotifierList[ClientMessage] 51 | 52 | def __init__(self, description: str): 53 | """Create a schedule for running functions on a cadence.""" 54 | self.description = description 55 | self._responses = AsyncNotifierList() 56 | 57 | def every(self, rate_description: str, handler: IntervalHandler) -> None: 58 | """ 59 | Register a function to be run at the specified rate. 60 | 61 | E.g. every("3 hours") 62 | """ 63 | self._registration_request = RegistrationRequest( 64 | schedule_name=self.description, 65 | every=ScheduleEvery(rate=rate_description.lower()), 66 | ) 67 | 68 | self.handler = handler 69 | 70 | Nitric._register_worker(self) # type: ignore pylint: disable=protected-access 71 | 72 | def cron(self, cron_expression: str, handler: IntervalHandler) -> None: 73 | """ 74 | Register a function to be run at the specified cron schedule. 75 | 76 | E.g. cron("3 * * * *") 77 | """ 78 | self._registration_request = RegistrationRequest( 79 | schedule_name=self.description, 80 | cron=ScheduleCron(expression=cron_expression), 81 | ) 82 | 83 | self.handler = handler 84 | 85 | Nitric._register_worker(self) # type: ignore pylint: disable=protected-access 86 | 87 | async def _schedule_request_iterator(self): 88 | # Register with the server 89 | yield ClientMessage(registration_request=self._registration_request) 90 | # wait for any responses for the server and send them 91 | async for response in self._responses: 92 | yield response 93 | 94 | async def start(self) -> None: 95 | """Register this schedule and start listening for requests.""" 96 | channel = ChannelManager.get_channel() 97 | schedules_stub = SchedulesStub(channel=channel) 98 | 99 | try: 100 | async for server_msg in schedules_stub.schedule(self._schedule_request_iterator()): 101 | msg_type, _ = betterproto.which_one_of(server_msg, "content") 102 | 103 | if msg_type == "registration_response": 104 | continue 105 | if msg_type == "interval_request": 106 | ctx = IntervalContext(server_msg) 107 | try: 108 | await self.handler(ctx) 109 | except Exception as e: # pylint: disable=broad-except 110 | logging.exception("An unhandled error occurred in a scheduled function: %s", e) 111 | resp = IntervalResponse() 112 | await self._responses.add_item(ClientMessage(id=server_msg.id, interval_response=resp)) 113 | except grpclib.exceptions.GRPCError as e: 114 | print(f"Stream terminated: {e.message}") 115 | except grpclib.exceptions.StreamTerminatedError: 116 | print("Stream from membrane closed.") 117 | finally: 118 | print("Closing client stream") 119 | channel.close() 120 | 121 | 122 | class Frequency(Enum): 123 | """Valid schedule frequencies.""" 124 | 125 | MINUTES = "minutes" 126 | HOURS = "hours" 127 | DAYS = "days" 128 | 129 | @staticmethod 130 | def from_str(value: str) -> Frequency: 131 | """Convert a string frequency value to Frequency.""" 132 | try: 133 | return Frequency[value.strip().upper()] 134 | except Exception: 135 | raise ValueError(f"{value} is not a valid frequency") 136 | 137 | @staticmethod 138 | def as_str_list() -> List[str]: 139 | """Return all frequency values as a list of strings.""" 140 | return [str(frequency.value) for frequency in Frequency] 141 | 142 | def as_time(self, rate: int) -> timedelta: 143 | """Convert the rate to minutes based on the frequency.""" 144 | if self == Frequency.MINUTES: 145 | return timedelta(minutes=rate) 146 | elif self == Frequency.HOURS: 147 | return timedelta(hours=rate) 148 | elif self == Frequency.DAYS: 149 | return timedelta(days=rate) 150 | else: 151 | raise ValueError(f"{self} is not a valid frequency") 152 | 153 | 154 | class Schedule: 155 | """A raw schedule to be deployed and assigned a rate or cron interval.""" 156 | 157 | def __init__(self, description: str): 158 | """Create a new schedule resource.""" 159 | self.description = description 160 | 161 | def every(self, every: str) -> Callable[[IntervalHandler], ScheduleServer]: 162 | """ 163 | Set the schedule interval. 164 | 165 | e.g. every('3 days'). 166 | """ 167 | 168 | def decorator(func: IntervalHandler) -> ScheduleServer: 169 | r = ScheduleServer(self.description) 170 | r.every(every, func) 171 | return r 172 | 173 | return decorator 174 | 175 | def cron(self, cron: str) -> Callable[[IntervalHandler], ScheduleServer]: 176 | """ 177 | Set the schedule interval. 178 | 179 | e.g. cron('3 * * * *'). 180 | """ 181 | 182 | def decorator(func: IntervalHandler) -> ScheduleServer: 183 | r = ScheduleServer(self.description) 184 | r.cron(cron, func) 185 | return r 186 | 187 | return decorator 188 | 189 | 190 | def schedule(description: str) -> Schedule: 191 | """Return a schedule.""" 192 | return Schedule(description=description) 193 | -------------------------------------------------------------------------------- /nitric/resources/sql.py: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright (c) 2021 Nitric Technologies Pty Ltd. 3 | # 4 | # This file is part of Nitric Python 3 SDK. 5 | # See https://github.com/nitrictech/python-sdk for further info. 6 | # 7 | # Licensed under the Apache License, Version 2.0 (the "License"); 8 | # you may not use this file except in compliance with the License. 9 | # You may obtain a copy of the License at 10 | # 11 | # http://www.apache.org/licenses/LICENSE-2.0 12 | # 13 | # Unless required by applicable law or agreed to in writing, software 14 | # distributed under the License is distributed on an "AS IS" BASIS, 15 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 16 | # See the License for the specific language governing permissions and 17 | # limitations under the License. 18 | # 19 | from __future__ import annotations 20 | 21 | from typing import Union 22 | 23 | from grpclib import GRPCError 24 | from grpclib.client import Channel 25 | 26 | from nitric.exception import exception_from_grpc_error 27 | from nitric.proto.resources.v1 import ( 28 | SqlDatabaseResource, 29 | SqlDatabaseMigrations, 30 | ResourceDeclareRequest, 31 | ResourceIdentifier, 32 | ResourceType, 33 | ) 34 | from nitric.resources.resource import Resource as BaseResource 35 | from nitric.channel import ChannelManager 36 | from nitric.application import Nitric 37 | 38 | from nitric.proto.sql.v1 import SqlStub, SqlConnectionStringRequest 39 | 40 | 41 | class Sql(BaseResource): 42 | """A SQL Database.""" 43 | 44 | _channel: Channel 45 | _sql_stub: SqlStub 46 | name: str 47 | migrations: Union[str, None] 48 | 49 | def __init__(self, name: str, migrations: Union[str, None] = None): 50 | """Construct a new SQL Database.""" 51 | super().__init__(name) 52 | 53 | self._channel: Union[Channel, None] = ChannelManager.get_channel() 54 | self._sql_stub = SqlStub(channel=self._channel) 55 | self.name = name 56 | self.migrations = migrations 57 | 58 | async def _register(self) -> None: 59 | try: 60 | await self._resources_stub.declare( 61 | resource_declare_request=ResourceDeclareRequest( 62 | id=ResourceIdentifier(name=self.name, type=ResourceType.SqlDatabase), 63 | sql_database=SqlDatabaseResource( 64 | migrations=SqlDatabaseMigrations(migrations_path=self.migrations if self.migrations else "") 65 | ), 66 | ), 67 | ) 68 | except GRPCError as grpc_err: 69 | raise exception_from_grpc_error(grpc_err) from grpc_err 70 | 71 | async def connection_string(self) -> str: 72 | """Return the connection string for this SQL Database.""" 73 | response = await self._sql_stub.connection_string(SqlConnectionStringRequest(database_name=self.name)) 74 | 75 | return response.connection_string 76 | 77 | 78 | def sql(name: str, migrations: Union[str, None] = None) -> Sql: 79 | """ 80 | Create and register a sql database. 81 | 82 | If a sql databse has already been registered with the same name, the original reference will be reused. 83 | """ 84 | return Nitric._create_resource(Sql, name, migrations) 85 | -------------------------------------------------------------------------------- /nitric/utils.py: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright (c) 2021 Nitric Technologies Pty Ltd. 3 | # 4 | # This file is part of Nitric Python 3 SDK. 5 | # See https://github.com/nitrictech/python-sdk for further info. 6 | # 7 | # Licensed under the Apache License, Version 2.0 (the "License"); 8 | # you may not use this file except in compliance with the License. 9 | # You may obtain a copy of the License at 10 | # 11 | # http://www.apache.org/licenses/LICENSE-2.0 12 | # 13 | # Unless required by applicable law or agreed to in writing, software 14 | # distributed under the License is distributed on an "AS IS" BASIS, 15 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 16 | # See the License for the specific language governing permissions and 17 | # limitations under the License. 18 | # 19 | from typing import Any, Optional 20 | 21 | from betterproto.lib.google.protobuf import Struct 22 | from google.protobuf.json_format import MessageToDict 23 | from google.protobuf.struct_pb2 import Struct as WorkingStruct 24 | 25 | 26 | # These functions convert to/from python dict <-> betterproto.lib.google.protobuf.Struct 27 | # the existing Struct().from_dict() method doesn't work for Structs, 28 | # it relies on Message meta information, that isn't available for dynamic structs. 29 | def dict_from_struct(struct: Optional[Struct]) -> dict[Any, Any]: 30 | """Construct a dict from a Struct.""" 31 | # Convert the bytes representation of the betterproto Struct into a protobuf Struct 32 | # in order to use the MessageToDict function to safely create a dict. 33 | if struct is None: 34 | return {} 35 | gpb_struct = WorkingStruct() 36 | gpb_struct.ParseFromString(bytes(struct)) 37 | return MessageToDict(gpb_struct) 38 | 39 | 40 | def struct_from_dict(dictionary: Optional[dict[Any, Any]]) -> Struct: 41 | """Construct a Struct from a dict.""" 42 | # Convert to dict into a Struct class from the protobuf library 43 | # since protobuf Structs are able to be created from a dict 44 | # unlike the Struct class from betterproto. 45 | if dictionary is None: 46 | return Struct() 47 | gpb_struct = WorkingStruct() 48 | gpb_struct.update(dictionary) 49 | # Convert the bytes representation of the protobuf Struct into the betterproto Struct 50 | # so that the returned Struct is compatible with other betterproto generated classes 51 | struct = Struct().parse(gpb_struct.SerializeToString()) 52 | return struct 53 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | requires = ["setuptools >= 40.9.0", "wheel"] 3 | build-backend = "setuptools.build_meta" 4 | 5 | [tool.black] 6 | line-length = 120 7 | include = '\.pyi?$' 8 | exclude = ''' 9 | /( 10 | \.git 11 | | \.pytest_cache 12 | | \.tox 13 | | \.coverage 14 | | build 15 | | contracts 16 | | dist 17 | )/ 18 | ''' 19 | 20 | [tool.pylint] 21 | max-line-length = 120 22 | disable = ["protected-access"] -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | import setuptools 2 | import re 3 | from subprocess import Popen, PIPE 4 | 5 | 6 | def get_current_version_tag(): 7 | process = Popen(["git", "describe", "--tags", "--match", "v[0-9]*"], stdout=PIPE) 8 | (output, err) = process.communicate() 9 | process.wait() 10 | 11 | tags = str(output, "utf-8").strip().split("\n") 12 | 13 | version_tags = [tag for tag in tags if re.match(r"^v?(\d*\.){2}\d$", tag)] 14 | rc_tags = [tag for tag in tags if re.match(r"^v?(\d*\.){2}\d*-rc\.\d*$", tag)] 15 | 16 | if len(version_tags) == 1: 17 | return version_tags.pop()[1:] 18 | elif len(rc_tags) == 1: 19 | base_tag, num_commits = rc_tags.pop().split("-rc.")[:2] 20 | return "{0}.dev{1}".format(base_tag, num_commits)[1:] 21 | else: 22 | return "0.0.0.dev0" 23 | 24 | 25 | with open("README.md", "r") as readme_file: 26 | long_description = readme_file.read() 27 | 28 | setuptools.setup( 29 | name="nitric", 30 | version=get_current_version_tag(), 31 | author="Nitric", 32 | author_email="team@nitric.io", 33 | description="The Nitric SDK for Python 3", 34 | long_description=long_description, 35 | long_description_content_type="text/markdown", 36 | url="https://github.com/nitrictech/python-sdk", 37 | packages=setuptools.find_packages(exclude=["tests", "tests.*"]), 38 | package_data={"nitric": ["py.typed"]}, 39 | license_files=("LICENSE.txt",), 40 | classifiers=[ 41 | "Programming Language :: Python :: 3", 42 | "Operating System :: OS Independent", 43 | ], 44 | setup_requires=["wheel"], 45 | install_requires=[ 46 | "asyncio", 47 | "protobuf==4.23.3", 48 | "betterproto==2.0.0b6", 49 | "opentelemetry-api", 50 | "opentelemetry-sdk", 51 | "opentelemetry-exporter-otlp-proto-grpc", 52 | "opentelemetry-instrumentation-grpc", 53 | ], 54 | extras_require={ 55 | "dev": [ 56 | "tox==3.20.1", 57 | "twine==3.2.0", 58 | "pytest==7.3.2", 59 | "pytest-cov==4.1.0", 60 | "pre-commit==2.12.0", 61 | "black==22.3", 62 | "flake8==3.9.1", 63 | "flake8", 64 | "flake8-bugbear", 65 | "flake8-comprehensions", 66 | "flake8-string-format", 67 | "pydocstyle==6.0.0", 68 | "pip-licenses==3.3.1", 69 | "licenseheaders==0.8.8", 70 | "pdoc3==0.9.2", 71 | "markupsafe==2.0.1", 72 | "betterproto[compiler]==2.0.0b6", 73 | # "grpcio==1.33.2", 74 | "grpcio-tools==1.62.0", 75 | "twine==3.2.0", 76 | "mypy==1.3.0", 77 | ] 78 | }, 79 | python_requires=">=3.11", 80 | ) 81 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright (c) 2021 Nitric Technologies Pty Ltd. 3 | # 4 | # This file is part of Nitric Python 3 SDK. 5 | # See https://github.com/nitrictech/python-sdk for further info. 6 | # 7 | # Licensed under the Apache License, Version 2.0 (the "License"); 8 | # you may not use this file except in compliance with the License. 9 | # You may obtain a copy of the License at 10 | # 11 | # http://www.apache.org/licenses/LICENSE-2.0 12 | # 13 | # Unless required by applicable law or agreed to in writing, software 14 | # distributed under the License is distributed on an "AS IS" BASIS, 15 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 16 | # See the License for the specific language governing permissions and 17 | # limitations under the License. 18 | # 19 | -------------------------------------------------------------------------------- /tests/resources/__init__.py: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright (c) 2021 Nitric Technologies Pty Ltd. 3 | # 4 | # This file is part of Nitric Python 3 SDK. 5 | # See https://github.com/nitrictech/python-sdk for further info. 6 | # 7 | # Licensed under the Apache License, Version 2.0 (the "License"); 8 | # you may not use this file except in compliance with the License. 9 | # You may obtain a copy of the License at 10 | # 11 | # http://www.apache.org/licenses/LICENSE-2.0 12 | # 13 | # Unless required by applicable law or agreed to in writing, software 14 | # distributed under the License is distributed on an "AS IS" BASIS, 15 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 16 | # See the License for the specific language governing permissions and 17 | # limitations under the License. 18 | # 19 | -------------------------------------------------------------------------------- /tests/resources/test_queues.py: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright (c) 2021 Nitric Technologies Pty Ltd. 3 | # 4 | # This file is part of Nitric Python 3 SDK. 5 | # See https://github.com/nitrictech/python-sdk for further info. 6 | # 7 | # Licensed under the Apache License, Version 2.0 (the "License"); 8 | # you may not use this file except in compliance with the License. 9 | # You may obtain a copy of the License at 10 | # 11 | # http://www.apache.org/licenses/LICENSE-2.0 12 | # 13 | # Unless required by applicable law or agreed to in writing, software 14 | # distributed under the License is distributed on an "AS IS" BASIS, 15 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 16 | # See the License for the specific language governing permissions and 17 | # limitations under the License. 18 | # 19 | from unittest import IsolatedAsyncioTestCase 20 | from unittest.mock import patch, AsyncMock 21 | from nitric.resources import queue 22 | 23 | from nitric.proto.resources.v1 import Action, ResourceDeclareRequest, ResourceIdentifier, ResourceType, PolicyResource 24 | 25 | # pylint: disable=protected-access,missing-function-docstring,missing-class-docstring 26 | 27 | 28 | class Object(object): 29 | pass 30 | 31 | 32 | class QueueTest(IsolatedAsyncioTestCase): 33 | def test_create_allow_sending(self): 34 | mock_declare = AsyncMock() 35 | mock_response = Object() 36 | mock_declare.return_value = mock_response 37 | 38 | with patch("nitric.proto.resources.v1.ResourcesStub.declare", mock_declare): 39 | queue("test-queue").allow("enqueue") 40 | 41 | # Check expected values were passed to Stub 42 | mock_declare.assert_called_with( 43 | resource_declare_request=ResourceDeclareRequest( 44 | id=ResourceIdentifier(type=ResourceType.Policy), 45 | policy=PolicyResource( 46 | principals=[ResourceIdentifier(type=ResourceType.Service)], 47 | actions=[ 48 | Action.QueueEnqueue, 49 | ], 50 | resources=[ResourceIdentifier(type=ResourceType.Queue, name="test-queue")], 51 | ), 52 | ) 53 | ) 54 | 55 | def test_create_allow_receiving(self): 56 | mock_declare = AsyncMock() 57 | mock_response = Object() 58 | mock_declare.return_value = mock_response 59 | 60 | with patch("nitric.proto.resources.v1.ResourcesStub.declare", mock_declare): 61 | queue("test-queue").allow("dequeue") 62 | 63 | # Check expected values were passed to Stub 64 | mock_declare.assert_called_with( 65 | resource_declare_request=ResourceDeclareRequest( 66 | id=ResourceIdentifier(type=ResourceType.Policy), 67 | policy=PolicyResource( 68 | principals=[ResourceIdentifier(type=ResourceType.Service)], 69 | actions=[ 70 | Action.QueueDequeue, 71 | ], 72 | resources=[ResourceIdentifier(type=ResourceType.Queue, name="test-queue")], 73 | ), 74 | ) 75 | ) 76 | 77 | def test_create_allow_all(self): 78 | mock_declare = AsyncMock() 79 | mock_response = Object() 80 | mock_declare.return_value = mock_response 81 | 82 | with patch("nitric.proto.resources.v1.ResourcesStub.declare", mock_declare): 83 | queue("test-queue").allow("enqueue", "dequeue") 84 | 85 | # Check expected values were passed to Stub 86 | mock_declare.assert_called_with( 87 | resource_declare_request=ResourceDeclareRequest( 88 | id=ResourceIdentifier(type=ResourceType.Policy), 89 | policy=PolicyResource( 90 | principals=[ResourceIdentifier(type=ResourceType.Service)], 91 | actions=[ 92 | Action.QueueEnqueue, 93 | Action.QueueDequeue, 94 | ], 95 | resources=[ResourceIdentifier(type=ResourceType.Queue, name="test-queue")], 96 | ), 97 | ) 98 | ) 99 | -------------------------------------------------------------------------------- /tests/resources/test_schedules.py: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright (c) 2021 Nitric Technologies Pty Ltd. 3 | # 4 | # This file is part of Nitric Python 3 SDK. 5 | # See https://github.com/nitrictech/python-sdk for further info. 6 | # 7 | # Licensed under the Apache License, Version 2.0 (the "License"); 8 | # you may not use this file except in compliance with the License. 9 | # You may obtain a copy of the License at 10 | # 11 | # http://www.apache.org/licenses/LICENSE-2.0 12 | # 13 | # Unless required by applicable law or agreed to in writing, software 14 | # distributed under the License is distributed on an "AS IS" BASIS, 15 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 16 | # See the License for the specific language governing permissions and 17 | # limitations under the License. 18 | # 19 | from unittest import IsolatedAsyncioTestCase 20 | 21 | from nitric.resources import schedule, ScheduleServer 22 | 23 | 24 | # pylint: disable=protected-access,missing-function-docstring,missing-class-docstring 25 | 26 | 27 | class Object(object): 28 | pass 29 | 30 | 31 | class ApiTest(IsolatedAsyncioTestCase): 32 | def test_create_schedule(self): 33 | test_schedule = ScheduleServer("test-schedule") 34 | 35 | assert test_schedule is not None 36 | assert test_schedule.description == "test-schedule" 37 | 38 | def test_create_schedule_decorator_every(self): 39 | # test_schedule = schedule("test-schedule", "3 hours")(lambda ctx: ctx) 40 | test_schedule = schedule("test-schedule-every") 41 | schedule_server = test_schedule.every("3 hours")(lambda ctx: ctx) 42 | 43 | assert test_schedule is not None 44 | assert test_schedule.description == "test-schedule-every" 45 | assert ( 46 | schedule_server._registration_request.schedule_name == "test-schedule-every" 47 | ) # pylint: disable=protected-access 48 | assert schedule_server._registration_request.every.rate == "3 hours" # pylint: disable=protected-access 49 | 50 | def test_create_schedule_decorator_cron(self): 51 | # test_schedule = schedule("test-schedule", "3 hours")(lambda ctx: ctx) 52 | test_schedule = schedule("test-schedule-cron") 53 | schedule_server = test_schedule.cron("* * * * *")(lambda ctx: ctx) 54 | 55 | assert test_schedule is not None 56 | assert test_schedule.description == "test-schedule-cron" 57 | assert ( 58 | schedule_server._registration_request.schedule_name == "test-schedule-cron" 59 | ) # pylint: disable=protected-access 60 | assert schedule_server._registration_request.cron.expression == "* * * * *" # pylint: disable=protected-access 61 | 62 | # TODO: Re-implement schedule validation 63 | # def test_every_with_invalid_rate_description_frequency(self): 64 | # test_schedule = Schedule("test-schedule") 65 | 66 | # try: 67 | # test_schedule.every("3 months", lambda ctx: ctx) 68 | # pytest.fail() 69 | # except Exception as e: 70 | # assert str(e).startswith("invalid rate expression, frequency") is True 71 | 72 | # TODO: Re-implement schedule validation 73 | # def test_every_with_missing_rate_description_frequency(self): 74 | # test_schedule = Schedule("test-schedule") 75 | 76 | # try: 77 | # test_schedule.every("3", lambda ctx: ctx) 78 | # pytest.fail() 79 | # except Exception as e: 80 | # assert str(e).startswith("invalid rate expression, frequency") is True 81 | 82 | # TODO: Re-implement schedule validation 83 | # def test_every_with_invalid_rate_description_rate(self): 84 | # test_schedule = Schedule("test-schedule") 85 | 86 | # try: 87 | # test_schedule.every("three days", lambda ctx: ctx) 88 | # pytest.fail() 89 | # except Exception as e: 90 | # assert str(e).startswith("invalid rate expression, expression") is True 91 | 92 | # TODO: Re-implement schedule validation 93 | # def test_every_with_invalid_rate_description_frequency_and_rate(self): 94 | # test_schedule = Schedule("test-schedule") 95 | 96 | # try: 97 | # test_schedule.every("three days", lambda ctx: ctx) 98 | # pytest.fail() 99 | # except Exception as e: 100 | # assert str(e).startswith("invalid rate expression, expression") is True 101 | 102 | # TODO: Re-implement schedule validation 103 | # def test_every_with_missing_rate_description_rate(self): 104 | # test_schedule = Schedule("test-schedule") 105 | 106 | # try: 107 | # test_schedule.every("months", lambda ctx: ctx) 108 | # pytest.fail() 109 | # except Exception as e: 110 | # assert str(e).startswith("invalid rate expression, frequency") is True 111 | -------------------------------------------------------------------------------- /tests/resources/test_secrets.py: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright (c) 2021 Nitric Technologies Pty Ltd. 3 | # 4 | # This file is part of Nitric Python 3 SDK. 5 | # See https://github.com/nitrictech/python-sdk for further info. 6 | # 7 | # Licensed under the Apache License, Version 2.0 (the "License"); 8 | # you may not use this file except in compliance with the License. 9 | # You may obtain a copy of the License at 10 | # 11 | # http://www.apache.org/licenses/LICENSE-2.0 12 | # 13 | # Unless required by applicable law or agreed to in writing, software 14 | # distributed under the License is distributed on an "AS IS" BASIS, 15 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 16 | # See the License for the specific language governing permissions and 17 | # limitations under the License. 18 | # 19 | from unittest import IsolatedAsyncioTestCase 20 | from unittest.mock import AsyncMock, patch 21 | 22 | import pytest 23 | from grpclib import GRPCError, Status 24 | 25 | from nitric.exception import UnknownException 26 | from nitric.proto.resources.v1 import Action, PolicyResource, ResourceDeclareRequest, ResourceIdentifier, ResourceType 27 | from nitric.proto.secrets.v1 import ( 28 | Secret, 29 | SecretAccessRequest, 30 | SecretAccessResponse, 31 | SecretPutRequest, 32 | SecretPutResponse, 33 | SecretVersion, 34 | ) 35 | from nitric.resources.secrets import SecretRef, SecretValue, secret 36 | 37 | # pylint: disable=protected-access,missing-function-docstring,missing-class-docstring 38 | 39 | 40 | class SecretsClientTest(IsolatedAsyncioTestCase): 41 | async def test_put(self): 42 | mock_put = AsyncMock() 43 | mock_response = SecretPutResponse( 44 | secret_version=SecretVersion(secret=Secret(name="test-secret"), version="test-version") 45 | ) 46 | mock_put.return_value = mock_response 47 | 48 | with patch("nitric.proto.secrets.v1.SecretManagerStub.put", mock_put): 49 | secret = SecretRef("test-secret") 50 | result = await secret.put(b"a test secret value") 51 | 52 | # Check expected values were passed to Stub 53 | mock_put.assert_called_once_with( 54 | secret_put_request=SecretPutRequest(secret=Secret(name="test-secret"), value=b"a test secret value") 55 | ) 56 | 57 | # Check the returned value 58 | assert result.version == "test-version" 59 | assert result.secret.name == "test-secret" 60 | 61 | async def test_put_string(self): 62 | mock_put = AsyncMock() 63 | mock_response = SecretPutResponse( 64 | secret_version=SecretVersion(secret=Secret(name="test-secret"), version="test-version") 65 | ) 66 | mock_put.return_value = mock_response 67 | 68 | with patch("nitric.proto.secrets.v1.SecretManagerStub.put", mock_put): 69 | secret = SecretRef("test-secret") 70 | await secret.put("a test secret value") # string, not bytes 71 | 72 | # Check expected values were passed to Stub 73 | mock_put.assert_called_once_with( 74 | secret_put_request=SecretPutRequest(secret=Secret(name="test-secret"), value=b"a test secret value") 75 | ) 76 | 77 | async def test_latest(self): 78 | version = SecretRef("test-secret").latest() 79 | 80 | assert version.secret.name == "test-secret" 81 | assert version.version == "latest" 82 | 83 | async def test_access(self): 84 | mock_access = AsyncMock() 85 | mock_response = SecretAccessResponse( 86 | secret_version=SecretVersion(secret=Secret(name="test-secret"), version="response-version"), 87 | value=b"super secret value", 88 | ) 89 | mock_access.return_value = mock_response 90 | 91 | with patch("nitric.proto.secrets.v1.SecretManagerStub.access", mock_access): 92 | version = SecretRef("test-secret").latest() 93 | result = await version.access() 94 | 95 | # Check expected values were passed to Stub 96 | mock_access.assert_called_once_with( 97 | secret_access_request=SecretAccessRequest( 98 | secret_version=SecretVersion(secret=Secret(name="test-secret"), version="latest") 99 | ) 100 | ) 101 | 102 | # Check the returned value 103 | assert result.version.version == "response-version" 104 | assert result.value == b"super secret value" 105 | 106 | async def test_value_to_string(self): 107 | value = SecretValue(version=None, value=b"secret value") 108 | 109 | assert value.as_string() == "secret value" 110 | assert str(value) == "secret value" 111 | 112 | async def test_value_to_bytes(self): 113 | value = SecretValue(version=None, value=b"secret value") 114 | 115 | assert value.as_bytes() == b"secret value" 116 | assert bytes(value) == b"secret value" 117 | 118 | async def test_put_error(self): 119 | mock_put = AsyncMock() 120 | mock_put.side_effect = GRPCError(Status.UNKNOWN, "test error") 121 | 122 | with patch("nitric.proto.secrets.v1.SecretManagerStub.put", mock_put): 123 | with pytest.raises(UnknownException) as e: 124 | secret = SecretRef("test-secret") 125 | await secret.put(b"a test secret value") 126 | 127 | async def test_access_error(self): 128 | mock_access = AsyncMock() 129 | mock_access.side_effect = GRPCError(Status.UNKNOWN, "test error") 130 | 131 | with patch("nitric.proto.secrets.v1.SecretManagerStub.access", mock_access): 132 | with pytest.raises(UnknownException) as e: 133 | await SecretRef("test-secret").latest().access() 134 | 135 | 136 | class Object(object): 137 | pass 138 | 139 | 140 | class SecretTest(IsolatedAsyncioTestCase): 141 | def test_allow_put(self): 142 | mock_declare = AsyncMock() 143 | mock_response = Object() 144 | mock_declare.return_value = mock_response 145 | 146 | with patch("nitric.proto.resources.v1.ResourcesStub.declare", mock_declare): 147 | secret("test-secret").allow("put") 148 | 149 | # Check expected values were passed to Stub 150 | mock_declare.assert_called_with( 151 | resource_declare_request=ResourceDeclareRequest( 152 | id=ResourceIdentifier(type=ResourceType.Policy), 153 | policy=PolicyResource( 154 | principals=[ResourceIdentifier(type=ResourceType.Service)], 155 | actions=[Action.SecretPut], 156 | resources=[ResourceIdentifier(type=ResourceType.Secret, name="test-secret")], 157 | ), 158 | ) 159 | ) 160 | 161 | def test_allow_access(self): 162 | mock_declare = AsyncMock() 163 | mock_response = Object() 164 | mock_declare.return_value = mock_response 165 | 166 | with patch("nitric.proto.resources.v1.ResourcesStub.declare", mock_declare): 167 | secret("test-secret").allow("access") 168 | 169 | # Check expected values were passed to Stub 170 | mock_declare.assert_called_with( 171 | resource_declare_request=ResourceDeclareRequest( 172 | id=ResourceIdentifier(type=ResourceType.Policy), 173 | policy=PolicyResource( 174 | principals=[ResourceIdentifier(type=ResourceType.Service)], 175 | actions=[Action.SecretAccess], 176 | resources=[ResourceIdentifier(type=ResourceType.Secret, name="test-secret")], 177 | ), 178 | ) 179 | ) 180 | -------------------------------------------------------------------------------- /tests/resources/test_sql.py: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright (c) 2021 Nitric Technologies Pty Ltd. 3 | # 4 | # This file is part of Nitric Python 3 SDK. 5 | # See https://github.com/nitrictech/python-sdk for further info. 6 | # 7 | # Licensed under the Apache License, Version 2.0 (the "License"); 8 | # you may not use this file except in compliance with the License. 9 | # You may obtain a copy of the License at 10 | # 11 | # http://www.apache.org/licenses/LICENSE-2.0 12 | # 13 | # Unless required by applicable law or agreed to in writing, software 14 | # distributed under the License is distributed on an "AS IS" BASIS, 15 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 16 | # See the License for the specific language governing permissions and 17 | # limitations under the License. 18 | # 19 | from unittest import IsolatedAsyncioTestCase 20 | from unittest.mock import AsyncMock, Mock, patch 21 | 22 | import pytest 23 | 24 | from nitric.proto.resources.v1 import ( 25 | ResourceDeclareRequest, 26 | ResourceIdentifier, 27 | ResourceType, 28 | SqlDatabaseResource, 29 | ) 30 | from nitric.resources import sql 31 | 32 | # pylint: disable=protected-access,missing-function-docstring,missing-class-docstring 33 | 34 | 35 | class Object(object): 36 | pass 37 | 38 | 39 | class MockAsyncChannel: 40 | def __init__(self): 41 | self.send = AsyncMock() 42 | self.close = Mock() 43 | self.done = Mock() 44 | 45 | 46 | class SqlTest(IsolatedAsyncioTestCase): 47 | def test_declare_sql(self): 48 | mock_declare = AsyncMock() 49 | mock_response = Object() 50 | mock_declare.return_value = mock_response 51 | 52 | with patch("nitric.proto.resources.v1.ResourcesStub.declare", mock_declare): 53 | sqldb = sql("test-sql") 54 | 55 | # Check expected values were passed to Stub 56 | mock_declare.assert_called_with( 57 | resource_declare_request=ResourceDeclareRequest( 58 | id=ResourceIdentifier(name="test-sql", type=ResourceType.SqlDatabase), 59 | sql_database=SqlDatabaseResource(), 60 | ) 61 | ) 62 | -------------------------------------------------------------------------------- /tests/resources/test_topics.py: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright (c) 2021 Nitric Technologies Pty Ltd. 3 | # 4 | # This file is part of Nitric Python 3 SDK. 5 | # See https://github.com/nitrictech/python-sdk for further info. 6 | # 7 | # Licensed under the Apache License, Version 2.0 (the "License"); 8 | # you may not use this file except in compliance with the License. 9 | # You may obtain a copy of the License at 10 | # 11 | # http://www.apache.org/licenses/LICENSE-2.0 12 | # 13 | # Unless required by applicable law or agreed to in writing, software 14 | # distributed under the License is distributed on an "AS IS" BASIS, 15 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 16 | # See the License for the specific language governing permissions and 17 | # limitations under the License. 18 | # 19 | from unittest import IsolatedAsyncioTestCase 20 | from unittest.mock import AsyncMock, Mock, patch 21 | 22 | import pytest 23 | from grpclib import GRPCError, Status 24 | 25 | from nitric.exception import UnknownException 26 | from nitric.proto.resources.v1 import Action, PolicyResource, ResourceDeclareRequest, ResourceIdentifier, ResourceType 27 | from nitric.proto.topics.v1 import TopicMessage, TopicPublishRequest 28 | from nitric.resources import topic 29 | from nitric.resources.topics import TopicRef 30 | from nitric.utils import struct_from_dict 31 | 32 | # pylint: disable=protected-access,missing-function-docstring,missing-class-docstring 33 | 34 | 35 | class Object(object): 36 | pass 37 | 38 | 39 | class EventClientTest(IsolatedAsyncioTestCase): 40 | async def test_publish(self): 41 | mock_publish = AsyncMock() 42 | mock_response = Object() 43 | mock_publish.return_value = mock_response 44 | 45 | payload = {"content": "of event"} 46 | 47 | with patch("nitric.proto.topics.v1.TopicsStub.publish", mock_publish): 48 | topic = TopicRef("test-topic") 49 | await topic.publish(payload) 50 | 51 | # Check expected values were passed to Stub 52 | # mock_publish.assert_called_once() 53 | mock_publish.assert_called_once_with( 54 | topic_publish_request=TopicPublishRequest( 55 | topic_name="test-topic", message=TopicMessage(struct_payload=struct_from_dict(payload)) 56 | ) 57 | ) 58 | 59 | async def test_publish_invalid_type(self): 60 | mock_publish = AsyncMock() 61 | mock_response = Object() 62 | mock_publish.return_value = mock_response 63 | 64 | with patch("nitric.proto.topics.v1.TopicsStub.publish", mock_publish): 65 | topic = TopicRef("test-topic") 66 | with pytest.raises(ValueError): 67 | await topic.publish((1, 2, 3)) 68 | 69 | async def test_publish_error(self): 70 | mock_publish = AsyncMock() 71 | mock_publish.side_effect = GRPCError(Status.UNKNOWN, "test error") 72 | 73 | with patch("nitric.proto.topics.v1.TopicsStub.publish", mock_publish): 74 | with pytest.raises(UnknownException): 75 | await TopicRef("test-topic").publish({}) 76 | 77 | 78 | class Object(object): 79 | pass 80 | 81 | 82 | class MockAsyncChannel: 83 | def __init__(self): 84 | self.send = AsyncMock() 85 | self.close = Mock() 86 | self.done = Mock() 87 | 88 | 89 | class TopicTest(IsolatedAsyncioTestCase): 90 | def test_create_allow_publishing(self): 91 | mock_declare = AsyncMock() 92 | mock_response = Object() 93 | mock_declare.return_value = mock_response 94 | 95 | with patch("nitric.proto.resources.v1.ResourcesStub.declare", mock_declare): 96 | topic("test-topic").allow("publish") 97 | 98 | # Check expected values were passed to Stub 99 | mock_declare.assert_called_with( 100 | resource_declare_request=ResourceDeclareRequest( 101 | id=ResourceIdentifier(type=ResourceType.Policy), 102 | policy=PolicyResource( 103 | principals=[ResourceIdentifier(type=ResourceType.Service)], 104 | actions=[Action.TopicPublish], 105 | resources=[ResourceIdentifier(type=ResourceType.Topic, name="test-topic")], 106 | ), 107 | ) 108 | ) 109 | -------------------------------------------------------------------------------- /tests/resources/test_websockets.py: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright (c) 2021 Nitric Technologies Pty Ltd. 3 | # 4 | # This file is part of Nitric Python 3 SDK. 5 | # See https://github.com/nitrictech/python-sdk for further info. 6 | # 7 | # Licensed under the Apache License, Version 2.0 (the "License"); 8 | # you may not use this file except in compliance with the License. 9 | # You may obtain a copy of the License at 10 | # 11 | # http://www.apache.org/licenses/LICENSE-2.0 12 | # 13 | # Unless required by applicable law or agreed to in writing, software 14 | # distributed under the License is distributed on an "AS IS" BASIS, 15 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 16 | # See the License for the specific language governing permissions and 17 | # limitations under the License. 18 | # 19 | from unittest import IsolatedAsyncioTestCase 20 | from unittest.mock import AsyncMock, Mock, patch 21 | 22 | from nitric.proto.resources.v1 import Action, PolicyResource, ResourceDeclareRequest, ResourceIdentifier, ResourceType 23 | from nitric.proto.websockets.v1 import WebsocketSendRequest 24 | from nitric.resources import Websocket, websocket 25 | from nitric.resources.websockets import WebsocketRef 26 | 27 | # pylint: disable=protected-access,missing-function-docstring,missing-class-docstring 28 | 29 | 30 | class Object(object): 31 | pass 32 | 33 | 34 | class WebsocketClientTest(IsolatedAsyncioTestCase): 35 | async def test_send(self): 36 | mock_send = AsyncMock() 37 | mock_response = Object() 38 | mock_send.return_value = mock_response 39 | test_data = b"test-data" 40 | 41 | with patch("nitric.proto.websockets.v1.WebsocketStub.send_message", mock_send): 42 | await WebsocketRef().send("test-socket", "test-connection", test_data) 43 | 44 | # Check expected values were passed to Stub 45 | mock_send.assert_called_once_with( 46 | websocket_send_request=WebsocketSendRequest( 47 | socket_name="test-socket", connection_id="test-connection", data=test_data 48 | ) 49 | ) 50 | 51 | 52 | class MockAsyncChannel: 53 | def __init__(self): 54 | self.send = AsyncMock() 55 | self.close = Mock() 56 | self.done = Mock() 57 | 58 | 59 | class WebsocketTest(IsolatedAsyncioTestCase): 60 | def test_create(self): 61 | mock_declare = AsyncMock() 62 | mock_response = Object() 63 | mock_declare.return_value = mock_response 64 | 65 | with patch("nitric.proto.resources.v1.ResourcesStub.declare", mock_declare): 66 | websocket("test-websocket") 67 | 68 | # Check expected values were passed to Stub 69 | mock_declare.assert_called_with( 70 | resource_declare_request=ResourceDeclareRequest( 71 | id=ResourceIdentifier(type=ResourceType.Policy), 72 | policy=PolicyResource( 73 | principals=[ResourceIdentifier(type=ResourceType.Service)], 74 | actions=[Action.WebsocketManage], 75 | resources=[ResourceIdentifier(type=ResourceType.Websocket, name="test-websocket")], 76 | ), 77 | ) 78 | ) 79 | 80 | 81 | class WebsocketClientTest(IsolatedAsyncioTestCase): 82 | async def test_send(self): 83 | mock_send = AsyncMock() 84 | mock_response = Object() 85 | mock_send.return_value = mock_response 86 | test_data = b"test-data" 87 | 88 | with patch("nitric.proto.websockets.v1.WebsocketStub.send_message", mock_send): 89 | await Websocket("testing").send("test-connection", test_data) 90 | 91 | # Check expected values were passed to Stub 92 | mock_send.assert_called_once_with( 93 | websocket_send_request=WebsocketSendRequest( 94 | socket_name="testing", connection_id="test-connection", data=test_data 95 | ) 96 | ) 97 | -------------------------------------------------------------------------------- /tests/test__utils.py: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright (c) 2021 Nitric Technologies Pty Ltd. 3 | # 4 | # This file is part of Nitric Python 3 SDK. 5 | # See https://github.com/nitrictech/python-sdk for further info. 6 | # 7 | # Licensed under the Apache License, Version 2.0 (the "License"); 8 | # you may not use this file except in compliance with the License. 9 | # You may obtain a copy of the License at 10 | # 11 | # http://www.apache.org/licenses/LICENSE-2.0 12 | # 13 | # Unless required by applicable law or agreed to in writing, software 14 | # distributed under the License is distributed on an "AS IS" BASIS, 15 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 16 | # See the License for the specific language governing permissions and 17 | # limitations under the License. 18 | # 19 | import copy 20 | 21 | from nitric.utils import struct_from_dict, dict_from_struct 22 | 23 | 24 | def test__dict_from_struct(): 25 | dict_val = { 26 | "a": True, 27 | "b": False, 28 | "c": 1, 29 | "d": 4.123, 30 | "e": "this is a string", 31 | "f": None, 32 | "g": {"ga": 1, "gb": "testing inner dict"}, 33 | "h": [1, 2, 3, "four"], 34 | } 35 | dict_copy = copy.deepcopy(dict_val) 36 | 37 | # Serialization and Deserialization shouldn't modify the object in most cases. 38 | assert dict_from_struct(struct_from_dict(dict_val)) == dict_copy 39 | -------------------------------------------------------------------------------- /tests/test_application.py: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright (c) 2021 Nitric Technologies Pty Ltd. 3 | # 4 | # This file is part of Nitric Python 3 SDK. 5 | # See https://github.com/nitrictech/python-sdk for further info. 6 | # 7 | # Licensed under the Apache License, Version 2.0 (the "License"); 8 | # you may not use this file except in compliance with the License. 9 | # You may obtain a copy of the License at 10 | # 11 | # http://www.apache.org/licenses/LICENSE-2.0 12 | # 13 | # Unless required by applicable law or agreed to in writing, software 14 | # distributed under the License is distributed on an "AS IS" BASIS, 15 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 16 | # See the License for the specific language governing permissions and 17 | # limitations under the License. 18 | # 19 | from unittest import IsolatedAsyncioTestCase 20 | from unittest.mock import patch, AsyncMock, Mock 21 | 22 | import pytest 23 | 24 | 25 | from nitric.exception import NitricUnavailableException 26 | from nitric.proto.deployments.v1 import ScheduleEvery 27 | from nitric.proto.schedules.v1 import RegistrationRequest 28 | from nitric.resources import Bucket, schedule 29 | from nitric.application import Nitric 30 | 31 | 32 | class Object(object): 33 | pass 34 | 35 | 36 | class MockAsyncChannel: 37 | def __init__(self): 38 | self.send = AsyncMock() 39 | self.close = Mock() 40 | self.done = Mock() 41 | 42 | 43 | class ApplicationTest(IsolatedAsyncioTestCase): 44 | def test_create_resource(self): 45 | application = Nitric() 46 | mock_make = Mock() 47 | mock_make.side_effect = ConnectionRefusedError("test error") 48 | 49 | with patch("nitric.resources.resource.Resource.make", mock_make): 50 | try: 51 | application._create_resource(Bucket, "test-bucket") 52 | except NitricUnavailableException as e: 53 | assert str(e).startswith("Unable to connect") 54 | 55 | def test_run_with_no_active_event_loop(self): 56 | application = Nitric() 57 | 58 | mock_running_loop = Mock() 59 | mock_running_loop.side_effect = RuntimeError("loop is not running") 60 | 61 | mock_event_loop = Mock() 62 | 63 | with patch("asyncio.get_event_loop", mock_event_loop): 64 | with patch("asyncio.get_running_loop", mock_running_loop): 65 | application.run() 66 | 67 | mock_running_loop.assert_called_once() 68 | mock_event_loop.assert_called_once() 69 | 70 | def test_run_with_keyboard_interrupt(self): 71 | application = Nitric() 72 | 73 | mock_running_loop = Mock() 74 | mock_running_loop.side_effect = KeyboardInterrupt("cancel") 75 | 76 | mock_event_loop = Mock() 77 | 78 | with patch("asyncio.get_event_loop", mock_event_loop): 79 | with patch("asyncio.get_running_loop", mock_running_loop): 80 | application.run() 81 | 82 | mock_running_loop.assert_called_once() 83 | mock_event_loop.assert_not_called() 84 | 85 | def test_run_with_connection_refused(self): 86 | application = Nitric() 87 | 88 | mock_running_loop = Mock() 89 | mock_running_loop.side_effect = ConnectionRefusedError("refusing connection") 90 | 91 | mock_event_loop = Mock() 92 | 93 | with patch("asyncio.get_event_loop", mock_event_loop): 94 | with patch("asyncio.get_running_loop", mock_running_loop): 95 | try: 96 | application.run() 97 | pytest.fail() 98 | except NitricUnavailableException as e: 99 | assert str(e).startswith("Unable to connect to nitric server") 100 | 101 | mock_running_loop.assert_called_once() 102 | mock_event_loop.assert_not_called() 103 | -------------------------------------------------------------------------------- /tests/test_exception.py: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright (c) 2021 Nitric Technologies Pty Ltd. 3 | # 4 | # This file is part of Nitric Python 3 SDK. 5 | # See https://github.com/nitrictech/python-sdk for further info. 6 | # 7 | # Licensed under the Apache License, Version 2.0 (the "License"); 8 | # you may not use this file except in compliance with the License. 9 | # You may obtain a copy of the License at 10 | # 11 | # http://www.apache.org/licenses/LICENSE-2.0 12 | # 13 | # Unless required by applicable law or agreed to in writing, software 14 | # distributed under the License is distributed on an "AS IS" BASIS, 15 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 16 | # See the License for the specific language governing permissions and 17 | # limitations under the License. 18 | # 19 | 20 | import pytest 21 | from grpclib import GRPCError, Status 22 | 23 | from nitric.exception import ( 24 | CancelledException, 25 | exception_from_grpc_error, 26 | _exception_code_map, 27 | UnknownException, 28 | InvalidArgumentException, 29 | DeadlineExceededException, 30 | NotFoundException, 31 | AlreadyExistsException, 32 | PermissionDeniedException, 33 | ResourceExhaustedException, 34 | FailedPreconditionException, 35 | AbortedException, 36 | OutOfRangeException, 37 | UnimplementedException, 38 | InternalException, 39 | UnavailableException, 40 | DataLossException, 41 | UnauthenticatedException, 42 | exception_from_grpc_code, 43 | ) 44 | 45 | 46 | expectedMapping = [ 47 | (0, Exception), 48 | (1, CancelledException), 49 | (2, UnknownException), 50 | (3, InvalidArgumentException), 51 | (4, DeadlineExceededException), 52 | (5, NotFoundException), 53 | (6, AlreadyExistsException), 54 | (7, PermissionDeniedException), 55 | (8, ResourceExhaustedException), 56 | (9, FailedPreconditionException), 57 | (10, AbortedException), 58 | (11, OutOfRangeException), 59 | (12, UnimplementedException), 60 | (13, InternalException), 61 | (14, UnavailableException), 62 | (15, DataLossException), 63 | (16, UnauthenticatedException), 64 | ] 65 | 66 | 67 | class TestException: 68 | @pytest.fixture(autouse=True) 69 | def init_exceptions(self): 70 | # Status codes that can be automatically converted to exceptions 71 | self.accepted_status_codes = set(_exception_code_map.keys()) 72 | 73 | # Ensure none of the status are missing from the test cases 74 | assert set(k for k, v in expectedMapping) == self.accepted_status_codes 75 | 76 | def test_all_codes_handled(self): 77 | # Status codes defined by betterproto 78 | all_grpc_status_codes = set(status.value for status in Status) 79 | 80 | assert all_grpc_status_codes == self.accepted_status_codes 81 | 82 | @pytest.mark.parametrize("test_value", expectedMapping) 83 | def test_grpc_code_to_exception(self, test_value): 84 | status, expected_class = test_value 85 | exception = exception_from_grpc_error(GRPCError(Status(status), "some error")) 86 | 87 | assert isinstance(exception, expected_class) 88 | 89 | def test_unexpected_status_code(self): 90 | assert isinstance(exception_from_grpc_code(100), UnknownException) 91 | -------------------------------------------------------------------------------- /tools/apache-2.tmpl: -------------------------------------------------------------------------------- 1 | Copyright (c) ${years} ${owner}. 2 | 3 | This file is part of ${projectname}. 4 | See ${projecturl} for further info. 5 | 6 | Licensed under the Apache License, Version 2.0 (the "License"); 7 | you may not use this file except in compliance with the License. 8 | You may obtain a copy of the License at 9 | 10 | http://www.apache.org/licenses/LICENSE-2.0 11 | 12 | Unless required by applicable law or agreed to in writing, software 13 | distributed under the License is distributed on an "AS IS" BASIS, 14 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | See the License for the specific language governing permissions and 16 | limitations under the License. 17 | -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | [tox] 2 | envlist = linter,py3.11 3 | 4 | [testenv] 5 | deps = 6 | -e .[dev] 7 | commands = 8 | pytest --cov=./nitric --cov-report=xml 9 | 10 | [testenv:linter] 11 | deps = 12 | flake8 13 | flake8-bugbear 14 | flake8-comprehensions 15 | flake8-string-format 16 | black 17 | pydocstyle 18 | pip-licenses 19 | commands = 20 | flake8 nitric 21 | black nitric tests tools 22 | pydocstyle nitric 23 | pip-licenses --allow-only="MIT License;BSD License;Zope Public License;Python Software Foundation License;Apache License 2.0;Apache Software License;MIT License, Mozilla Public License 2.0 (MPL 2.0);MIT;BSD License, Apache Software License;3-Clause BSD License;Historical Permission Notice and Disclaimer (HPND);Mozilla Public License 2.0 (MPL 2.0);Apache Software License, BSD License;BSD;Python Software Foundation License, MIT License;Public Domain;Public Domain, Python Software Foundation License, BSD License, GNU General Public License (GPL);GNU Library or Lesser General Public License (LGPL);LGPL;Apache Software License, MIT License" --ignore-packages nitric nitric-api asyncio 24 | 25 | [flake8] 26 | exclude = 27 | venv 28 | tests 29 | build 30 | dist 31 | .git 32 | .tox 33 | nitric/proto 34 | examples 35 | testproj 36 | ignore = F821, W503, F723 37 | max-line-length = 120 38 | 39 | [pydocstyle] 40 | ignore = D100, D105, D203, D212, D415 41 | match = (?!(test_|setup)).*\.py 42 | match_dir = (?!(venv|build|examples|dist|tests|.git|.tox|proto)).* --------------------------------------------------------------------------------