diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml new file mode 100644 index 0000000..9e02835 --- /dev/null +++ b/.github/workflows/publish.yml @@ -0,0 +1,51 @@ +name: Publish to PyPI + +on: + release: + types: + - created + +jobs: + build: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v4 + + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: "3.11" + + - name: Install build tools + run: pip install hatch + + - name: Build package + run: hatch build + + - name: Upload build artifacts + uses: actions/upload-artifact@v4 + with: + name: dist + path: dist/ + + publish: + needs: build + runs-on: ubuntu-latest + + environment: + name: pypi + url: https://pypi.org/project/pysendpulse/ + + permissions: + id-token: write + + steps: + - name: Download build artifacts + uses: actions/download-artifact@v4 + with: + name: dist + path: dist/ + + - name: Publish to PyPI + uses: pypa/gh-action-pypi-publish@release/v1 diff --git a/.github/workflows/pythonpublish.yml b/.github/workflows/pythonpublish.yml deleted file mode 100644 index 2a20e26..0000000 --- a/.github/workflows/pythonpublish.yml +++ /dev/null @@ -1,32 +0,0 @@ -# This workflows will upload a Python Package using Twine when a release is created -# For more information see: https://help.github.com/en/actions/language-and-framework-guides/using-python-with-github-actions#publishing-to-package-registries - -name: Upload Python Package - -on: - release: - types: - - created - -jobs: - deploy: - - runs-on: ubuntu-latest - - steps: - - uses: actions/checkout@v2 - - name: Set up Python - uses: actions/setup-python@v1 - with: - python-version: '3.x' - - name: Install dependencies - run: | - python -m pip install --upgrade pip - pip install setuptools wheel twine - - name: Build and publish - env: - TWINE_USERNAME: ${{ secrets.PYPI_USERNAME }} - TWINE_PASSWORD: ${{ secrets.PYPI_PASSWORD }} - run: | - python setup.py sdist bdist_wheel - twine upload dist/* diff --git a/.gitignore b/.gitignore index 913b870..f3aac28 100644 --- a/.gitignore +++ b/.gitignore @@ -1,31 +1,37 @@ -*~ -env/ -venv/ - -# Byte-compiled / optimized / DLL files +# Python __pycache__/ *.py[cod] -*$py.class - -# C extensions +*.pyo +*.pyd *.so +*.egg +*.egg-info/ +dist/ +build/ +wheels/ -# Translations -*.mo -*.pot +# Virtual environment +.venv/ +venv/ +env/ -# Django stuff: -*.log +# Type checking +.mypy_cache/ -# sqlite3 -*.sqlite3 +# Linter +.ruff_cache/ -# sockets and pid -*.sock -*.pid -/*.gitkeep -*.save -*.retry +# Tests +.pytest_cache/ +htmlcov/ +.coverage +coverage.xml + +# IDE +.idea/ +.vscode/ +*.iml + +# macOS +.DS_Store -dist/ -*.egg-info diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..00e4102 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,68 @@ +# Changelog + +All notable changes to this project will be documented in this file. + +## [2.0.0] — 2026-08-25 + +Complete rewrite of `pysendpulse` as version 2.0.0. + +### BC Breaks + +- **License** — changed from Apache-2.0 to MIT. +- **Entry point changed** — use `from sendpulse import Client` instead of `from pysendpulse.pysendpulse import PySendPulse`. +- **Constructor** — `Client(client_id=..., client_secret=...)` or `Client(api_key=...)`; the old positional `(REST_API_ID, REST_API_SECRET, TOKEN_STORAGE)` signature is gone. +- **Service layer** — flat methods on the client (`add_campaign()`, `smtp_send_mail()`, etc.) replaced by typed facades (`email_service()`, `smtp_service()`, etc.). +- **Exceptions** — typed hierarchy (`AuthException`, `RateLimitException`, `ApiException`, `NetworkException`, `ProtocolException`) replaces the old generic error handling. +- **Python 3.11+** required (was 3.x without a strict minimum). +- **Storage** — Memcached/file backends replaced by `FileTokenStorage` and `InMemoryTokenStorage`. + +### Added + +- Typed service facades: `email_service()`, `smtp_service()`, `sms_service()`, `crm_service()`, `chatbot_service()`. +- Services and models generated from OpenAPI specs — all endpoints covered. +- `TokenStorage` protocol with `FileTokenStorage` (atomic write) and `InMemoryTokenStorage`. +- Auto-retry on `401` — cached token is invalidated and a fresh one fetched before retrying once. +- Context manager support (`with Client(...) as client:`). +- Custom `HttpClient` and `TokenStorage` injection points for testing and custom transports. + +--- + +## [0.1.8] — 2025-02-11 + +- Fixed re-send request handling. + +## [0.1.7] — 2023-07-21 + +- Updated version metadata. + +## [0.1.6] — 2023-07-21 + +- Fixed response getter. + +## [0.1.5] — 2023-03-31 + +- Version bump. + +## [0.1.4] — 2022-07-01 + +- Added SMS functional methods. + +## [0.1.3] — 2021-09-28 + +- Added Automation360 method. + +## [0.1.2] — 2021-02-04 + +- Added custom token path support. + +## [0.1.1] — 2020-06-10 + +- Release preparation. + +## [0.1.0] — 2020-04-13 + +- Fixed SMS variables check. + +## [0.0.9] — 2020-03-26 + +- Initial stable release. diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..a918a01 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2015 SendPulse + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/LICENSE.md b/LICENSE.md deleted file mode 100644 index edfc319..0000000 --- a/LICENSE.md +++ /dev/null @@ -1,201 +0,0 @@ - Apache License - Version 2.0, January 2004 - http://www.apache.org/licenses/ - - TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION - - 1. Definitions. - - "License" shall mean the terms and conditions for use, reproduction, - and distribution as defined by Sections 1 through 9 of this document. - - "Licensor" shall mean the copyright owner or entity authorized by - the copyright owner that is granting the License. - - "Legal Entity" shall mean the union of the acting entity and all - other entities that control, are controlled by, or are under common - control with that entity. For the purposes of this definition, - "control" means (i) the power, direct or indirect, to cause the - direction or management of such entity, whether by contract or - otherwise, or (ii) ownership of fifty percent (50%) or more of the - outstanding shares, or (iii) beneficial ownership of such entity. - - "You" (or "Your") shall mean an individual or Legal Entity - exercising permissions granted by this License. - - "Source" form shall mean the preferred form for making modifications, - including but not limited to software source code, documentation - source, and configuration files. - - "Object" form shall mean any form resulting from mechanical - transformation or translation of a Source form, including but - not limited to compiled object code, generated documentation, - and conversions to other media types. - - "Work" shall mean the work of authorship, whether in Source or - Object form, made available under the License, as indicated by a - copyright notice that is included in or attached to the work - (an example is provided in the Appendix below). - - "Derivative Works" shall mean any work, whether in Source or Object - form, that is based on (or derived from) the Work and for which the - editorial revisions, annotations, elaborations, or other modifications - represent, as a whole, an original work of authorship. For the purposes - of this License, Derivative Works shall not include works that remain - separable from, or merely link (or bind by name) to the interfaces of, - the Work and Derivative Works thereof. - - "Contribution" shall mean any work of authorship, including - the original version of the Work and any modifications or additions - to that Work or Derivative Works thereof, that is intentionally - submitted to Licensor for inclusion in the Work by the copyright owner - or by an individual or Legal Entity authorized to submit on behalf of - the copyright owner. For the purposes of this definition, "submitted" - means any form of electronic, verbal, or written communication sent - to the Licensor or its representatives, including but not limited to - communication on electronic mailing lists, source code control systems, - and issue tracking systems that are managed by, or on behalf of, the - Licensor for the purpose of discussing and improving the Work, but - excluding communication that is conspicuously marked or otherwise - designated in writing by the copyright owner as "Not a Contribution." - - "Contributor" shall mean Licensor and any individual or Legal Entity - on behalf of whom a Contribution has been received by Licensor and - subsequently incorporated within the Work. - - 2. Grant of Copyright License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - copyright license to reproduce, prepare Derivative Works of, - publicly display, publicly perform, sublicense, and distribute the - Work and such Derivative Works in Source or Object form. - - 3. Grant of Patent License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - (except as stated in this section) patent license to make, have made, - use, offer to sell, sell, import, and otherwise transfer the Work, - where such license applies only to those patent claims licensable - by such Contributor that are necessarily infringed by their - Contribution(s) alone or by combination of their Contribution(s) - with the Work to which such Contribution(s) was submitted. If You - institute patent litigation against any entity (including a - cross-claim or counterclaim in a lawsuit) alleging that the Work - or a Contribution incorporated within the Work constitutes direct - or contributory patent infringement, then any patent licenses - granted to You under this License for that Work shall terminate - as of the date such litigation is filed. - - 4. Redistribution. You may reproduce and distribute copies of the - Work or Derivative Works thereof in any medium, with or without - modifications, and in Source or Object form, provided that You - meet the following conditions: - - (a) You must give any other recipients of the Work or - Derivative Works a copy of this License; and - - (b) You must cause any modified files to carry prominent notices - stating that You changed the files; and - - (c) You must retain, in the Source form of any Derivative Works - that You distribute, all copyright, patent, trademark, and - attribution notices from the Source form of the Work, - excluding those notices that do not pertain to any part of - the Derivative Works; and - - (d) If the Work includes a "NOTICE" text file as part of its - distribution, then any Derivative Works that You distribute must - include a readable copy of the attribution notices contained - within such NOTICE file, excluding those notices that do not - pertain to any part of the Derivative Works, in at least one - of the following places: within a NOTICE text file distributed - as part of the Derivative Works; within the Source form or - documentation, if provided along with the Derivative Works; or, - within a display generated by the Derivative Works, if and - wherever such third-party notices normally appear. The contents - of the NOTICE file are for informational purposes only and - do not modify the License. You may add Your own attribution - notices within Derivative Works that You distribute, alongside - or as an addendum to the NOTICE text from the Work, provided - that such additional attribution notices cannot be construed - as modifying the License. - - You may add Your own copyright statement to Your modifications and - may provide additional or different license terms and conditions - for use, reproduction, or distribution of Your modifications, or - for any such Derivative Works as a whole, provided Your use, - reproduction, and distribution of the Work otherwise complies with - the conditions stated in this License. - - 5. Submission of Contributions. Unless You explicitly state otherwise, - any Contribution intentionally submitted for inclusion in the Work - by You to the Licensor shall be under the terms and conditions of - this License, without any additional terms or conditions. - Notwithstanding the above, nothing herein shall supersede or modify - the terms of any separate license agreement you may have executed - with Licensor regarding such Contributions. - - 6. Trademarks. This License does not grant permission to use the trade - names, trademarks, service marks, or product names of the Licensor, - except as required for reasonable and customary use in describing the - origin of the Work and reproducing the content of the NOTICE file. - - 7. Disclaimer of Warranty. Unless required by applicable law or - agreed to in writing, Licensor provides the Work (and each - Contributor provides its Contributions) on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or - implied, including, without limitation, any warranties or conditions - of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A - PARTICULAR PURPOSE. You are solely responsible for determining the - appropriateness of using or redistributing the Work and assume any - risks associated with Your exercise of permissions under this License. - - 8. Limitation of Liability. In no event and under no legal theory, - whether in tort (including negligence), contract, or otherwise, - unless required by applicable law (such as deliberate and grossly - negligent acts) or agreed to in writing, shall any Contributor be - liable to You for damages, including any direct, indirect, special, - incidental, or consequential damages of any character arising as a - result of this License or out of the use or inability to use the - Work (including but not limited to damages for loss of goodwill, - work stoppage, computer failure or malfunction, or any and all - other commercial damages or losses), even if such Contributor - has been advised of the possibility of such damages. - - 9. Accepting Warranty or Additional Liability. While redistributing - the Work or Derivative Works thereof, You may choose to offer, - and charge a fee for, acceptance of support, warranty, indemnity, - or other liability obligations and/or rights consistent with this - License. However, in accepting such obligations, You may act only - on Your own behalf and on Your sole responsibility, not on behalf - of any other Contributor, and only if You agree to indemnify, - defend, and hold each Contributor harmless for any liability - incurred by, or claims asserted against, such Contributor by reason - of your accepting any such warranty or additional liability. - - END OF TERMS AND CONDITIONS - - APPENDIX: How to apply the Apache License to your work. - - To apply the Apache License to your work, attach the following - boilerplate notice, with the fields enclosed by brackets "{}" - replaced with your own identifying information. (Don't include - the brackets!) The text should be enclosed in the appropriate - comment syntax for the file format. We also recommend that a - file or class name and description of purpose be included on the - same "printed page" as the copyright notice for easier - identification within third-party archives. - - Copyright 2015 SendPulse - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. diff --git a/README.md b/README.md index 725959a..bf0ac23 100644 --- a/README.md +++ b/README.md @@ -1,15 +1,203 @@ -![Upload Python Package](https://github.com/sendpulse/sendpulse-rest-api-python/workflows/Upload%20Python%20Package/badge.svg?event=release) +# SendPulse REST API — Python SDK -# sendpulse-rest-api +Official Python client for the [SendPulse REST API](https://sendpulse.com/integrations/api). -A simple SendPulse REST client library and example for Python. +- Python **≥ 3.11**, single runtime dependency (`httpx`) +- OAuth 2.0 and API key authentication with automatic token caching +- Services generated from OpenAPI specs; thin ergonomic layer on top +- mypy strict · ruff · 39 unit tests -## Install using pipy +## Installation -```sh +```bash pip install pysendpulse ``` -## Examples +## Quick start -See a list of examples [here](https://github.com/sendpulse/sendpulse-rest-api-python/blob/master/pysendpulse/examples/sendpulse-rest-api-example.py) +### API key auth + +```python +from sendpulse import Client + +client = Client(api_key="YOUR_API_KEY") +``` + +### OAuth (client credentials) + +```python +client = Client(client_id="YOUR_CLIENT_ID", client_secret="YOUR_CLIENT_SECRET") +``` + +Tokens are fetched automatically and stored on disk between requests. + +## Services + +| Method | Service | +|---|---| +| `client.email_service()` | Bulk email campaigns and mailing lists | +| `client.smtp_service()` | Transactional SMTP emails | +| `client.sms_service()` | SMS campaigns | +| `client.crm_service()` | CRM contacts and deals | +| `client.chatbot_service()` | Chatbot bots | + +## Email service + +```python +email = client.email_service() + +# Campaigns +campaigns = email.campaigns().get_campaigns(limit=50) +campaign = email.campaigns().get_campaign_by_id(123) + +# Mailing lists +lists = email.mailing_lists().get_mailing_lists() +email.mailing_lists().create_mailing_list(body={"name": "My list"}) +email.mailing_lists().update_mailing_list(456, body={"name": "Renamed"}) +email.mailing_lists().delete_mailing_list(456) +``` + +## SMTP service + +```python +smtp = client.smtp_service() + +emails = smtp.emails().get_smtp_emails(limit=100, offset=0) +email = smtp.emails().get_smtp_email_info("message-id") + +smtp.emails().send_smtp_email(body={ + "email": { + "html": "

Hello

", + "text": "Hello", + "subject": "Test", + "from": {"name": "Sender", "email": "sender@example.com"}, + "to": [{"name": "Recipient", "email": "user@example.com"}], + } +}) +``` + +## SMS service + +```python +sms = client.sms_service() + +campaigns = sms.campaigns().get_sms_campaigns() +campaign = sms.campaigns().get_sms_campaign_info(789) +``` + +## CRM service + +```python +crm = client.crm_service() + +# Contacts +contacts = crm.contacts().get_contacts_list() +contacts = crm.contacts().get_contact_list_by_email(body={"email": "alice@example.com"}) +contact = crm.contacts().get_contact_by_id(1) + +# Deals +deals = crm.deals().get_deals_list() +deals = crm.deals().get_deals_list(body={"pipeline_id": 5}) +deal = crm.deals().get_deal(10) +``` + +## Chatbot service + +```python +bots = client.chatbot_service().bots().get_bots() +``` + +## Error handling + +```python +from sendpulse import ( + Client, + AuthException, # 401 / 403 — wrong credentials or insufficient permissions + RateLimitException, # 429 — too many requests + ApiException, # any other 4xx / 5xx from the API + NetworkException, # connection timeout, DNS failure, etc. + ProtocolException, # response received but could not be parsed (malformed JSON) +) + +try: + campaigns = client.email_service().campaigns().get_campaigns() +except AuthException as e: + # check credentials + print(e.http_status, e.raw_body) +except RateLimitException: + # back off and retry + pass +except ApiException as e: + print(e.http_status, e.raw_body) +except ProtocolException as e: + # unexpected response format + print(e) +except NetworkException as e: + # transport error + print(e) +``` + +## Configuration + +```python +client = Client( + api_key="key", + connect_timeout=10.0, # seconds, default + request_timeout=30.0, # seconds, default +) +``` + +## Custom HTTP client + +Any object that implements `send(request: Request) -> Response` is accepted: + +```python +from sendpulse.http.request import Request +from sendpulse.http.response import Response + +class MyHttpClient: + def send(self, request: Request) -> Response: + ... + +client = Client(api_key="key", http_client=MyHttpClient()) +``` + +## Custom token storage + +```python +from sendpulse.auth.token_storage import InMemoryTokenStorage + +# In-memory (tokens lost on process exit — useful for tests) +storage = InMemoryTokenStorage() + +client = Client( + client_id="id", + client_secret="secret", + token_storage=storage, +) +``` + +Default is `FileTokenStorage` (system temp directory or custom `cache_dir`). + +## Context manager + +Use `Client` as a context manager to ensure the underlying HTTP connection is released: + +```python +with Client(api_key="YOUR_API_KEY") as client: + lists = client.email_service().mailing_lists().get_mailing_lists() +``` + +## Documentation + +| Document | Description | +|---|---| +| [CHANGELOG.md](CHANGELOG.md) | Release history | +| [docs/authentication.md](docs/authentication.md) | OAuth 2.0 and API key auth, token storage options | +| [docs/exceptions.md](docs/exceptions.md) | Exception hierarchy and error handling patterns | +| [docs/testing.md](docs/testing.md) | How to test your code using `FakeHttpClient` | +| [docs/fastapi.md](docs/fastapi.md) | FastAPI integration guide | + +## License + +MIT diff --git a/docs/authentication.md b/docs/authentication.md new file mode 100644 index 0000000..83bc0dc --- /dev/null +++ b/docs/authentication.md @@ -0,0 +1,140 @@ +# Authentication + +The SDK supports two authentication methods: **OAuth 2.0** (recommended) and **API key**. + +## OAuth 2.0 (client credentials) + +```python +from sendpulse import Client + +client = Client( + client_id="YOUR_CLIENT_ID", + client_secret="YOUR_CLIENT_SECRET", +) +``` + +- Tokens are fetched automatically on the first request +- Cached and reused until 5 minutes before expiry +- On a `401` response the SDK invalidates the token, fetches a new one, and retries once +- You never handle tokens manually + +Obtain `client_id` and `client_secret` from your [SendPulse API settings](https://sendpulse.com/settings/api). + +## API Key + +```python +client = Client(api_key="YOUR_API_KEY") +``` + +- Simpler setup, no token management +- The key is sent as a `Bearer` header on every request +- Does not support automatic refresh — if the key is revoked, requests fail with `AuthException` + +Use API key auth for quick scripts or environments where OAuth token caching is not practical. + +--- + +## Token Storage + +Token storage only applies to OAuth. The SDK ships with two implementations. + +### FileTokenStorage (default) + +Tokens are stored as JSON files in a per-user subdirectory of the system temp directory (e.g. `/tmp/sendpulse-1000-tokens`), or in a custom path. Suitable for single-server setups. + +```python +# default — uses tempfile.gettempdir() +client = Client(client_id="...", client_secret="...") + +# custom directory +client = Client( + client_id="...", + client_secret="...", + cache_dir="/var/cache/myapp", +) +``` + +Files are written atomically (temp file + replace). + +### InMemoryTokenStorage + +Stores the token in memory for the duration of the process. Suitable for CLI scripts and tests. + +```python +from sendpulse.auth.token_storage import InMemoryTokenStorage + +client = Client( + client_id="...", + client_secret="...", + token_storage=InMemoryTokenStorage(), +) +``` + +--- + +## Choosing a storage + +| Environment | Recommended storage | +|---|---| +| Single server, long-running process | `FileTokenStorage` | +| Multiple servers / containers | Custom storage backed by Redis (see below) | +| CLI script / one-off job | `InMemoryTokenStorage` | +| Tests | `InMemoryTokenStorage` | +| FastAPI | `InMemoryTokenStorage` or custom (see [fastapi.md](fastapi.md)) | + +--- + +## Custom TokenStorage + +Implement the `TokenStorage` protocol to use any backend: + +```python +from sendpulse.auth.token_storage import TokenData +from sendpulse.auth.protocol import TokenStorageProtocol + +class RedisTokenStorage(TokenStorageProtocol): + def __init__(self, redis_client): + self._redis = redis_client + + def get(self, key: str) -> TokenData | None: + raw = self._redis.get(f"sendpulse:{key}") + if raw is None: + return None + import json + data = json.loads(raw) + return TokenData( + access_token=data["access_token"], + token_type=data["token_type"], + expires_at=data["expires_at"], + ) + + def set(self, key: str, token: TokenData) -> None: + import json, time + ttl = max(0, token["expires_at"] - int(time.time())) + self._redis.setex( + f"sendpulse:{key}", + ttl, + json.dumps({ + "access_token": token["access_token"], + "token_type": token["token_type"], + "expires_at": token["expires_at"], + }), + ) + + def delete(self, key: str) -> None: + self._redis.delete(f"sendpulse:{key}") + +client = Client( + client_id="...", + client_secret="...", + token_storage=RedisTokenStorage(redis_client), +) +``` + +The `TokenData` object passed to `set()` is a `TypedDict` with this shape: + +```python +token["access_token"] # str +token["token_type"] # str — always "Bearer" +token["expires_at"] # int — Unix timestamp +``` diff --git a/docs/exceptions.md b/docs/exceptions.md new file mode 100644 index 0000000..51b9224 --- /dev/null +++ b/docs/exceptions.md @@ -0,0 +1,163 @@ +# Exception Reference + +All SDK exceptions inherit from `Exception` and are thrown only — never swallowed internally. + +## Hierarchy + +``` +Exception +├── SendPulseException +│ ├── AuthException — 401, 403 +│ ├── RateLimitException — 429 +│ └── ApiException — other 4xx, 5xx +├── NetworkException — transport / httpx failure +└── ProtocolException — response received but cannot be parsed +``` + +`SendPulseException` carries two attributes: + +```python +e.http_status # int — HTTP status code +e.raw_body # str — raw response body +``` + +`NetworkException` and `ProtocolException` do not have these — they occur before or outside of a valid HTTP exchange. + +--- + +## AuthException + +**When:** API returns `401` or `403`. + +**Causes:** +- Invalid or expired API key +- Invalid OAuth credentials (`client_id` / `client_secret`) +- Insufficient permissions for the requested resource + +**What to do:** check your credentials. For OAuth, the SDK automatically retries once with a fresh token on `401` before throwing — so if you see this exception, the refresh also failed. + +```python +except AuthException as e: + print(e.http_status) # 401 or 403 + print(e.raw_body) # {"error": "..."} +``` + +--- + +## RateLimitException + +**When:** API returns `429`. + +**Causes:** exceeded the request rate limit (SendPulse allows up to 10 requests per second). + +**What to do:** back off and retry. Consider queuing high-volume operations. + +```python +except RateLimitException: + time.sleep(1) + # retry or dispatch to queue +``` + +--- + +## ApiException + +**When:** API returns any other `4xx` or `5xx` response. + +**Causes:** +- `400` — malformed request body +- `404` — resource not found +- `422` — validation error +- `500` — internal server error on SendPulse side + +```python +except ApiException as e: + print(e.http_status) # e.g. 422 + print(e.raw_body) # {"message": "The email field is required."} +``` + +--- + +## NetworkException + +**When:** the HTTP request could not be completed at the transport level. + +**Causes:** +- DNS resolution failure +- Connection refused or timed out +- SSL/TLS handshake error +- httpx transport error + +No HTTP response was received. Retrying after a delay is often appropriate. + +```python +except NetworkException as e: + print(e) # "Connection error: ..." +``` + +--- + +## ProtocolException + +**When:** a response was received but could not be parsed. + +**Causes:** +- API returned malformed JSON (e.g. during a server-side incident) +- OAuth token endpoint returned an unexpected response shape + +Unlike `NetworkException`, the network itself worked. Retrying is unlikely to help until the API-side issue is resolved. + +```python +except ProtocolException as e: + print(e) # "Failed to decode response body: ..." +``` + +--- + +## Recommended catch order + +Catch from most specific to least specific: + +```python +from sendpulse import ( + AuthException, + RateLimitException, + ApiException, + ProtocolException, + NetworkException, +) + +try: + result = client.smtp_service().emails().send_smtp_email(payload) +except AuthException as e: + # credentials problem — do not retry + pass +except RateLimitException: + # slow down — retry after delay + pass +except ApiException as e: + # API rejected the request — log and inspect e.raw_body + pass +except ProtocolException as e: + # unexpected response format — log and alert + pass +except NetworkException as e: + # transport failure — retry after delay + pass +``` + +To catch any HTTP-level SDK exception in one block: + +```python +from sendpulse import SendPulseException + +try: + # ... + pass +except SendPulseException as e: + # covers AuthException, RateLimitException, ApiException + print(e.http_status, e.raw_body) +except Exception as e: + # covers NetworkException and ProtocolException + print(e) +``` diff --git a/docs/fastapi.md b/docs/fastapi.md new file mode 100644 index 0000000..7ea3c5e --- /dev/null +++ b/docs/fastapi.md @@ -0,0 +1,247 @@ +# FastAPI Integration + +## Installation + +```bash +pip install sendpulse-rest-api pydantic-settings +``` + +## Configuration + +Create `config.py` using `pydantic-settings`: + +```python +# config.py +from pydantic_settings import BaseSettings, SettingsConfigDict + +class Settings(BaseSettings): + sendpulse_client_id: str = "" + sendpulse_client_secret: str = "" + # or API key auth: + # sendpulse_api_key: str = "" + + model_config = SettingsConfigDict(env_file=".env", env_file_encoding="utf-8") +``` + +Add to `.env`: + +``` +SENDPULSE_CLIENT_ID=your-client-id +SENDPULSE_CLIENT_SECRET=your-client-secret +``` + +--- + +## Registering the Client + +### Option 1 — lifespan singleton (recommended) + +Create the client once at startup and share it for the lifetime of the application: + +```python +# main.py +from contextlib import asynccontextmanager +from functools import lru_cache +from fastapi import FastAPI +from sendpulse import Client +from sendpulse.auth.token_storage import InMemoryTokenStorage +from config import Settings + +@lru_cache +def get_settings() -> Settings: + return Settings() + +_sendpulse_client: Client | None = None + +@asynccontextmanager +async def lifespan(app: FastAPI): + global _sendpulse_client + settings = get_settings() + _sendpulse_client = Client( + client_id=settings.sendpulse_client_id, + client_secret=settings.sendpulse_client_secret, + token_storage=InMemoryTokenStorage(), + ) + yield + _sendpulse_client = None + +app = FastAPI(lifespan=lifespan) + +def get_sendpulse() -> Client: + assert _sendpulse_client is not None + return _sendpulse_client +``` + +### Option 2 — `lru_cache` dependency + +Simpler alternative; client is created on first request: + +```python +from functools import lru_cache +from fastapi import Depends +from sendpulse import Client +from sendpulse.auth.token_storage import InMemoryTokenStorage +from config import Settings + +@lru_cache +def get_settings() -> Settings: + return Settings() + +@lru_cache +def get_sendpulse(settings: Settings = Depends(get_settings)) -> Client: + return Client( + client_id=settings.sendpulse_client_id, + client_secret=settings.sendpulse_client_secret, + token_storage=InMemoryTokenStorage(), + ) +``` + +--- + +## Usage in routes + +```python +from fastapi import APIRouter, Depends +from sendpulse import Client + +router = APIRouter(prefix="/campaigns", tags=["campaigns"]) + +@router.get("/") +def list_campaigns(sendpulse: Client = Depends(get_sendpulse)): + return sendpulse.email_service().campaigns().get_campaigns(limit=20) + +@router.get("/{campaign_id}") +def get_campaign(campaign_id: int, sendpulse: Client = Depends(get_sendpulse)): + return sendpulse.email_service().campaigns().get_campaign_by_id(campaign_id) +``` + +--- + +## Error Handling + +Use FastAPI exception handlers to translate SDK exceptions into HTTP responses: + +```python +from fastapi import Request +from fastapi.responses import JSONResponse +from sendpulse import AuthException, RateLimitException, ApiException, NetworkException, ProtocolException + +@app.exception_handler(AuthException) +async def auth_exception_handler(request: Request, exc: AuthException): + return JSONResponse(status_code=502, content={"detail": "SendPulse authentication failed"}) + +@app.exception_handler(RateLimitException) +async def rate_limit_handler(request: Request, exc: RateLimitException): + return JSONResponse(status_code=429, content={"detail": "SendPulse rate limit exceeded"}) + +@app.exception_handler(ApiException) +async def api_exception_handler(request: Request, exc: ApiException): + return JSONResponse(status_code=502, content={"detail": f"SendPulse API error: {exc.http_status}"}) + +@app.exception_handler(NetworkException) +@app.exception_handler(ProtocolException) +async def connection_error_handler(request: Request, exc: Exception): + return JSONResponse(status_code=502, content={"detail": "SendPulse connection error"}) +``` + +--- + +## Background Tasks + +For non-blocking sends use FastAPI's `BackgroundTasks`: + +```python +from fastapi import BackgroundTasks, Depends +from sendpulse import Client + +@router.post("/send-welcome") +def send_welcome( + email: str, + background_tasks: BackgroundTasks, + sendpulse: Client = Depends(get_sendpulse), +): + def _send(): + sendpulse.smtp_service().emails().send_smtp_email({ + "email": { + "html": "

Welcome!

", + "subject": "Welcome to our service", + "from": {"name": "App", "email": "no-reply@example.com"}, + "to": [{"email": email}], + } + }) + + background_tasks.add_task(_send) + return {"status": "queued"} +``` + +For high-volume or retry-capable sends, use **Celery**: + +```python +# tasks.py +from celery import Celery +from sendpulse import Client +from config import Settings + +celery = Celery("tasks", broker="redis://localhost:6379/0") +settings = Settings() +_client = Client( + client_id=settings.sendpulse_client_id, + client_secret=settings.sendpulse_client_secret, +) + +@celery.task(autoretry_for=(Exception,), retry_backoff=True, max_retries=3) +def send_email(payload: dict) -> None: + _client.smtp_service().emails().send_smtp_email(payload) +``` + +```python +# in your route +from tasks import send_email + +send_email.delay(payload) +``` + +--- + +## Testing + +Override the `get_sendpulse` dependency in tests using `app.dependency_overrides`: + +```python +# test_campaigns.py +import pytest +from fastapi.testclient import TestClient +from sendpulse import Client +from main import app, get_sendpulse + +class FakeHttpClient: + def __init__(self, *responses): + from collections import deque + self._queue = deque(responses) + def send(self, request): + return self._queue.popleft() + +def make_fake_client(responses): + from sendpulse.http.response import Response + fake_http = FakeHttpClient(*responses) + return Client(api_key="test", http_client=fake_http) + +@pytest.fixture +def test_client(): + return TestClient(app) + +def test_list_campaigns_returns_data(test_client): + from sendpulse.http.response import Response + + fake_sp = make_fake_client([ + Response(200, {}, '[{"id": 1, "name": "Promo"}]') + ]) + app.dependency_overrides[get_sendpulse] = lambda: fake_sp + + response = test_client.get("/campaigns/") + + assert response.status_code == 200 + assert response.json()[0]["name"] == "Promo" + + app.dependency_overrides.clear() +``` diff --git a/docs/testing.md b/docs/testing.md new file mode 100644 index 0000000..18ebfe3 --- /dev/null +++ b/docs/testing.md @@ -0,0 +1,187 @@ +# Testing + +## Testing your own code that uses the SDK + +The recommended approach is to **not mock `Client` at all** — instead, substitute the HTTP transport layer with a fake. This way your tests exercise real SDK logic (request building, response parsing, model hydration) without making network calls. + +### FakeHttpClient + +Create a simple fake that implements the same interface as the SDK's HTTP client: + +```python +from dataclasses import dataclass, field +from sendpulse.http.request import Request +from sendpulse.http.response import Response + + +@dataclass +class FakeHttpClient: + responses: list[Response] = field(default_factory=list) + calls: list[Request] = field(default_factory=list) + _index: int = field(default=0, init=False) + + def send(self, request: Request) -> Response: + self.calls.append(request) + response = self.responses[self._index] + self._index += 1 + return response + + @property + def call_count(self) -> int: + return self._index + + @staticmethod + def ok(body: str = '{"ok": true}') -> Response: + return Response(status_code=200, headers={}, body=body) + + @staticmethod + def error(status: int, body: str = "error") -> Response: + return Response(status_code=status, headers={}, body=body) +``` + +Pass it to `Client` via the `http_client` argument: + +```python +from sendpulse import Client +from sendpulse.auth.token_storage import InMemoryTokenStorage + +fake = FakeHttpClient(responses=[FakeHttpClient.ok('[{"id": 1, "name": "Newsletter"}]')]) + +client = Client(api_key="test-key", http_client=fake) +``` + +Now `client` makes no real HTTP calls — every `send()` returns the next queued response. + +--- + +### pytest fixture + +```python +# conftest.py +import pytest +from sendpulse import Client +from sendpulse.auth.token_storage import InMemoryTokenStorage + + +@pytest.fixture +def fake_http(): + return FakeHttpClient() + + +@pytest.fixture +def sendpulse_client(fake_http): + return Client(api_key="test-key", http_client=fake_http), fake_http +``` + +### Full example + +```python +def test_get_campaigns_returns_mapped_models(sendpulse_client): + client, fake = sendpulse_client + fake.responses.append(FakeHttpClient.ok('[{"id": 42, "name": "Black Friday"}]')) + + campaigns = client.email_service().campaigns().get_campaigns() + + assert len(campaigns) == 1 + assert campaigns[0].id == 42 + assert campaigns[0].name == "Black Friday" + + +def test_get_campaigns_builds_correct_request(sendpulse_client): + client, fake = sendpulse_client + fake.responses.append(FakeHttpClient.ok("[]")) + + client.email_service().campaigns().get_campaigns(limit=50, offset=10) + + req = fake.calls[-1] + assert "limit=50" in req.uri + assert "offset=10" in req.uri +``` + +--- + +## Testing error handling + +Queue an error response to verify your exception handling: + +```python +from sendpulse import AuthException, RateLimitException +import pytest + + +def test_handles_auth_error(sendpulse_client): + client, fake = sendpulse_client + fake.responses.append(FakeHttpClient.error(401, '{"error": "Unauthorized"}')) + + with pytest.raises(AuthException): + client.email_service().campaigns().get_campaigns() + + +def test_handles_rate_limit(sendpulse_client): + client, fake = sendpulse_client + fake.responses.append(FakeHttpClient.error(429, '{"error": "Too Many Requests"}')) + + with pytest.raises(RateLimitException): + client.smtp_service().emails().send_smtp_email({}) +``` + +--- + +## Token storage in tests + +Use `InMemoryTokenStorage` to avoid file I/O and keep tests isolated: + +```python +from sendpulse import Client +from sendpulse.auth.token_storage import InMemoryTokenStorage + +token_resp = '{"access_token": "tok", "token_type": "Bearer", "expires_in": 3600}' + +fake = FakeHttpClient(responses=[ + FakeHttpClient.ok(token_resp), # token fetch + FakeHttpClient.ok('{"id": 1}'), # actual API call +]) + +client = Client( + client_id="test-id", + client_secret="test-secret", + http_client=fake, + token_storage=InMemoryTokenStorage(), +) +``` + +--- + +## Testing code that wraps the SDK + +If you inject `Client` into your own services, type-hint against it and override in tests: + +```python +# my_service.py +from sendpulse import Client + +class EmailNotifier: + def __init__(self, sendpulse: Client) -> None: + self._sp = sendpulse + + def notify(self, to: str, subject: str, html: str) -> None: + self._sp.smtp_service().emails().send_smtp_email({ + "email": { + "html": html, "subject": subject, + "from": {"name": "App", "email": "no-reply@example.com"}, + "to": [{"email": to}], + } + }) + + +# test_my_service.py +def test_notify_sends_email(): + fake = FakeHttpClient(responses=[FakeHttpClient.ok()]) + client = Client(api_key="test", http_client=fake) + notifier = EmailNotifier(sendpulse=client) + + notifier.notify("user@example.com", "Hello", "

Hi

") + + assert fake.call_count == 1 + assert "smtp" in fake.calls[0].uri +``` diff --git a/examples/quickstart.py b/examples/quickstart.py new file mode 100644 index 0000000..38ea735 --- /dev/null +++ b/examples/quickstart.py @@ -0,0 +1,59 @@ +"""SendPulse Python SDK — quickstart example.""" + +import os +import sys + +from sendpulse import ( + ApiException, + AuthException, + Client, + NetworkException, + RateLimitException, +) + +# ── OAuth (recommended for production) ─────────────────────────────────────── +client = Client( + client_id=os.environ.get("SENDPULSE_CLIENT_ID", "your-client-id"), + client_secret=os.environ.get("SENDPULSE_CLIENT_SECRET", "your-client-secret"), +) + +# ── or a static API key ─────────────────────────────────────────────────────── +# client = Client(api_key=os.environ.get("SENDPULSE_API_KEY", "your-api-key")) + +# ── with custom token cache and timeouts ────────────────────────────────────── +# client = Client( +# client_id="your-client-id", +# client_secret="your-client-secret", +# cache_dir="/var/cache/sendpulse", +# connect_timeout=5.0, +# request_timeout=15.0, +# ) + +# ── list campaigns ──────────────────────────────────────────────────────────── +try: + campaigns = client.email_service().campaigns().get_campaigns(limit=10) + + print(f"Campaigns ({len(campaigns)}):") + for campaign in campaigns: + print(f" [{campaign.id}] {campaign.name}") + +except AuthException as e: + # 401 or 403 — invalid key or token + print(f"Auth error: {e}") + sys.exit(1) +except RateLimitException: + # 429 — request rate limit exceeded (10 req/s) + print("Rate limited — slow down") + sys.exit(1) +except ApiException as e: + # other 4xx / 5xx + print(f"API error {e.http_status}: {e.raw_body}") + sys.exit(1) +except NetworkException as e: + # network error, timeout, or invalid JSON in response + print(f"Network error: {e}") + sys.exit(1) + +# ── fetch a single campaign by ID ───────────────────────────────────────────── +# campaign = client.email_service().campaigns().get_campaign_by_id(12345) +# print(campaign.name) diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..30938f6 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,62 @@ +[build-system] +requires = ["hatchling"] +build-backend = "hatchling.build" + +[project] +name = "pysendpulse" +dynamic = ["version"] +description = "Official Python SDK for the SendPulse REST API" +readme = "README.md" +license = { file = "LICENSE" } +requires-python = ">=3.11" +keywords = ["sendpulse", "api", "rest", "sdk", "email", "sms", "crm", "chatbots"] +classifiers = [ + "Development Status :: 5 - Production/Stable", + "Intended Audience :: Developers", + "License :: OSI Approved :: MIT License", + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", + "Programming Language :: Python :: 3.13", + "Topic :: Communications :: Email", + "Topic :: Software Development :: Libraries :: Python Modules", +] +authors = [ + { name = "Maksym Ustymenko", email = "tech@sendpulse.com" }, +] +dependencies = [ + "httpx>=0.27", +] + +[project.optional-dependencies] +dev = [ + "pytest>=8.0", + "mypy>=1.10", + "ruff>=0.4", +] + +[project.urls] +Homepage = "https://github.com/sendpulse/sendpulse-rest-api-python" +Repository = "https://github.com/sendpulse/sendpulse-rest-api-python" + +[tool.hatch.version] +path = "sendpulse/__init__.py" + +[tool.hatch.build.targets.wheel] +packages = ["sendpulse"] + +[tool.mypy] +python_version = "3.11" +strict = true +exclude = ["sendpulse/generated"] + +[tool.ruff] +target-version = "py311" +line-length = 100 +exclude = ["sendpulse/generated", ".venv"] + +[tool.ruff.lint] +select = ["E", "F", "I", "UP"] + +[tool.pytest.ini_options] +testpaths = ["tests"] diff --git a/pysendpulse/__init__.py b/pysendpulse/__init__.py deleted file mode 100644 index b021993..0000000 --- a/pysendpulse/__init__.py +++ /dev/null @@ -1,5 +0,0 @@ -__author__ = 'Maksym Ustymenko' -__author_email__ = 'tech@sendpulse.com' -__copyright__ = 'Copyright 2017, SendPulse' -__credits__ = ['Maksym Ustymenko', ] -__version__ = '0.1.8' diff --git a/pysendpulse/examples/sendpulse-rest-api-example.py b/pysendpulse/examples/sendpulse-rest-api-example.py deleted file mode 100644 index 58255c2..0000000 --- a/pysendpulse/examples/sendpulse-rest-api-example.py +++ /dev/null @@ -1,256 +0,0 @@ -# -*-coding:utf8-*- - -""" SendPulse REST API usage example - -Documentation: - https://login.sendpulse.com/manual/rest-api/ - https://sendpulse.com/api -""" - -from pysendpulse.pysendpulse import PySendPulse - -if __name__ == "__main__": - REST_API_ID = '' - REST_API_SECRET = '' - TOKEN_STORAGE = 'memcached' - MEMCACHED_HOST = '127.0.0.1:11211' - SPApiProxy = PySendPulse(REST_API_ID, REST_API_SECRET, TOKEN_STORAGE, memcached_host=MEMCACHED_HOST) - - # Get list of tasks - SPApiProxy.push_get_tasks() - - # Get list of websites - SPApiProxy.push_get_websites() - - # Get amount of websites - SPApiProxy.push_count_websites() - - # Get list of variables for website - SPApiProxy.push_get_variables(WEBSITE_ID) - - # Get list of subscriptions for website - SPApiProxy.push_get_subscriptions(WEBSITE_ID) - - # Get amount of subscriptions for website - SPApiProxy.push_count_subscriptions(WEBSITE_ID) - - # Activate/Deactivate subscriber, state=1 - activate, state=2 - deactivate - SPApiProxy.push_set_subscription_state(SUBSCRIBER_ID, STATE) - - # Create new push task - SPApiProxy.push_create('Hello!', WEBSITE_ID, 'This is my first push message', '10', - {'filter_lang': 'en', 'filter': '{"variable_name":"some","operator":"or","conditions":[{"condition":"likewith","value":"a"},{"condition":"notequal","value":"b"}]}'}) - - # Get balance in Japanese Yen - SPApiProxy.get_balance('JPY') - - # Get Mailing Lists list example - SPApiProxy.get_list_of_addressbooks() - - # Get Mailing Lists list with limit and offset example - SPApiProxy.get_list_of_addressbooks(offset=5, limit=2) - - # Add emails with variables to addressbook - emails_for_add = [ - { - 'email': 'test1@test1.com', - 'variables': { - 'name': 'test11', - 'number': '11' - } - }, - {'email': 'test2@test2.com'}, - { - 'email': 'test3@test3.com', - 'variables': { - 'firstname': 'test33', - 'age': 33, - 'date': '2015-09-30' - } - } - ] - SPApiProxy.add_emails_to_addressbook(ADDRESSBOOK_ID, emails_for_add) - - # Delete email from addressbook - emails_for_delete = ['test4@test4.com'] - SPApiProxy.delete_emails_from_addressbook(ADDRESSBOOK_ID, emails_for_delete) - - # Get a list of variables available on a mailing list - SPApiProxy.get_addressbook_variables(ADDRESSBOOK_ID) - - # Changing a variable for an email contact - SPApiProxy.set_variables_for_email(ADDRESSBOOK_ID, 'example@email.com', [{'name': 'foo', 'value': 'bar'}]) - - # Get campaigns statistic for list of emails - emails_list = ['test@test.com'] - SPApiProxy.get_emails_stat_by_campaigns(emails_list) - - # Add sender "FROM" email - SPApiProxy.add_sender('jane.roe@domain.com', 'Jane Roe') - - # Get list of senders - SPApiProxy.get_list_of_senders() - - # Add emails to unsubscribe list - SPApiProxy.smtp_add_emails_to_unsubscribe([ - {'email': 'test_1@domain_1.com', 'comment': 'comment_1'}, - {'email': 'test_2@domain_2.com', 'comment': 'comment_2'} - ]) - - # Create new email campaign with attaches - task_body = "

Hello, John!

This is the test task from https://sendpulse.com/api REST API!

" - SPApiProxy.add_campaign(from_email='jane.roe@domain.com', - from_name='Jane Roe', - subject='Test campaign from REST API', - body=task_body, - addressbook_id=ADDRESSBOOK_ID, - campaign_name='Test campaign from REST API', - attachments={'attach1.txt': '12345\n', 'attach2.txt': '54321\n'}) - - # Send mail using SMTP - email = { - 'subject': 'This is the test task from REST API', - 'html': '

Hello, John!

This is the test task from https://sendpulse.com/api REST API!

', - 'text': 'Hello, John!\nThis is the test task from https://sendpulse.com/api REST API!', - 'from': {'name': 'John Doe', 'email': 'john.doe@domain.com'}, - 'to': [ - {'name': 'Jane Roe', 'email': 'jane.roe@domain.com'} - ], - 'bcc': [ - {'name': 'Richard Roe', 'email': 'richard.roe@domain.com'} - ] - } - SPApiProxy.smtp_send_mail(email) - - # Send mail with template using SMTP - email = { - 'subject': 'This is the test task from REST API', - 'from': {'name': 'John Doe', 'email': 'john.doe@domain.com'}, - 'to': [ - {'name': 'Jane Roe', 'email': 'jane.roe@domain.com'} - ], - "template": { - 'id': '73606', # ID of the template uploaded in the service. Use this - # (https://sendpulse.com/integrations/api/bulk-email#template-list) - # method to get the template ID (use either real_id or id parameter from the reply) - 'variables': { - 'foo': 'value', - 'bar': 'value' - } - }, - } - SPApiProxy.smtp_send_mail_with_template(email) - - # **************** SMS *************** - - # Add phones to address book - phones_for_add = [ - '11111111111', - '22222222222' - ] - SPApiProxy.sms_add_phones(ADDRESSBOOK_ID, phones_for_add) - - # Add phones to address book - phones_for_add = { - "11111111111": - [ - [ - {"name": "test1", "type": "date", "value": "2018-10-10 23:00:00"}, - {"name": "test2", "type": "string", "value": "asdasd"}, - {"name": "test3", "type": "number", "value": "123"} - ] - ], - "22222222222": - [ - [ - {"name": "test1", "type": "date", "value": "2018-10-10 23:00:00"}, - {"name": "test2", "type": "string", "value": "czxczx"}, - {"name": "test3", "type": "number", "value": "456"} - ] - ] - } - - SPApiProxy.sms_add_phones_with_variables(ADDRESSBOOK_ID, phones_for_add) - - # Update phones variables from the address book - phones_for_update = [ - '11111111111' - ] - variables = [ - { - "name": "name", "type": "string", "value": "Michael" - } - ] - SPApiProxy.sms_update_phones_variables(ADDRESSBOOK_ID, phones_for_update, variables) - - # Get information about phone from the address book - SPApiProxy.sms_get_phone_info(ADDRESSBOOK_ID, '1111111111') - - # Remove phones to address book - phones_for_remove = [ - '11111111111', - '22222222222' - ] - SPApiProxy.sms_delete_phones(ADDRESSBOOK_ID, phones_for_remove) - - # Get phones from the blacklist - SPApiProxy.sms_get_blacklist() - - # Add phones to blacklist - phones_for_add_to_blacklist = [ - '111222227', - '222333337' - ] - SPApiProxy.sms_add_phones_to_blacklist(phones_for_add_to_blacklist, 'test') - - # Remove phones from blacklist - phones_for_remove = [ - '11111111111', - '22222222222' - ] - SPApiProxy.sms_delete_phones_from_blacklist(phones_for_remove) - - # Get info by phones from the blacklist - phones = [ - '11111111111', - '22222222222' - ] - SPApiProxy.sms_get_phones_info_from_blacklist(phones) - - # Create new sms campaign by addressbook_id - SPApiProxy.sms_add_campaign_by_addressbook_id(SENDER_NAME, ADDRESSBOOK_ID, 'test') - - # Create new sms campaign by some phones - phones_for_send = [ - '11111111111' - ] - SPApiProxy.sms_add_campaign_by_phones(SENDER_NAME, phones_for_send, 'test') - - # Get list of sms campaigns - date_from = '2018-04-10 23:00:00' - date_to = '2018-05-10 23:00:00' - SPApiProxy.sms_get_list_campaigns(date_from, date_to) - - # Get information about sms campaign - SPApiProxy.sms_get_campaign_info(CAMPAIGN_ID) - - # Cancel sms campaign - SPApiProxy.sms_cancel_campaign(CAMPAIGN_ID) - - # Get cost sms campaign - SPApiProxy.sms_get_campaign_cost('sender', 'test', ADDRESSBOOK_ID) - # SPApiProxy.sms_get_campaign_cost('sender', 'test', None, ['111111111']) - - # Remove sms campaign - SPApiProxy.sms_delete_campaign(CAMPAIGN_ID) - - # **************** EVENT *************** - - # Start event - params = { - "email": "test1@test1.com", - "phone": "+123456789", - "var_1": "var_1_value" - } - - SPApiProxy.send_event('registration', params); \ No newline at end of file diff --git a/pysendpulse/pysendpulse.py b/pysendpulse/pysendpulse.py deleted file mode 100644 index 934723d..0000000 --- a/pysendpulse/pysendpulse.py +++ /dev/null @@ -1,1210 +0,0 @@ -# -*- encoding:utf8 -*- - -""" API wrapper for interacting with SendPulse REST API -Documentation: - https://login.sendpulse.com/manual/rest-api/ - https://sendpulse.com/api -""" - -import os -import memcache -import requests -import logging -import base64 -from hashlib import md5 -from deprecated import deprecated - -try: - import simplejson as json -except ImportError: - try: - import json - except ImportError: - try: - from django.utils import simplejson as json - except ImportError: - raise ImportError('A json library is required to use this python library') - -logger = logging.getLogger(__name__) -logger.propagate = False -ch = logging.StreamHandler() -ch.setFormatter(logging.Formatter('%(levelname)-8s [%(asctime)s] %(message)s')) -logger.addHandler(ch) - -class PySendPulse: - """ SendPulse REST API python wrapper - """ - __api_url = "https://api.sendpulse.com" - __user_id = None - __secret = None - __token = None - __token_file_path = "" - __token_hash_name = None - __storage_type = "FILE" - __refresh_token = 0 - __memcached_host = "127.0.0.1:11211" - - MEMCACHED_VALUE_TIMEOUT = 3600 - ALLOWED_STORAGE_TYPES = ['FILE', 'MEMCACHED'] - - def __init__(self, user_id, secret, storage_type="FILE", token_file_path="", memcached_host="127.0.0.1:11211"): - """ SendPulse API constructor - - @param user_id: string REST API ID from SendPulse settings - @param secret: string REST API Secret from SendPulse settings - @param storage_type: string FILE|MEMCACHED - @param memcached_host: string Host for Memcached server, default is 127.0.0.1:11211 - @raise: Exception empty credentials or get token failed - """ - logger.info("Initialization SendPulse REST API Class") - if not user_id or not secret: - raise Exception("Empty ID or SECRET") - - self.__user_id = user_id - self.__secret = secret - self.__storage_type = storage_type.upper() - self.__token_file_path = token_file_path - self.__memcached_host = memcached_host - m = md5() - m.update("{}::{}".format(user_id, secret).encode('utf-8')) - self.__token_hash_name = m.hexdigest() - if self.__storage_type not in self.ALLOWED_STORAGE_TYPES: - logger.warning("Wrong storage type '{}'. Allowed storage types are: {}".format(storage_type, self.ALLOWED_STORAGE_TYPES)) - logger.warning("Try to use 'FILE' instead.") - self.__storage_type = 'FILE' - logger.debug("Try to get security token from '{}'".format(self.__storage_type, )) - if self.__storage_type == "MEMCACHED": - mc = memcache.Client([self.__memcached_host]) - self.__token = mc.get(self.__token_hash_name) - else: # file - filepath = "{}{}".format(self.__token_file_path, self.__token_hash_name) - if os.path.isfile(filepath): - with open(filepath, 'rb') as f: - self.__token = f.readline() - - else: - logger.warning("Can't find file '{}' to read security token.".format(filepath)) - logger.debug("Got: '{}'".format(self.__token, )) - if not self.__token and not self.__get_token(): - raise Exception("Could not connect to API. Please, check your ID and SECRET") - - def __get_token(self): - """ Get new token from API server and store it in storage - @return: boolean - """ - logger.debug("Try to get new token from server") - self.__refresh_token += 1 - data = { - "grant_type": "client_credentials", - "client_id": self.__user_id, - "client_secret": self.__secret, - } - response = self.__send_request("oauth/access_token", "POST", data, False) - if response.status_code != 200: - return False - self.__refresh_token = 0 - self.__token = response.json()['access_token'] - logger.debug("Got: '{}'".format(self.__token, )) - if self.__storage_type == "MEMCACHED": - logger.debug("Try to set token '{}' into 'MEMCACHED'".format(self.__token, )) - mc = memcache.Client([self.__memcached_host]) - mc.set(self.__token_hash_name, self.__token, self.MEMCACHED_VALUE_TIMEOUT) - else: - filepath = "{}{}".format(self.__token_file_path, self.__token_hash_name) - try: - if not os.path.isdir(self.__token_file_path): - os.makedirs(self.__token_file_path, exist_ok=True) - - with open(filepath, 'w') as f: - f.write(self.__token) - logger.debug("Set token '{}' into 'FILE' '{}'".format(self.__token, filepath)) - except IOError: - logger.warning("Can't create 'FILE' to store security token. Please, check your settings.") - if self.__token: - return True - return False - - def __send_request(self, path, method="GET", params=None, use_token=True, use_json_content_type=False): - """ Form and send request to API service - - @param path: sring what API url need to call - @param method: HTTP method GET|POST|PUT|DELETE - @param params: dict argument need to send to server - @param use_token: boolean need to use token or not - @param use_json_content_type: boolean need to convert params data to json or not - @return: HTTP requests library object http://www.python-requests.org/ - """ - url = "{}/{}".format(self.__api_url, path) - method.upper() - logger.debug("__send_request method: {} url: '{}' with parameters: {}".format(method, url, params)) - if type(params) not in (dict, list): - params = {} - if use_token and self.__token: - headers = {'Authorization': 'Bearer {}'.format(self.__token)} - else: - headers = {} - # if use_json_content_type and params: - headers['Content-Type'] = 'application/json' - params = json.dumps(params) - - if method == "POST": - response = requests.post(url, headers=headers, data=params) - elif method == "PUT": - response = requests.put(url, headers=headers, data=params) - elif method == "DELETE": - response = requests.delete(url, headers=headers, data=params) - else: - response = requests.get(url, headers=headers, params=params) - if response.status_code == 401 and self.__refresh_token == 0: - self.__get_token() - return self.__send_request(path, method, json.loads(params), use_token) - elif response.status_code == 404: - logger.warning("404: Sorry, the page you are looking for could not be found.") - logger.debug("Raw_server_response: {}".format(response.text, )) - elif response.status_code == 500: - logger.critical("Whoops, looks like something went wrong on the server. Please contact with out support tech@sendpulse.com.") - else: - try: - logger.debug("Request response: {}".format(response.json(), )) - except: - logger.critical("Raw server response: {}".format(response.text, )) - return response - - def __handle_result(self, data): - """ Process request results - - @param data: a Response object from the Python Requests package - @return: dictionary with response message and/or http code - """ - try: - result = data.json() - errors = {} - except: - result = {} - errors = { - 'is_error': True, - 'http_code': data.status_code, - 'message': "Response is empty, invalid or not JSON." - } - - if data.ok: - logger.debug("Handle result: {}".format(result, )) - else: - errors = { - 'is_error': True, - 'http_code': data.status_code - } - if data.status_code == 404: - errors['message'] = "Sorry, the page you are looking for {} could not be found.".format(data.url, ) - elif data.status_code == 500: - errors['message'] = "Whoops, looks like something went wrong on the server. Please contact with out support tech@sendpulse.com." - - logger.debug("Handle result: {}".format(errors, )) - - # return object that maintains backward-compatibility - if not data.ok: - result = {'data': errors} - return result - - def __handle_error(self, custom_message=None): - """ Process request errors - - @param custom_message: - @return: dictionary with response custom error message and/or error code - """ - message = {'is_error': True} - if custom_message is not None: - message['message'] = custom_message - logger.error("Handle error: {}".format(message, )) - return message - - # ------------------------------------------------------------------ # - # BALANCE # - # ------------------------------------------------------------------ # - - def get_balance(self, currency=None): - """ Get balance - - @param currency: USD, EUR, GBP, UAH, RUR, INR, JPY - @return: dictionary with response message - """ - logger.info("Function call: get_balance") - return self.__handle_result(self.__send_request('balance/{}'.format(currency.upper() if currency else ''), )) - - # ------------------------------------------------------------------ # - # ADDRESSBOOKS # - # ------------------------------------------------------------------ # - - def add_addressbook(self, addressbook_name): - """ Create addressbook - - @param addressbook_name: string name for addressbook - @return: dictionary with response message - """ - logger.info("Function call: create_addressbook: '{}'".format(addressbook_name, )) - return self.__handle_error("Empty AddressBook name") if not addressbook_name else self.__handle_result(self.__send_request('addressbooks', 'POST', {'bookName': addressbook_name})) - - def edit_addressbook(self, id, new_addressbook_name): - """ Edit addressbook name - - @param id: unsigned int addressbook ID - @param new_addressbook_name: string new name for addressbook - @return: dictionary with response message - """ - logger.info("Function call: edit_addressbook: '{}' with new addressbook name '{}'".format(id, new_addressbook_name)) - if not id or not new_addressbook_name: - return self.__handle_error("Empty new name or addressbook id") - return self.__handle_result(self.__send_request('addressbooks/{}'.format(id), 'PUT', {'name': new_addressbook_name})) - - def delete_addressbook(self, id): - """ Remove addressbook - - @param id: unsigned int addressbook ID - @return: dictionary with response message - """ - logger.info("Function call: remove_addressbook: '{}'".format(id, )) - return self.__handle_error("Empty addressbook id") if not id else self.__handle_result(self.__send_request('addressbooks/{}'.format(id), 'DELETE')) - - def get_list_of_addressbooks(self, limit=0, offset=0): - """ Get list of addressbooks - - @param limit: unsigned int max limit of records. The max value is 100 - @param offset: unsigned int how many records pass before selection - @return: dictionary with response message - """ - logger.info("Function call: get_list_of_addressbooks") - return self.__handle_result(self.__send_request('addressbooks', 'GET', {'limit': limit or 0, 'offset': offset or 0})) - - def get_addressbook_info(self, id): - """ Get information about addressbook - - @param id: unsigned int addressbook ID - @return: dictionary with response message - """ - logger.info("Function call: get_addressbook_info: '{}'".format(id, )) - return self.__handle_error("Empty addressbook id") if not id else self.__handle_result(self.__send_request('addressbooks/{}'.format(id))) - - def get_addressbook_variables(self, id): - """ Get a list of variables available on a mailing list - - @param id: unsigned int addressbook ID - @return: list with variables of addressbook - """ - logger.info("Function call: get_addressbook_variables_list: '{}'".format(id, )) - return self.__handle_error("Empty addressbook id") if not id else self.__handle_result(self.__send_request('addressbooks/{}/variables'.format(id))) - - # ------------------------------------------------------------------ # - # EMAIL ADDRESSES # - # ------------------------------------------------------------------ # - - def get_emails_from_addressbook(self, id, limit=0, offset=0): - """ List email addresses from addressbook - - @param id: unsigned int addressbook ID - @param limit: unsigned int max limit of records. The max value is 100 - @param offset: unsigned int how many records pass before selection - @return: dictionary with response message - """ - logger.info("Function call: get_emails_from_addressbook: '{}'".format(id, )) - return self.__handle_error("Empty addressbook id") if not id else self.__handle_result(self.__send_request('addressbooks/{}/emails'.format(id), 'GET', {'limit': limit or 0, 'offset': offset or 0})) - - def add_emails_to_addressbook(self, id, emails): - """ Add new emails to addressbook - - @param id: unsigned int addressbook ID - @param emails: list of dictionaries [ - {'email': 'test@test.com', 'variables': {'varname_1': 'value_1', ..., 'varname_n': 'value_n' }}, - {...}, - {'email': 'testn@testn.com'}} - ] - @return: dictionary with response message - """ - logger.info("Function call: add_emails_to_addressbook into: {}".format(id, )) - if not id or not emails: - self.__handle_error("Empty addressbook id or emails") - try: - emails = json.dumps(emails) - except: - logger.debug("Emails: {}".format(emails)) - return self.__handle_error("Emails list can't be converted by JSON library") - return self.__handle_result(self.__send_request('addressbooks/{}/emails'.format(id), 'POST', {'emails': emails})) - - def delete_emails_from_addressbook(self, id, emails): - """ Delete email addresses from addressbook - - @param id: unsigned int addressbook ID - @param emails: list of emails ['test_1@test_1.com', ..., 'test_n@test_n.com'] - @return: dictionary with response message - """ - logger.info("Function call: delete_emails_from_addressbook from: {}".format(id, )) - if not id or not emails: - self.__handle_error("Empty addressbook id or emails") - try: - emails = json.dumps(emails) - except: - logger.debug("Emails: {}".format(emails)) - return self.__handle_error("Emails list can't be converted by JSON library") - return self.__handle_result(self.__send_request('addressbooks/{}/emails'.format(id), 'DELETE', {'emails': emails})) - - def get_emails_stat_by_campaigns(self, emails): - """ Get campaigns statistic for list of emails - - @param emails: list of emails ['test_1@test_1.com', ..., 'test_n@test_n.com'] - @return: dictionary with response message - """ - logger.info("Function call: get_emails_stat_by_campaigns") - if not emails: - self.__handle_error("Empty emails") - try: - emails = json.dumps(emails) - except: - logger.debug("Emails: {}".format(emails)) - return self.__handle_error("Emails list can't be converted by JSON library") - return self.__handle_result(self.__send_request('emails/campaigns', 'POST', {'emails': emails})) - - def set_variables_for_email(self, id, email, variables): - """ Set variables for email - - @param id: unsigned int addressbook ID - @param email: string - @param variables: dictionary - @return: dictionary with response message - """ - logger.info("Function call: set_variables_for_email: '{}' with email: '{}' new variables: '{}'".format(id, email, variables)) - return self.__handle_error("Empty addressbook id") if not id else self.__handle_result(self.__send_request('addressbooks/{}/emails/variable'.format(id), 'POST', {'email': email, 'variables': variables}, True, True)) - - # ------------------------------------------------------------------ # - # EMAIL CAMPAIGNS # - # ------------------------------------------------------------------ # - - def get_campaign_cost(self, id): - """ Get cost of campaign based on addressbook - - @param id: unsigned int addressbook ID - @return: dictionary with response message - """ - logger.info("Function call: get_campaign_cost: '{}'".format(id, )) - return self.__handle_error("Empty addressbook id") if not id else self.__handle_result(self.__send_request('addressbooks/{}/cost'.format(id))) - - def get_list_of_campaigns(self, limit=0, offset=0): - """ Get list of campaigns - - @param limit: unsigned int max limit of records. The max value is 100 - @param offset: unsigned int how many records pass before selection - @return: dictionary with response message - """ - logger.info("Function call: get_list_of_campaigns") - return self.__handle_result(self.__send_request('campaigns', 'GET', {'limit': limit or 0, 'offset': offset or 0})) - - def get_campaign_info(self, id): - """ Get information about campaign - - @param id: unsigned int campaign ID - @return: dictionary with response message - """ - logger.info("Function call: get_campaign_info from: {}".format(id, )) - return self.__handle_error("Empty campaign id") if not id else self.__handle_result(self.__send_request('campaigns/{}'.format(id, ))) - - def get_campaign_stat_by_countries(self, id): - """ Get information about campaign - - @param id: unsigned int campaign ID - @return: dictionary with response message - """ - logger.info("Function call: get_campaign_stat_by_countries from: '{}'".format(id, )) - return self.__handle_error("Empty campaign id") if not id else self.__handle_result(self.__send_request('campaigns/{}/countries'.format(id, ))) - - def get_campaign_stat_by_referrals(self, id): - """ Get campaign statistic by referrals - - @param id: unsigned int campaign ID - @return: dictionary with response message - """ - logger.info("Function call: get_campaign_stat_by_referrals from: '{}'".format(id, )) - return self.__handle_error("Empty campaign id") if not id else self.__handle_result(self.__send_request('campaigns/{}/referrals'.format(id, ))) - - def add_campaign(self, from_email, from_name, subject, body, addressbook_id, campaign_name='', attachments=None): - """ Create new campaign - - @param from_email: string senders email - @param from_name: string senders name - @param subject: string campaign title - @param body: string campaign body - @param addressbook_id: unsigned int addressbook ID - @param campaign_name: string campaign name - @param attachments: dictionary with {filename_1: filebody_1, ..., filename_n: filebody_n} - @return: dictionary with response message - """ - if not attachments: - attachments = {} - logger.info("Function call: create_campaign") - if not from_name or not from_email: - return self.__handle_error('Seems you pass not all data for sender: Email or Name') - elif not subject or not body: - return self.__handle_error('Seems you pass not all data for task: Title or Body') - elif not addressbook_id: - return self.__handle_error('Seems you not pass addressbook ID') - if not attachments: - attachments = {} - return self.__handle_result(self.__send_request('campaigns', 'POST', { - 'sender_name': from_name, - 'sender_email': from_email, - 'subject': subject, - 'body': base64.b64encode(body), - 'list_id': addressbook_id, - 'name': campaign_name, - 'attachments': json.dumps(attachments) - })) - - def cancel_campaign(self, id): - """ Cancel campaign - - @param id: unsigned int campaign ID - @return: dictionary with response message - """ - logger.info("Function call: cancel_campaign : '{}'".format(id, )) - return self.__handle_error("Empty campaign id") if not id else self.__handle_result(self.__send_request('campaigns/{}'.format(id, ), 'DELETE')) - - # ------------------------------------------------------------------ # - # EMAIL SENDERS # - # ------------------------------------------------------------------ # - - def get_list_of_senders(self): - """ List of all senders - - @return: dictionary with response message - """ - logger.info("Function call: get_senders") - return self.__handle_result(self.__send_request('senders')) - - def add_sender(self, email, name): - """ Add sender - @param email: string sender from email - @param name: string senders from name - @return: dictionary with response message - """ - logger.info("Function call: add_sender: '{}' '{}'".format(email, name)) - if not name or not email: - return self.__handle_error("Seems you passing not all data for sender: Email: '{}' or Name: '{}'".format(email, name)) - return self.__handle_result(self.__send_request('senders', 'POST', {'email': email, 'name': name})) - - def delete_sender(self, email): - """ Delete sender - @param email: string sender from email - @return: dictionary with response message - """ - logger.info("Function call: delete_sender: '{}'".format(email, )) - return self.__handle_error('Empty sender email') if not email else self.__handle_result(self.__send_request('senders', 'DELETE', {'email': email})) - - def activate_sender(self, email, code): - """ Activate new sender - @param email: string sender from email - @param code: string activation code - @return: dictionary with response message - """ - logger.info("Function call: activate_sender '{}' with code '{}'".format(email, code)) - if not email or not code: - return self.__handle_error("Empty email '{}' or activation code '{}'".format(email, code)) - return self.__handle_result(self.__send_request('senders/{}/code'.format(email, ), 'POST', {'code': code})) - - def send_sender_activation_email(self, email): - """ Request email with activation code - - @param email: string sender from email - @return: dictionary with response message - """ - logger.info("Function call: send_sender_activation_email for '{}'".format(email, )) - return self.__handle_error('Empty sender email') if not email else self.__handle_result(self.__send_request('senders/{}/code'.format(email, ))) - - # ------------------------------------------------------------------ # - # EMAILS # - # ------------------------------------------------------------------ # - - def get_email_info_from_one_addressbooks(self, id, email): - """ Get information about email address from one addressbook - - @param id: unsigned int addressbook ID - @param email: string valid email address - @return: dictionary with response message - """ - logger.info("Function call: get_email_info_from_one_addressbooks from: '{}'".format(id, )) - if not id or not email: - self.__handle_error("Empty addressbook id or email") - return self.__handle_result(self.__send_request('addressbooks/{}/emails/{}'.format(id, email))) - - def get_email_info_from_all_addressbooks(self, email): - """ Get global information about email - - @param email: string email - @return: dictionary with response message - """ - logger.info("Function call: get_email_info_from_all_addressbooks for '{}'".format(email, )) - return self.__handle_error('Empty email') if not email else self.__handle_result(self.__send_request('emails/{}'.format(email, ))) - - def delete_email_from_all_addressooks(self, email): - """ Remove email from all addressbooks - - @param email: string email - @return: dictionary with response message - """ - logger.info("Function call: delete_email_from_all_addressooks for '{}'".format(email, )) - return self.__handle_error('Empty email') if not email else self.__handle_result(self.__send_request('emails/{}'.format(email, ), 'DELETE')) - - def get_email_statistic_by_campaigns(self, email): - """ Get email statistic by all campaigns - - @param email: string email - @return: dictionary with response message - """ - logger.info("Function call: get_email_statistic_by_campaigns for '{}'".format(email, )) - return self.__handle_error('Empty email') if not email else self.__handle_result(self.__send_request('emails/{}/campaigns'.format(email, ))) - - def get_emails_in_blacklist(self, limit=0, offset=0): - """ Get all emails from blacklist - - @param limit: unsigned int max limit of records. The max value is 100 - @param offset: unsigned int how many records pass before selection - @return: dictionary with response message - """ - logger.info("Function call: get_emails_in_blacklist") - return self.__handle_result(self.__send_request('blacklist', 'GET', {'limit': limit or 0, 'offset': offset or 0})) - - def add_email_to_blacklist(self, email, comment=''): - """ Add email to blacklist - - @param email: string emails divided by commas 'email_1, ..., email_n' - @param comment: string describing why email added to blacklist - @return: dictionary with response message - """ - logger.info("Function call: add_email_to_blacklist for '{}'".format(email, )) - return self.__handle_error('Empty email') if not email else self.__handle_result(self.__send_request('blacklist', 'POST', {'emails': base64.b64encode(email), 'comment': comment})) - - def delete_email_from_blacklist(self, email): - """ Remove emails from blacklist - - @param email: string email - @return: dictionary with response message - """ - logger.info("Function call: delete_email_from_blacklist for '{}'".format(email, )) - return self.__handle_error('Empty email') if not email else self.__handle_result(self.__send_request('blacklist', 'DELETE', {'emails': base64.b64encode(email)})) - - # ------------------------------------------------------------------ # - # SMTP # - # ------------------------------------------------------------------ # - - def smtp_get_list_of_emails(self, limit=0, offset=0, date_from=None, date_to=None, sender=None, recipient=None): - """ SMTP: get list of emails - - @param limit: unsigned int max limit of records. The max value is 100 - @param offset: unsigned int how many records pass before selection - @param date_from: string date for filter in 'YYYY-MM-DD' - @param date_to: string date for filter in 'YYYY-MM-DD' - @param sender: string from email - @param recipient: string for email - @return: dictionary with response message - """ - logger.info("Function call: smtp_get_list_of_emails") - return self.__handle_result(self.__send_request('smtp/emails', 'GET', { - 'limit': limit, - 'offset': offset, - 'from': date_from, - 'to': date_to, - 'sender': sender, - 'recipient': recipient - })) - - def smtp_get_email_info_by_id(self, id): - """ Get information about email by ID - - @param id: unsigned int email id - @return: dictionary with response message - """ - logger.info("Function call: smtp_get_email_info_by_id for '{}'".format(id, )) - return self.__handle_error('Empty email') if not id else self.__handle_result(self.__send_request('smtp/emails/{}'.format(id, ))) - - def smtp_add_emails_to_unsubscribe(self, emails): - """ SMTP: add emails to unsubscribe list - - @param emails: list of dictionaries [{'email': 'test_1@test_1.com', 'comment': 'comment_1'}, ..., {'email': 'test_n@test_n.com', 'comment': 'comment_n'}] - @return: dictionary with response message - """ - logger.info("Function call: smtp_add_emails_to_unsubscribe") - return self.__handle_error('Empty email') if not emails else self.__handle_result(self.__send_request('smtp/unsubscribe', 'POST', {'emails': json.dumps(emails)})) - - def smtp_delete_emails_from_unsubscribe(self, emails): - """ SMTP: remove emails from unsubscribe list - - @param emails: list of dictionaries ['test_1@test_1.com', ..., 'test_n@test_n.com'] - @return: dictionary with response message - """ - logger.info("Function call: smtp_delete_emails_from_unsubscribe") - return self.__handle_error('Empty email') if not emails else self.__handle_result(self.__send_request('smtp/unsubscribe', 'DELETE', {'emails': json.dumps(emails)})) - - def smtp_get_list_of_ip(self): - """ SMTP: get list of IP - - @return: dictionary with response message - """ - logger.info("Function call: smtp_get_list_of_ip") - return self.__handle_result(self.__send_request('smtp/ips')) - - def smtp_get_list_of_allowed_domains(self): - """ SMTP: get list of allowed domains - - @return: dictionary with response message - """ - logger.info("Function call: smtp_get_list_of_allowed_domains") - return self.__handle_result(self.__send_request('smtp/domains')) - - def smtp_add_domain(self, email): - """ SMTP: add and verify new domain - - @param email: string valid email address on the domain you want to verify. We will send an email message to the specified email address with a verification link. - @return: dictionary with response message - """ - logger.info("Function call: smtp_add_domain") - return self.__handle_error('Empty email') if not email else self.__handle_result(self.__send_request('smtp/domains', 'POST', {'email': email})) - - def smtp_verify_domain(self, email): - """ SMTP: verify domain already added domain - - @param email: string valid email address on the domain you want to verify. We will send an email message to the specified email address with a verification link. - @return: dictionary with response message - """ - logger.info("Function call: smtp_verify_domain") - return self.__handle_error('Empty email') if not email else self.__handle_result(self.__send_request('smtp/domains/{}'.format(email, ))) - - def smtp_send_mail(self, email): - """ SMTP: send email - - @param email: string valid email address. We will send an email message to the specified email address with a verification link. - @return: dictionary with response message - """ - logger.info("Function call: smtp_send_mail") - if not email.get('template') and not email.get('html') and not email.get('text'): - return self.__handle_error('Missing email body - specify a template, html or text content') - elif not email.get('subject'): - return self.__handle_error('Seems we have empty subject') - elif not email.get('from') or not email.get('to'): - return self.__handle_error("Seems we have empty some credentials 'from': '{}' or 'to': '{}' fields".format(email.get('from'), email.get('to'))) - email['html'] = base64.b64encode(email.get('html').encode('utf-8')).decode('utf-8') if email['html'] else None - return self.__handle_result(self.__send_request('smtp/emails', 'POST', {'email': json.dumps(email)})) - - def smtp_send_mail_with_template(self, email): - """ SMTP: send email with custom template - - @param email: string valid email address. We will send an email message to the specified email address with a verification link. - @return: dictionary with response message - """ - logger.info("Function call: smtp_send_mail_with_template") - if not email.get('template'): - return self.__handle_error('Seems we have empty template') - elif not email.get('template').get('id'): - return self.__handle_error('Seems we have empty template id') - email['html'] = email['text'] = None - return self.smtp_send_mail(email) - - # ------------------------------------------------------------------ # - # PUSH # - # ------------------------------------------------------------------ # - - def push_get_tasks(self, limit=0, offset=0): - """ PUSH: get list of tasks - - @param limit: unsigned int max limit of records. The max value is 100 - @param offset: unsigned int how many records pass before selection - @return: dictionary with response message - """ - logger.info("Function call: push_get_tasks") - return self.__handle_result(self.__send_request('push/tasks', 'GET', {'limit': limit or 0, 'offset': offset or 0})) - - def push_get_websites(self, limit=0, offset=0): - """ PUSH: get list of websites - - @param limit: unsigned int max limit of records. The max value is 100 - @param offset: unsigned int how many records pass before selection - @return: dictionary with response message - """ - logger.info("Function call: push_get_websites") - return self.__handle_result(self.__send_request('push/websites', 'GET', {'limit': limit or 0, 'offset': offset or 0})) - - def push_count_websites(self): - """ PUSH: get amount of websites - - @return: dictionary with response message - """ - logger.info("Function call: push_count_websites") - return self.__handle_result(self.__send_request('push/websites/total', 'GET', {})) - - def push_get_variables(self, id): - """ PUSH: get list of all variables for website - - @param id: unsigned int website id - @return: dictionary with response message - """ - logger.info("Function call: push_get_variables for {}".format(id)) - return self.__handle_result(self.__send_request('push/websites/{}/variables'.format(id), 'GET', {})) - - def push_get_subscriptions(self, id, limit=0, offset=0): - """ PUSH: get list of all subscriptions for website - - @param limit: unsigned int max limit of records. The max value is 100 - @param offset: unsigned int how many records pass before selection - @param id: unsigned int website id - @return: dictionary with response message - """ - logger.info("Function call: push_get_subscriptions for {}".format(id)) - return self.__handle_result(self.__send_request('push/websites/{}/subscriptions'.format(id), 'GET', {'limit': limit or 0, 'offset': offset or 0})) - - def push_count_subscriptions(self, id): - """ PUSH: get amount of subscriptions for website - - @param id: unsigned int website id - @return: dictionary with response message - """ - logger.info("Function call: push_count_subscriptions for {}".format(id)) - return self.__handle_result(self.__send_request('push/websites/{}/subscriptions/total'.format(id), 'GET', {})) - - def push_set_subscription_state(self, subscription_id, state_value): - """ PUSH: get amount of subscriptions for website - - @param subscription_id: unsigned int subscription id - @param state_value: unsigned int state value. Can be 0 or 1 - @return: dictionary with response message - """ - logger.info("Function call: push_set_subscription_state for {} to state {}".format(subscription_id, state_value)) - return self.__handle_result(self.__send_request('/push/subscriptions/state', 'POST', {'id': subscription_id, 'state': state_value})) - - def push_create(self, title, website_id, body, ttl, additional_params={}): - """ PUSH: create new push - - @param title: string push title - @param website_id: unsigned int website id - @param body: string push body - @param ttl: unsigned int ttl for push messages - @param additional_params: dictionary additional params for push task - @return: dictionary with response message - """ - data_to_send = { - 'title': title, - 'website_id': website_id, - 'body': body, - 'ttl': ttl - } - if additional_params: - data_to_send.update(additional_params) - - logger.info("Function call: push_create") - return self.__handle_result(self.__send_request('/push/tasks', 'POST', data_to_send)) - - # ------------------------------------------------------------------ # - # SMS # - # ------------------------------------------------------------------ # - - def sms_add_phones(self, addressbook_id, phones): - """ SMS: add phones from the address book - - @return: dictionary with response message - """ - if not addressbook_id or not phones: - return self.__handle_error("Empty addressbook id or phones") - try: - phones = json.dumps(phones) - except: - logger.debug("Phones: {}".format(phones)) - return self.__handle_error("Phones list can't be converted by JSON library") - - data_to_send = { - 'addressBookId': addressbook_id, - 'phones': phones - } - - logger.info("Function call: sms_add_phones") - return self.__handle_result(self.__send_request('sms/numbers', 'POST', data_to_send)) - - def sms_add_phones_with_variables(self, addressbook_id, phones): - """ SMS: add phones with variables from the address book - - @return: dictionary with response message - """ - if not addressbook_id or not phones: - return self.__handle_error("Empty addressbook id or phones") - try: - phones = json.dumps(phones) - except: - logger.debug("Phones: {}".format(phones)) - return self.__handle_error("Phones list can't be converted by JSON library") - - data_to_send = { - 'addressBookId': addressbook_id, - 'phones': phones - } - - logger.info("Function call: sms_add_phones_with_variables") - return self.__handle_result(self.__send_request('sms/numbers/variables', 'POST', data_to_send)) - - def sms_delete_phones(self, addressbook_id, phones): - """ SMS: remove phones from the address book - - @return: dictionary with response message - """ - if not addressbook_id or not phones: - return self.__handle_error("Empty addressbook id or phones") - try: - phones = json.dumps(phones) - except: - logger.debug("Phones: {}".format(phones)) - return self.__handle_error("Phones list can't be converted by JSON library") - - data_to_send = { - 'addressBookId': addressbook_id, - 'phones': phones - } - - logger.info("Function call: sms_delete_phones") - return self.__handle_result(self.__send_request('sms/numbers', 'DELETE', data_to_send)) - - def sms_get_phone_info(self, addressbook_id, phone): - """ SMS: Get information about phone from the address book - - @return: dictionary with response message - """ - if not addressbook_id or not phone: - return self.__handle_error("Empty addressbook id or phone") - - logger.info("Function call: sms_get_phone_info") - return self.__handle_result(self.__send_request('sms/numbers/info/' + str(addressbook_id) + '/' + str(phone), 'GET')) - - def sms_update_phones_variables(self, addressbook_id, phones, variables): - """ SMS: update phones variables from the address book - - @return: dictionary with response message - """ - if not addressbook_id or not phones or not variables: - return self.__handle_error("Empty addressbook id or phones or variables") - try: - phones = json.dumps(phones) - except: - logger.debug("Phones: {}".format(phones)) - return self.__handle_error("Phones list can't be converted by JSON library") - - try: - variables = json.dumps(variables) - except: - logger.debug("Variables: {}".format(variables)) - return self.__handle_error("Variables list can't be converted by JSON library") - - data_to_send = { - 'addressBookId': addressbook_id, - 'phones': phones, - 'variables': variables - } - - logger.info("Function call: sms_update_phones_variables") - return self.__handle_result(self.__send_request('sms/numbers', 'PUT', data_to_send)) - - def sms_get_blacklist(self): - """ SMS: get phones from the blacklist - - @return: dictionary with response message - """ - logger.info("Function call: sms_get_blacklist") - return self.__handle_result(self.__send_request('sms/black_list', 'GET', {})) - - def sms_get_phones_info_from_blacklist(self, phones): - """ SMS: get info by phones from the blacklist - - @param phones: array phones - @return: dictionary with response message - """ - if not phones: - return self.__handle_error("Empty phones") - try: - phones = json.dumps(phones) - except: - logger.debug("Phones: {}".format(phones)) - return self.__handle_error("Phones list can't be converted by JSON library") - - data_to_send = { - 'phones': phones - } - - logger.info("Function call: sms_add_phones_to_blacklist") - return self.__handle_result(self.__send_request('sms/black_list/by_numbers', 'GET', data_to_send)) - - def sms_add_phones_to_blacklist(self, phones, comment): - """ SMS: add phones to blacklist - - @param phones: array phones - @param comment: string describing why phones added to blacklist - @return: dictionary with response message - """ - if not phones: - return self.__handle_error("Empty phones") - try: - phones = json.dumps(phones) - except: - logger.debug("Phones: {}".format(phones)) - return self.__handle_error("Phones list can't be converted by JSON library") - - data_to_send = { - 'phones': phones, - 'description': comment - } - - logger.info("Function call: sms_add_phones_to_blacklist") - return self.__handle_result(self.__send_request('sms/black_list', 'POST', data_to_send)) - - def sms_delete_phones_from_blacklist(self, phones): - """ SMS: remove phones from blacklist - - @param phones: array phones - @return: dictionary with response message - """ - if not phones: - return self.__handle_error("Empty phones") - try: - phones = json.dumps(phones) - except: - logger.debug("Phones: {}".format(phones)) - return self.__handle_error("Phones list can't be converted by JSON library") - - data_to_send = { - 'phones': phones - } - - logger.info("Function call: sms_add_phones_to_blacklist") - return self.__handle_result(self.__send_request('sms/black_list', 'DELETE', data_to_send)) - - @deprecated(version='0.1.4', reason="You should use sms_add_campaign_by_addressbook_id") - def sms_add_campaign(self, sender_name, addressbook_id, body, date=None, transliterate=False): - """ Create new sms campaign - - @deprecated: use method sms_delete_phonesfrom_blacklist - @param sender_name: string senders name - @param addressbook_id: unsigned int addressbook ID - @param body: string campaign body - @param date: string date for filter in 'Y-m-d H:i:s' - @param transliterate: boolean need to transliterate sms body or not - @return: dictionary with response message - """ - - logger.info("Function call: sms_create_campaign") - if not sender_name: - return self.__handle_error('Seems you not pass sender name') - if not addressbook_id: - return self.__handle_error('Seems you not pass addressbook ID') - if not body: - return self.__handle_error('Seems you not pass sms text') - - data_to_send = { - 'sender': sender_name, - 'addressBookId': addressbook_id, - 'body': body, - 'date': date, - 'transliterate': transliterate, - } - - return self.__handle_result(self.__send_request('sms/campaigns', 'POST', data_to_send)) - - @deprecated(version='0.1.4', reason="You should use sms_add_campaign_by_phones") - def sms_send(self, sender_name, phones, body, date=None, transliterate=False): - """ Send sms by some phones - - @param sender_name: string senders name - @param phones: array phones - @param body: string campaign body - @param date: string date for filter in 'Y-m-d H:i:s' - @param transliterate: boolean need to transliterate sms body or not - @return: dictionary with response message - """ - - logger.info("Function call: sms_send") - if not sender_name: - return self.__handle_error('Seems you not pass sender name') - if not phones: - return self.__handle_error("Empty phones") - if not body: - return self.__handle_error('Seems you not pass sms text') - - try: - phones = json.dumps(phones) - except: - logger.debug("Phones: {}".format(phones)) - return self.__handle_error("Phones list can't be converted by JSON library") - - data_to_send = { - 'sender': sender_name, - 'phones': phones, - 'body': body, - 'date': date, - 'transliterate': transliterate, - } - - return self.__handle_result(self.__send_request('sms/send', 'POST', data_to_send)) - - def sms_add_campaign_by_addressbook_id(self, sender_name, addressbook_id, body, additional_params={}): - """ Create new sms campaign by addressbook_id - - @param sender_name: string senders name - @param addressbook_id: unsigned int addressbook ID - @param body: string campaign body - @param additional_params: dictionary additional params for sms task - @return: dictionary with response message - """ - - logger.info("Function call: sms_add_campaign_by_addressbook_id") - if not sender_name: - return self.__handle_error('Seems you not pass sender name') - if not addressbook_id: - return self.__handle_error('Seems you not pass addressbook ID') - if not body: - return self.__handle_error('Seems you not pass sms text') - - data_to_send = { - 'sender': sender_name, - 'addressBookId': addressbook_id, - 'body': body - } - - if additional_params: - data_to_send.update(additional_params) - - return self.__handle_result(self.__send_request('sms/campaigns', 'POST', data_to_send)) - - def sms_add_campaign_by_phones(self, sender_name, phones, body, additional_params={}): - """ Create new sms campaign by some phones - - @param sender_name: string senders name - @param phones: array phones - @param body: string campaign body - @param additional_params: dictionary additional params for sms task - @return: dictionary with response message - """ - - logger.info("Function call: sms_add_campaign_by_phones") - if not sender_name: - return self.__handle_error('Seems you not pass sender name') - if not phones: - return self.__handle_error('Seems you not pass phones') - if not body: - return self.__handle_error('Seems you not pass sms text') - - try: - phones = json.dumps(phones) - except: - logger.debug("Phones: {}".format(phones)) - return self.__handle_error("Phones list can't be converted by JSON library") - - data_to_send = { - 'sender': sender_name, - 'phones': phones, - 'body': body, - } - - if additional_params: - data_to_send.update(additional_params) - - return self.__handle_result(self.__send_request('sms/send', 'POST', data_to_send)) - - def sms_get_list_campaigns(self, date_from, date_to): - """ SMS: get list of campaigns - - @param date_from: string date for filter in 'Y-m-d H:i:s' - @param date_to: string date for filter in 'Y-m-d H:i:s' - @return: dictionary with response message - """ - logger.info("Function call: sms_get_list_campaigns") - - data_to_send = { - 'dateFrom': date_from, - 'dateTo': date_to - } - return self.__handle_result(self.__send_request('sms/campaigns/list', 'GET', data_to_send)) - - def sms_get_campaign_info(self, id): - """ Get information about sms campaign - - @param id: unsigned int campaign ID - @return: dictionary with response message - """ - if not id: - return self.__handle_error("Empty campaign id") - - logger.info("Function call: sms_get_campaign_info from: {}".format(id, )) - return self.__handle_result(self.__send_request('/sms/campaigns/info/{}'.format(id, ))) - - def sms_cancel_campaign(self, id): - """ Cancel sms campaign - - @param id: unsigned int campaign ID - @return: dictionary with response message - """ - if not id: - return self.__handle_error("Empty campaign id") - - logger.info("Function call: sms_cancel_campaign : '{}'".format(id, )) - return self.__handle_result(self.__send_request('sms/campaigns/cancel/{}'.format(id, ), 'PUT')) - - def sms_get_campaign_cost(self, sender, body, addressbook_id=None, phones=None): - """ Get cost sms campaign - - @param id: unsigned int campaign ID - @return: dictionary with response message - """ - if not sender: - return self.__handle_error("Empty sender") - if not body: - return self.__handle_error("Empty sms body") - if not addressbook_id and not phones: - return self.__handle_error("Empty addressbook id or phones") - - data_to_send = { - 'sender': sender, - 'body': body, - 'addressBookId': addressbook_id - } - if phones: - try: - data_to_send.update({'phones': json.dumps(phones)}) - except: - logger.debug("Phones: {}".format(phones)) - return self.__handle_error("Phones list can't be converted by JSON library") - - logger.info("Function call: sms_get_campaign_cost") - return self.__handle_result(self.__send_request('sms/campaigns/cost', 'GET', data_to_send)) - - def sms_delete_campaign(self, id): - """ SMS: remove sms campaign - - @return: dictionary with response message - """ - if not id: - return self.__handle_error("Empty sms campaign id") - - data_to_send = { - 'id': id - } - - logger.info("Function call: sms_delete_campaign") - return self.__handle_result(self.__send_request('sms/campaigns', 'DELETE', data_to_send)) - - # ------------------------------------------------------------------ # - # EVENTS # - # ------------------------------------------------------------------ # - - def send_event(self, event_name, body): - """ Send event by slug - - @param event_name: string event name - @param body: array body {'email': 'test@test.com', 'phone': '+123456789': 'var_1':'var_1_value'} - @return: dictionary with response message - """ - - logger.info("Function call: send_event") - if not event_name: - return self.__handle_error('Seems you not pass event slug') - if not body: - return self.__handle_error('Seems you not pass body') - - return self.__handle_result(self.__send_request('/events/name/{}'.format(event_name, ), 'POST', body)) diff --git a/sendpulse/__init__.py b/sendpulse/__init__.py new file mode 100644 index 0000000..fe3dff4 --- /dev/null +++ b/sendpulse/__init__.py @@ -0,0 +1,27 @@ +__version__ = "2.0.0" +__author__ = "Maksym Ustymenko" +__author_email__ = "tech@sendpulse.com" +__copyright__ = "Copyright 2015, SendPulse" +__credits__ = ["Maksym Ustymenko"] + +from sendpulse.client import Client +from sendpulse.exception.exceptions import ( + ApiException, + AuthException, + NetworkException, + ProtocolException, + RateLimitException, + SendPulseError, + SendPulseException, +) + +__all__ = [ + "Client", + "SendPulseError", + "SendPulseException", + "AuthException", + "RateLimitException", + "ApiException", + "NetworkException", + "ProtocolException", +] diff --git a/sendpulse/auth/__init__.py b/sendpulse/auth/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/sendpulse/auth/auth.py b/sendpulse/auth/auth.py new file mode 100644 index 0000000..9223ba3 --- /dev/null +++ b/sendpulse/auth/auth.py @@ -0,0 +1,31 @@ +from __future__ import annotations + +from sendpulse.auth.token_manager import TokenManager + + +class ApiKeyAuth: + def __init__(self, api_key: str) -> None: + self._api_key = api_key + + def get_authorization_header(self) -> str: + return f"Bearer {self._api_key}" + + def invalidate(self) -> None: + pass + + def supports_refresh(self) -> bool: + return False + + +class OAuthAuth: + def __init__(self, token_manager: TokenManager) -> None: + self._token_manager = token_manager + + def get_authorization_header(self) -> str: + return f"Bearer {self._token_manager.get_token()}" + + def invalidate(self) -> None: + self._token_manager.invalidate() + + def supports_refresh(self) -> bool: + return True diff --git a/sendpulse/auth/protocol.py b/sendpulse/auth/protocol.py new file mode 100644 index 0000000..9fec328 --- /dev/null +++ b/sendpulse/auth/protocol.py @@ -0,0 +1,11 @@ +from __future__ import annotations + +from typing import Protocol + +from sendpulse.auth.token_storage import TokenData + + +class TokenStorageProtocol(Protocol): + def get(self, key: str) -> TokenData | None: ... + def set(self, key: str, token: TokenData) -> None: ... + def delete(self, key: str) -> None: ... diff --git a/sendpulse/auth/token_manager.py b/sendpulse/auth/token_manager.py new file mode 100644 index 0000000..a0a9e19 --- /dev/null +++ b/sendpulse/auth/token_manager.py @@ -0,0 +1,93 @@ +from __future__ import annotations + +import hashlib +import json +import threading +import time +from urllib.parse import urlencode + +from sendpulse.auth.protocol import TokenStorageProtocol +from sendpulse.auth.token_storage import TokenData +from sendpulse.exception.exceptions import AuthException, ProtocolException +from sendpulse.http.protocol import HttpClient +from sendpulse.http.request import Request + + +class TokenManager: + EXPIRY_BUFFER = 300 + + def __init__( + self, + http_client: HttpClient, + client_id: str, + client_secret: str, + base_url: str, + storage: TokenStorageProtocol, + ) -> None: + self._http = http_client + self._client_id = client_id + self._client_secret = client_secret + self._base_url = base_url.rstrip("/") + self._storage = storage + self._storage_key = hashlib.sha256(client_id.encode()).hexdigest() + self._invalidated = False + self._lock = threading.Lock() + + def get_token(self) -> str: + with self._lock: + if not self._invalidated: + cached = self._storage.get(self._storage_key) + if cached and cached["expires_at"] > time.time() + self.EXPIRY_BUFFER: + return cached["access_token"] + return self._fetch_and_store() + + def invalidate(self) -> None: + with self._lock: + self._invalidated = True + self._storage.delete(self._storage_key) + + def _fetch_and_store(self) -> str: + body = urlencode( + { + "grant_type": "client_credentials", + "client_id": self._client_id, + "client_secret": self._client_secret, + } + ) + + request = Request( + method="POST", + uri=f"{self._base_url}/oauth/access_token", + headers={"Content-Type": "application/x-www-form-urlencoded"}, + body=body, + ) + + response = self._http.send(request) + + if response.status_code != 200: + raise AuthException(response.status_code, response.body, "OAuth token fetch failed") + + try: + data: object = json.loads(response.body) + except json.JSONDecodeError as e: + raise ProtocolException(f"Failed to decode OAuth response: {e}") from e + + if ( + not isinstance(data, dict) + or not isinstance(data.get("access_token"), str) + or not isinstance(data.get("token_type"), str) + or not isinstance(data.get("expires_in"), (int, float)) + or data["expires_in"] <= 0 + ): + raise ProtocolException("Invalid OAuth token response shape") + + token: TokenData = { + "access_token": data["access_token"], + "token_type": data["token_type"], + "expires_at": int(time.time()) + int(data["expires_in"]), + } + + self._storage.set(self._storage_key, token) + self._invalidated = False + + return token["access_token"] diff --git a/sendpulse/auth/token_storage.py b/sendpulse/auth/token_storage.py new file mode 100644 index 0000000..185b993 --- /dev/null +++ b/sendpulse/auth/token_storage.py @@ -0,0 +1,113 @@ +from __future__ import annotations + +import json +import os +import tempfile +from typing import TypedDict + +try: + import fcntl as _fcntl + + _HAS_FCNTL = True +except ImportError: + _HAS_FCNTL = False + + +class TokenData(TypedDict): + access_token: str + token_type: str + expires_at: int + + +class FileTokenStorage: + def __init__(self, cache_dir: str | None = None) -> None: + if cache_dir: + self._dir = cache_dir + else: + dirname = f"sendpulse-{os.getuid()}-tokens" if hasattr(os, "getuid") else "sendpulse-tokens" + self._dir = os.path.join(tempfile.gettempdir(), dirname) + os.makedirs(self._dir, mode=0o700, exist_ok=True) + + def get(self, key: str) -> TokenData | None: + path = self._path(key) + flags = os.O_RDONLY + if hasattr(os, "O_NOFOLLOW"): + flags |= os.O_NOFOLLOW + try: + fd = os.open(path, flags) + except OSError: + return None + try: + with os.fdopen(fd, "r") as f: + if _HAS_FCNTL: + _fcntl.flock(f, _fcntl.LOCK_SH) + try: + data: object = json.load(f) + finally: + _fcntl.flock(f, _fcntl.LOCK_UN) + else: + data = json.load(f) + except (OSError, json.JSONDecodeError): + return None + + if not self._is_valid(data): + return None + + return data # type: ignore[return-value] + + def set(self, key: str, token: TokenData) -> None: + path = self._path(key) + try: + fd, tmp = tempfile.mkstemp(dir=self._dir, prefix="sp_", suffix=".json.tmp") + try: + with os.fdopen(fd, "w") as f: + if _HAS_FCNTL: + _fcntl.flock(f, _fcntl.LOCK_EX) + try: + json.dump(token, f) + finally: + _fcntl.flock(f, _fcntl.LOCK_UN) + else: + json.dump(token, f) + except Exception: + try: + os.unlink(tmp) + except OSError: + pass + raise + os.replace(tmp, path) + except OSError as e: + raise RuntimeError(f"Cannot write token cache file: {path}") from e + + def delete(self, key: str) -> None: + path = self._path(key) + try: + os.unlink(path) + except FileNotFoundError: + pass + + def _path(self, key: str) -> str: + return os.path.join(self._dir, f"sp_token_{key}.json") + + @staticmethod + def _is_valid(data: object) -> bool: + return ( + isinstance(data, dict) + and isinstance(data.get("access_token"), str) + and isinstance(data.get("token_type"), str) + and isinstance(data.get("expires_at"), int) + ) + + +class InMemoryTokenStorage: + def __init__(self) -> None: + self._store: dict[str, TokenData] = {} + + def get(self, key: str) -> TokenData | None: + return self._store.get(key) + + def set(self, key: str, token: TokenData) -> None: + self._store[key] = token + + def delete(self, key: str) -> None: + self._store.pop(key, None) diff --git a/sendpulse/client.py b/sendpulse/client.py new file mode 100644 index 0000000..f66e969 --- /dev/null +++ b/sendpulse/client.py @@ -0,0 +1,142 @@ +from __future__ import annotations + +from typing import TYPE_CHECKING, Any +from urllib.parse import urlparse + +from sendpulse.auth.auth import ApiKeyAuth, OAuthAuth +from sendpulse.auth.protocol import TokenStorageProtocol +from sendpulse.auth.token_manager import TokenManager +from sendpulse.auth.token_storage import FileTokenStorage +from sendpulse.config import Config +from sendpulse.http.httpx_client import HttpxClient +from sendpulse.http.protocol import HttpClient +from sendpulse.http.request import Request +from sendpulse.response.validator import ResponseValidator + +_BASE_URL = "https://api.sendpulse.com" + +if TYPE_CHECKING: + from sendpulse.generated.chatbot.service.chatbot_service import ChatbotService + from sendpulse.generated.crm.service.crm_service import CrmService + from sendpulse.generated.email.service.email_service import EmailService + from sendpulse.generated.sms.service.sms_service import SmsService + from sendpulse.generated.smtp.service.smtp_service import SmtpService + + +class Client: + def __init__( + self, + api_key: str | None = None, + client_id: str | None = None, + client_secret: str | None = None, + connect_timeout: float = 10.0, + request_timeout: float = 30.0, + cache_dir: str | None = None, + token_storage: TokenStorageProtocol | None = None, + http_client: HttpClient | None = None, + ) -> None: + self._config = Config( + api_key=api_key, + client_id=client_id, + client_secret=client_secret, + connect_timeout=connect_timeout, + request_timeout=request_timeout, + cache_dir=cache_dir, + ) + self._http: HttpClient = http_client or HttpxClient( + connect_timeout=self._config.connect_timeout, + request_timeout=self._config.request_timeout, + ) + self._auth = self._build_auth(token_storage) + self._validator = ResponseValidator() + + self._email_service: EmailService | None = None + self._smtp_service: SmtpService | None = None + self._sms_service: SmsService | None = None + self._crm_service: CrmService | None = None + self._chatbot_service: ChatbotService | None = None + + def email_service(self) -> EmailService: + from sendpulse.generated.email.service.email_service import EmailService + if self._email_service is None: + self._email_service = EmailService(self) + return self._email_service + + def smtp_service(self) -> SmtpService: + from sendpulse.generated.smtp.service.smtp_service import SmtpService + if self._smtp_service is None: + self._smtp_service = SmtpService(self) + return self._smtp_service + + def sms_service(self) -> SmsService: + from sendpulse.generated.sms.service.sms_service import SmsService + if self._sms_service is None: + self._sms_service = SmsService(self) + return self._sms_service + + def crm_service(self) -> CrmService: + from sendpulse.generated.crm.service.crm_service import CrmService + if self._crm_service is None: + self._crm_service = CrmService(self) + return self._crm_service + + def chatbot_service(self) -> ChatbotService: + from sendpulse.generated.chatbot.service.chatbot_service import ChatbotService + if self._chatbot_service is None: + self._chatbot_service = ChatbotService(self) + return self._chatbot_service + + def close(self) -> None: + if hasattr(self._http, "close"): + self._http.close() + + def __enter__(self) -> Client: + return self + + def __exit__(self, *args: object) -> None: + self.close() + + def send(self, request: Request) -> dict[str, Any] | list[Any]: + authed = self._with_auth(request) + response = self._http.send(authed) + + if response.status_code == 401 and self._auth.supports_refresh(): + self._auth.invalidate() + authed = self._with_auth(request) + response = self._http.send(authed) + + return self._validator.validate(response) + + def _with_auth(self, request: Request) -> Request: + uri = request.uri.lstrip("/") + if urlparse(uri).scheme: + raise ValueError(f"Request URI must be a relative path, got: {uri!r}") + + headers = { + "Content-Type": "application/json", + "Accept": "application/json", + **request.headers, + # Applied last so caller headers cannot override SDK authentication. + "Authorization": self._auth.get_authorization_header(), + } + return Request( + method=request.method, + uri=f"{_BASE_URL}/{uri}", + headers=headers, + body=request.body, + params=request.params, + ) + + def _build_auth(self, token_storage: TokenStorageProtocol | None) -> ApiKeyAuth | OAuthAuth: + if not self._config.is_oauth: + return ApiKeyAuth(self._config.api_key or "") + + storage: TokenStorageProtocol = token_storage or FileTokenStorage(self._config.cache_dir) + manager = TokenManager( + http_client=self._http, + client_id=self._config.client_id or "", + client_secret=self._config.client_secret or "", + base_url=_BASE_URL, + storage=storage, + ) + return OAuthAuth(manager) diff --git a/sendpulse/config.py b/sendpulse/config.py new file mode 100644 index 0000000..80439a6 --- /dev/null +++ b/sendpulse/config.py @@ -0,0 +1,30 @@ +from __future__ import annotations + +from dataclasses import dataclass, field + + +@dataclass(slots=True) +class Config: + api_key: str | None = field(default=None, repr=False) + client_id: str | None = field(default=None, repr=False) + client_secret: str | None = field(default=None, repr=False) + connect_timeout: float = 10.0 + request_timeout: float = 30.0 + cache_dir: str | None = None + + def __post_init__(self) -> None: + self._validate() + + def _validate(self) -> None: + has_api_key = bool(self.api_key) + has_oauth = bool(self.client_id) and bool(self.client_secret) + + if not has_api_key and not has_oauth: + raise ValueError("Provide either api_key or both client_id and client_secret.") + + if has_api_key and (self.client_id or self.client_secret): + raise ValueError("Provide either api_key or client_id+client_secret, not both.") + + @property + def is_oauth(self) -> bool: + return self.client_id is not None diff --git a/sendpulse/exception/__init__.py b/sendpulse/exception/__init__.py new file mode 100644 index 0000000..c1faa22 --- /dev/null +++ b/sendpulse/exception/__init__.py @@ -0,0 +1,19 @@ +from sendpulse.exception.exceptions import ( + ApiException, + AuthException, + NetworkException, + ProtocolException, + RateLimitException, + SendPulseError, + SendPulseException, +) + +__all__ = [ + "SendPulseError", + "SendPulseException", + "AuthException", + "RateLimitException", + "ApiException", + "NetworkException", + "ProtocolException", +] diff --git a/sendpulse/exception/exceptions.py b/sendpulse/exception/exceptions.py new file mode 100644 index 0000000..4baa923 --- /dev/null +++ b/sendpulse/exception/exceptions.py @@ -0,0 +1,34 @@ +from __future__ import annotations + + +class SendPulseError(Exception): + """Base class for all exceptions raised by this SDK.""" + + +class SendPulseException(SendPulseError): + """API-level error — carries HTTP status and raw response body.""" + + def __init__(self, http_status: int, raw_body: str, message: str = "") -> None: + self.http_status = http_status + self.raw_body = raw_body + super().__init__(message or raw_body) + + +class AuthException(SendPulseException): + """401 / 403 — wrong credentials or insufficient permissions.""" + + +class RateLimitException(SendPulseException): + """429 — too many requests.""" + + +class ApiException(SendPulseException): + """Any other 4xx / 5xx response from the API.""" + + +class NetworkException(SendPulseError): + """Transport-level error — connection refused, timeout, DNS failure, etc.""" + + +class ProtocolException(SendPulseError): + """Response received but could not be parsed (malformed JSON from the API).""" diff --git a/sendpulse/generated/__init__.py b/sendpulse/generated/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/sendpulse/generated/chatbot/__init__.py b/sendpulse/generated/chatbot/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/sendpulse/generated/chatbot/model/Account.py b/sendpulse/generated/chatbot/model/Account.py new file mode 100644 index 0000000..35c2f49 --- /dev/null +++ b/sendpulse/generated/chatbot/model/Account.py @@ -0,0 +1,19 @@ +from __future__ import annotations + +from dataclasses import dataclass +from typing import Any + + +@dataclass(slots=True) +class Account: + tariff: dict[str, Any] | None = None + statistics: dict[str, Any] | None = None + services: list[Any] | None = None + + @classmethod + def from_dict(cls, data: dict[str, Any]) -> Account: + return cls( + tariff=data.get("tariff"), + statistics=data.get("statistics"), + services=data.get("services"), + ) diff --git a/sendpulse/generated/chatbot/model/Bot.py b/sendpulse/generated/chatbot/model/Bot.py new file mode 100644 index 0000000..b56fe40 --- /dev/null +++ b/sendpulse/generated/chatbot/model/Bot.py @@ -0,0 +1,23 @@ +from __future__ import annotations + +from dataclasses import dataclass +from typing import Any + + +@dataclass(slots=True) +class Bot: + id: str | None = None + channel_data: dict[str, Any] | None = None + inbox: dict[str, Any] | None = None + status: int | None = None + created_at: str | None = None + + @classmethod + def from_dict(cls, data: dict[str, Any]) -> Bot: + return cls( + id=data.get("id"), + channel_data=data.get("channel_data"), + inbox=data.get("inbox"), + status=data.get("status"), + created_at=data.get("created_at"), + ) diff --git a/sendpulse/generated/chatbot/model/Dialog.py b/sendpulse/generated/chatbot/model/Dialog.py new file mode 100644 index 0000000..b9e155d --- /dev/null +++ b/sendpulse/generated/chatbot/model/Dialog.py @@ -0,0 +1,35 @@ +from __future__ import annotations + +from dataclasses import dataclass +from typing import Any + + +@dataclass(slots=True) +class Dialog: + _id: str | None = None + bot_id: str | None = None + contact: dict[str, Any] | None = None + last_inbox_message: dict[str, Any] | None = None + last_outbox_message: dict[str, Any] | None = None + service: int | None = None + user_id: int | None = None + inbox_unread_count: int | None = None + is_chat_opened: bool | None = None + created_at: str | None = None + updated_at: str | None = None + + @classmethod + def from_dict(cls, data: dict[str, Any]) -> Dialog: + return cls( + _id=data.get("_id"), + bot_id=data.get("bot_id"), + contact=data.get("contact"), + last_inbox_message=data.get("last_inbox_message"), + last_outbox_message=data.get("last_outbox_message"), + service=data.get("service"), + user_id=data.get("user_id"), + inbox_unread_count=data.get("inbox_unread_count"), + is_chat_opened=data.get("is_chat_opened"), + created_at=data.get("created_at"), + updated_at=data.get("updated_at"), + ) diff --git a/sendpulse/generated/chatbot/model/SuccessResponse.py b/sendpulse/generated/chatbot/model/SuccessResponse.py new file mode 100644 index 0000000..8697919 --- /dev/null +++ b/sendpulse/generated/chatbot/model/SuccessResponse.py @@ -0,0 +1,17 @@ +from __future__ import annotations + +from dataclasses import dataclass +from typing import Any + + +@dataclass(slots=True) +class SuccessResponse: + success: bool | None = None + data: Any = None + + @classmethod + def from_dict(cls, data: dict[str, Any]) -> SuccessResponse: + return cls( + success=data.get("success"), + data=data.get("data"), + ) diff --git a/sendpulse/generated/chatbot/model/__init__.py b/sendpulse/generated/chatbot/model/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/sendpulse/generated/chatbot/service/__init__.py b/sendpulse/generated/chatbot/service/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/sendpulse/generated/chatbot/service/account_resource.py b/sendpulse/generated/chatbot/service/account_resource.py new file mode 100644 index 0000000..8b4697a --- /dev/null +++ b/sendpulse/generated/chatbot/service/account_resource.py @@ -0,0 +1,14 @@ +from __future__ import annotations + +from typing import Any + +from sendpulse.http.request import Request +from sendpulse.service.abstract import AbstractService + + +class AccountResource(AbstractService): + def get_account(self) -> dict[str, Any]: + return self._send(Request( + method="GET", + uri="/account", + )) diff --git a/sendpulse/generated/chatbot/service/bots_resource.py b/sendpulse/generated/chatbot/service/bots_resource.py new file mode 100644 index 0000000..b13192f --- /dev/null +++ b/sendpulse/generated/chatbot/service/bots_resource.py @@ -0,0 +1,14 @@ +from __future__ import annotations + +from typing import Any + +from sendpulse.http.request import Request +from sendpulse.service.abstract import AbstractService + + +class BotsResource(AbstractService): + def get_bots(self) -> dict[str, Any]: + return self._send(Request( + method="GET", + uri="/bots", + )) diff --git a/sendpulse/generated/chatbot/service/chatbot_service.py b/sendpulse/generated/chatbot/service/chatbot_service.py new file mode 100644 index 0000000..55d2421 --- /dev/null +++ b/sendpulse/generated/chatbot/service/chatbot_service.py @@ -0,0 +1,17 @@ +from __future__ import annotations + +from sendpulse.service.abstract import AbstractService +from sendpulse.generated.chatbot.service.dialogs_resource import DialogsResource +from sendpulse.generated.chatbot.service.account_resource import AccountResource +from sendpulse.generated.chatbot.service.bots_resource import BotsResource + + +class ChatbotService(AbstractService): + def dialogs(self) -> DialogsResource: + return DialogsResource(self._client) + + def account(self) -> AccountResource: + return AccountResource(self._client) + + def bots(self) -> BotsResource: + return BotsResource(self._client) diff --git a/sendpulse/generated/chatbot/service/dialogs_resource.py b/sendpulse/generated/chatbot/service/dialogs_resource.py new file mode 100644 index 0000000..9cc1ea4 --- /dev/null +++ b/sendpulse/generated/chatbot/service/dialogs_resource.py @@ -0,0 +1,16 @@ +from __future__ import annotations + +from typing import Any + +from sendpulse.http.request import Request +from sendpulse.service.abstract import AbstractService + + +class DialogsResource(AbstractService): + def get_dialogs(self, size: int | None = None, skip: int | None = None, search_after: str | None = None, order: str | None = None) -> dict[str, Any]: + params = {k: v for k, v in {"size": size, "skip": skip, "search_after": search_after, "order": order}.items() if v is not None} + return self._send(Request( + method="GET", + uri="/dialogs", + params=params or None, + )) diff --git a/sendpulse/generated/crm/__init__.py b/sendpulse/generated/crm/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/sendpulse/generated/crm/model/AttachmentResource.py b/sendpulse/generated/crm/model/AttachmentResource.py new file mode 100644 index 0000000..20d0b3c --- /dev/null +++ b/sendpulse/generated/crm/model/AttachmentResource.py @@ -0,0 +1,25 @@ +from __future__ import annotations + +from dataclasses import dataclass +from typing import Any + + +@dataclass(slots=True) +class AttachmentResource: + id: int | None = None + link: list[Any] | None = None + entityId: int | None = None + entityType: str | None = None + createdAt: str | None = None + updatedAt: str | None = None + + @classmethod + def from_dict(cls, data: dict[str, Any]) -> AttachmentResource: + return cls( + id=data.get("id"), + link=data.get("link"), + entityId=data.get("entityId"), + entityType=data.get("entityType"), + createdAt=data.get("createdAt"), + updatedAt=data.get("updatedAt"), + ) diff --git a/sendpulse/generated/crm/model/Attribute.py b/sendpulse/generated/crm/model/Attribute.py new file mode 100644 index 0000000..aa71503 --- /dev/null +++ b/sendpulse/generated/crm/model/Attribute.py @@ -0,0 +1,27 @@ +from __future__ import annotations + +from dataclasses import dataclass +from typing import Any + + +@dataclass(slots=True) +class Attribute: + id: int | None = None + name: str | None = None + status: int | None = None + type: int | None = None + mandatory: bool | None = None + order: int | None = None + options: list[Any] | None = None + + @classmethod + def from_dict(cls, data: dict[str, Any]) -> Attribute: + return cls( + id=data.get("id"), + name=data.get("name"), + status=data.get("status"), + type=data.get("type"), + mandatory=data.get("mandatory"), + order=data.get("order"), + options=data.get("options"), + ) diff --git a/sendpulse/generated/crm/model/AttributeValue.py b/sendpulse/generated/crm/model/AttributeValue.py new file mode 100644 index 0000000..7336e38 --- /dev/null +++ b/sendpulse/generated/crm/model/AttributeValue.py @@ -0,0 +1,17 @@ +from __future__ import annotations + +from dataclasses import dataclass +from typing import Any + + +@dataclass(slots=True) +class AttributeValue: + id: float | None = None + value: str | None = None + + @classmethod + def from_dict(cls, data: dict[str, Any]) -> AttributeValue: + return cls( + id=data.get("id"), + value=data.get("value"), + ) diff --git a/sendpulse/generated/crm/model/Attributes.py b/sendpulse/generated/crm/model/Attributes.py new file mode 100644 index 0000000..d65e7e7 --- /dev/null +++ b/sendpulse/generated/crm/model/Attributes.py @@ -0,0 +1,23 @@ +from __future__ import annotations + +from dataclasses import dataclass +from typing import Any + +from sendpulse.generated.crm.model.AttributeValue import AttributeValue + + +@dataclass(slots=True) +class Attributes: + type: int | None = None + name: str | None = None + status: bool | None = None + value: list[AttributeValue] | None = None + + @classmethod + def from_dict(cls, data: dict[str, Any]) -> Attributes: + return cls( + type=data.get("type"), + name=data.get("name"), + status=data.get("status"), + value=[AttributeValue.from_dict(i) for i in data["value"]] if isinstance(data.get("value"), list) else None, + ) diff --git a/sendpulse/generated/crm/model/AttributesProperty.py b/sendpulse/generated/crm/model/AttributesProperty.py new file mode 100644 index 0000000..a599988 --- /dev/null +++ b/sendpulse/generated/crm/model/AttributesProperty.py @@ -0,0 +1,13 @@ +from __future__ import annotations + +from dataclasses import dataclass +from typing import Any + + +@dataclass(slots=True) +class AttributesProperty: + pass + + @classmethod + def from_dict(cls, data: dict[str, Any]) -> AttributesProperty: + return cls() diff --git a/sendpulse/generated/crm/model/BoardAttribute.py b/sendpulse/generated/crm/model/BoardAttribute.py new file mode 100644 index 0000000..9eade43 --- /dev/null +++ b/sendpulse/generated/crm/model/BoardAttribute.py @@ -0,0 +1,27 @@ +from __future__ import annotations + +from dataclasses import dataclass +from typing import Any + + +@dataclass(slots=True) +class BoardAttribute: + id: int | None = None + name: str | None = None + type: int | None = None + mandatory: bool | None = None + options: list[Any] | None = None + status: int | None = None + order: int | None = None + + @classmethod + def from_dict(cls, data: dict[str, Any]) -> BoardAttribute: + return cls( + id=data.get("id"), + name=data.get("name"), + type=data.get("type"), + mandatory=data.get("mandatory"), + options=data.get("options"), + status=data.get("status"), + order=data.get("order"), + ) diff --git a/sendpulse/generated/crm/model/BoardSettings.py b/sendpulse/generated/crm/model/BoardSettings.py new file mode 100644 index 0000000..cef16cc --- /dev/null +++ b/sendpulse/generated/crm/model/BoardSettings.py @@ -0,0 +1,21 @@ +from __future__ import annotations + +from dataclasses import dataclass +from typing import Any + + +@dataclass(slots=True) +class BoardSettings: + id: int | None = None + name: str | None = None + value: str | None = None + boardId: float | None = None + + @classmethod + def from_dict(cls, data: dict[str, Any]) -> BoardSettings: + return cls( + id=data.get("id"), + name=data.get("name"), + value=data.get("value"), + boardId=data.get("boardId"), + ) diff --git a/sendpulse/generated/crm/model/BoardSteps.py b/sendpulse/generated/crm/model/BoardSteps.py new file mode 100644 index 0000000..129b1ab --- /dev/null +++ b/sendpulse/generated/crm/model/BoardSteps.py @@ -0,0 +1,23 @@ +from __future__ import annotations + +from dataclasses import dataclass +from typing import Any + + +@dataclass(slots=True) +class BoardSteps: + id: int | None = None + name: str | None = None + color: str | None = None + order: int | None = None + boardId: int | None = None + + @classmethod + def from_dict(cls, data: dict[str, Any]) -> BoardSteps: + return cls( + id=data.get("id"), + name=data.get("name"), + color=data.get("color"), + order=data.get("order"), + boardId=data.get("boardId"), + ) diff --git a/sendpulse/generated/crm/model/Boards.py b/sendpulse/generated/crm/model/Boards.py new file mode 100644 index 0000000..2f186a7 --- /dev/null +++ b/sendpulse/generated/crm/model/Boards.py @@ -0,0 +1,31 @@ +from __future__ import annotations + +from dataclasses import dataclass +from typing import Any + +from sendpulse.generated.crm.model.BoardAttribute import BoardAttribute +from sendpulse.generated.crm.model.BoardSettings import BoardSettings +from sendpulse.generated.crm.model.BoardSteps import BoardSteps + + +@dataclass(slots=True) +class Boards: + id: int | None = None + name: str | None = None + userId: int | None = None + order: int | None = None + steps: BoardSteps | None = None + settings: BoardSettings | None = None + attributes: BoardAttribute | None = None + + @classmethod + def from_dict(cls, data: dict[str, Any]) -> Boards: + return cls( + id=data.get("id"), + name=data.get("name"), + userId=data.get("userId"), + order=data.get("order"), + steps=BoardSteps.from_dict(data["steps"]) if isinstance(data.get("steps"), dict) else None, + settings=BoardSettings.from_dict(data["settings"]) if isinstance(data.get("settings"), dict) else None, + attributes=BoardAttribute.from_dict(data["attributes"]) if isinstance(data.get("attributes"), dict) else None, + ) diff --git a/sendpulse/generated/crm/model/ChecklistItems.py b/sendpulse/generated/crm/model/ChecklistItems.py new file mode 100644 index 0000000..017f4fe --- /dev/null +++ b/sendpulse/generated/crm/model/ChecklistItems.py @@ -0,0 +1,21 @@ +from __future__ import annotations + +from dataclasses import dataclass +from typing import Any + + +@dataclass(slots=True) +class ChecklistItems: + id: int | None = None + name: str | None = None + order: float | None = None + isDone: bool | None = None + + @classmethod + def from_dict(cls, data: dict[str, Any]) -> ChecklistItems: + return cls( + id=data.get("id"), + name=data.get("name"), + order=data.get("order"), + isDone=data.get("isDone"), + ) diff --git a/sendpulse/generated/crm/model/Comment.py b/sendpulse/generated/crm/model/Comment.py new file mode 100644 index 0000000..1eae31a --- /dev/null +++ b/sendpulse/generated/crm/model/Comment.py @@ -0,0 +1,25 @@ +from __future__ import annotations + +from dataclasses import dataclass +from typing import Any + + +@dataclass(slots=True) +class Comment: + id: int | None = None + userId: int | None = None + text: str | None = None + status: int | None = None + createdAt: str | None = None + updatedAt: str | None = None + + @classmethod + def from_dict(cls, data: dict[str, Any]) -> Comment: + return cls( + id=data.get("id"), + userId=data.get("userId"), + text=data.get("text"), + status=data.get("status"), + createdAt=data.get("createdAt"), + updatedAt=data.get("updatedAt"), + ) diff --git a/sendpulse/generated/crm/model/CommentCompany.py b/sendpulse/generated/crm/model/CommentCompany.py new file mode 100644 index 0000000..ec6d652 --- /dev/null +++ b/sendpulse/generated/crm/model/CommentCompany.py @@ -0,0 +1,21 @@ +from __future__ import annotations + +from dataclasses import dataclass +from typing import Any + + +@dataclass(slots=True) +class CommentCompany: + id: int | None = None + message: str | None = None + createdAt: str | None = None + updatedAt: str | None = None + + @classmethod + def from_dict(cls, data: dict[str, Any]) -> CommentCompany: + return cls( + id=data.get("id"), + message=data.get("message"), + createdAt=data.get("createdAt"), + updatedAt=data.get("updatedAt"), + ) diff --git a/sendpulse/generated/crm/model/Company.py b/sendpulse/generated/crm/model/Company.py new file mode 100644 index 0000000..bd43cb9 --- /dev/null +++ b/sendpulse/generated/crm/model/Company.py @@ -0,0 +1,47 @@ +from __future__ import annotations + +from dataclasses import dataclass +from typing import Any + +from sendpulse.generated.crm.model.AttachmentResource import AttachmentResource +from sendpulse.generated.crm.model.Attribute import Attribute +from sendpulse.generated.crm.model.Email import Email +from sendpulse.generated.crm.model.Messenger import Messenger +from sendpulse.generated.crm.model.Phone import Phone + + +@dataclass(slots=True) +class Company: + id: int | None = None + companyName: str | None = None + responsibleId: int | None = None + address: str | None = None + annualBusinessVolume: int | None = None + currency: str | None = None + messengers: list[Messenger] | None = None + phones: list[Phone] | None = None + emails: list[Email] | None = None + attributes: list[Attribute] | None = None + contacts: list[Any] | None = None + attachments: list[AttachmentResource] | None = None + createdAt: str | None = None + updatedAt: str | None = None + + @classmethod + def from_dict(cls, data: dict[str, Any]) -> Company: + return cls( + id=data.get("id"), + companyName=data.get("companyName"), + responsibleId=data.get("responsibleId"), + address=data.get("address"), + annualBusinessVolume=data.get("annualBusinessVolume"), + currency=data.get("currency"), + messengers=[Messenger.from_dict(i) for i in data["messengers"]] if isinstance(data.get("messengers"), list) else None, + phones=[Phone.from_dict(i) for i in data["phones"]] if isinstance(data.get("phones"), list) else None, + emails=[Email.from_dict(i) for i in data["emails"]] if isinstance(data.get("emails"), list) else None, + attributes=[Attribute.from_dict(i) for i in data["attributes"]] if isinstance(data.get("attributes"), list) else None, + contacts=data.get("contacts"), + attachments=[AttachmentResource.from_dict(i) for i in data["attachments"]] if isinstance(data.get("attachments"), list) else None, + createdAt=data.get("createdAt"), + updatedAt=data.get("updatedAt"), + ) diff --git a/sendpulse/generated/crm/model/CompanySortData.py b/sendpulse/generated/crm/model/CompanySortData.py new file mode 100644 index 0000000..79b28fe --- /dev/null +++ b/sendpulse/generated/crm/model/CompanySortData.py @@ -0,0 +1,25 @@ +from __future__ import annotations + +from dataclasses import dataclass +from typing import Any + + +@dataclass(slots=True) +class CompanySortData: + id: int | None = None + companyName: str | None = None + responsibleId: int | None = None + address: str | None = None + annualBusinessVolume: int | None = None + currency: str | None = None + + @classmethod + def from_dict(cls, data: dict[str, Any]) -> CompanySortData: + return cls( + id=data.get("id"), + companyName=data.get("companyName"), + responsibleId=data.get("responsibleId"), + address=data.get("address"), + annualBusinessVolume=data.get("annualBusinessVolume"), + currency=data.get("currency"), + ) diff --git a/sendpulse/generated/crm/model/Contact.py b/sendpulse/generated/crm/model/Contact.py new file mode 100644 index 0000000..6c31fe8 --- /dev/null +++ b/sendpulse/generated/crm/model/Contact.py @@ -0,0 +1,60 @@ +from __future__ import annotations + +from dataclasses import dataclass +from typing import Any + +from sendpulse.generated.crm.model.ContactAttributeValue import ContactAttributeValue +from sendpulse.generated.crm.model.ContactComment import ContactComment +from sendpulse.generated.crm.model.ContactEmail import ContactEmail +from sendpulse.generated.crm.model.ContactHistory import ContactHistory +from sendpulse.generated.crm.model.ContactMessenger import ContactMessenger +from sendpulse.generated.crm.model.ContactPhone import ContactPhone +from sendpulse.generated.crm.model.ContactTag import ContactTag +from sendpulse.generated.crm.model.EntityAttachment import EntityAttachment + + +@dataclass(slots=True) +class Contact: + id: int | None = None + userId: int | None = None + sourceType: str | None = None + responsibleId: int | None = None + firstName: str | None = None + lastName: str | None = None + dealsQty: int | None = None + externalContactId: str | None = None + comments: list[ContactComment] | None = None + tags: list[ContactTag] | None = None + phones: list[ContactPhone] | None = None + emails: list[ContactEmail] | None = None + messengers: list[ContactMessenger] | None = None + attributes: list[ContactAttributeValue] | None = None + history: list[ContactHistory] | None = None + tasks: list[Any] | None = None + createdAt: str | None = None + updatedAt: str | None = None + attachments: EntityAttachment | None = None + + @classmethod + def from_dict(cls, data: dict[str, Any]) -> Contact: + return cls( + id=data.get("id"), + userId=data.get("userId"), + sourceType=data.get("sourceType"), + responsibleId=data.get("responsibleId"), + firstName=data.get("firstName"), + lastName=data.get("lastName"), + dealsQty=data.get("dealsQty"), + externalContactId=data.get("externalContactId"), + comments=[ContactComment.from_dict(i) for i in data["comments"]] if isinstance(data.get("comments"), list) else None, + tags=[ContactTag.from_dict(i) for i in data["tags"]] if isinstance(data.get("tags"), list) else None, + phones=[ContactPhone.from_dict(i) for i in data["phones"]] if isinstance(data.get("phones"), list) else None, + emails=[ContactEmail.from_dict(i) for i in data["emails"]] if isinstance(data.get("emails"), list) else None, + messengers=[ContactMessenger.from_dict(i) for i in data["messengers"]] if isinstance(data.get("messengers"), list) else None, + attributes=[ContactAttributeValue.from_dict(i) for i in data["attributes"]] if isinstance(data.get("attributes"), list) else None, + history=[ContactHistory.from_dict(i) for i in data["history"]] if isinstance(data.get("history"), list) else None, + tasks=data.get("tasks"), + createdAt=data.get("createdAt"), + updatedAt=data.get("updatedAt"), + attachments=EntityAttachment.from_dict(data["attachments"]) if isinstance(data.get("attachments"), dict) else None, + ) diff --git a/sendpulse/generated/crm/model/ContactAttribute.py b/sendpulse/generated/crm/model/ContactAttribute.py new file mode 100644 index 0000000..2055fe3 --- /dev/null +++ b/sendpulse/generated/crm/model/ContactAttribute.py @@ -0,0 +1,23 @@ +from __future__ import annotations + +from dataclasses import dataclass +from typing import Any + + +@dataclass(slots=True) +class ContactAttribute: + id: int | None = None + name: str | None = None + status: int | None = None + order: int | None = None + options: list[Any] | None = None + + @classmethod + def from_dict(cls, data: dict[str, Any]) -> ContactAttribute: + return cls( + id=data.get("id"), + name=data.get("name"), + status=data.get("status"), + order=data.get("order"), + options=data.get("options"), + ) diff --git a/sendpulse/generated/crm/model/ContactAttributeValue.py b/sendpulse/generated/crm/model/ContactAttributeValue.py new file mode 100644 index 0000000..d03b634 --- /dev/null +++ b/sendpulse/generated/crm/model/ContactAttributeValue.py @@ -0,0 +1,33 @@ +from __future__ import annotations + +from dataclasses import dataclass +from typing import Any + + +@dataclass(slots=True) +class ContactAttributeValue: + id: int | None = None + name: str | None = None + status: int | None = None + type: int | None = None + mandatory: bool | None = None + contactCardShow: bool | None = None + order: int | None = None + options: list[Any] | None = None + value: Any = None + default: bool | None = None + + @classmethod + def from_dict(cls, data: dict[str, Any]) -> ContactAttributeValue: + return cls( + id=data.get("id"), + name=data.get("name"), + status=data.get("status"), + type=data.get("type"), + mandatory=data.get("mandatory"), + contactCardShow=data.get("contactCardShow"), + order=data.get("order"), + options=data.get("options"), + value=data.get("value"), + default=data.get("default"), + ) diff --git a/sendpulse/generated/crm/model/ContactComment.py b/sendpulse/generated/crm/model/ContactComment.py new file mode 100644 index 0000000..94d5981 --- /dev/null +++ b/sendpulse/generated/crm/model/ContactComment.py @@ -0,0 +1,31 @@ +from __future__ import annotations + +from dataclasses import dataclass +from typing import Any + +from sendpulse.generated.crm.model.EntityAttachment import EntityAttachment + + +@dataclass(slots=True) +class ContactComment: + id: int | None = None + userId: int | None = None + text: str | None = None + createdAt: str | None = None + updatedAt: str | None = None + attachments: EntityAttachment | None = None + childCount: int | None = None + childUsers: list[Any] | None = None + + @classmethod + def from_dict(cls, data: dict[str, Any]) -> ContactComment: + return cls( + id=data.get("id"), + userId=data.get("userId"), + text=data.get("text"), + createdAt=data.get("createdAt"), + updatedAt=data.get("updatedAt"), + attachments=EntityAttachment.from_dict(data["attachments"]) if isinstance(data.get("attachments"), dict) else None, + childCount=data.get("childCount"), + childUsers=data.get("childUsers"), + ) diff --git a/sendpulse/generated/crm/model/ContactEmail.py b/sendpulse/generated/crm/model/ContactEmail.py new file mode 100644 index 0000000..bfa633a --- /dev/null +++ b/sendpulse/generated/crm/model/ContactEmail.py @@ -0,0 +1,19 @@ +from __future__ import annotations + +from dataclasses import dataclass +from typing import Any + + +@dataclass(slots=True) +class ContactEmail: + id: int | None = None + email: str | None = None + isMain: bool | None = None + + @classmethod + def from_dict(cls, data: dict[str, Any]) -> ContactEmail: + return cls( + id=data.get("id"), + email=data.get("email"), + isMain=data.get("isMain"), + ) diff --git a/sendpulse/generated/crm/model/ContactHistory.py b/sendpulse/generated/crm/model/ContactHistory.py new file mode 100644 index 0000000..753d3e8 --- /dev/null +++ b/sendpulse/generated/crm/model/ContactHistory.py @@ -0,0 +1,25 @@ +from __future__ import annotations + +from dataclasses import dataclass +from typing import Any + + +@dataclass(slots=True) +class ContactHistory: + id: int | None = None + userId: int | None = None + contactId: int | None = None + eventType: str | None = None + eventTime: str | None = None + eventData: dict[str, Any] | None = None + + @classmethod + def from_dict(cls, data: dict[str, Any]) -> ContactHistory: + return cls( + id=data.get("id"), + userId=data.get("userId"), + contactId=data.get("contactId"), + eventType=data.get("eventType"), + eventTime=data.get("eventTime"), + eventData=data.get("eventData"), + ) diff --git a/sendpulse/generated/crm/model/ContactMessenger.py b/sendpulse/generated/crm/model/ContactMessenger.py new file mode 100644 index 0000000..4febcf2 --- /dev/null +++ b/sendpulse/generated/crm/model/ContactMessenger.py @@ -0,0 +1,29 @@ +from __future__ import annotations + +from dataclasses import dataclass +from typing import Any + + +@dataclass(slots=True) +class ContactMessenger: + id: int | None = None + typeId: int | None = None + login: str | None = None + botId: str | None = None + contactId: str | None = None + status: int | None = None + chatbotUrl: str | None = None + isMainChatbot: bool | None = None + + @classmethod + def from_dict(cls, data: dict[str, Any]) -> ContactMessenger: + return cls( + id=data.get("id"), + typeId=data.get("typeId"), + login=data.get("login"), + botId=data.get("botId"), + contactId=data.get("contactId"), + status=data.get("status"), + chatbotUrl=data.get("chatbotUrl"), + isMainChatbot=data.get("isMainChatbot"), + ) diff --git a/sendpulse/generated/crm/model/ContactPhone.py b/sendpulse/generated/crm/model/ContactPhone.py new file mode 100644 index 0000000..4b9b12b --- /dev/null +++ b/sendpulse/generated/crm/model/ContactPhone.py @@ -0,0 +1,19 @@ +from __future__ import annotations + +from dataclasses import dataclass +from typing import Any + + +@dataclass(slots=True) +class ContactPhone: + id: int | None = None + phone: str | None = None + isMain: bool | None = None + + @classmethod + def from_dict(cls, data: dict[str, Any]) -> ContactPhone: + return cls( + id=data.get("id"), + phone=data.get("phone"), + isMain=data.get("isMain"), + ) diff --git a/sendpulse/generated/crm/model/ContactTag.py b/sendpulse/generated/crm/model/ContactTag.py new file mode 100644 index 0000000..d8e7a5f --- /dev/null +++ b/sendpulse/generated/crm/model/ContactTag.py @@ -0,0 +1,25 @@ +from __future__ import annotations + +from dataclasses import dataclass +from typing import Any + + +@dataclass(slots=True) +class ContactTag: + id: int | None = None + name: str | None = None + colorText: str | None = None + colorBackground: str | None = None + contactCount: int | None = None + taskCount: int | None = None + + @classmethod + def from_dict(cls, data: dict[str, Any]) -> ContactTag: + return cls( + id=data.get("id"), + name=data.get("name"), + colorText=data.get("colorText"), + colorBackground=data.get("colorBackground"), + contactCount=data.get("contactCount"), + taskCount=data.get("taskCount"), + ) diff --git a/sendpulse/generated/crm/model/ContactsAutocomplete.py b/sendpulse/generated/crm/model/ContactsAutocomplete.py new file mode 100644 index 0000000..14db61e --- /dev/null +++ b/sendpulse/generated/crm/model/ContactsAutocomplete.py @@ -0,0 +1,13 @@ +from __future__ import annotations + +from dataclasses import dataclass +from typing import Any + + +@dataclass(slots=True) +class ContactsAutocomplete: + pass + + @classmethod + def from_dict(cls, data: dict[str, Any]) -> ContactsAutocomplete: + return cls() diff --git a/sendpulse/generated/crm/model/CustomTab.py b/sendpulse/generated/crm/model/CustomTab.py new file mode 100644 index 0000000..0836714 --- /dev/null +++ b/sendpulse/generated/crm/model/CustomTab.py @@ -0,0 +1,29 @@ +from __future__ import annotations + +from dataclasses import dataclass +from typing import Any + + +@dataclass(slots=True) +class CustomTab: + id: int | None = None + name: str | None = None + userId: int | None = None + isVisible: bool | None = None + isDefault: bool | None = None + type: int | None = None + entityType: int | None = None + entityId: int | None = None + + @classmethod + def from_dict(cls, data: dict[str, Any]) -> CustomTab: + return cls( + id=data.get("id"), + name=data.get("name"), + userId=data.get("userId"), + isVisible=data.get("isVisible"), + isDefault=data.get("isDefault"), + type=data.get("type"), + entityType=data.get("entityType"), + entityId=data.get("entityId"), + ) diff --git a/sendpulse/generated/crm/model/CustomTabWithAttribute.py b/sendpulse/generated/crm/model/CustomTabWithAttribute.py new file mode 100644 index 0000000..ca73ace --- /dev/null +++ b/sendpulse/generated/crm/model/CustomTabWithAttribute.py @@ -0,0 +1,31 @@ +from __future__ import annotations + +from dataclasses import dataclass +from typing import Any + + +@dataclass(slots=True) +class CustomTabWithAttribute: + id: int | None = None + name: str | None = None + userId: int | None = None + isVisible: bool | None = None + isDefault: bool | None = None + type: int | None = None + entityType: int | None = None + entityId: int | None = None + attributes: list[Any] | None = None + + @classmethod + def from_dict(cls, data: dict[str, Any]) -> CustomTabWithAttribute: + return cls( + id=data.get("id"), + name=data.get("name"), + userId=data.get("userId"), + isVisible=data.get("isVisible"), + isDefault=data.get("isDefault"), + type=data.get("type"), + entityType=data.get("entityType"), + entityId=data.get("entityId"), + attributes=data.get("attributes"), + ) diff --git a/sendpulse/generated/crm/model/Deal.py b/sendpulse/generated/crm/model/Deal.py new file mode 100644 index 0000000..d371367 --- /dev/null +++ b/sendpulse/generated/crm/model/Deal.py @@ -0,0 +1,65 @@ +from __future__ import annotations + +from dataclasses import dataclass +from typing import Any + +from sendpulse.generated.crm.model.DealAttributeValue import DealAttributeValue +from sendpulse.generated.crm.model.DealComment import DealComment +from sendpulse.generated.crm.model.DealExpiration import DealExpiration +from sendpulse.generated.crm.model.DealHistory import DealHistory +from sendpulse.generated.crm.model.DealSourceType import DealSourceType +from sendpulse.generated.crm.model.DealStatusProperty import DealStatusProperty +from sendpulse.generated.crm.model.EntityAttachment import EntityAttachment + + +@dataclass(slots=True) +class Deal: + id: int | None = None + pipelineId: int | None = None + status: DealStatusProperty | None = None + stepId: int | None = None + responsibleId: int | None = None + number: int | None = None + name: str | None = None + price: float | None = None + currency: str | None = None + profit: float | None = None + hasExpense: bool | None = None + order: int | None = None + sourceType: DealSourceType | None = None + sourceId: int | None = None + history: list[DealHistory] | None = None + comments: list[DealComment] | None = None + attributes: list[DealAttributeValue] | None = None + expiration: DealExpiration | None = None + attachments: EntityAttachment | None = None + tasks: list[Any] | None = None + createdAt: str | None = None + updatedAt: str | None = None + + @classmethod + def from_dict(cls, data: dict[str, Any]) -> Deal: + return cls( + id=data.get("id"), + pipelineId=data.get("pipelineId"), + status=DealStatusProperty.from_dict(data["status"]) if isinstance(data.get("status"), dict) else None, + stepId=data.get("stepId"), + responsibleId=data.get("responsibleId"), + number=data.get("number"), + name=data.get("name"), + price=data.get("price"), + currency=data.get("currency"), + profit=data.get("profit"), + hasExpense=data.get("hasExpense"), + order=data.get("order"), + sourceType=DealSourceType.from_dict(data["sourceType"]) if isinstance(data.get("sourceType"), dict) else None, + sourceId=data.get("sourceId"), + history=[DealHistory.from_dict(i) for i in data["history"]] if isinstance(data.get("history"), list) else None, + comments=[DealComment.from_dict(i) for i in data["comments"]] if isinstance(data.get("comments"), list) else None, + attributes=[DealAttributeValue.from_dict(i) for i in data["attributes"]] if isinstance(data.get("attributes"), list) else None, + expiration=DealExpiration.from_dict(data["expiration"]) if isinstance(data.get("expiration"), dict) else None, + attachments=EntityAttachment.from_dict(data["attachments"]) if isinstance(data.get("attachments"), dict) else None, + tasks=data.get("tasks"), + createdAt=data.get("createdAt"), + updatedAt=data.get("updatedAt"), + ) diff --git a/sendpulse/generated/crm/model/DealAttribute.py b/sendpulse/generated/crm/model/DealAttribute.py new file mode 100644 index 0000000..edaaf94 --- /dev/null +++ b/sendpulse/generated/crm/model/DealAttribute.py @@ -0,0 +1,29 @@ +from __future__ import annotations + +from dataclasses import dataclass +from typing import Any + +from sendpulse.generated.crm.model.DefaultStatusProperty import DefaultStatusProperty + + +@dataclass(slots=True) +class DealAttribute: + id: int | None = None + name: str | None = None + status: DefaultStatusProperty | None = None + type: int | None = None + mandatory: bool | None = None + order: int | None = None + options: list[Any] | None = None + + @classmethod + def from_dict(cls, data: dict[str, Any]) -> DealAttribute: + return cls( + id=data.get("id"), + name=data.get("name"), + status=DefaultStatusProperty.from_dict(data["status"]) if isinstance(data.get("status"), dict) else None, + type=data.get("type"), + mandatory=data.get("mandatory"), + order=data.get("order"), + options=data.get("options"), + ) diff --git a/sendpulse/generated/crm/model/DealAttributeValue.py b/sendpulse/generated/crm/model/DealAttributeValue.py new file mode 100644 index 0000000..268957f --- /dev/null +++ b/sendpulse/generated/crm/model/DealAttributeValue.py @@ -0,0 +1,31 @@ +from __future__ import annotations + +from dataclasses import dataclass +from typing import Any + + +@dataclass(slots=True) +class DealAttributeValue: + id: int | None = None + pipelineId: int | None = None + name: str | None = None + status: int | None = None + type: int | None = None + mandatory: bool | None = None + order: int | None = None + options: list[Any] | None = None + value: Any = None + + @classmethod + def from_dict(cls, data: dict[str, Any]) -> DealAttributeValue: + return cls( + id=data.get("id"), + pipelineId=data.get("pipelineId"), + name=data.get("name"), + status=data.get("status"), + type=data.get("type"), + mandatory=data.get("mandatory"), + order=data.get("order"), + options=data.get("options"), + value=data.get("value"), + ) diff --git a/sendpulse/generated/crm/model/DealComment.py b/sendpulse/generated/crm/model/DealComment.py new file mode 100644 index 0000000..2ef88fa --- /dev/null +++ b/sendpulse/generated/crm/model/DealComment.py @@ -0,0 +1,35 @@ +from __future__ import annotations + +from dataclasses import dataclass +from typing import Any + +from sendpulse.generated.crm.model.EntityAttachment import EntityAttachment + + +@dataclass(slots=True) +class DealComment: + id: int | None = None + userId: int | None = None + eventData: dict[str, Any] | None = None + status: int | None = None + eventTime: str | None = None + createdAt: str | None = None + updatedAt: str | None = None + attachments: list[EntityAttachment] | None = None + childCount: int | None = None + childUsers: list[Any] | None = None + + @classmethod + def from_dict(cls, data: dict[str, Any]) -> DealComment: + return cls( + id=data.get("id"), + userId=data.get("userId"), + eventData=data.get("eventData"), + status=data.get("status"), + eventTime=data.get("eventTime"), + createdAt=data.get("createdAt"), + updatedAt=data.get("updatedAt"), + attachments=[EntityAttachment.from_dict(i) for i in data["attachments"]] if isinstance(data.get("attachments"), list) else None, + childCount=data.get("childCount"), + childUsers=data.get("childUsers"), + ) diff --git a/sendpulse/generated/crm/model/DealDetailed.py b/sendpulse/generated/crm/model/DealDetailed.py new file mode 100644 index 0000000..aa41ff3 --- /dev/null +++ b/sendpulse/generated/crm/model/DealDetailed.py @@ -0,0 +1,57 @@ +from __future__ import annotations + +from dataclasses import dataclass +from typing import Any + +from sendpulse.generated.crm.model.DealAttributeValue import DealAttributeValue +from sendpulse.generated.crm.model.DealComment import DealComment +from sendpulse.generated.crm.model.DealHistory import DealHistory +from sendpulse.generated.crm.model.DealSourceType import DealSourceType +from sendpulse.generated.crm.model.DealStatusProperty import DealStatusProperty + + +@dataclass(slots=True) +class DealDetailed: + id: int | None = None + pipelineId: int | None = None + status: DealStatusProperty | None = None + stepId: int | None = None + responsibleId: int | None = None + number: int | None = None + name: str | None = None + price: float | None = None + currency: str | None = None + profit: float | None = None + hasExpense: bool | None = None + order: int | None = None + sourceType: DealSourceType | None = None + sourceId: int | None = None + createdAt: str | None = None + updatedAt: str | None = None + history: DealHistory | None = None + comments: DealComment | None = None + attributes: DealAttributeValue | None = None + + @classmethod + def from_dict(cls, data: dict[str, Any]) -> DealDetailed: + return cls( + id=data.get("id"), + pipelineId=data.get("pipelineId"), + status=DealStatusProperty.from_dict(data["status"]) if isinstance(data.get("status"), dict) else None, + stepId=data.get("stepId"), + responsibleId=data.get("responsibleId"), + number=data.get("number"), + name=data.get("name"), + price=data.get("price"), + currency=data.get("currency"), + profit=data.get("profit"), + hasExpense=data.get("hasExpense"), + order=data.get("order"), + sourceType=DealSourceType.from_dict(data["sourceType"]) if isinstance(data.get("sourceType"), dict) else None, + sourceId=data.get("sourceId"), + createdAt=data.get("createdAt"), + updatedAt=data.get("updatedAt"), + history=DealHistory.from_dict(data["history"]) if isinstance(data.get("history"), dict) else None, + comments=DealComment.from_dict(data["comments"]) if isinstance(data.get("comments"), dict) else None, + attributes=DealAttributeValue.from_dict(data["attributes"]) if isinstance(data.get("attributes"), dict) else None, + ) diff --git a/sendpulse/generated/crm/model/DealExpiration.py b/sendpulse/generated/crm/model/DealExpiration.py new file mode 100644 index 0000000..9586b37 --- /dev/null +++ b/sendpulse/generated/crm/model/DealExpiration.py @@ -0,0 +1,27 @@ +from __future__ import annotations + +from dataclasses import dataclass +from typing import Any + + +@dataclass(slots=True) +class DealExpiration: + date: str | None = None + time: str | None = None + dateTime: str | None = None + notificationEnabled: bool | None = None + notifyIn: str | None = None + expired: bool | None = None + expires_within_day: bool | None = None + + @classmethod + def from_dict(cls, data: dict[str, Any]) -> DealExpiration: + return cls( + date=data.get("date"), + time=data.get("time"), + dateTime=data.get("dateTime"), + notificationEnabled=data.get("notificationEnabled"), + notifyIn=data.get("notifyIn"), + expired=data.get("expired"), + expires_within_day=data.get("expires_within_day"), + ) diff --git a/sendpulse/generated/crm/model/DealHistory.py b/sendpulse/generated/crm/model/DealHistory.py new file mode 100644 index 0000000..0d21742 --- /dev/null +++ b/sendpulse/generated/crm/model/DealHistory.py @@ -0,0 +1,25 @@ +from __future__ import annotations + +from dataclasses import dataclass +from typing import Any + + +@dataclass(slots=True) +class DealHistory: + id: int | None = None + userId: int | None = None + eventData: list[Any] | None = None + eventType: str | None = None + eventTime: str | None = None + currentData: dict[str, Any] | None = None + + @classmethod + def from_dict(cls, data: dict[str, Any]) -> DealHistory: + return cls( + id=data.get("id"), + userId=data.get("userId"), + eventData=data.get("eventData"), + eventType=data.get("eventType"), + eventTime=data.get("eventTime"), + currentData=data.get("currentData"), + ) diff --git a/sendpulse/generated/crm/model/DealSourceType.py b/sendpulse/generated/crm/model/DealSourceType.py new file mode 100644 index 0000000..413750f --- /dev/null +++ b/sendpulse/generated/crm/model/DealSourceType.py @@ -0,0 +1,17 @@ +from __future__ import annotations + +from dataclasses import dataclass +from typing import Any + + +@dataclass(slots=True) +class DealSourceType: + id: int | None = None + name: str | None = None + + @classmethod + def from_dict(cls, data: dict[str, Any]) -> DealSourceType: + return cls( + id=data.get("id"), + name=data.get("name"), + ) diff --git a/sendpulse/generated/crm/model/DealStatusProperty.py b/sendpulse/generated/crm/model/DealStatusProperty.py new file mode 100644 index 0000000..1be2b79 --- /dev/null +++ b/sendpulse/generated/crm/model/DealStatusProperty.py @@ -0,0 +1,13 @@ +from __future__ import annotations + +from dataclasses import dataclass +from typing import Any + + +@dataclass(slots=True) +class DealStatusProperty: + pass + + @classmethod + def from_dict(cls, data: dict[str, Any]) -> DealStatusProperty: + return cls() diff --git a/sendpulse/generated/crm/model/DealType.py b/sendpulse/generated/crm/model/DealType.py new file mode 100644 index 0000000..2e6b992 --- /dev/null +++ b/sendpulse/generated/crm/model/DealType.py @@ -0,0 +1,19 @@ +from __future__ import annotations + +from dataclasses import dataclass +from typing import Any + + +@dataclass(slots=True) +class DealType: + id: int | None = None + name: str | None = None + status: int | None = None + + @classmethod + def from_dict(cls, data: dict[str, Any]) -> DealType: + return cls( + id=data.get("id"), + name=data.get("name"), + status=data.get("status"), + ) diff --git a/sendpulse/generated/crm/model/DealsAutocomplete.py b/sendpulse/generated/crm/model/DealsAutocomplete.py new file mode 100644 index 0000000..000e0a5 --- /dev/null +++ b/sendpulse/generated/crm/model/DealsAutocomplete.py @@ -0,0 +1,13 @@ +from __future__ import annotations + +from dataclasses import dataclass +from typing import Any + + +@dataclass(slots=True) +class DealsAutocomplete: + pass + + @classmethod + def from_dict(cls, data: dict[str, Any]) -> DealsAutocomplete: + return cls() diff --git a/sendpulse/generated/crm/model/DefaultStatusProperty.py b/sendpulse/generated/crm/model/DefaultStatusProperty.py new file mode 100644 index 0000000..79dad2f --- /dev/null +++ b/sendpulse/generated/crm/model/DefaultStatusProperty.py @@ -0,0 +1,13 @@ +from __future__ import annotations + +from dataclasses import dataclass +from typing import Any + + +@dataclass(slots=True) +class DefaultStatusProperty: + pass + + @classmethod + def from_dict(cls, data: dict[str, Any]) -> DefaultStatusProperty: + return cls() diff --git a/sendpulse/generated/crm/model/Email.py b/sendpulse/generated/crm/model/Email.py new file mode 100644 index 0000000..4126c94 --- /dev/null +++ b/sendpulse/generated/crm/model/Email.py @@ -0,0 +1,19 @@ +from __future__ import annotations + +from dataclasses import dataclass +from typing import Any + + +@dataclass(slots=True) +class Email: + id: int | None = None + isMain: bool | None = None + email: str | None = None + + @classmethod + def from_dict(cls, data: dict[str, Any]) -> Email: + return cls( + id=data.get("id"), + isMain=data.get("isMain"), + email=data.get("email"), + ) diff --git a/sendpulse/generated/crm/model/EntityAttachment.py b/sendpulse/generated/crm/model/EntityAttachment.py new file mode 100644 index 0000000..2e5b114 --- /dev/null +++ b/sendpulse/generated/crm/model/EntityAttachment.py @@ -0,0 +1,25 @@ +from __future__ import annotations + +from dataclasses import dataclass +from typing import Any + + +@dataclass(slots=True) +class EntityAttachment: + id: int | None = None + link: str | None = None + entityId: float | None = None + entityType: str | None = None + createdAt: str | None = None + updatedAt: str | None = None + + @classmethod + def from_dict(cls, data: dict[str, Any]) -> EntityAttachment: + return cls( + id=data.get("id"), + link=data.get("link"), + entityId=data.get("entityId"), + entityType=data.get("entityType"), + createdAt=data.get("createdAt"), + updatedAt=data.get("updatedAt"), + ) diff --git a/sendpulse/generated/crm/model/EntityFilterList.py b/sendpulse/generated/crm/model/EntityFilterList.py new file mode 100644 index 0000000..34d7060 --- /dev/null +++ b/sendpulse/generated/crm/model/EntityFilterList.py @@ -0,0 +1,13 @@ +from __future__ import annotations + +from dataclasses import dataclass +from typing import Any + + +@dataclass(slots=True) +class EntityFilterList: + pass + + @classmethod + def from_dict(cls, data: dict[str, Any]) -> EntityFilterList: + return cls() diff --git a/sendpulse/generated/crm/model/FilterExpressionProperty.py b/sendpulse/generated/crm/model/FilterExpressionProperty.py new file mode 100644 index 0000000..9eaaf71 --- /dev/null +++ b/sendpulse/generated/crm/model/FilterExpressionProperty.py @@ -0,0 +1,13 @@ +from __future__ import annotations + +from dataclasses import dataclass +from typing import Any + + +@dataclass(slots=True) +class FilterExpressionProperty: + pass + + @classmethod + def from_dict(cls, data: dict[str, Any]) -> FilterExpressionProperty: + return cls() diff --git a/sendpulse/generated/crm/model/History.py b/sendpulse/generated/crm/model/History.py new file mode 100644 index 0000000..d59aa76 --- /dev/null +++ b/sendpulse/generated/crm/model/History.py @@ -0,0 +1,19 @@ +from __future__ import annotations + +from dataclasses import dataclass +from typing import Any + + +@dataclass(slots=True) +class History: + eventType: str | None = None + eventData: list[Any] | None = None + date: str | None = None + + @classmethod + def from_dict(cls, data: dict[str, Any]) -> History: + return cls( + eventType=data.get("eventType"), + eventData=data.get("eventData"), + date=data.get("date"), + ) diff --git a/sendpulse/generated/crm/model/IntegerSourceTypeProperty.py b/sendpulse/generated/crm/model/IntegerSourceTypeProperty.py new file mode 100644 index 0000000..efd98fd --- /dev/null +++ b/sendpulse/generated/crm/model/IntegerSourceTypeProperty.py @@ -0,0 +1,13 @@ +from __future__ import annotations + +from dataclasses import dataclass +from typing import Any + + +@dataclass(slots=True) +class IntegerSourceTypeProperty: + pass + + @classmethod + def from_dict(cls, data: dict[str, Any]) -> IntegerSourceTypeProperty: + return cls() diff --git a/sendpulse/generated/crm/model/ManagerSettings.py b/sendpulse/generated/crm/model/ManagerSettings.py new file mode 100644 index 0000000..3a933a6 --- /dev/null +++ b/sendpulse/generated/crm/model/ManagerSettings.py @@ -0,0 +1,17 @@ +from __future__ import annotations + +from dataclasses import dataclass +from typing import Any + + +@dataclass(slots=True) +class ManagerSettings: + sectionId: int | None = None + responsibleId: int | None = None + + @classmethod + def from_dict(cls, data: dict[str, Any]) -> ManagerSettings: + return cls( + sectionId=data.get("sectionId"), + responsibleId=data.get("responsibleId"), + ) diff --git a/sendpulse/generated/crm/model/Messenger.py b/sendpulse/generated/crm/model/Messenger.py new file mode 100644 index 0000000..328bba6 --- /dev/null +++ b/sendpulse/generated/crm/model/Messenger.py @@ -0,0 +1,19 @@ +from __future__ import annotations + +from dataclasses import dataclass +from typing import Any + + +@dataclass(slots=True) +class Messenger: + id: int | None = None + typeId: int | None = None + login: str | None = None + + @classmethod + def from_dict(cls, data: dict[str, Any]) -> Messenger: + return cls( + id=data.get("id"), + typeId=data.get("typeId"), + login=data.get("login"), + ) diff --git a/sendpulse/generated/crm/model/MessengerType.py b/sendpulse/generated/crm/model/MessengerType.py new file mode 100644 index 0000000..a100b19 --- /dev/null +++ b/sendpulse/generated/crm/model/MessengerType.py @@ -0,0 +1,19 @@ +from __future__ import annotations + +from dataclasses import dataclass +from typing import Any + +from sendpulse.generated.crm.model.MessengerTypeProperty import MessengerTypeProperty + + +@dataclass(slots=True) +class MessengerType: + id: MessengerTypeProperty | None = None + name: str | None = None + + @classmethod + def from_dict(cls, data: dict[str, Any]) -> MessengerType: + return cls( + id=MessengerTypeProperty.from_dict(data["id"]) if isinstance(data.get("id"), dict) else None, + name=data.get("name"), + ) diff --git a/sendpulse/generated/crm/model/MessengerTypeProperty.py b/sendpulse/generated/crm/model/MessengerTypeProperty.py new file mode 100644 index 0000000..adecfba --- /dev/null +++ b/sendpulse/generated/crm/model/MessengerTypeProperty.py @@ -0,0 +1,13 @@ +from __future__ import annotations + +from dataclasses import dataclass +from typing import Any + + +@dataclass(slots=True) +class MessengerTypeProperty: + pass + + @classmethod + def from_dict(cls, data: dict[str, Any]) -> MessengerTypeProperty: + return cls() diff --git a/sendpulse/generated/crm/model/Pagination.py b/sendpulse/generated/crm/model/Pagination.py new file mode 100644 index 0000000..6dbab06 --- /dev/null +++ b/sendpulse/generated/crm/model/Pagination.py @@ -0,0 +1,21 @@ +from __future__ import annotations + +from dataclasses import dataclass +from typing import Any + + +@dataclass(slots=True) +class Pagination: + currentPage: int | None = None + lastPage: int | None = None + perPage: int | None = None + total: int | None = None + + @classmethod + def from_dict(cls, data: dict[str, Any]) -> Pagination: + return cls( + currentPage=data.get("currentPage"), + lastPage=data.get("lastPage"), + perPage=data.get("perPage"), + total=data.get("total"), + ) diff --git a/sendpulse/generated/crm/model/Payment.py b/sendpulse/generated/crm/model/Payment.py new file mode 100644 index 0000000..e1d326e --- /dev/null +++ b/sendpulse/generated/crm/model/Payment.py @@ -0,0 +1,35 @@ +from __future__ import annotations + +from dataclasses import dataclass +from typing import Any + +from sendpulse.generated.crm.model.PaymentOrderPrice import PaymentOrderPrice + + +@dataclass(slots=True) +class Payment: + id: int | None = None + name: str | None = None + price: PaymentOrderPrice | None = None + status: int | None = None + orderId: str | None = None + paymentDescription: str | None = None + paymentMethod: str | None = None + createdAt: str | None = None + promoCode: str | None = None + promoCodeDiscount: float | None = None + + @classmethod + def from_dict(cls, data: dict[str, Any]) -> Payment: + return cls( + id=data.get("id"), + name=data.get("name"), + price=PaymentOrderPrice.from_dict(data["price"]) if isinstance(data.get("price"), dict) else None, + status=data.get("status"), + orderId=data.get("orderId"), + paymentDescription=data.get("paymentDescription"), + paymentMethod=data.get("paymentMethod"), + createdAt=data.get("createdAt"), + promoCode=data.get("promoCode"), + promoCodeDiscount=data.get("promoCodeDiscount"), + ) diff --git a/sendpulse/generated/crm/model/PaymentOrderPrice.py b/sendpulse/generated/crm/model/PaymentOrderPrice.py new file mode 100644 index 0000000..acf9be3 --- /dev/null +++ b/sendpulse/generated/crm/model/PaymentOrderPrice.py @@ -0,0 +1,17 @@ +from __future__ import annotations + +from dataclasses import dataclass +from typing import Any + + +@dataclass(slots=True) +class PaymentOrderPrice: + amount: str | None = None + currency: str | None = None + + @classmethod + def from_dict(cls, data: dict[str, Any]) -> PaymentOrderPrice: + return cls( + amount=data.get("amount"), + currency=data.get("currency"), + ) diff --git a/sendpulse/generated/crm/model/Phone.py b/sendpulse/generated/crm/model/Phone.py new file mode 100644 index 0000000..ab26005 --- /dev/null +++ b/sendpulse/generated/crm/model/Phone.py @@ -0,0 +1,19 @@ +from __future__ import annotations + +from dataclasses import dataclass +from typing import Any + + +@dataclass(slots=True) +class Phone: + id: int | None = None + isMain: bool | None = None + phone: str | None = None + + @classmethod + def from_dict(cls, data: dict[str, Any]) -> Phone: + return cls( + id=data.get("id"), + isMain=data.get("isMain"), + phone=data.get("phone"), + ) diff --git a/sendpulse/generated/crm/model/Pipeline.py b/sendpulse/generated/crm/model/Pipeline.py new file mode 100644 index 0000000..6acf7f6 --- /dev/null +++ b/sendpulse/generated/crm/model/Pipeline.py @@ -0,0 +1,31 @@ +from __future__ import annotations + +from dataclasses import dataclass +from typing import Any + +from sendpulse.generated.crm.model.DefaultStatusProperty import DefaultStatusProperty +from sendpulse.generated.crm.model.Setting import Setting +from sendpulse.generated.crm.model.Step import Step + + +@dataclass(slots=True) +class Pipeline: + id: int | None = None + userId: int | None = None + name: str | None = None + status: DefaultStatusProperty | None = None + order: int | None = None + steps: list[Step] | None = None + settings: list[Setting] | None = None + + @classmethod + def from_dict(cls, data: dict[str, Any]) -> Pipeline: + return cls( + id=data.get("id"), + userId=data.get("userId"), + name=data.get("name"), + status=DefaultStatusProperty.from_dict(data["status"]) if isinstance(data.get("status"), dict) else None, + order=data.get("order"), + steps=[Step.from_dict(i) for i in data["steps"]] if isinstance(data.get("steps"), list) else None, + settings=[Setting.from_dict(i) for i in data["settings"]] if isinstance(data.get("settings"), list) else None, + ) diff --git a/sendpulse/generated/crm/model/Setting.py b/sendpulse/generated/crm/model/Setting.py new file mode 100644 index 0000000..9cd2863 --- /dev/null +++ b/sendpulse/generated/crm/model/Setting.py @@ -0,0 +1,17 @@ +from __future__ import annotations + +from dataclasses import dataclass +from typing import Any + + +@dataclass(slots=True) +class Setting: + name: str | None = None + value: str | None = None + + @classmethod + def from_dict(cls, data: dict[str, Any]) -> Setting: + return cls( + name=data.get("name"), + value=data.get("value"), + ) diff --git a/sendpulse/generated/crm/model/Source.py b/sendpulse/generated/crm/model/Source.py new file mode 100644 index 0000000..1f02ae3 --- /dev/null +++ b/sendpulse/generated/crm/model/Source.py @@ -0,0 +1,17 @@ +from __future__ import annotations + +from dataclasses import dataclass +from typing import Any + + +@dataclass(slots=True) +class Source: + id: int | None = None + name: str | None = None + + @classmethod + def from_dict(cls, data: dict[str, Any]) -> Source: + return cls( + id=data.get("id"), + name=data.get("name"), + ) diff --git a/sendpulse/generated/crm/model/Step.py b/sendpulse/generated/crm/model/Step.py new file mode 100644 index 0000000..615340c --- /dev/null +++ b/sendpulse/generated/crm/model/Step.py @@ -0,0 +1,31 @@ +from __future__ import annotations + +from dataclasses import dataclass +from typing import Any + +from sendpulse.generated.crm.model.DefaultStatusProperty import DefaultStatusProperty + + +@dataclass(slots=True) +class Step: + id: int | None = None + pipelineId: int | None = None + name: str | None = None + order: int | None = None + status: DefaultStatusProperty | None = None + color: str | None = None + addEndHours: int | None = None + notifyIn: str | None = None + + @classmethod + def from_dict(cls, data: dict[str, Any]) -> Step: + return cls( + id=data.get("id"), + pipelineId=data.get("pipelineId"), + name=data.get("name"), + order=data.get("order"), + status=DefaultStatusProperty.from_dict(data["status"]) if isinstance(data.get("status"), dict) else None, + color=data.get("color"), + addEndHours=data.get("addEndHours"), + notifyIn=data.get("notifyIn"), + ) diff --git a/sendpulse/generated/crm/model/TaskAttachment.py b/sendpulse/generated/crm/model/TaskAttachment.py new file mode 100644 index 0000000..a77d5f8 --- /dev/null +++ b/sendpulse/generated/crm/model/TaskAttachment.py @@ -0,0 +1,19 @@ +from __future__ import annotations + +from dataclasses import dataclass +from typing import Any + + +@dataclass(slots=True) +class TaskAttachment: + id: int | None = None + link: str | None = None + taskId: int | None = None + + @classmethod + def from_dict(cls, data: dict[str, Any]) -> TaskAttachment: + return cls( + id=data.get("id"), + link=data.get("link"), + taskId=data.get("taskId"), + ) diff --git a/sendpulse/generated/crm/model/TaskAutocomplete.py b/sendpulse/generated/crm/model/TaskAutocomplete.py new file mode 100644 index 0000000..5571037 --- /dev/null +++ b/sendpulse/generated/crm/model/TaskAutocomplete.py @@ -0,0 +1,13 @@ +from __future__ import annotations + +from dataclasses import dataclass +from typing import Any + + +@dataclass(slots=True) +class TaskAutocomplete: + pass + + @classmethod + def from_dict(cls, data: dict[str, Any]) -> TaskAutocomplete: + return cls() diff --git a/sendpulse/generated/crm/model/TaskChecklist.py b/sendpulse/generated/crm/model/TaskChecklist.py new file mode 100644 index 0000000..f69dd02 --- /dev/null +++ b/sendpulse/generated/crm/model/TaskChecklist.py @@ -0,0 +1,25 @@ +from __future__ import annotations + +from dataclasses import dataclass +from typing import Any + +from sendpulse.generated.crm.model.ChecklistItems import ChecklistItems + + +@dataclass(slots=True) +class TaskChecklist: + id: int | None = None + name: str | None = None + taskId: int | None = None + isDone: bool | None = None + items: ChecklistItems | None = None + + @classmethod + def from_dict(cls, data: dict[str, Any]) -> TaskChecklist: + return cls( + id=data.get("id"), + name=data.get("name"), + taskId=data.get("taskId"), + isDone=data.get("isDone"), + items=ChecklistItems.from_dict(data["items"]) if isinstance(data.get("items"), dict) else None, + ) diff --git a/sendpulse/generated/crm/model/TaskComment.py b/sendpulse/generated/crm/model/TaskComment.py new file mode 100644 index 0000000..4829785 --- /dev/null +++ b/sendpulse/generated/crm/model/TaskComment.py @@ -0,0 +1,35 @@ +from __future__ import annotations + +from dataclasses import dataclass +from typing import Any + +from sendpulse.generated.crm.model.EntityAttachment import EntityAttachment + + +@dataclass(slots=True) +class TaskComment: + id: int | None = None + name: str | None = None + taskId: int | None = None + userId: int | None = None + body: str | None = None + createdAt: str | None = None + updatedAt: str | None = None + attachments: list[EntityAttachment] | None = None + childCount: int | None = None + childUsers: list[Any] | None = None + + @classmethod + def from_dict(cls, data: dict[str, Any]) -> TaskComment: + return cls( + id=data.get("id"), + name=data.get("name"), + taskId=data.get("taskId"), + userId=data.get("userId"), + body=data.get("body"), + createdAt=data.get("createdAt"), + updatedAt=data.get("updatedAt"), + attachments=[EntityAttachment.from_dict(i) for i in data["attachments"]] if isinstance(data.get("attachments"), list) else None, + childCount=data.get("childCount"), + childUsers=data.get("childUsers"), + ) diff --git a/sendpulse/generated/crm/model/TaskHistory.py b/sendpulse/generated/crm/model/TaskHistory.py new file mode 100644 index 0000000..6eab4fd --- /dev/null +++ b/sendpulse/generated/crm/model/TaskHistory.py @@ -0,0 +1,25 @@ +from __future__ import annotations + +from dataclasses import dataclass +from typing import Any + + +@dataclass(slots=True) +class TaskHistory: + id: int | None = None + taskId: int | None = None + eventTime: str | None = None + eventType: str | None = None + eventData: dict[str, Any] | None = None + userId: float | None = None + + @classmethod + def from_dict(cls, data: dict[str, Any]) -> TaskHistory: + return cls( + id=data.get("id"), + taskId=data.get("taskId"), + eventTime=data.get("eventTime"), + eventType=data.get("eventType"), + eventData=data.get("eventData"), + userId=data.get("userId"), + ) diff --git a/sendpulse/generated/crm/model/TaskPriorityProperty.py b/sendpulse/generated/crm/model/TaskPriorityProperty.py new file mode 100644 index 0000000..3e92d38 --- /dev/null +++ b/sendpulse/generated/crm/model/TaskPriorityProperty.py @@ -0,0 +1,13 @@ +from __future__ import annotations + +from dataclasses import dataclass +from typing import Any + + +@dataclass(slots=True) +class TaskPriorityProperty: + pass + + @classmethod + def from_dict(cls, data: dict[str, Any]) -> TaskPriorityProperty: + return cls() diff --git a/sendpulse/generated/crm/model/TaskRepeats.py b/sendpulse/generated/crm/model/TaskRepeats.py new file mode 100644 index 0000000..bd334b7 --- /dev/null +++ b/sendpulse/generated/crm/model/TaskRepeats.py @@ -0,0 +1,21 @@ +from __future__ import annotations + +from dataclasses import dataclass +from typing import Any + + +@dataclass(slots=True) +class TaskRepeats: + type: int | None = None + startDate: str | None = None + endSetting: dict[str, Any] | None = None + setting: dict[str, Any] | None = None + + @classmethod + def from_dict(cls, data: dict[str, Any]) -> TaskRepeats: + return cls( + type=data.get("type"), + startDate=data.get("startDate"), + endSetting=data.get("endSetting"), + setting=data.get("setting"), + ) diff --git a/sendpulse/generated/crm/model/TaskTag.py b/sendpulse/generated/crm/model/TaskTag.py new file mode 100644 index 0000000..5da06ea --- /dev/null +++ b/sendpulse/generated/crm/model/TaskTag.py @@ -0,0 +1,21 @@ +from __future__ import annotations + +from dataclasses import dataclass +from typing import Any + + +@dataclass(slots=True) +class TaskTag: + id: int | None = None + name: str | None = None + colorText: str | None = None + colorBackground: str | None = None + + @classmethod + def from_dict(cls, data: dict[str, Any]) -> TaskTag: + return cls( + id=data.get("id"), + name=data.get("name"), + colorText=data.get("colorText"), + colorBackground=data.get("colorBackground"), + ) diff --git a/sendpulse/generated/crm/model/TaskTotals.py b/sendpulse/generated/crm/model/TaskTotals.py new file mode 100644 index 0000000..140da1d --- /dev/null +++ b/sendpulse/generated/crm/model/TaskTotals.py @@ -0,0 +1,17 @@ +from __future__ import annotations + +from dataclasses import dataclass +from typing import Any + + +@dataclass(slots=True) +class TaskTotals: + total: int | None = None + steps: list[Any] | None = None + + @classmethod + def from_dict(cls, data: dict[str, Any]) -> TaskTotals: + return cls( + total=data.get("total"), + steps=data.get("steps"), + ) diff --git a/sendpulse/generated/crm/model/Tasks.py b/sendpulse/generated/crm/model/Tasks.py new file mode 100644 index 0000000..6f7d4ed --- /dev/null +++ b/sendpulse/generated/crm/model/Tasks.py @@ -0,0 +1,78 @@ +from __future__ import annotations + +from dataclasses import dataclass +from typing import Any + +from sendpulse.generated.crm.model.Attributes import Attributes +from sendpulse.generated.crm.model.EntityAttachment import EntityAttachment +from sendpulse.generated.crm.model.TaskChecklist import TaskChecklist +from sendpulse.generated.crm.model.TaskComment import TaskComment +from sendpulse.generated.crm.model.TaskHistory import TaskHistory +from sendpulse.generated.crm.model.TaskTag import TaskTag + + +@dataclass(slots=True) +class Tasks: + id: int | None = None + name: str | None = None + userId: int | None = None + responsibleId: int | None = None + parentId: int | None = None + boardId: int | None = None + stepId: int | None = None + priority: int | None = None + order: int | None = None + description: int | None = None + alert: str | None = None + repeat: int | None = None + startAt: str | None = None + finishAt: str | None = None + observers: list[Any] | None = None + checklists: TaskChecklist | None = None + attachments: EntityAttachment | None = None + attributes: Attributes | None = None + comments: TaskComment | None = None + histories: TaskHistory | None = None + tags: TaskTag | None = None + deals: list[Any] | None = None + tasks: list[Any] | None = None + contacts: list[Any] | None = None + subTasks: list[Any] | None = None + updatedDaysAt: float | None = None + createdDaysAt: float | None = None + createdAt: str | None = None + updatedAt: str | None = None + + @classmethod + def from_dict(cls, data: dict[str, Any]) -> Tasks: + return cls( + id=data.get("id"), + name=data.get("name"), + userId=data.get("userId"), + responsibleId=data.get("responsibleId"), + parentId=data.get("parentId"), + boardId=data.get("boardId"), + stepId=data.get("stepId"), + priority=data.get("priority"), + order=data.get("order"), + description=data.get("description"), + alert=data.get("alert"), + repeat=data.get("repeat"), + startAt=data.get("startAt"), + finishAt=data.get("finishAt"), + observers=data.get("observers"), + checklists=TaskChecklist.from_dict(data["checklists"]) if isinstance(data.get("checklists"), dict) else None, + attachments=EntityAttachment.from_dict(data["attachments"]) if isinstance(data.get("attachments"), dict) else None, + attributes=Attributes.from_dict(data["attributes"]) if isinstance(data.get("attributes"), dict) else None, + comments=TaskComment.from_dict(data["comments"]) if isinstance(data.get("comments"), dict) else None, + histories=TaskHistory.from_dict(data["histories"]) if isinstance(data.get("histories"), dict) else None, + tags=TaskTag.from_dict(data["tags"]) if isinstance(data.get("tags"), dict) else None, + deals=data.get("deals"), + tasks=data.get("tasks"), + contacts=data.get("contacts"), + subTasks=data.get("subTasks"), + updatedDaysAt=data.get("updatedDaysAt"), + createdDaysAt=data.get("createdDaysAt"), + createdAt=data.get("createdAt"), + updatedAt=data.get("updatedAt"), + ) diff --git a/sendpulse/generated/crm/model/TasksAttribute.py b/sendpulse/generated/crm/model/TasksAttribute.py new file mode 100644 index 0000000..56a582e --- /dev/null +++ b/sendpulse/generated/crm/model/TasksAttribute.py @@ -0,0 +1,13 @@ +from __future__ import annotations + +from dataclasses import dataclass +from typing import Any + + +@dataclass(slots=True) +class TasksAttribute: + pass + + @classmethod + def from_dict(cls, data: dict[str, Any]) -> TasksAttribute: + return cls() diff --git a/sendpulse/generated/crm/model/TasksConnection.py b/sendpulse/generated/crm/model/TasksConnection.py new file mode 100644 index 0000000..c93d544 --- /dev/null +++ b/sendpulse/generated/crm/model/TasksConnection.py @@ -0,0 +1,25 @@ +from __future__ import annotations + +from dataclasses import dataclass +from typing import Any + +from sendpulse.generated.crm.model.ContactsAutocomplete import ContactsAutocomplete +from sendpulse.generated.crm.model.DealsAutocomplete import DealsAutocomplete +from sendpulse.generated.crm.model.TaskAutocomplete import TaskAutocomplete + + +@dataclass(slots=True) +class TasksConnection: + tasks: TaskAutocomplete | None = None + deals: DealsAutocomplete | None = None + contacts: ContactsAutocomplete | None = None + subTasks: TaskAutocomplete | None = None + + @classmethod + def from_dict(cls, data: dict[str, Any]) -> TasksConnection: + return cls( + tasks=TaskAutocomplete.from_dict(data["tasks"]) if isinstance(data.get("tasks"), dict) else None, + deals=DealsAutocomplete.from_dict(data["deals"]) if isinstance(data.get("deals"), dict) else None, + contacts=ContactsAutocomplete.from_dict(data["contacts"]) if isinstance(data.get("contacts"), dict) else None, + subTasks=TaskAutocomplete.from_dict(data["subTasks"]) if isinstance(data.get("subTasks"), dict) else None, + ) diff --git a/sendpulse/generated/crm/model/TelephonyCall.py b/sendpulse/generated/crm/model/TelephonyCall.py new file mode 100644 index 0000000..7b290e0 --- /dev/null +++ b/sendpulse/generated/crm/model/TelephonyCall.py @@ -0,0 +1,37 @@ +from __future__ import annotations + +from dataclasses import dataclass +from typing import Any + + +@dataclass(slots=True) +class TelephonyCall: + id: int | None = None + integrationId: int | None = None + integrationGroupId: int | None = None + responsibleId: int | None = None + phone: str | None = None + callDuration: int | None = None + callType: int | None = None + state: int | None = None + callRecordLink: str | None = None + createdAt: str | None = None + contact: dict[str, Any] | None = None + deal: dict[str, Any] | None = None + + @classmethod + def from_dict(cls, data: dict[str, Any]) -> TelephonyCall: + return cls( + id=data.get("id"), + integrationId=data.get("integrationId"), + integrationGroupId=data.get("integrationGroupId"), + responsibleId=data.get("responsibleId"), + phone=data.get("phone"), + callDuration=data.get("callDuration"), + callType=data.get("callType"), + state=data.get("state"), + callRecordLink=data.get("callRecordLink"), + createdAt=data.get("createdAt"), + contact=data.get("contact"), + deal=data.get("deal"), + ) diff --git a/sendpulse/generated/crm/model/User.py b/sendpulse/generated/crm/model/User.py new file mode 100644 index 0000000..19df442 --- /dev/null +++ b/sendpulse/generated/crm/model/User.py @@ -0,0 +1,29 @@ +from __future__ import annotations + +from dataclasses import dataclass +from typing import Any + + +@dataclass(slots=True) +class User: + id: int | None = None + firstname: str | None = None + lastname: str | None = None + email: str | None = None + avatar: str | None = None + lang: str | None = None + timezone: int | None = None + currency: str | None = None + + @classmethod + def from_dict(cls, data: dict[str, Any]) -> User: + return cls( + id=data.get("id"), + firstname=data.get("firstname"), + lastname=data.get("lastname"), + email=data.get("email"), + avatar=data.get("avatar"), + lang=data.get("lang"), + timezone=data.get("timezone"), + currency=data.get("currency"), + ) diff --git a/sendpulse/generated/crm/model/UserPaymentData.py b/sendpulse/generated/crm/model/UserPaymentData.py new file mode 100644 index 0000000..401ab0a --- /dev/null +++ b/sendpulse/generated/crm/model/UserPaymentData.py @@ -0,0 +1,51 @@ +from __future__ import annotations + +from dataclasses import dataclass +from typing import Any + + +@dataclass(slots=True) +class UserPaymentData: + id: int | None = None + userId: int | None = None + contactId: int | None = None + dealId: int | None = None + status: int | None = None + firstName: str | None = None + lastName: str | None = None + name: str | None = None + responsibleId: int | None = None + price: dict[str, Any] | None = None + description: str | None = None + merchantName: str | None = None + merchantUuid: str | None = None + paymentMethod: str | None = None + promoCode: str | None = None + promoCodeDiscount: str | None = None + createdAt: str | None = None + externalContactId: str | None = None + paymentItems: list[Any] | None = None + + @classmethod + def from_dict(cls, data: dict[str, Any]) -> UserPaymentData: + return cls( + id=data.get("id"), + userId=data.get("userId"), + contactId=data.get("contactId"), + dealId=data.get("dealId"), + status=data.get("status"), + firstName=data.get("firstName"), + lastName=data.get("lastName"), + name=data.get("name"), + responsibleId=data.get("responsibleId"), + price=data.get("price"), + description=data.get("description"), + merchantName=data.get("merchantName"), + merchantUuid=data.get("merchantUuid"), + paymentMethod=data.get("paymentMethod"), + promoCode=data.get("promoCode"), + promoCodeDiscount=data.get("promoCodeDiscount"), + createdAt=data.get("createdAt"), + externalContactId=data.get("externalContactId"), + paymentItems=data.get("paymentItems"), + ) diff --git a/sendpulse/generated/crm/model/__init__.py b/sendpulse/generated/crm/model/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/sendpulse/generated/crm/service/__init__.py b/sendpulse/generated/crm/service/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/sendpulse/generated/crm/service/attachments_resource.py b/sendpulse/generated/crm/service/attachments_resource.py new file mode 100644 index 0000000..8a3f78a --- /dev/null +++ b/sendpulse/generated/crm/service/attachments_resource.py @@ -0,0 +1,36 @@ +from __future__ import annotations + +import json +from typing import Any + +from sendpulse.http.request import Request +from sendpulse.service.abstract import AbstractService + + +class AttachmentsResource(AbstractService): + def create_attachment(self, body: dict[str, Any] | list[Any] | None = None) -> dict[str, Any]: + return self._send(Request( + method="POST", + uri="/attachments", + body=json.dumps(body) if body else None, + )) + + def create_attachments_batch(self, body: dict[str, Any] | list[Any] | None = None) -> dict[str, Any]: + return self._send(Request( + method="POST", + uri="/attachments/batch", + body=json.dumps(body) if body else None, + )) + + def update_attachment(self, attachmentId: int, body: dict[str, Any] | list[Any] | None = None) -> dict[str, Any]: + return self._send(Request( + method="PUT", + uri=f"/attachments/{attachmentId}", + body=json.dumps(body) if body else None, + )) + + def delete_attachment(self, attachmentId: int) -> dict[str, Any]: + return self._send(Request( + method="DELETE", + uri=f"/attachments/{attachmentId}", + )) diff --git a/sendpulse/generated/crm/service/board_attributes_resource.py b/sendpulse/generated/crm/service/board_attributes_resource.py new file mode 100644 index 0000000..7551f60 --- /dev/null +++ b/sendpulse/generated/crm/service/board_attributes_resource.py @@ -0,0 +1,29 @@ +from __future__ import annotations + +import json +from typing import Any + +from sendpulse.http.request import Request +from sendpulse.service.abstract import AbstractService + + +class BoardAttributesResource(AbstractService): + def get_board_attributes(self, boardId: int) -> dict[str, Any]: + return self._send(Request( + method="GET", + uri=f"/boards/{boardId}/attributes", + )) + + def create_board_attribute(self, boardId: int, body: dict[str, Any] | list[Any] | None = None) -> dict[str, Any]: + return self._send(Request( + method="POST", + uri=f"/boards/{boardId}/attributes", + body=json.dumps(body) if body else None, + )) + + def update_board_attribute(self, boardId: int, attributeId: int, body: dict[str, Any] | list[Any] | None = None) -> dict[str, Any]: + return self._send(Request( + method="PUT", + uri=f"/boards/{boardId}/attributes/{attributeId}", + body=json.dumps(body) if body else None, + )) diff --git a/sendpulse/generated/crm/service/checklist_items_resource.py b/sendpulse/generated/crm/service/checklist_items_resource.py new file mode 100644 index 0000000..0b62f9d --- /dev/null +++ b/sendpulse/generated/crm/service/checklist_items_resource.py @@ -0,0 +1,36 @@ +from __future__ import annotations + +import json +from typing import Any + +from sendpulse.http.request import Request +from sendpulse.service.abstract import AbstractService + + +class ChecklistItemsResource(AbstractService): + def create_checklist_items(self, checklistId: int, body: dict[str, Any] | list[Any] | None = None) -> dict[str, Any]: + return self._send(Request( + method="POST", + uri=f"/checklists/{checklistId}/items", + body=json.dumps(body) if body else None, + )) + + def reorder_checklist_item(self, checklistId: int, itemId: int, body: dict[str, Any] | list[Any] | None = None) -> dict[str, Any]: + return self._send(Request( + method="POST", + uri=f"/checklists/{checklistId}/items/{itemId}", + body=json.dumps(body) if body else None, + )) + + def update_checklist_item(self, checklistId: int, itemId: int, body: dict[str, Any] | list[Any] | None = None) -> dict[str, Any]: + return self._send(Request( + method="PUT", + uri=f"/checklists/{checklistId}/items/{itemId}", + body=json.dumps(body) if body else None, + )) + + def delete_checklist_item(self, checklistId: int, itemId: int) -> dict[str, Any]: + return self._send(Request( + method="DELETE", + uri=f"/checklists/{checklistId}/items/{itemId}", + )) diff --git a/sendpulse/generated/crm/service/company_attributes_resource.py b/sendpulse/generated/crm/service/company_attributes_resource.py new file mode 100644 index 0000000..ab13395 --- /dev/null +++ b/sendpulse/generated/crm/service/company_attributes_resource.py @@ -0,0 +1,35 @@ +from __future__ import annotations + +import json +from typing import Any + +from sendpulse.http.request import Request +from sendpulse.service.abstract import AbstractService + + +class CompanyAttributesResource(AbstractService): + def get_company_attributes(self) -> dict[str, Any]: + return self._send(Request( + method="GET", + uri="/companies/attributes", + )) + + def create_company_attribute(self, body: dict[str, Any] | list[Any] | None = None) -> dict[str, Any]: + return self._send(Request( + method="POST", + uri="/companies/attributes", + body=json.dumps(body) if body else None, + )) + + def update_company_attribute(self, attributeId: int, attribute: str, body: dict[str, Any] | list[Any] | None = None) -> dict[str, Any]: + return self._send(Request( + method="PUT", + uri=f"/companies/attributes/{attribute}", + body=json.dumps(body) if body else None, + )) + + def delete_company_attribute(self, attributeId: int, attribute: str) -> dict[str, Any]: + return self._send(Request( + method="DELETE", + uri=f"/companies/attributes/{attribute}", + )) diff --git a/sendpulse/generated/crm/service/company_history_resource.py b/sendpulse/generated/crm/service/company_history_resource.py new file mode 100644 index 0000000..be2aef2 --- /dev/null +++ b/sendpulse/generated/crm/service/company_history_resource.py @@ -0,0 +1,16 @@ +from __future__ import annotations + +from typing import Any + +from sendpulse.http.request import Request +from sendpulse.service.abstract import AbstractService + + +class CompanyHistoryResource(AbstractService): + def get_company_history(self, companyId: str, dateFrom: str | None = None, dateTo: str | None = None, limit: int | None = None, offset: int | None = None) -> dict[str, Any]: + params = {k: v for k, v in {"dateFrom": dateFrom, "dateTo": dateTo, "limit": limit, "offset": offset}.items() if v is not None} + return self._send(Request( + method="GET", + uri=f"/companies/{companyId}/history", + params=params or None, + )) diff --git a/sendpulse/generated/crm/service/company_resource.py b/sendpulse/generated/crm/service/company_resource.py new file mode 100644 index 0000000..10bde26 --- /dev/null +++ b/sendpulse/generated/crm/service/company_resource.py @@ -0,0 +1,49 @@ +from __future__ import annotations + +import json +from typing import Any + +from sendpulse.http.request import Request +from sendpulse.service.abstract import AbstractService + + +class CompanyResource(AbstractService): + def get_companies_short_data(self, body: dict[str, Any] | list[Any] | None = None) -> dict[str, Any]: + return self._send(Request( + method="POST", + uri="/companies/short-data", + body=json.dumps(body) if body else None, + )) + + def get_companies_list(self, body: dict[str, Any] | list[Any] | None = None) -> dict[str, Any]: + return self._send(Request( + method="POST", + uri="/companies/list", + body=json.dumps(body) if body else None, + )) + + def create_company(self, body: dict[str, Any] | list[Any] | None = None) -> dict[str, Any]: + return self._send(Request( + method="POST", + uri="/companies", + body=json.dumps(body) if body else None, + )) + + def get_company_by_id(self, companyId: int) -> dict[str, Any]: + return self._send(Request( + method="GET", + uri=f"/companies/{companyId}", + )) + + def update_company(self, companyId: int, body: dict[str, Any] | list[Any] | None = None) -> dict[str, Any]: + return self._send(Request( + method="PUT", + uri=f"/companies/{companyId}", + body=json.dumps(body) if body else None, + )) + + def delete_company(self, companyId: int) -> dict[str, Any]: + return self._send(Request( + method="DELETE", + uri=f"/companies/{companyId}", + )) diff --git a/sendpulse/generated/crm/service/contact_attributes_resource.py b/sendpulse/generated/crm/service/contact_attributes_resource.py new file mode 100644 index 0000000..d2457c2 --- /dev/null +++ b/sendpulse/generated/crm/service/contact_attributes_resource.py @@ -0,0 +1,35 @@ +from __future__ import annotations + +import json +from typing import Any + +from sendpulse.http.request import Request +from sendpulse.service.abstract import AbstractService + + +class ContactAttributesResource(AbstractService): + def get_contact_attributes(self) -> dict[str, Any]: + return self._send(Request( + method="GET", + uri="/contacts/attributes", + )) + + def create_contact_attribute(self, body: dict[str, Any] | list[Any] | None = None) -> dict[str, Any]: + return self._send(Request( + method="POST", + uri="/contacts/attributes", + body=json.dumps(body) if body else None, + )) + + def update_contact_attribute(self, attributeId: int, body: dict[str, Any] | list[Any] | None = None) -> dict[str, Any]: + return self._send(Request( + method="PUT", + uri=f"/contacts/attributes/{attributeId}", + body=json.dumps(body) if body else None, + )) + + def delete_contact_attribute(self, attributeId: int) -> dict[str, Any]: + return self._send(Request( + method="DELETE", + uri=f"/contacts/attributes/{attributeId}", + )) diff --git a/sendpulse/generated/crm/service/contact_attributes_value_resource.py b/sendpulse/generated/crm/service/contact_attributes_value_resource.py new file mode 100644 index 0000000..822ce1c --- /dev/null +++ b/sendpulse/generated/crm/service/contact_attributes_value_resource.py @@ -0,0 +1,35 @@ +from __future__ import annotations + +import json +from typing import Any + +from sendpulse.http.request import Request +from sendpulse.service.abstract import AbstractService + + +class ContactAttributesValueResource(AbstractService): + def get_contact_attributes_by_id(self, contactId: int) -> dict[str, Any]: + return self._send(Request( + method="GET", + uri=f"/contacts/{contactId}/attributes", + )) + + def add_contact_attribute_value(self, contactId: int, body: dict[str, Any] | list[Any] | None = None) -> dict[str, Any]: + return self._send(Request( + method="POST", + uri=f"/contacts/{contactId}/attributes", + body=json.dumps(body) if body else None, + )) + + def update_contact_attribute_value(self, contactId: int, attributeId: int, body: dict[str, Any] | list[Any] | None = None) -> dict[str, Any]: + return self._send(Request( + method="PUT", + uri=f"/contacts/{contactId}/attributes/{attributeId}", + body=json.dumps(body) if body else None, + )) + + def delete_contact_attribute_value(self, contactId: int, attributeId: int) -> dict[str, Any]: + return self._send(Request( + method="DELETE", + uri=f"/contacts/{contactId}/attributes/{attributeId}", + )) diff --git a/sendpulse/generated/crm/service/contact_attributes_values_resource.py b/sendpulse/generated/crm/service/contact_attributes_values_resource.py new file mode 100644 index 0000000..fa39f12 --- /dev/null +++ b/sendpulse/generated/crm/service/contact_attributes_values_resource.py @@ -0,0 +1,16 @@ +from __future__ import annotations + +import json +from typing import Any + +from sendpulse.http.request import Request +from sendpulse.service.abstract import AbstractService + + +class ContactAttributesValuesResource(AbstractService): + def batch_store_contact_attribute_values(self, contactId: int, body: dict[str, Any] | list[Any] | None = None) -> dict[str, Any]: + return self._send(Request( + method="POST", + uri=f"/contacts/{contactId}/attributes/batch", + body=json.dumps(body) if body else None, + )) diff --git a/sendpulse/generated/crm/service/contact_email_addresses_resource.py b/sendpulse/generated/crm/service/contact_email_addresses_resource.py new file mode 100644 index 0000000..dc541a9 --- /dev/null +++ b/sendpulse/generated/crm/service/contact_email_addresses_resource.py @@ -0,0 +1,29 @@ +from __future__ import annotations + +import json +from typing import Any + +from sendpulse.http.request import Request +from sendpulse.service.abstract import AbstractService + + +class ContactEmailAddressesResource(AbstractService): + def add_contact_emails(self, contactId: int, body: dict[str, Any] | list[Any] | None = None) -> dict[str, Any]: + return self._send(Request( + method="POST", + uri=f"/contacts/{contactId}/emails", + body=json.dumps(body) if body else None, + )) + + def update_contact_email(self, contactId: int, emailId: int, body: dict[str, Any] | list[Any] | None = None) -> dict[str, Any]: + return self._send(Request( + method="PUT", + uri=f"/contacts/{contactId}/emails/{emailId}", + body=json.dumps(body) if body else None, + )) + + def delete_contact_email(self, contactId: int, emailId: int) -> dict[str, Any]: + return self._send(Request( + method="DELETE", + uri=f"/contacts/{contactId}/emails/{emailId}", + )) diff --git a/sendpulse/generated/crm/service/contact_history_resource.py b/sendpulse/generated/crm/service/contact_history_resource.py new file mode 100644 index 0000000..08f0d44 --- /dev/null +++ b/sendpulse/generated/crm/service/contact_history_resource.py @@ -0,0 +1,17 @@ +from __future__ import annotations + +from typing import Any + +from sendpulse.generated.crm.model.ContactHistory import ContactHistory +from sendpulse.http.request import Request +from sendpulse.service.abstract import AbstractService + + +class ContactHistoryResource(AbstractService): + def get_contact_history(self, contactId: int, fromDate: str, toDate: str) -> list[ContactHistory]: + params = {k: v for k, v in {"fromDate": fromDate, "toDate": toDate}.items() if v is not None} + return [ContactHistory.from_dict(i) for i in self._send_list(Request( + method="GET", + uri=f"/contacts/{contactId}/history", + params=params or None, + ))] diff --git a/sendpulse/generated/crm/service/contact_phone_number_resource.py b/sendpulse/generated/crm/service/contact_phone_number_resource.py new file mode 100644 index 0000000..2fd2ca8 --- /dev/null +++ b/sendpulse/generated/crm/service/contact_phone_number_resource.py @@ -0,0 +1,29 @@ +from __future__ import annotations + +import json +from typing import Any + +from sendpulse.http.request import Request +from sendpulse.service.abstract import AbstractService + + +class ContactPhoneNumberResource(AbstractService): + def add_contact_phone(self, contactId: int, body: dict[str, Any] | list[Any] | None = None) -> dict[str, Any]: + return self._send(Request( + method="POST", + uri=f"/contacts/{contactId}/phones", + body=json.dumps(body) if body else None, + )) + + def update_contact_phone(self, contactId: int, phoneId: int, body: dict[str, Any] | list[Any] | None = None) -> dict[str, Any]: + return self._send(Request( + method="PUT", + uri=f"/contacts/{contactId}/phones/{phoneId}", + body=json.dumps(body) if body else None, + )) + + def delete_contact_phone(self, contactId: int, phoneId: int) -> dict[str, Any]: + return self._send(Request( + method="DELETE", + uri=f"/contacts/{contactId}/phones/{phoneId}", + )) diff --git a/sendpulse/generated/crm/service/contact_tags_resource.py b/sendpulse/generated/crm/service/contact_tags_resource.py new file mode 100644 index 0000000..5324fe0 --- /dev/null +++ b/sendpulse/generated/crm/service/contact_tags_resource.py @@ -0,0 +1,49 @@ +from __future__ import annotations + +import json +from typing import Any + +from sendpulse.http.request import Request +from sendpulse.service.abstract import AbstractService + + +class ContactTagsResource(AbstractService): + def add_tag_to_contact(self, tagId: int, contactId: int) -> dict[str, Any]: + return self._send(Request( + method="POST", + uri=f"/contact-tags/{tagId}/contact/{contactId}", + )) + + def delete_contact_tag_from_contact(self, tagId: int, contactId: int) -> dict[str, Any]: + return self._send(Request( + method="DELETE", + uri=f"/contact-tags/{tagId}/contact/{contactId}", + )) + + def list_contact_tags(self, name: str | None = None, search: str | None = None) -> dict[str, Any]: + params = {k: v for k, v in {"name": name, "search": search}.items() if v is not None} + return self._send(Request( + method="GET", + uri="/contact-tags", + params=params or None, + )) + + def create_contact_tag(self, body: dict[str, Any] | list[Any] | None = None) -> dict[str, Any]: + return self._send(Request( + method="POST", + uri="/contact-tags", + body=json.dumps(body) if body else None, + )) + + def update_contact_tag(self, tagId: int, body: dict[str, Any] | list[Any] | None = None) -> dict[str, Any]: + return self._send(Request( + method="PUT", + uri=f"/contact-tags/{tagId}", + body=json.dumps(body) if body else None, + )) + + def delete_contact_tag(self, tagId: int) -> dict[str, Any]: + return self._send(Request( + method="DELETE", + uri=f"/contact-tags/{tagId}", + )) diff --git a/sendpulse/generated/crm/service/contacts_messengers_resource.py b/sendpulse/generated/crm/service/contacts_messengers_resource.py new file mode 100644 index 0000000..d2a3d5c --- /dev/null +++ b/sendpulse/generated/crm/service/contacts_messengers_resource.py @@ -0,0 +1,29 @@ +from __future__ import annotations + +import json +from typing import Any + +from sendpulse.http.request import Request +from sendpulse.service.abstract import AbstractService + + +class ContactsMessengersResource(AbstractService): + def add_contact_messenger(self, contactId: int, body: dict[str, Any] | list[Any] | None = None) -> dict[str, Any]: + return self._send(Request( + method="POST", + uri=f"/contacts/{contactId}/messengers", + body=json.dumps(body) if body else None, + )) + + def update_contact_messenger(self, contactId: int, messengerId: int, body: dict[str, Any] | list[Any] | None = None) -> dict[str, Any]: + return self._send(Request( + method="PUT", + uri=f"/contacts/{contactId}/messengers/{messengerId}", + body=json.dumps(body) if body else None, + )) + + def remove_contact_messenger(self, contactId: int, messengerId: int) -> dict[str, Any]: + return self._send(Request( + method="DELETE", + uri=f"/contacts/{contactId}/messengers/{messengerId}", + )) diff --git a/sendpulse/generated/crm/service/contacts_resource.py b/sendpulse/generated/crm/service/contacts_resource.py new file mode 100644 index 0000000..9f7e939 --- /dev/null +++ b/sendpulse/generated/crm/service/contacts_resource.py @@ -0,0 +1,111 @@ +from __future__ import annotations + +import json +from typing import Any + +from sendpulse.http.request import Request +from sendpulse.service.abstract import AbstractService + + +class ContactsResource(AbstractService): + def get_contacts_list(self, body: dict[str, Any] | list[Any] | None = None) -> dict[str, Any]: + return self._send(Request( + method="POST", + uri="/contacts/get-list", + body=json.dumps(body) if body else None, + )) + + def get_contact_list_by_email(self, body: dict[str, Any] | list[Any] | None = None) -> dict[str, Any]: + return self._send(Request( + method="POST", + uri="/contacts/get-list-by-email", + body=json.dumps(body) if body else None, + )) + + def create_contact(self, body: dict[str, Any] | list[Any] | None = None) -> dict[str, Any]: + return self._send(Request( + method="POST", + uri="/contacts", + body=json.dumps(body) if body else None, + )) + + def get_contact_by_id(self, contactId: int) -> dict[str, Any]: + return self._send(Request( + method="GET", + uri=f"/contacts/{contactId}", + )) + + def update_contact(self, contactId: int, body: dict[str, Any] | list[Any] | None = None) -> dict[str, Any]: + return self._send(Request( + method="PUT", + uri=f"/contacts/{contactId}", + body=json.dumps(body) if body else None, + )) + + def delete_contact_by_id(self, contactId: int) -> dict[str, Any]: + return self._send(Request( + method="DELETE", + uri=f"/contacts/{contactId}", + )) + + def get_contact_deals(self, contactId: int) -> dict[str, Any]: + return self._send(Request( + method="GET", + uri=f"/contacts/{contactId}/deals", + )) + + def add_contact_comment(self, contactId: int, body: dict[str, Any] | list[Any] | None = None) -> dict[str, Any]: + return self._send(Request( + method="POST", + uri=f"/contacts/{contactId}/comments", + body=json.dumps(body) if body else None, + )) + + def update_contact_comment(self, contactId: int, commentId: int, body: dict[str, Any] | list[Any] | None = None) -> dict[str, Any]: + return self._send(Request( + method="PUT", + uri=f"/contacts/{contactId}/comments/{commentId}", + body=json.dumps(body) if body else None, + )) + + def delete_contact_comment(self, contactId: int, commentId: int) -> dict[str, Any]: + return self._send(Request( + method="DELETE", + uri=f"/contacts/{contactId}/comments/{commentId}", + )) + + def get_contact_edu_payments(self, contactId: int) -> dict[str, Any]: + return self._send(Request( + method="GET", + uri=f"/contacts/{contactId}/edu-payments", + )) + + def get_contact_edu_statistic(self, contactId: int) -> dict[str, Any]: + return self._send(Request( + method="GET", + uri=f"/contacts/{contactId}/edu-statistic", + )) + + def get_contact_by_external_id(self, externalContactId: int) -> dict[str, Any]: + return self._send(Request( + method="GET", + uri=f"/contacts/external/{externalContactId}", + )) + + def get_contact_by_messenger_external_id(self, messengerContactId: str) -> dict[str, Any]: + return self._send(Request( + method="GET", + uri=f"/contacts/messenger-external/{messengerContactId}", + )) + + def add_contact_company_relation(self, contactId: int, companyId: int) -> dict[str, Any]: + return self._send(Request( + method="POST", + uri=f"/contacts/{contactId}/relation/{companyId}", + )) + + def delete_contact_company_relation(self, contactId: int, companyId: int) -> dict[str, Any]: + return self._send(Request( + method="DELETE", + uri=f"/contacts/{contactId}/relation/{companyId}", + )) diff --git a/sendpulse/generated/crm/service/crm_service.py b/sendpulse/generated/crm/service/crm_service.py new file mode 100644 index 0000000..180355d --- /dev/null +++ b/sendpulse/generated/crm/service/crm_service.py @@ -0,0 +1,181 @@ +from __future__ import annotations + +from sendpulse.service.abstract import AbstractService +from sendpulse.generated.crm.service.users_resource import UsersResource +from sendpulse.generated.crm.service.pipelines_resource import PipelinesResource +from sendpulse.generated.crm.service.pipeline_steps_resource import PipelineStepsResource +from sendpulse.generated.crm.service.deals_resource import DealsResource +from sendpulse.generated.crm.service.deal_notes_resource import DealNotesResource +from sendpulse.generated.crm.service.deal_contacts_resource import DealContactsResource +from sendpulse.generated.crm.service.deal_attributes_resource import DealAttributesResource +from sendpulse.generated.crm.service.deal_attribute_value_resource import DealAttributeValueResource +from sendpulse.generated.crm.service.messengers_types_resource import MessengersTypesResource +from sendpulse.generated.crm.service.payments_resource import PaymentsResource +from sendpulse.generated.crm.service.contacts_resource import ContactsResource +from sendpulse.generated.crm.service.contact_phone_number_resource import ContactPhoneNumberResource +from sendpulse.generated.crm.service.contact_email_addresses_resource import ContactEmailAddressesResource +from sendpulse.generated.crm.service.contacts_messengers_resource import ContactsMessengersResource +from sendpulse.generated.crm.service.contact_tags_resource import ContactTagsResource +from sendpulse.generated.crm.service.contact_attributes_resource import ContactAttributesResource +from sendpulse.generated.crm.service.contact_attributes_value_resource import ContactAttributesValueResource +from sendpulse.generated.crm.service.contact_attributes_values_resource import ContactAttributesValuesResource +from sendpulse.generated.crm.service.tasks_boards_resource import TasksBoardsResource +from sendpulse.generated.crm.service.board_attributes_resource import BoardAttributesResource +from sendpulse.generated.crm.service.tasks_steps_resource import TasksStepsResource +from sendpulse.generated.crm.service.tasks_resource import TasksResource +from sendpulse.generated.crm.service.task_comments_resource import TaskCommentsResource +from sendpulse.generated.crm.service.task_tags_resource import TaskTagsResource +from sendpulse.generated.crm.service.task_entity_resource import TaskEntityResource +from sendpulse.generated.crm.service.task_checklist_resource import TaskChecklistResource +from sendpulse.generated.crm.service.checklist_items_resource import ChecklistItemsResource +from sendpulse.generated.crm.service.task_attributes_resource import TaskAttributesResource +from sendpulse.generated.crm.service.telephony_resource import TelephonyResource +from sendpulse.generated.crm.service.deal_expiration_resource import DealExpirationResource +from sendpulse.generated.crm.service.file_manager_resource import FileManagerResource +from sendpulse.generated.crm.service.attachments_resource import AttachmentsResource +from sendpulse.generated.crm.service.company_resource import CompanyResource +from sendpulse.generated.crm.service.company_history_resource import CompanyHistoryResource +from sendpulse.generated.crm.service.company_attributes_resource import CompanyAttributesResource +from sendpulse.generated.crm.service.emails_resource import EmailsResource +from sendpulse.generated.crm.service.phones_resource import PhonesResource +from sendpulse.generated.crm.service.messengers_resource import MessengersResource +from sendpulse.generated.crm.service.deal_history_resource import DealHistoryResource +from sendpulse.generated.crm.service.contact_history_resource import ContactHistoryResource +from sendpulse.generated.crm.service.task_history_resource import TaskHistoryResource +from sendpulse.generated.crm.service.e_commerce_product_resource import ECommerceProductResource +from sendpulse.generated.crm.service.manager_settings_resource import ManagerSettingsResource +from sendpulse.generated.crm.service.custom_tab_resource import CustomTabResource + + +class CrmService(AbstractService): + def users(self) -> UsersResource: + return UsersResource(self._client) + + def pipelines(self) -> PipelinesResource: + return PipelinesResource(self._client) + + def pipeline_steps(self) -> PipelineStepsResource: + return PipelineStepsResource(self._client) + + def deals(self) -> DealsResource: + return DealsResource(self._client) + + def deal_notes(self) -> DealNotesResource: + return DealNotesResource(self._client) + + def deal_contacts(self) -> DealContactsResource: + return DealContactsResource(self._client) + + def deal_attributes(self) -> DealAttributesResource: + return DealAttributesResource(self._client) + + def deal_attribute_value(self) -> DealAttributeValueResource: + return DealAttributeValueResource(self._client) + + def messengers_types(self) -> MessengersTypesResource: + return MessengersTypesResource(self._client) + + def payments(self) -> PaymentsResource: + return PaymentsResource(self._client) + + def contacts(self) -> ContactsResource: + return ContactsResource(self._client) + + def contact_phone_number(self) -> ContactPhoneNumberResource: + return ContactPhoneNumberResource(self._client) + + def contact_email_addresses(self) -> ContactEmailAddressesResource: + return ContactEmailAddressesResource(self._client) + + def contacts_messengers(self) -> ContactsMessengersResource: + return ContactsMessengersResource(self._client) + + def contact_tags(self) -> ContactTagsResource: + return ContactTagsResource(self._client) + + def contact_attributes(self) -> ContactAttributesResource: + return ContactAttributesResource(self._client) + + def contact_attributes_value(self) -> ContactAttributesValueResource: + return ContactAttributesValueResource(self._client) + + def contact_attributes_values(self) -> ContactAttributesValuesResource: + return ContactAttributesValuesResource(self._client) + + def tasks_boards(self) -> TasksBoardsResource: + return TasksBoardsResource(self._client) + + def board_attributes(self) -> BoardAttributesResource: + return BoardAttributesResource(self._client) + + def tasks_steps(self) -> TasksStepsResource: + return TasksStepsResource(self._client) + + def tasks(self) -> TasksResource: + return TasksResource(self._client) + + def task_comments(self) -> TaskCommentsResource: + return TaskCommentsResource(self._client) + + def task_tags(self) -> TaskTagsResource: + return TaskTagsResource(self._client) + + def task_entity(self) -> TaskEntityResource: + return TaskEntityResource(self._client) + + def task_checklist(self) -> TaskChecklistResource: + return TaskChecklistResource(self._client) + + def checklist_items(self) -> ChecklistItemsResource: + return ChecklistItemsResource(self._client) + + def task_attributes(self) -> TaskAttributesResource: + return TaskAttributesResource(self._client) + + def telephony(self) -> TelephonyResource: + return TelephonyResource(self._client) + + def deal_expiration(self) -> DealExpirationResource: + return DealExpirationResource(self._client) + + def file_manager(self) -> FileManagerResource: + return FileManagerResource(self._client) + + def attachments(self) -> AttachmentsResource: + return AttachmentsResource(self._client) + + def company(self) -> CompanyResource: + return CompanyResource(self._client) + + def company_history(self) -> CompanyHistoryResource: + return CompanyHistoryResource(self._client) + + def company_attributes(self) -> CompanyAttributesResource: + return CompanyAttributesResource(self._client) + + def emails(self) -> EmailsResource: + return EmailsResource(self._client) + + def phones(self) -> PhonesResource: + return PhonesResource(self._client) + + def messengers(self) -> MessengersResource: + return MessengersResource(self._client) + + def deal_history(self) -> DealHistoryResource: + return DealHistoryResource(self._client) + + def contact_history(self) -> ContactHistoryResource: + return ContactHistoryResource(self._client) + + def task_history(self) -> TaskHistoryResource: + return TaskHistoryResource(self._client) + + def e_commerce_product(self) -> ECommerceProductResource: + return ECommerceProductResource(self._client) + + def manager_settings(self) -> ManagerSettingsResource: + return ManagerSettingsResource(self._client) + + def custom_tab(self) -> CustomTabResource: + return CustomTabResource(self._client) diff --git a/sendpulse/generated/crm/service/custom_tab_resource.py b/sendpulse/generated/crm/service/custom_tab_resource.py new file mode 100644 index 0000000..f7d7399 --- /dev/null +++ b/sendpulse/generated/crm/service/custom_tab_resource.py @@ -0,0 +1,51 @@ +from __future__ import annotations + +import json +from typing import Any + +from sendpulse.generated.crm.model.CustomTab import CustomTab +from sendpulse.http.request import Request +from sendpulse.service.abstract import AbstractService + + +class CustomTabResource(AbstractService): + def get_custom_tabs(self) -> dict[str, Any]: + return self._send(Request( + method="GET", + uri="/custom-tab", + )) + + def create_custom_tab(self, body: dict[str, Any] | list[Any] | None = None) -> dict[str, Any]: + return self._send(Request( + method="POST", + uri="/custom-tab", + body=json.dumps(body) if body else None, + )) + + def update_custom_tab(self, customTabId: float, body: dict[str, Any] | list[Any] | None = None) -> CustomTab: + return CustomTab.from_dict(self._send(Request( + method="PUT", + uri=f"/custom-tab/{customTabId}", + body=json.dumps(body) if body else None, + ))) + + def delete_custom_tab(self, customTabId: float, body: dict[str, Any] | list[Any] | None = None) -> dict[str, Any]: + return self._send(Request( + method="DELETE", + uri=f"/custom-tab/{customTabId}", + body=json.dumps(body) if body else None, + )) + + def add_custom_tab_relation(self, customTabId: float, body: dict[str, Any] | list[Any] | None = None) -> dict[str, Any]: + return self._send(Request( + method="POST", + uri=f"/custom-tab/{customTabId}/relation", + body=json.dumps(body) if body else None, + )) + + def delete_custom_tab_relation(self, customTabId: float, body: dict[str, Any] | list[Any] | None = None) -> dict[str, Any]: + return self._send(Request( + method="DELETE", + uri=f"/custom-tab/{customTabId}/relation", + body=json.dumps(body) if body else None, + )) diff --git a/sendpulse/generated/crm/service/deal_attribute_value_resource.py b/sendpulse/generated/crm/service/deal_attribute_value_resource.py new file mode 100644 index 0000000..5b71e95 --- /dev/null +++ b/sendpulse/generated/crm/service/deal_attribute_value_resource.py @@ -0,0 +1,35 @@ +from __future__ import annotations + +import json +from typing import Any + +from sendpulse.http.request import Request +from sendpulse.service.abstract import AbstractService + + +class DealAttributeValueResource(AbstractService): + def get_deal_attribute_values(self, dealId: int) -> dict[str, Any]: + return self._send(Request( + method="GET", + uri=f"/deals/{dealId}/attributes", + )) + + def add_deal_attribute(self, dealId: int, body: dict[str, Any] | list[Any] | None = None) -> dict[str, Any]: + return self._send(Request( + method="POST", + uri=f"/deals/{dealId}/attributes", + body=json.dumps(body) if body else None, + )) + + def list_deal_attributes_by_pipeline(self, pipelineId: int) -> dict[str, Any]: + return self._send(Request( + method="GET", + uri=f"/deals/attributes/{pipelineId}", + )) + + def update_deal_attribute_value(self, dealId: int, attributeId: int, body: dict[str, Any] | list[Any] | None = None) -> dict[str, Any]: + return self._send(Request( + method="PUT", + uri=f"/deals/{dealId}/attributes/{attributeId}", + body=json.dumps(body) if body else None, + )) diff --git a/sendpulse/generated/crm/service/deal_attributes_resource.py b/sendpulse/generated/crm/service/deal_attributes_resource.py new file mode 100644 index 0000000..3a388a9 --- /dev/null +++ b/sendpulse/generated/crm/service/deal_attributes_resource.py @@ -0,0 +1,41 @@ +from __future__ import annotations + +import json +from typing import Any + +from sendpulse.http.request import Request +from sendpulse.service.abstract import AbstractService + + +class DealAttributesResource(AbstractService): + def list_pipeline_attributes(self, pipelineId: int) -> dict[str, Any]: + return self._send(Request( + method="GET", + uri=f"/pipelines/{pipelineId}/attributes", + )) + + def create_pipeline_attribute(self, pipelineId: int, body: dict[str, Any] | list[Any] | None = None) -> dict[str, Any]: + return self._send(Request( + method="POST", + uri=f"/pipelines/{pipelineId}/attributes", + body=json.dumps(body) if body else None, + )) + + def update_pipeline_attribute(self, pipelineId: int, attributeId: int, body: dict[str, Any] | list[Any] | None = None) -> dict[str, Any]: + return self._send(Request( + method="PUT", + uri=f"/pipelines/{pipelineId}/attributes/{attributeId}", + body=json.dumps(body) if body else None, + )) + + def delete_pipeline_attribute(self, pipelineId: int, attributeId: int) -> dict[str, Any]: + return self._send(Request( + method="DELETE", + uri=f"/pipelines/{pipelineId}/attributes/{attributeId}", + )) + + def delete_deal_attribute(self, dealId: int, attributeId: int) -> dict[str, Any]: + return self._send(Request( + method="DELETE", + uri=f"/deals/{dealId}/attributes/{attributeId}", + )) diff --git a/sendpulse/generated/crm/service/deal_contacts_resource.py b/sendpulse/generated/crm/service/deal_contacts_resource.py new file mode 100644 index 0000000..9c5b4d9 --- /dev/null +++ b/sendpulse/generated/crm/service/deal_contacts_resource.py @@ -0,0 +1,26 @@ +from __future__ import annotations + +from typing import Any + +from sendpulse.http.request import Request +from sendpulse.service.abstract import AbstractService + + +class DealContactsResource(AbstractService): + def get_deal_contacts(self, dealId: int) -> dict[str, Any]: + return self._send(Request( + method="GET", + uri=f"/deals/{dealId}/contacts", + )) + + def add_contact_to_deal(self, dealId: int, contactId: int) -> dict[str, Any]: + return self._send(Request( + method="POST", + uri=f"/deals/{dealId}/contacts/{contactId}", + )) + + def remove_deal_contact(self, dealId: int, contactId: int) -> dict[str, Any]: + return self._send(Request( + method="DELETE", + uri=f"/deals/{dealId}/contacts/{contactId}", + )) diff --git a/sendpulse/generated/crm/service/deal_expiration_resource.py b/sendpulse/generated/crm/service/deal_expiration_resource.py new file mode 100644 index 0000000..631af7a --- /dev/null +++ b/sendpulse/generated/crm/service/deal_expiration_resource.py @@ -0,0 +1,22 @@ +from __future__ import annotations + +import json +from typing import Any + +from sendpulse.http.request import Request +from sendpulse.service.abstract import AbstractService + + +class DealExpirationResource(AbstractService): + def upsert_deal_expiration(self, dealId: int, body: dict[str, Any] | list[Any] | None = None) -> dict[str, Any]: + return self._send(Request( + method="PUT", + uri=f"/deals/{dealId}/expiration", + body=json.dumps(body) if body else None, + )) + + def remove_deal_expiration(self, dealId: int) -> dict[str, Any]: + return self._send(Request( + method="DELETE", + uri=f"/deals/{dealId}/expiration", + )) diff --git a/sendpulse/generated/crm/service/deal_history_resource.py b/sendpulse/generated/crm/service/deal_history_resource.py new file mode 100644 index 0000000..b24d3d1 --- /dev/null +++ b/sendpulse/generated/crm/service/deal_history_resource.py @@ -0,0 +1,16 @@ +from __future__ import annotations + +from typing import Any + +from sendpulse.http.request import Request +from sendpulse.service.abstract import AbstractService + + +class DealHistoryResource(AbstractService): + def get_deal_history(self, dealId: int, fromDate: str, toDate: str) -> dict[str, Any]: + params = {k: v for k, v in {"fromDate": fromDate, "toDate": toDate}.items() if v is not None} + return self._send(Request( + method="GET", + uri=f"/deals/{dealId}/history", + params=params or None, + )) diff --git a/sendpulse/generated/crm/service/deal_notes_resource.py b/sendpulse/generated/crm/service/deal_notes_resource.py new file mode 100644 index 0000000..25e1758 --- /dev/null +++ b/sendpulse/generated/crm/service/deal_notes_resource.py @@ -0,0 +1,35 @@ +from __future__ import annotations + +import json +from typing import Any + +from sendpulse.http.request import Request +from sendpulse.service.abstract import AbstractService + + +class DealNotesResource(AbstractService): + def get_deal_comments(self, dealId: int) -> dict[str, Any]: + return self._send(Request( + method="GET", + uri=f"/deals/{dealId}/comments", + )) + + def add_deal_comment(self, dealId: int, body: dict[str, Any] | list[Any] | None = None) -> dict[str, Any]: + return self._send(Request( + method="POST", + uri=f"/deals/{dealId}/comments", + body=json.dumps(body) if body else None, + )) + + def update_deal_comment(self, dealId: int, commentId: int, body: dict[str, Any] | list[Any] | None = None) -> dict[str, Any]: + return self._send(Request( + method="PUT", + uri=f"/deals/{dealId}/comments/{commentId}", + body=json.dumps(body) if body else None, + )) + + def delete_deal_comment(self, dealId: int, commentId: int) -> dict[str, Any]: + return self._send(Request( + method="DELETE", + uri=f"/deals/{dealId}/comments/{commentId}", + )) diff --git a/sendpulse/generated/crm/service/deals_resource.py b/sendpulse/generated/crm/service/deals_resource.py new file mode 100644 index 0000000..a3b1c15 --- /dev/null +++ b/sendpulse/generated/crm/service/deals_resource.py @@ -0,0 +1,49 @@ +from __future__ import annotations + +import json +from typing import Any + +from sendpulse.http.request import Request +from sendpulse.service.abstract import AbstractService + + +class DealsResource(AbstractService): + def get_deals_list(self, body: dict[str, Any] | list[Any] | None = None) -> dict[str, Any]: + return self._send(Request( + method="POST", + uri="/deals/get-list", + body=json.dumps(body) if body else None, + )) + + def create_deal(self, body: dict[str, Any] | list[Any] | None = None) -> dict[str, Any]: + return self._send(Request( + method="POST", + uri="/deals", + body=json.dumps(body) if body else None, + )) + + def get_deal(self, dealId: int) -> dict[str, Any]: + return self._send(Request( + method="GET", + uri=f"/deals/{dealId}", + )) + + def update_deal_by_id(self, dealId: int, body: dict[str, Any] | list[Any] | None = None) -> dict[str, Any]: + return self._send(Request( + method="PUT", + uri=f"/deals/{dealId}", + body=json.dumps(body) if body else None, + )) + + def delete_deal(self, dealId: int) -> dict[str, Any]: + return self._send(Request( + method="DELETE", + uri=f"/deals/{dealId}", + )) + + def change_deal_pipeline(self, dealId: int, body: dict[str, Any] | list[Any] | None = None) -> dict[str, Any]: + return self._send(Request( + method="POST", + uri=f"/deals/{dealId}/change-pipeline", + body=json.dumps(body) if body else None, + )) diff --git a/sendpulse/generated/crm/service/e_commerce_product_resource.py b/sendpulse/generated/crm/service/e_commerce_product_resource.py new file mode 100644 index 0000000..f6f28d8 --- /dev/null +++ b/sendpulse/generated/crm/service/e_commerce_product_resource.py @@ -0,0 +1,133 @@ +from __future__ import annotations + +import json +from typing import Any + +from sendpulse.http.request import Request +from sendpulse.service.abstract import AbstractService + + +class ECommerceProductResource(AbstractService): + def get_products_by_filter(self, body: dict[str, Any] | list[Any] | None = None) -> dict[str, Any]: + return self._send(Request( + method="POST", + uri="/products/all", + body=json.dumps(body) if body else None, + )) + + def get_category_product(self, productId: float, categoryId: float) -> dict[str, Any]: + return self._send(Request( + method="GET", + uri=f"/products/categories/{categoryId}/{productId}", + )) + + def add_product_to_deal(self, body: dict[str, Any] | list[Any] | None = None) -> dict[str, Any]: + return self._send(Request( + method="POST", + uri="/products/deals", + body=json.dumps(body) if body else None, + )) + + def get_products_by_deal_id(self, dealId: float) -> dict[str, Any]: + return self._send(Request( + method="GET", + uri=f"/products/deals/{dealId}", + )) + + def update_products_in_deal(self, headId: int, body: dict[str, Any] | list[Any] | None = None) -> dict[str, Any]: + return self._send(Request( + method="PUT", + uri=f"/products/deals/{headId}", + body=json.dumps(body) if body else None, + )) + + def get_products_by_contact_deals(self, contactId: float) -> dict[str, Any]: + return self._send(Request( + method="GET", + uri=f"/products/contacts/{contactId}/deals", + )) + + def detach_product_from_deal(self, categoryId: float, productId: float, headId: float) -> dict[str, Any]: + return self._send(Request( + method="DELETE", + uri=f"/products/categories/{categoryId}/{productId}/deals/{headId}", + )) + + def get_product_by_id(self, productId: float) -> dict[str, Any]: + return self._send(Request( + method="GET", + uri=f"/products/{productId}", + )) + + def update_product(self, productId: float, body: dict[str, Any] | list[Any] | None = None) -> dict[str, Any]: + return self._send(Request( + method="PUT", + uri=f"/products/{productId}", + body=json.dumps(body) if body else None, + )) + + def delete_product(self, productId: float) -> dict[str, Any]: + return self._send(Request( + method="DELETE", + uri=f"/products/{productId}", + )) + + def update_product_category(self, categoryId: float, body: dict[str, Any] | list[Any] | None = None) -> dict[str, Any]: + return self._send(Request( + method="PUT", + uri=f"/products/categories/{categoryId}", + body=json.dumps(body) if body else None, + )) + + def delete_product_category(self, categoryId: float) -> dict[str, Any]: + return self._send(Request( + method="DELETE", + uri=f"/products/categories/{categoryId}", + )) + + def get_product_categories(self) -> dict[str, Any]: + return self._send(Request( + method="GET", + uri="/products/categories", + )) + + def create_product_category(self, body: dict[str, Any] | list[Any] | None = None) -> dict[str, Any]: + return self._send(Request( + method="POST", + uri="/products/categories", + body=json.dumps(body) if body else None, + )) + + def update_product_prices(self, productId: float, body: dict[str, Any] | list[Any] | None = None) -> dict[str, Any]: + return self._send(Request( + method="PUT", + uri=f"/products/{productId}/prices", + body=json.dumps(body) if body else None, + )) + + def create_product_sections(self, productId: float, body: dict[str, Any] | list[Any] | None = None) -> dict[str, Any]: + return self._send(Request( + method="POST", + uri=f"/products/{productId}/sections", + body=json.dumps(body) if body else None, + )) + + def update_product_sections(self, productId: float, body: dict[str, Any] | list[Any] | None = None) -> dict[str, Any]: + return self._send(Request( + method="PUT", + uri=f"/products/{productId}/sections", + body=json.dumps(body) if body else None, + )) + + def delete_product_section(self, productId: float, sectionId: float) -> dict[str, Any]: + return self._send(Request( + method="DELETE", + uri=f"/products/{productId}/sections/{sectionId}", + )) + + def create_product(self, body: dict[str, Any] | list[Any] | None = None) -> dict[str, Any]: + return self._send(Request( + method="POST", + uri="/products", + body=json.dumps(body) if body else None, + )) diff --git a/sendpulse/generated/crm/service/emails_resource.py b/sendpulse/generated/crm/service/emails_resource.py new file mode 100644 index 0000000..b9370b9 --- /dev/null +++ b/sendpulse/generated/crm/service/emails_resource.py @@ -0,0 +1,42 @@ +from __future__ import annotations + +import json +from typing import Any + +from sendpulse.http.request import Request +from sendpulse.service.abstract import AbstractService + + +class EmailsResource(AbstractService): + def get_company_emails(self, entityType: str, entityId: int, companyId: str) -> dict[str, Any]: + return self._send(Request( + method="GET", + uri=f"/companies/{companyId}/emails", + )) + + def create_company_email(self, entityType: str, entityId: int, companyId: str, body: dict[str, Any] | list[Any] | None = None) -> dict[str, Any]: + return self._send(Request( + method="POST", + uri=f"/companies/{companyId}/emails", + body=json.dumps(body) if body else None, + )) + + def batch_create_company_emails(self, companyId: int, body: dict[str, Any] | list[Any] | None = None) -> dict[str, Any]: + return self._send(Request( + method="POST", + uri=f"/companies/{companyId}/emails/batch", + body=json.dumps(body) if body else None, + )) + + def update_company_email(self, companyId: int, emailId: int, body: dict[str, Any] | list[Any] | None = None) -> dict[str, Any]: + return self._send(Request( + method="PUT", + uri=f"/companies/{companyId}/emails/{emailId}", + body=json.dumps(body) if body else None, + )) + + def delete_company_email(self, companyId: int, emailId: int) -> dict[str, Any]: + return self._send(Request( + method="DELETE", + uri=f"/companies/{companyId}/emails/{emailId}", + )) diff --git a/sendpulse/generated/crm/service/file_manager_resource.py b/sendpulse/generated/crm/service/file_manager_resource.py new file mode 100644 index 0000000..0a86a51 --- /dev/null +++ b/sendpulse/generated/crm/service/file_manager_resource.py @@ -0,0 +1,16 @@ +from __future__ import annotations + +import json +from typing import Any + +from sendpulse.http.request import Request +from sendpulse.service.abstract import AbstractService + + +class FileManagerResource(AbstractService): + def upload_files(self, body: dict[str, Any] | list[Any] | None = None) -> dict[str, Any]: + return self._send(Request( + method="POST", + uri="/files/upload", + body=json.dumps(body) if body else None, + )) diff --git a/sendpulse/generated/crm/service/manager_settings_resource.py b/sendpulse/generated/crm/service/manager_settings_resource.py new file mode 100644 index 0000000..c3ed178 --- /dev/null +++ b/sendpulse/generated/crm/service/manager_settings_resource.py @@ -0,0 +1,48 @@ +from __future__ import annotations + +import json +from typing import Any + +from sendpulse.http.request import Request +from sendpulse.service.abstract import AbstractService + + +class ManagerSettingsResource(AbstractService): + def list_manager_settings_sections(self) -> dict[str, Any]: + return self._send(Request( + method="GET", + uri="/manager-settings/sections", + )) + + def get_manager_settings_managers(self) -> dict[str, Any]: + return self._send(Request( + method="GET", + uri="/manager-settings/managers", + )) + + def create_manager_settings(self, body: dict[str, Any] | list[Any] | None = None) -> dict[str, Any]: + return self._send(Request( + method="POST", + uri="/manager-settings", + body=json.dumps(body) if body else None, + )) + + def update_manager_setting(self, settingId: int, body: dict[str, Any] | list[Any] | None = None) -> dict[str, Any]: + return self._send(Request( + method="PUT", + uri=f"/manager-settings/{settingId}", + body=json.dumps(body) if body else None, + )) + + def delete_managers_from_manager_setting(self, settingId: int, body: dict[str, Any] | list[Any] | None = None) -> dict[str, Any]: + return self._send(Request( + method="DELETE", + uri=f"/manager-settings/{settingId}", + body=json.dumps(body) if body else None, + )) + + def get_managers_by_section(self, sectionId: int) -> dict[str, Any]: + return self._send(Request( + method="GET", + uri=f"/manager-settings/{sectionId}", + )) diff --git a/sendpulse/generated/crm/service/messengers_resource.py b/sendpulse/generated/crm/service/messengers_resource.py new file mode 100644 index 0000000..b9b49a9 --- /dev/null +++ b/sendpulse/generated/crm/service/messengers_resource.py @@ -0,0 +1,42 @@ +from __future__ import annotations + +import json +from typing import Any + +from sendpulse.http.request import Request +from sendpulse.service.abstract import AbstractService + + +class MessengersResource(AbstractService): + def get_company_messengers(self, companyId: int) -> dict[str, Any]: + return self._send(Request( + method="GET", + uri=f"/companies/{companyId}/messengers", + )) + + def create_company_messenger(self, entityType: str, entityId: int, companyId: str, body: dict[str, Any] | list[Any] | None = None) -> dict[str, Any]: + return self._send(Request( + method="POST", + uri=f"/companies/{companyId}/messengers", + body=json.dumps(body) if body else None, + )) + + def batch_create_company_messengers(self, companyId: int, body: dict[str, Any] | list[Any] | None = None) -> dict[str, Any]: + return self._send(Request( + method="POST", + uri=f"/companies/{companyId}/messengers/batch", + body=json.dumps(body) if body else None, + )) + + def update_company_messenger(self, entityType: str, entityId: int, messengerId: int, companyId: str, body: dict[str, Any] | list[Any] | None = None) -> dict[str, Any]: + return self._send(Request( + method="PUT", + uri=f"/companies/{companyId}/messengers/{messengerId}", + body=json.dumps(body) if body else None, + )) + + def delete_company_messenger(self, entityType: str, entityId: int, messengerId: int, companyId: str) -> dict[str, Any]: + return self._send(Request( + method="DELETE", + uri=f"/companies/{companyId}/messengers/{messengerId}", + )) diff --git a/sendpulse/generated/crm/service/messengers_types_resource.py b/sendpulse/generated/crm/service/messengers_types_resource.py new file mode 100644 index 0000000..a8dc83b --- /dev/null +++ b/sendpulse/generated/crm/service/messengers_types_resource.py @@ -0,0 +1,14 @@ +from __future__ import annotations + +from typing import Any + +from sendpulse.http.request import Request +from sendpulse.service.abstract import AbstractService + + +class MessengersTypesResource(AbstractService): + def get_messenger_types(self) -> dict[str, Any]: + return self._send(Request( + method="GET", + uri="/messenger-types", + )) diff --git a/sendpulse/generated/crm/service/payments_resource.py b/sendpulse/generated/crm/service/payments_resource.py new file mode 100644 index 0000000..030cb5f --- /dev/null +++ b/sendpulse/generated/crm/service/payments_resource.py @@ -0,0 +1,46 @@ +from __future__ import annotations + +import json +from typing import Any + +from sendpulse.http.request import Request +from sendpulse.service.abstract import AbstractService + + +class PaymentsResource(AbstractService): + def get_all_payments(self) -> dict[str, Any]: + return self._send(Request( + method="GET", + uri="/payments/all", + )) + + def get_deal_payments(self, dealId: str) -> dict[str, Any]: + return self._send(Request( + method="GET", + uri=f"/payments/deals/{dealId}", + )) + + def get_payments_by_contact_id(self, contactId: str) -> dict[str, Any]: + return self._send(Request( + method="GET", + uri=f"/payments/contacts/{contactId}", + )) + + def create_payment(self, body: dict[str, Any] | list[Any] | None = None) -> dict[str, Any]: + return self._send(Request( + method="POST", + uri="/payments", + body=json.dumps(body) if body else None, + )) + + def approve_payment(self, paymentId: float) -> dict[str, Any]: + return self._send(Request( + method="POST", + uri=f"/payments/{paymentId}/approve", + )) + + def cancel_payment(self, paymentId: float) -> dict[str, Any]: + return self._send(Request( + method="POST", + uri=f"/payments/{paymentId}/cancel", + )) diff --git a/sendpulse/generated/crm/service/phones_resource.py b/sendpulse/generated/crm/service/phones_resource.py new file mode 100644 index 0000000..3a38589 --- /dev/null +++ b/sendpulse/generated/crm/service/phones_resource.py @@ -0,0 +1,42 @@ +from __future__ import annotations + +import json +from typing import Any + +from sendpulse.http.request import Request +from sendpulse.service.abstract import AbstractService + + +class PhonesResource(AbstractService): + def get_company_phones(self, companyId: int) -> dict[str, Any]: + return self._send(Request( + method="GET", + uri=f"/companies/{companyId}/phones", + )) + + def create_company_phone(self, companyId: int, body: dict[str, Any] | list[Any] | None = None) -> dict[str, Any]: + return self._send(Request( + method="POST", + uri=f"/companies/{companyId}/phones", + body=json.dumps(body) if body else None, + )) + + def batch_create_company_phones(self, companyId: int, body: dict[str, Any] | list[Any] | None = None) -> dict[str, Any]: + return self._send(Request( + method="POST", + uri=f"/companies/{companyId}/phones/batch", + body=json.dumps(body) if body else None, + )) + + def update_company_phone(self, entityType: str, entityId: int, phoneId: int, companyId: str, body: dict[str, Any] | list[Any] | None = None) -> dict[str, Any]: + return self._send(Request( + method="PUT", + uri=f"/companies/{companyId}/phones/{phoneId}", + body=json.dumps(body) if body else None, + )) + + def delete_company_phone(self, entityType: str, entityId: int, phoneId: int, companyId: str) -> dict[str, Any]: + return self._send(Request( + method="DELETE", + uri=f"/companies/{companyId}/phones/{phoneId}", + )) diff --git a/sendpulse/generated/crm/service/pipeline_steps_resource.py b/sendpulse/generated/crm/service/pipeline_steps_resource.py new file mode 100644 index 0000000..d36e6a4 --- /dev/null +++ b/sendpulse/generated/crm/service/pipeline_steps_resource.py @@ -0,0 +1,35 @@ +from __future__ import annotations + +import json +from typing import Any + +from sendpulse.http.request import Request +from sendpulse.service.abstract import AbstractService + + +class PipelineStepsResource(AbstractService): + def get_pipeline_steps(self, pipelineId: int) -> dict[str, Any]: + return self._send(Request( + method="GET", + uri=f"/pipelines/{pipelineId}/steps", + )) + + def create_pipeline_step(self, pipelineId: int, body: dict[str, Any] | list[Any] | None = None) -> dict[str, Any]: + return self._send(Request( + method="POST", + uri=f"/pipelines/{pipelineId}/steps", + body=json.dumps(body) if body else None, + )) + + def update_pipeline_step(self, pipelineId: int, stepId: int, body: dict[str, Any] | list[Any] | None = None) -> dict[str, Any]: + return self._send(Request( + method="PUT", + uri=f"/pipelines/{pipelineId}/steps/{stepId}", + body=json.dumps(body) if body else None, + )) + + def delete_pipeline_step(self, pipelineId: int, stepId: int) -> dict[str, Any]: + return self._send(Request( + method="DELETE", + uri=f"/pipelines/{pipelineId}/steps/{stepId}", + )) diff --git a/sendpulse/generated/crm/service/pipelines_resource.py b/sendpulse/generated/crm/service/pipelines_resource.py new file mode 100644 index 0000000..9bbe077 --- /dev/null +++ b/sendpulse/generated/crm/service/pipelines_resource.py @@ -0,0 +1,35 @@ +from __future__ import annotations + +import json +from typing import Any + +from sendpulse.http.request import Request +from sendpulse.service.abstract import AbstractService + + +class PipelinesResource(AbstractService): + def get_pipelines(self) -> dict[str, Any]: + return self._send(Request( + method="GET", + uri="/pipelines", + )) + + def create_pipeline(self, body: dict[str, Any] | list[Any] | None = None) -> dict[str, Any]: + return self._send(Request( + method="POST", + uri="/pipelines", + body=json.dumps(body) if body else None, + )) + + def get_pipeline_by_id(self, pipelineId: int) -> dict[str, Any]: + return self._send(Request( + method="GET", + uri=f"/pipelines/{pipelineId}", + )) + + def update_pipeline(self, pipelineId: int, body: dict[str, Any] | list[Any] | None = None) -> dict[str, Any]: + return self._send(Request( + method="PUT", + uri=f"/pipelines/{pipelineId}", + body=json.dumps(body) if body else None, + )) diff --git a/sendpulse/generated/crm/service/task_attributes_resource.py b/sendpulse/generated/crm/service/task_attributes_resource.py new file mode 100644 index 0000000..a1ab695 --- /dev/null +++ b/sendpulse/generated/crm/service/task_attributes_resource.py @@ -0,0 +1,29 @@ +from __future__ import annotations + +import json +from typing import Any + +from sendpulse.http.request import Request +from sendpulse.service.abstract import AbstractService + + +class TaskAttributesResource(AbstractService): + def create_task_attribute_value(self, taskId: int, body: dict[str, Any] | list[Any] | None = None) -> dict[str, Any]: + return self._send(Request( + method="POST", + uri=f"/tasks/{taskId}/attributes", + body=json.dumps(body) if body else None, + )) + + def update_task_attribute_value(self, attributeId: int, valueId: int, boardId: str, body: dict[str, Any] | list[Any] | None = None) -> dict[str, Any]: + return self._send(Request( + method="PUT", + uri=f"/{boardId}/attributes/{attributeId}/values/{valueId}", + body=json.dumps(body) if body else None, + )) + + def delete_attribute_value(self, attributeId: int, valueId: int, boardId: str) -> dict[str, Any]: + return self._send(Request( + method="DELETE", + uri=f"/{boardId}/attributes/{attributeId}/values/{valueId}", + )) diff --git a/sendpulse/generated/crm/service/task_checklist_resource.py b/sendpulse/generated/crm/service/task_checklist_resource.py new file mode 100644 index 0000000..634a849 --- /dev/null +++ b/sendpulse/generated/crm/service/task_checklist_resource.py @@ -0,0 +1,49 @@ +from __future__ import annotations + +import json +from typing import Any + +from sendpulse.http.request import Request +from sendpulse.service.abstract import AbstractService + + +class TaskChecklistResource(AbstractService): + def create_task_checklist(self, taskId: int, body: dict[str, Any] | list[Any] | None = None) -> dict[str, Any]: + return self._send(Request( + method="POST", + uri=f"/tasks/{taskId}/checklists/single", + body=json.dumps(body) if body else None, + )) + + def update_task_checklist_single(self, taskId: int, checklistId: int, body: dict[str, Any] | list[Any] | None = None) -> dict[str, Any]: + return self._send(Request( + method="PUT", + uri=f"/tasks/{taskId}/checklists/{checklistId}/single", + body=json.dumps(body) if body else None, + )) + + def get_task_checklists(self, taskId: int) -> dict[str, Any]: + return self._send(Request( + method="GET", + uri=f"/tasks/{taskId}/checklists", + )) + + def create_task_checklists(self, taskId: int, body: dict[str, Any] | list[Any] | None = None) -> dict[str, Any]: + return self._send(Request( + method="POST", + uri=f"/tasks/{taskId}/checklists", + body=json.dumps(body) if body else None, + )) + + def update_task_checklists(self, taskId: int, body: dict[str, Any] | list[Any] | None = None) -> dict[str, Any]: + return self._send(Request( + method="PUT", + uri=f"/tasks/{taskId}/checklists", + body=json.dumps(body) if body else None, + )) + + def delete_task_checklist(self, taskId: int, checklistId: int) -> dict[str, Any]: + return self._send(Request( + method="DELETE", + uri=f"/tasks/{taskId}/checklists/{checklistId}", + )) diff --git a/sendpulse/generated/crm/service/task_comments_resource.py b/sendpulse/generated/crm/service/task_comments_resource.py new file mode 100644 index 0000000..f8aac73 --- /dev/null +++ b/sendpulse/generated/crm/service/task_comments_resource.py @@ -0,0 +1,23 @@ +from __future__ import annotations + +import json +from typing import Any + +from sendpulse.http.request import Request +from sendpulse.service.abstract import AbstractService + + +class TaskCommentsResource(AbstractService): + def add_task_comment(self, taskId: int, body: dict[str, Any] | list[Any] | None = None) -> dict[str, Any]: + return self._send(Request( + method="POST", + uri=f"/tasks/{taskId}/comments", + body=json.dumps(body) if body else None, + )) + + def update_task_comment(self, taskId: int, commentId: int, body: dict[str, Any] | list[Any] | None = None) -> dict[str, Any]: + return self._send(Request( + method="PUT", + uri=f"/tasks/{taskId}/comments/{commentId}", + body=json.dumps(body) if body else None, + )) diff --git a/sendpulse/generated/crm/service/task_entity_resource.py b/sendpulse/generated/crm/service/task_entity_resource.py new file mode 100644 index 0000000..697c495 --- /dev/null +++ b/sendpulse/generated/crm/service/task_entity_resource.py @@ -0,0 +1,34 @@ +from __future__ import annotations + +import json +from typing import Any + +from sendpulse.http.request import Request +from sendpulse.service.abstract import AbstractService + + +class TaskEntityResource(AbstractService): + def detach_contact_from_task(self, taskId: int, contactId: int) -> dict[str, Any]: + return self._send(Request( + method="DELETE", + uri=f"/task-contacts/{taskId}/contact/{contactId}", + )) + + def detach_deal_from_task(self, taskId: int, dealId: int) -> dict[str, Any]: + return self._send(Request( + method="DELETE", + uri=f"/task-deals/{taskId}/deal/{dealId}", + )) + + def detach_task_from_task(self, taskHeadId: int, taskId: int) -> dict[str, Any]: + return self._send(Request( + method="DELETE", + uri=f"/task-to-task/{taskHeadId}/task/{taskId}", + )) + + def attach_entities_to_task(self, taskId: int, body: dict[str, Any] | list[Any] | None = None) -> dict[str, Any]: + return self._send(Request( + method="POST", + uri=f"/tasks/{taskId}/entity-attach", + body=json.dumps(body) if body else None, + )) diff --git a/sendpulse/generated/crm/service/task_history_resource.py b/sendpulse/generated/crm/service/task_history_resource.py new file mode 100644 index 0000000..8c572dd --- /dev/null +++ b/sendpulse/generated/crm/service/task_history_resource.py @@ -0,0 +1,16 @@ +from __future__ import annotations + +from typing import Any + +from sendpulse.http.request import Request +from sendpulse.service.abstract import AbstractService + + +class TaskHistoryResource(AbstractService): + def get_task_history(self, taskId: int, fromDate: str, toDate: str) -> dict[str, Any]: + params = {k: v for k, v in {"fromDate": fromDate, "toDate": toDate}.items() if v is not None} + return self._send(Request( + method="GET", + uri=f"/tasks/{taskId}/history", + params=params or None, + )) diff --git a/sendpulse/generated/crm/service/task_tags_resource.py b/sendpulse/generated/crm/service/task_tags_resource.py new file mode 100644 index 0000000..f5d23b8 --- /dev/null +++ b/sendpulse/generated/crm/service/task_tags_resource.py @@ -0,0 +1,41 @@ +from __future__ import annotations + +import json +from typing import Any + +from sendpulse.http.request import Request +from sendpulse.service.abstract import AbstractService + + +class TaskTagsResource(AbstractService): + def detach_tag_from_task(self, tagId: int, taskId: int) -> dict[str, Any]: + return self._send(Request( + method="DELETE", + uri=f"/task-tags/{tagId}/task/{taskId}", + )) + + def get_task_tags(self) -> dict[str, Any]: + return self._send(Request( + method="GET", + uri="/task-tags", + )) + + def create_task_tag(self, body: dict[str, Any] | list[Any] | None = None) -> dict[str, Any]: + return self._send(Request( + method="POST", + uri="/task-tags", + body=json.dumps(body) if body else None, + )) + + def update_task_tag(self, tagId: int, body: dict[str, Any] | list[Any] | None = None) -> dict[str, Any]: + return self._send(Request( + method="PUT", + uri=f"/task-tags/{tagId}", + body=json.dumps(body) if body else None, + )) + + def delete_task_tag(self, tagId: int) -> dict[str, Any]: + return self._send(Request( + method="DELETE", + uri=f"/task-tags/{tagId}", + )) diff --git a/sendpulse/generated/crm/service/tasks_boards_resource.py b/sendpulse/generated/crm/service/tasks_boards_resource.py new file mode 100644 index 0000000..29c1b35 --- /dev/null +++ b/sendpulse/generated/crm/service/tasks_boards_resource.py @@ -0,0 +1,35 @@ +from __future__ import annotations + +import json +from typing import Any + +from sendpulse.http.request import Request +from sendpulse.service.abstract import AbstractService + + +class TasksBoardsResource(AbstractService): + def get_boards(self) -> dict[str, Any]: + return self._send(Request( + method="GET", + uri="/boards", + )) + + def create_board(self, body: dict[str, Any] | list[Any] | None = None) -> dict[str, Any]: + return self._send(Request( + method="POST", + uri="/boards", + body=json.dumps(body) if body else None, + )) + + def get_board_by_id(self, boardId: int) -> dict[str, Any]: + return self._send(Request( + method="GET", + uri=f"/boards/{boardId}", + )) + + def update_board(self, boardId: int, body: dict[str, Any] | list[Any] | None = None) -> dict[str, Any]: + return self._send(Request( + method="PUT", + uri=f"/boards/{boardId}", + body=json.dumps(body) if body else None, + )) diff --git a/sendpulse/generated/crm/service/tasks_resource.py b/sendpulse/generated/crm/service/tasks_resource.py new file mode 100644 index 0000000..2a0a256 --- /dev/null +++ b/sendpulse/generated/crm/service/tasks_resource.py @@ -0,0 +1,62 @@ +from __future__ import annotations + +import json +from typing import Any + +from sendpulse.http.request import Request +from sendpulse.service.abstract import AbstractService + + +class TasksResource(AbstractService): + def list_tasks(self, body: dict[str, Any] | list[Any] | None = None) -> dict[str, Any]: + return self._send(Request( + method="POST", + uri="/tasks/list", + body=json.dumps(body) if body else None, + )) + + def create_task(self, body: dict[str, Any] | list[Any] | None = None) -> dict[str, Any]: + return self._send(Request( + method="POST", + uri="/tasks", + body=json.dumps(body) if body else None, + )) + + def get_task_repeat_templates(self) -> dict[str, Any]: + return self._send(Request( + method="GET", + uri="/tasks-repeat/templates", + )) + + def get_task_by_id(self, taskId: int) -> dict[str, Any]: + return self._send(Request( + method="GET", + uri=f"/tasks/{taskId}", + )) + + def update_task_by_id(self, taskId: int, body: dict[str, Any] | list[Any] | None = None) -> dict[str, Any]: + return self._send(Request( + method="PUT", + uri=f"/tasks/{taskId}", + body=json.dumps(body) if body else None, + )) + + def delete_task(self, taskId: int) -> dict[str, Any]: + return self._send(Request( + method="DELETE", + uri=f"/tasks/{taskId}", + )) + + def set_task_parent(self, taskId: int, body: dict[str, Any] | list[Any] | None = None) -> dict[str, Any]: + return self._send(Request( + method="POST", + uri=f"/tasks/{taskId}/parent", + body=json.dumps(body) if body else None, + )) + + def change_task_step_order(self, taskId: int, stepId: int, body: dict[str, Any] | list[Any] | None = None) -> dict[str, Any]: + return self._send(Request( + method="POST", + uri=f"/tasks/{taskId}/steps/{stepId}/order", + body=json.dumps(body) if body else None, + )) diff --git a/sendpulse/generated/crm/service/tasks_steps_resource.py b/sendpulse/generated/crm/service/tasks_steps_resource.py new file mode 100644 index 0000000..aa57998 --- /dev/null +++ b/sendpulse/generated/crm/service/tasks_steps_resource.py @@ -0,0 +1,23 @@ +from __future__ import annotations + +import json +from typing import Any + +from sendpulse.http.request import Request +from sendpulse.service.abstract import AbstractService + + +class TasksStepsResource(AbstractService): + def create_board_steps(self, boardId: int, body: dict[str, Any] | list[Any] | None = None) -> dict[str, Any]: + return self._send(Request( + method="POST", + uri=f"/boards/{boardId}/steps", + body=json.dumps(body) if body else None, + )) + + def update_board_step(self, boardId: int, stepId: int, body: dict[str, Any] | list[Any] | None = None) -> dict[str, Any]: + return self._send(Request( + method="PUT", + uri=f"/boards/{boardId}/steps/{stepId}", + body=json.dumps(body) if body else None, + )) diff --git a/sendpulse/generated/crm/service/telephony_resource.py b/sendpulse/generated/crm/service/telephony_resource.py new file mode 100644 index 0000000..f8940e5 --- /dev/null +++ b/sendpulse/generated/crm/service/telephony_resource.py @@ -0,0 +1,37 @@ +from __future__ import annotations + +import json +from typing import Any + +from sendpulse.http.request import Request +from sendpulse.service.abstract import AbstractService + + +class TelephonyResource(AbstractService): + def get_telephony_calls(self, body: dict[str, Any] | list[Any] | None = None) -> dict[str, Any]: + return self._send(Request( + method="POST", + uri="/telephony/calls/get-list", + body=json.dumps(body) if body else None, + )) + + def get_contact_calls(self, contactId: int, body: dict[str, Any] | list[Any] | None = None) -> dict[str, Any]: + return self._send(Request( + method="POST", + uri=f"/contacts/{contactId}/get-calls", + body=json.dumps(body) if body else None, + )) + + def get_deal_calls(self, dealId: int, body: dict[str, Any] | list[Any] | None = None) -> dict[str, Any]: + return self._send(Request( + method="POST", + uri=f"/deals/{dealId}/get-calls", + body=json.dumps(body) if body else None, + )) + + def attach_call_to_deal(self, dealId: int, body: dict[str, Any] | list[Any] | None = None) -> dict[str, Any]: + return self._send(Request( + method="POST", + uri=f"/deals/{dealId}/attach-call", + body=json.dumps(body) if body else None, + )) diff --git a/sendpulse/generated/crm/service/users_resource.py b/sendpulse/generated/crm/service/users_resource.py new file mode 100644 index 0000000..7fe22e0 --- /dev/null +++ b/sendpulse/generated/crm/service/users_resource.py @@ -0,0 +1,14 @@ +from __future__ import annotations + +from typing import Any + +from sendpulse.http.request import Request +from sendpulse.service.abstract import AbstractService + + +class UsersResource(AbstractService): + def get_users(self) -> dict[str, Any]: + return self._send(Request( + method="GET", + uri="/users", + )) diff --git a/sendpulse/generated/email/__init__.py b/sendpulse/generated/email/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/sendpulse/generated/email/model/AddEmailsDoubleOptIn.py b/sendpulse/generated/email/model/AddEmailsDoubleOptIn.py new file mode 100644 index 0000000..16e83ad --- /dev/null +++ b/sendpulse/generated/email/model/AddEmailsDoubleOptIn.py @@ -0,0 +1,17 @@ +from __future__ import annotations + +from dataclasses import dataclass +from typing import Any + + +@dataclass(slots=True) +class AddEmailsDoubleOptIn: + emails: list[Any] | None = None + confirmation: str | None = None + + @classmethod + def from_dict(cls, data: dict[str, Any]) -> AddEmailsDoubleOptIn: + return cls( + emails=data.get("emails"), + confirmation=data.get("confirmation"), + ) diff --git a/sendpulse/generated/email/model/AddEmailsSingleOptIn.py b/sendpulse/generated/email/model/AddEmailsSingleOptIn.py new file mode 100644 index 0000000..5dd5a53 --- /dev/null +++ b/sendpulse/generated/email/model/AddEmailsSingleOptIn.py @@ -0,0 +1,15 @@ +from __future__ import annotations + +from dataclasses import dataclass +from typing import Any + + +@dataclass(slots=True) +class AddEmailsSingleOptIn: + emails: list[Any] | None = None + + @classmethod + def from_dict(cls, data: dict[str, Any]) -> AddEmailsSingleOptIn: + return cls( + emails=data.get("emails"), + ) diff --git a/sendpulse/generated/email/model/BalanceResponse.py b/sendpulse/generated/email/model/BalanceResponse.py new file mode 100644 index 0000000..6c29c51 --- /dev/null +++ b/sendpulse/generated/email/model/BalanceResponse.py @@ -0,0 +1,17 @@ +from __future__ import annotations + +from dataclasses import dataclass +from typing import Any + + +@dataclass(slots=True) +class BalanceResponse: + currency: str | None = None + balance_currency: float | None = None + + @classmethod + def from_dict(cls, data: dict[str, Any]) -> BalanceResponse: + return cls( + currency=data.get("currency"), + balance_currency=data.get("balance_currency"), + ) diff --git a/sendpulse/generated/email/model/BlacklistAddRequest.py b/sendpulse/generated/email/model/BlacklistAddRequest.py new file mode 100644 index 0000000..43291df --- /dev/null +++ b/sendpulse/generated/email/model/BlacklistAddRequest.py @@ -0,0 +1,17 @@ +from __future__ import annotations + +from dataclasses import dataclass +from typing import Any + + +@dataclass(slots=True) +class BlacklistAddRequest: + emails: str | None = None + comment: str | None = None + + @classmethod + def from_dict(cls, data: dict[str, Any]) -> BlacklistAddRequest: + return cls( + emails=data.get("emails"), + comment=data.get("comment"), + ) diff --git a/sendpulse/generated/email/model/BlacklistDeleteRequest.py b/sendpulse/generated/email/model/BlacklistDeleteRequest.py new file mode 100644 index 0000000..70cd09c --- /dev/null +++ b/sendpulse/generated/email/model/BlacklistDeleteRequest.py @@ -0,0 +1,15 @@ +from __future__ import annotations + +from dataclasses import dataclass +from typing import Any + + +@dataclass(slots=True) +class BlacklistDeleteRequest: + emails: str | None = None + + @classmethod + def from_dict(cls, data: dict[str, Any]) -> BlacklistDeleteRequest: + return cls( + emails=data.get("emails"), + ) diff --git a/sendpulse/generated/email/model/CampaignByListSummary.py b/sendpulse/generated/email/model/CampaignByListSummary.py new file mode 100644 index 0000000..91bc149 --- /dev/null +++ b/sendpulse/generated/email/model/CampaignByListSummary.py @@ -0,0 +1,15 @@ +from __future__ import annotations + +from dataclasses import dataclass +from typing import Any + + +@dataclass(slots=True) +class CampaignByListSummary: + task_id: int | None = None + + @classmethod + def from_dict(cls, data: dict[str, Any]) -> CampaignByListSummary: + return cls( + task_id=data.get("task_id"), + ) diff --git a/sendpulse/generated/email/model/CampaignCost.py b/sendpulse/generated/email/model/CampaignCost.py new file mode 100644 index 0000000..b2bb89f --- /dev/null +++ b/sendpulse/generated/email/model/CampaignCost.py @@ -0,0 +1,21 @@ +from __future__ import annotations + +from dataclasses import dataclass +from typing import Any + + +@dataclass(slots=True) +class CampaignCost: + cur: str | None = None + sent_emails_qty: int | None = None + overdraftAllEmailsPrice: float | None = None + result: bool | None = None + + @classmethod + def from_dict(cls, data: dict[str, Any]) -> CampaignCost: + return cls( + cur=data.get("cur"), + sent_emails_qty=data.get("sent_emails_qty"), + overdraftAllEmailsPrice=data.get("overdraftAllEmailsPrice"), + result=data.get("result"), + ) diff --git a/sendpulse/generated/email/model/CampaignCreateRequest.py b/sendpulse/generated/email/model/CampaignCreateRequest.py new file mode 100644 index 0000000..f6a7179 --- /dev/null +++ b/sendpulse/generated/email/model/CampaignCreateRequest.py @@ -0,0 +1,45 @@ +from __future__ import annotations + +from dataclasses import dataclass +from typing import Any + + +@dataclass(slots=True) +class CampaignCreateRequest: + sender_name: str | None = None + sender_email: str | None = None + subject: str | None = None + body: str | None = None + template_id: Any = None + list_id: Any = None + segment_id: int | None = None + is_test: bool | None = None + send_date: str | None = None + name: str | None = None + use_dynamic_list: bool | None = None + attachments: dict[str, Any] | None = None + attachments_binary: dict[str, Any] | None = None + type: str | None = None + body_amp: str | None = None + stats: dict[str, Any] | None = None + + @classmethod + def from_dict(cls, data: dict[str, Any]) -> CampaignCreateRequest: + return cls( + sender_name=data.get("sender_name"), + sender_email=data.get("sender_email"), + subject=data.get("subject"), + body=data.get("body"), + template_id=data.get("template_id"), + list_id=data.get("list_id"), + segment_id=data.get("segment_id"), + is_test=data.get("is_test"), + send_date=data.get("send_date"), + name=data.get("name"), + use_dynamic_list=data.get("use_dynamic_list"), + attachments=data.get("attachments"), + attachments_binary=data.get("attachments_binary"), + type=data.get("type"), + body_amp=data.get("body_amp"), + stats=data.get("stats"), + ) diff --git a/sendpulse/generated/email/model/CampaignCreateResponse.py b/sendpulse/generated/email/model/CampaignCreateResponse.py new file mode 100644 index 0000000..6e28bcc --- /dev/null +++ b/sendpulse/generated/email/model/CampaignCreateResponse.py @@ -0,0 +1,25 @@ +from __future__ import annotations + +from dataclasses import dataclass +from typing import Any + + +@dataclass(slots=True) +class CampaignCreateResponse: + id: int | None = None + status: int | None = None + count: int | None = None + tariff_email_qty: int | None = None + overdraft_price: str | None = None + ovedraft_currency: str | None = None + + @classmethod + def from_dict(cls, data: dict[str, Any]) -> CampaignCreateResponse: + return cls( + id=data.get("id"), + status=data.get("status"), + count=data.get("count"), + tariff_email_qty=data.get("tariff_email_qty"), + overdraft_price=data.get("overdraft_price"), + ovedraft_currency=data.get("ovedraft_currency"), + ) diff --git a/sendpulse/generated/email/model/CampaignDetails.py b/sendpulse/generated/email/model/CampaignDetails.py new file mode 100644 index 0000000..d237b6b --- /dev/null +++ b/sendpulse/generated/email/model/CampaignDetails.py @@ -0,0 +1,19 @@ +from __future__ import annotations + +from dataclasses import dataclass +from typing import Any + + +@dataclass(slots=True) +class CampaignDetails: + id: int | None = None + name: str | None = None + status: int | None = None + + @classmethod + def from_dict(cls, data: dict[str, Any]) -> CampaignDetails: + return cls( + id=data.get("id"), + name=data.get("name"), + status=data.get("status"), + ) diff --git a/sendpulse/generated/email/model/CampaignSummary.py b/sendpulse/generated/email/model/CampaignSummary.py new file mode 100644 index 0000000..5dd5364 --- /dev/null +++ b/sendpulse/generated/email/model/CampaignSummary.py @@ -0,0 +1,19 @@ +from __future__ import annotations + +from dataclasses import dataclass +from typing import Any + + +@dataclass(slots=True) +class CampaignSummary: + id: int | None = None + name: str | None = None + status: int | None = None + + @classmethod + def from_dict(cls, data: dict[str, Any]) -> CampaignSummary: + return cls( + id=data.get("id"), + name=data.get("name"), + status=data.get("status"), + ) diff --git a/sendpulse/generated/email/model/CampaignUpdateRequest.py b/sendpulse/generated/email/model/CampaignUpdateRequest.py new file mode 100644 index 0000000..f669747 --- /dev/null +++ b/sendpulse/generated/email/model/CampaignUpdateRequest.py @@ -0,0 +1,17 @@ +from __future__ import annotations + +from dataclasses import dataclass +from typing import Any + + +@dataclass(slots=True) +class CampaignUpdateRequest: + name: str | None = None + subject: str | None = None + + @classmethod + def from_dict(cls, data: dict[str, Any]) -> CampaignUpdateRequest: + return cls( + name=data.get("name"), + subject=data.get("subject"), + ) diff --git a/sendpulse/generated/email/model/DetailedBalanceResponse.py b/sendpulse/generated/email/model/DetailedBalanceResponse.py new file mode 100644 index 0000000..d5744cd --- /dev/null +++ b/sendpulse/generated/email/model/DetailedBalanceResponse.py @@ -0,0 +1,21 @@ +from __future__ import annotations + +from dataclasses import dataclass +from typing import Any + + +@dataclass(slots=True) +class DetailedBalanceResponse: + balance: dict[str, Any] | None = None + email: dict[str, Any] | None = None + smtp: dict[str, Any] | None = None + push: dict[str, Any] | None = None + + @classmethod + def from_dict(cls, data: dict[str, Any]) -> DetailedBalanceResponse: + return cls( + balance=data.get("balance"), + email=data.get("email"), + smtp=data.get("smtp"), + push=data.get("push"), + ) diff --git a/sendpulse/generated/email/model/EmailAcrossBooks.py b/sendpulse/generated/email/model/EmailAcrossBooks.py new file mode 100644 index 0000000..7d8eac6 --- /dev/null +++ b/sendpulse/generated/email/model/EmailAcrossBooks.py @@ -0,0 +1,17 @@ +from __future__ import annotations + +from dataclasses import dataclass +from typing import Any + + +@dataclass(slots=True) +class EmailAcrossBooks: + book_id: int | None = None + email: str | None = None + + @classmethod + def from_dict(cls, data: dict[str, Any]) -> EmailAcrossBooks: + return cls( + book_id=data.get("book_id"), + email=data.get("email"), + ) diff --git a/sendpulse/generated/email/model/EmailCampaignDeliveryInfo.py b/sendpulse/generated/email/model/EmailCampaignDeliveryInfo.py new file mode 100644 index 0000000..2f25719 --- /dev/null +++ b/sendpulse/generated/email/model/EmailCampaignDeliveryInfo.py @@ -0,0 +1,23 @@ +from __future__ import annotations + +from dataclasses import dataclass +from typing import Any + + +@dataclass(slots=True) +class EmailCampaignDeliveryInfo: + sent_date: str | None = None + global_status: int | None = None + global_status_explain: str | None = None + detail_status: int | None = None + detail_status_explain: str | None = None + + @classmethod + def from_dict(cls, data: dict[str, Any]) -> EmailCampaignDeliveryInfo: + return cls( + sent_date=data.get("sent_date"), + global_status=data.get("global_status"), + global_status_explain=data.get("global_status_explain"), + detail_status=data.get("detail_status"), + detail_status_explain=data.get("detail_status_explain"), + ) diff --git a/sendpulse/generated/email/model/EmailContact.py b/sendpulse/generated/email/model/EmailContact.py new file mode 100644 index 0000000..7c003dd --- /dev/null +++ b/sendpulse/generated/email/model/EmailContact.py @@ -0,0 +1,21 @@ +from __future__ import annotations + +from dataclasses import dataclass +from typing import Any + + +@dataclass(slots=True) +class EmailContact: + email: str | None = None + status: int | None = None + status_explain: str | None = None + variables: dict[str, Any] | None = None + + @classmethod + def from_dict(cls, data: dict[str, Any]) -> EmailContact: + return cls( + email=data.get("email"), + status=data.get("status"), + status_explain=data.get("status_explain"), + variables=data.get("variables"), + ) diff --git a/sendpulse/generated/email/model/EmailContactBasic.py b/sendpulse/generated/email/model/EmailContactBasic.py new file mode 100644 index 0000000..e119a76 --- /dev/null +++ b/sendpulse/generated/email/model/EmailContactBasic.py @@ -0,0 +1,19 @@ +from __future__ import annotations + +from dataclasses import dataclass +from typing import Any + + +@dataclass(slots=True) +class EmailContactBasic: + email: str | None = None + status: int | None = None + status_explain: str | None = None + + @classmethod + def from_dict(cls, data: dict[str, Any]) -> EmailContactBasic: + return cls( + email=data.get("email"), + status=data.get("status"), + status_explain=data.get("status_explain"), + ) diff --git a/sendpulse/generated/email/model/EmailDetails.py b/sendpulse/generated/email/model/EmailDetails.py new file mode 100644 index 0000000..5f55e63 --- /dev/null +++ b/sendpulse/generated/email/model/EmailDetails.py @@ -0,0 +1,17 @@ +from __future__ import annotations + +from dataclasses import dataclass +from typing import Any + + +@dataclass(slots=True) +class EmailDetails: + list_name: str | None = None + source: str | None = None + + @classmethod + def from_dict(cls, data: dict[str, Any]) -> EmailDetails: + return cls( + list_name=data.get("list_name"), + source=data.get("source"), + ) diff --git a/sendpulse/generated/email/model/EmailInfoShort.py b/sendpulse/generated/email/model/EmailInfoShort.py new file mode 100644 index 0000000..8a327a0 --- /dev/null +++ b/sendpulse/generated/email/model/EmailInfoShort.py @@ -0,0 +1,15 @@ +from __future__ import annotations + +from dataclasses import dataclass +from typing import Any + + +@dataclass(slots=True) +class EmailInfoShort: + book_id: int | None = None + + @classmethod + def from_dict(cls, data: dict[str, Any]) -> EmailInfoShort: + return cls( + book_id=data.get("book_id"), + ) diff --git a/sendpulse/generated/email/model/ListContactInfo.py b/sendpulse/generated/email/model/ListContactInfo.py new file mode 100644 index 0000000..c22639a --- /dev/null +++ b/sendpulse/generated/email/model/ListContactInfo.py @@ -0,0 +1,25 @@ +from __future__ import annotations + +from dataclasses import dataclass +from typing import Any + + +@dataclass(slots=True) +class ListContactInfo: + email: str | None = None + abook_id: str | None = None + phone: str | None = None + status: int | None = None + status_explain: str | None = None + variables: list[Any] | None = None + + @classmethod + def from_dict(cls, data: dict[str, Any]) -> ListContactInfo: + return cls( + email=data.get("email"), + abook_id=data.get("abook_id"), + phone=data.get("phone"), + status=data.get("status"), + status_explain=data.get("status_explain"), + variables=data.get("variables"), + ) diff --git a/sendpulse/generated/email/model/MailingList.py b/sendpulse/generated/email/model/MailingList.py new file mode 100644 index 0000000..6ce2f7a --- /dev/null +++ b/sendpulse/generated/email/model/MailingList.py @@ -0,0 +1,29 @@ +from __future__ import annotations + +from dataclasses import dataclass +from typing import Any + + +@dataclass(slots=True) +class MailingList: + id: int | None = None + name: str | None = None + all_email_qty: int | None = None + active_email_qty: int | None = None + inactive_email_qty: int | None = None + creationdate: str | None = None + status: int | None = None + status_explain: str | None = None + + @classmethod + def from_dict(cls, data: dict[str, Any]) -> MailingList: + return cls( + id=data.get("id"), + name=data.get("name"), + all_email_qty=data.get("all_email_qty"), + active_email_qty=data.get("active_email_qty"), + inactive_email_qty=data.get("inactive_email_qty"), + creationdate=data.get("creationdate"), + status=data.get("status"), + status_explain=data.get("status_explain"), + ) diff --git a/sendpulse/generated/email/model/MailingListId.py b/sendpulse/generated/email/model/MailingListId.py new file mode 100644 index 0000000..738dda0 --- /dev/null +++ b/sendpulse/generated/email/model/MailingListId.py @@ -0,0 +1,15 @@ +from __future__ import annotations + +from dataclasses import dataclass +from typing import Any + + +@dataclass(slots=True) +class MailingListId: + id: int | None = None + + @classmethod + def from_dict(cls, data: dict[str, Any]) -> MailingListId: + return cls( + id=data.get("id"), + ) diff --git a/sendpulse/generated/email/model/ReferralStat.py b/sendpulse/generated/email/model/ReferralStat.py new file mode 100644 index 0000000..c922199 --- /dev/null +++ b/sendpulse/generated/email/model/ReferralStat.py @@ -0,0 +1,17 @@ +from __future__ import annotations + +from dataclasses import dataclass +from typing import Any + + +@dataclass(slots=True) +class ReferralStat: + link: str | None = None + count: int | None = None + + @classmethod + def from_dict(cls, data: dict[str, Any]) -> ReferralStat: + return cls( + link=data.get("link"), + count=data.get("count"), + ) diff --git a/sendpulse/generated/email/model/ResultTrue.py b/sendpulse/generated/email/model/ResultTrue.py new file mode 100644 index 0000000..368a726 --- /dev/null +++ b/sendpulse/generated/email/model/ResultTrue.py @@ -0,0 +1,15 @@ +from __future__ import annotations + +from dataclasses import dataclass +from typing import Any + + +@dataclass(slots=True) +class ResultTrue: + result: bool | None = None + + @classmethod + def from_dict(cls, data: dict[str, Any]) -> ResultTrue: + return cls( + result=data.get("result"), + ) diff --git a/sendpulse/generated/email/model/ResultTrueWithId.py b/sendpulse/generated/email/model/ResultTrueWithId.py new file mode 100644 index 0000000..a27dc24 --- /dev/null +++ b/sendpulse/generated/email/model/ResultTrueWithId.py @@ -0,0 +1,17 @@ +from __future__ import annotations + +from dataclasses import dataclass +from typing import Any + + +@dataclass(slots=True) +class ResultTrueWithId: + result: bool | None = None + id: int | None = None + + @classmethod + def from_dict(cls, data: dict[str, Any]) -> ResultTrueWithId: + return cls( + result=data.get("result"), + id=data.get("id"), + ) diff --git a/sendpulse/generated/email/model/Sender.py b/sendpulse/generated/email/model/Sender.py new file mode 100644 index 0000000..c7f1a5d --- /dev/null +++ b/sendpulse/generated/email/model/Sender.py @@ -0,0 +1,17 @@ +from __future__ import annotations + +from dataclasses import dataclass +from typing import Any + + +@dataclass(slots=True) +class Sender: + email: str | None = None + name: str | None = None + + @classmethod + def from_dict(cls, data: dict[str, Any]) -> Sender: + return cls( + email=data.get("email"), + name=data.get("name"), + ) diff --git a/sendpulse/generated/email/model/SenderActivationResponse.py b/sendpulse/generated/email/model/SenderActivationResponse.py new file mode 100644 index 0000000..78ae3af --- /dev/null +++ b/sendpulse/generated/email/model/SenderActivationResponse.py @@ -0,0 +1,15 @@ +from __future__ import annotations + +from dataclasses import dataclass +from typing import Any + + +@dataclass(slots=True) +class SenderActivationResponse: + result: bool | None = None + + @classmethod + def from_dict(cls, data: dict[str, Any]) -> SenderActivationResponse: + return cls( + result=data.get("result"), + ) diff --git a/sendpulse/generated/email/model/SubscriberStats.py b/sendpulse/generated/email/model/SubscriberStats.py new file mode 100644 index 0000000..a6ad393 --- /dev/null +++ b/sendpulse/generated/email/model/SubscriberStats.py @@ -0,0 +1,15 @@ +from __future__ import annotations + +from dataclasses import dataclass +from typing import Any + + +@dataclass(slots=True) +class SubscriberStats: + statistic: dict[str, Any] | None = None + + @classmethod + def from_dict(cls, data: dict[str, Any]) -> SubscriberStats: + return cls( + statistic=data.get("statistic"), + ) diff --git a/sendpulse/generated/email/model/Tag.py b/sendpulse/generated/email/model/Tag.py new file mode 100644 index 0000000..f99de6c --- /dev/null +++ b/sendpulse/generated/email/model/Tag.py @@ -0,0 +1,19 @@ +from __future__ import annotations + +from dataclasses import dataclass +from typing import Any + + +@dataclass(slots=True) +class Tag: + id: int | None = None + name: str | None = None + color: str | None = None + + @classmethod + def from_dict(cls, data: dict[str, Any]) -> Tag: + return cls( + id=data.get("id"), + name=data.get("name"), + color=data.get("color"), + ) diff --git a/sendpulse/generated/email/model/TagCreateRequest.py b/sendpulse/generated/email/model/TagCreateRequest.py new file mode 100644 index 0000000..75d4367 --- /dev/null +++ b/sendpulse/generated/email/model/TagCreateRequest.py @@ -0,0 +1,17 @@ +from __future__ import annotations + +from dataclasses import dataclass +from typing import Any + + +@dataclass(slots=True) +class TagCreateRequest: + name: str | None = None + color: str | None = None + + @classmethod + def from_dict(cls, data: dict[str, Any]) -> TagCreateRequest: + return cls( + name=data.get("name"), + color=data.get("color"), + ) diff --git a/sendpulse/generated/email/model/TagListResponse.py b/sendpulse/generated/email/model/TagListResponse.py new file mode 100644 index 0000000..970b3dd --- /dev/null +++ b/sendpulse/generated/email/model/TagListResponse.py @@ -0,0 +1,21 @@ +from __future__ import annotations + +from dataclasses import dataclass +from typing import Any + +from sendpulse.generated.email.model.Tag import Tag + + +@dataclass(slots=True) +class TagListResponse: + tags: list[Tag] | None = None + user_id: int | None = None + version: str | None = None + + @classmethod + def from_dict(cls, data: dict[str, Any]) -> TagListResponse: + return cls( + tags=[Tag.from_dict(i) for i in data["tags"]] if isinstance(data.get("tags"), list) else None, + user_id=data.get("user_id"), + version=data.get("version"), + ) diff --git a/sendpulse/generated/email/model/TagPinUnpinEmailRequest.py b/sendpulse/generated/email/model/TagPinUnpinEmailRequest.py new file mode 100644 index 0000000..d1f34db --- /dev/null +++ b/sendpulse/generated/email/model/TagPinUnpinEmailRequest.py @@ -0,0 +1,17 @@ +from __future__ import annotations + +from dataclasses import dataclass +from typing import Any + + +@dataclass(slots=True) +class TagPinUnpinEmailRequest: + email: str | None = None + tags: list[Any] | None = None + + @classmethod + def from_dict(cls, data: dict[str, Any]) -> TagPinUnpinEmailRequest: + return cls( + email=data.get("email"), + tags=data.get("tags"), + ) diff --git a/sendpulse/generated/email/model/TagPinUnpinPhoneRequest.py b/sendpulse/generated/email/model/TagPinUnpinPhoneRequest.py new file mode 100644 index 0000000..524b43e --- /dev/null +++ b/sendpulse/generated/email/model/TagPinUnpinPhoneRequest.py @@ -0,0 +1,17 @@ +from __future__ import annotations + +from dataclasses import dataclass +from typing import Any + + +@dataclass(slots=True) +class TagPinUnpinPhoneRequest: + phone: str | None = None + tags: list[Any] | None = None + + @classmethod + def from_dict(cls, data: dict[str, Any]) -> TagPinUnpinPhoneRequest: + return cls( + phone=data.get("phone"), + tags=data.get("tags"), + ) diff --git a/sendpulse/generated/email/model/TagQueueResponse.py b/sendpulse/generated/email/model/TagQueueResponse.py new file mode 100644 index 0000000..3565558 --- /dev/null +++ b/sendpulse/generated/email/model/TagQueueResponse.py @@ -0,0 +1,29 @@ +from __future__ import annotations + +from dataclasses import dataclass +from typing import Any + + +@dataclass(slots=True) +class TagQueueResponse: + code: str | None = None + description: str | None = None + failure: bool | None = None + http_code: int | None = None + queue_id: str | None = None + success: bool | None = None + user_id: int | None = None + version: str | None = None + + @classmethod + def from_dict(cls, data: dict[str, Any]) -> TagQueueResponse: + return cls( + code=data.get("code"), + description=data.get("description"), + failure=data.get("failure"), + http_code=data.get("http_code"), + queue_id=data.get("queue_id"), + success=data.get("success"), + user_id=data.get("user_id"), + version=data.get("version"), + ) diff --git a/sendpulse/generated/email/model/TagUpdateRequest.py b/sendpulse/generated/email/model/TagUpdateRequest.py new file mode 100644 index 0000000..711381c --- /dev/null +++ b/sendpulse/generated/email/model/TagUpdateRequest.py @@ -0,0 +1,17 @@ +from __future__ import annotations + +from dataclasses import dataclass +from typing import Any + + +@dataclass(slots=True) +class TagUpdateRequest: + name: str | None = None + color: str | None = None + + @classmethod + def from_dict(cls, data: dict[str, Any]) -> TagUpdateRequest: + return cls( + name=data.get("name"), + color=data.get("color"), + ) diff --git a/sendpulse/generated/email/model/TemplateCreation.py b/sendpulse/generated/email/model/TemplateCreation.py new file mode 100644 index 0000000..3d2be73 --- /dev/null +++ b/sendpulse/generated/email/model/TemplateCreation.py @@ -0,0 +1,17 @@ +from __future__ import annotations + +from dataclasses import dataclass +from typing import Any + + +@dataclass(slots=True) +class TemplateCreation: + result: bool | None = None + real_id: int | None = None + + @classmethod + def from_dict(cls, data: dict[str, Any]) -> TemplateCreation: + return cls( + result=data.get("result"), + real_id=data.get("real_id"), + ) diff --git a/sendpulse/generated/email/model/TemplateDetails.py b/sendpulse/generated/email/model/TemplateDetails.py new file mode 100644 index 0000000..9a53446 --- /dev/null +++ b/sendpulse/generated/email/model/TemplateDetails.py @@ -0,0 +1,17 @@ +from __future__ import annotations + +from dataclasses import dataclass +from typing import Any + + +@dataclass(slots=True) +class TemplateDetails: + id: str | None = None + name: str | None = None + + @classmethod + def from_dict(cls, data: dict[str, Any]) -> TemplateDetails: + return cls( + id=data.get("id"), + name=data.get("name"), + ) diff --git a/sendpulse/generated/email/model/TemplateSummary.py b/sendpulse/generated/email/model/TemplateSummary.py new file mode 100644 index 0000000..deaeb36 --- /dev/null +++ b/sendpulse/generated/email/model/TemplateSummary.py @@ -0,0 +1,17 @@ +from __future__ import annotations + +from dataclasses import dataclass +from typing import Any + + +@dataclass(slots=True) +class TemplateSummary: + id: str | None = None + name: str | None = None + + @classmethod + def from_dict(cls, data: dict[str, Any]) -> TemplateSummary: + return cls( + id=data.get("id"), + name=data.get("name"), + ) diff --git a/sendpulse/generated/email/model/TotalCount.py b/sendpulse/generated/email/model/TotalCount.py new file mode 100644 index 0000000..8059d15 --- /dev/null +++ b/sendpulse/generated/email/model/TotalCount.py @@ -0,0 +1,15 @@ +from __future__ import annotations + +from dataclasses import dataclass +from typing import Any + + +@dataclass(slots=True) +class TotalCount: + total: int | None = None + + @classmethod + def from_dict(cls, data: dict[str, Any]) -> TotalCount: + return cls( + total=data.get("total"), + ) diff --git a/sendpulse/generated/email/model/VariableDefinition.py b/sendpulse/generated/email/model/VariableDefinition.py new file mode 100644 index 0000000..3c2cce6 --- /dev/null +++ b/sendpulse/generated/email/model/VariableDefinition.py @@ -0,0 +1,17 @@ +from __future__ import annotations + +from dataclasses import dataclass +from typing import Any + + +@dataclass(slots=True) +class VariableDefinition: + name: str | None = None + type: str | None = None + + @classmethod + def from_dict(cls, data: dict[str, Any]) -> VariableDefinition: + return cls( + name=data.get("name"), + type=data.get("type"), + ) diff --git a/sendpulse/generated/email/model/Webhook.py b/sendpulse/generated/email/model/Webhook.py new file mode 100644 index 0000000..a62d7e5 --- /dev/null +++ b/sendpulse/generated/email/model/Webhook.py @@ -0,0 +1,21 @@ +from __future__ import annotations + +from dataclasses import dataclass +from typing import Any + + +@dataclass(slots=True) +class Webhook: + id: int | None = None + user_id: int | None = None + url: str | None = None + action: str | None = None + + @classmethod + def from_dict(cls, data: dict[str, Any]) -> Webhook: + return cls( + id=data.get("id"), + user_id=data.get("user_id"), + url=data.get("url"), + action=data.get("action"), + ) diff --git a/sendpulse/generated/email/model/WebhookCreateRequest.py b/sendpulse/generated/email/model/WebhookCreateRequest.py new file mode 100644 index 0000000..ab1ac2e --- /dev/null +++ b/sendpulse/generated/email/model/WebhookCreateRequest.py @@ -0,0 +1,17 @@ +from __future__ import annotations + +from dataclasses import dataclass +from typing import Any + + +@dataclass(slots=True) +class WebhookCreateRequest: + url: str | None = None + actions: list[Any] | None = None + + @classmethod + def from_dict(cls, data: dict[str, Any]) -> WebhookCreateRequest: + return cls( + url=data.get("url"), + actions=data.get("actions"), + ) diff --git a/sendpulse/generated/email/model/WebhookListResponse.py b/sendpulse/generated/email/model/WebhookListResponse.py new file mode 100644 index 0000000..93feca1 --- /dev/null +++ b/sendpulse/generated/email/model/WebhookListResponse.py @@ -0,0 +1,19 @@ +from __future__ import annotations + +from dataclasses import dataclass +from typing import Any + +from sendpulse.generated.email.model.Webhook import Webhook + + +@dataclass(slots=True) +class WebhookListResponse: + success: bool | None = None + data: list[Webhook] | None = None + + @classmethod + def from_dict(cls, data: dict[str, Any]) -> WebhookListResponse: + return cls( + success=data.get("success"), + data=[Webhook.from_dict(i) for i in data["data"]] if isinstance(data.get("data"), list) else None, + ) diff --git a/sendpulse/generated/email/model/WebhookResponse.py b/sendpulse/generated/email/model/WebhookResponse.py new file mode 100644 index 0000000..4f22606 --- /dev/null +++ b/sendpulse/generated/email/model/WebhookResponse.py @@ -0,0 +1,19 @@ +from __future__ import annotations + +from dataclasses import dataclass +from typing import Any + +from sendpulse.generated.email.model.Webhook import Webhook + + +@dataclass(slots=True) +class WebhookResponse: + success: bool | None = None + data: Webhook | None = None + + @classmethod + def from_dict(cls, data: dict[str, Any]) -> WebhookResponse: + return cls( + success=data.get("success"), + data=Webhook.from_dict(data["data"]) if isinstance(data.get("data"), dict) else None, + ) diff --git a/sendpulse/generated/email/model/WebhookResult.py b/sendpulse/generated/email/model/WebhookResult.py new file mode 100644 index 0000000..f001b0a --- /dev/null +++ b/sendpulse/generated/email/model/WebhookResult.py @@ -0,0 +1,17 @@ +from __future__ import annotations + +from dataclasses import dataclass +from typing import Any + + +@dataclass(slots=True) +class WebhookResult: + success: bool | None = None + data: list[Any] | None = None + + @classmethod + def from_dict(cls, data: dict[str, Any]) -> WebhookResult: + return cls( + success=data.get("success"), + data=data.get("data"), + ) diff --git a/sendpulse/generated/email/model/WebhookUpdateRequest.py b/sendpulse/generated/email/model/WebhookUpdateRequest.py new file mode 100644 index 0000000..3fb4183 --- /dev/null +++ b/sendpulse/generated/email/model/WebhookUpdateRequest.py @@ -0,0 +1,15 @@ +from __future__ import annotations + +from dataclasses import dataclass +from typing import Any + + +@dataclass(slots=True) +class WebhookUpdateRequest: + url: str | None = None + + @classmethod + def from_dict(cls, data: dict[str, Any]) -> WebhookUpdateRequest: + return cls( + url=data.get("url"), + ) diff --git a/sendpulse/generated/email/model/__init__.py b/sendpulse/generated/email/model/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/sendpulse/generated/email/service/__init__.py b/sendpulse/generated/email/service/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/sendpulse/generated/email/service/balance_resource.py b/sendpulse/generated/email/service/balance_resource.py new file mode 100644 index 0000000..6368694 --- /dev/null +++ b/sendpulse/generated/email/service/balance_resource.py @@ -0,0 +1,28 @@ +from __future__ import annotations + +from typing import Any + +from sendpulse.generated.email.model.BalanceResponse import BalanceResponse +from sendpulse.generated.email.model.DetailedBalanceResponse import DetailedBalanceResponse +from sendpulse.http.request import Request +from sendpulse.service.abstract import AbstractService + + +class BalanceResource(AbstractService): + def get_balance(self) -> BalanceResponse: + return BalanceResponse.from_dict(self._send(Request( + method="GET", + uri="/balance", + ))) + + def get_balance_by_currency(self, currency: str) -> BalanceResponse: + return BalanceResponse.from_dict(self._send(Request( + method="GET", + uri=f"/balance/{currency}", + ))) + + def get_detailed_balance(self) -> DetailedBalanceResponse: + return DetailedBalanceResponse.from_dict(self._send(Request( + method="GET", + uri="/user/balance/detail", + ))) diff --git a/sendpulse/generated/email/service/blacklist_resource.py b/sendpulse/generated/email/service/blacklist_resource.py new file mode 100644 index 0000000..06faeb9 --- /dev/null +++ b/sendpulse/generated/email/service/blacklist_resource.py @@ -0,0 +1,30 @@ +from __future__ import annotations + +import json +from typing import Any + +from sendpulse.generated.email.model.ResultTrue import ResultTrue +from sendpulse.http.request import Request +from sendpulse.service.abstract import AbstractService + + +class BlacklistResource(AbstractService): + def get_blacklist(self) -> dict[str, Any]: + return self._send(Request( + method="GET", + uri="/blacklist", + )) + + def add_to_blacklist(self, body: dict[str, Any] | list[Any] | None = None) -> ResultTrue: + return ResultTrue.from_dict(self._send(Request( + method="POST", + uri="/blacklist", + body=json.dumps(body) if body else None, + ))) + + def remove_from_blacklist(self, body: dict[str, Any] | list[Any] | None = None) -> ResultTrue: + return ResultTrue.from_dict(self._send(Request( + method="DELETE", + uri="/blacklist", + body=json.dumps(body) if body else None, + ))) diff --git a/sendpulse/generated/email/service/campaigns_resource.py b/sendpulse/generated/email/service/campaigns_resource.py new file mode 100644 index 0000000..c0136d6 --- /dev/null +++ b/sendpulse/generated/email/service/campaigns_resource.py @@ -0,0 +1,70 @@ +from __future__ import annotations + +import json +from typing import Any + +from sendpulse.generated.email.model.CampaignByListSummary import CampaignByListSummary +from sendpulse.generated.email.model.CampaignCreateResponse import CampaignCreateResponse +from sendpulse.generated.email.model.CampaignDetails import CampaignDetails +from sendpulse.generated.email.model.CampaignSummary import CampaignSummary +from sendpulse.generated.email.model.ReferralStat import ReferralStat +from sendpulse.generated.email.model.ResultTrue import ResultTrue +from sendpulse.generated.email.model.ResultTrueWithId import ResultTrueWithId +from sendpulse.http.request import Request +from sendpulse.service.abstract import AbstractService + + +class CampaignsResource(AbstractService): + def get_campaigns(self, limit: int | None = None, offset: int | None = None, order: str | None = None, status: list[Any] | None = None, planed: bool | None = None) -> list[CampaignSummary]: + params = {k: v for k, v in {"limit": limit, "offset": offset, "order": order, "status": status, "planed": planed}.items() if v is not None} + return [CampaignSummary.from_dict(i) for i in self._send_list(Request( + method="GET", + uri="/campaigns", + params=params or None, + ))] + + def create_campaign(self, body: dict[str, Any] | list[Any] | None = None) -> CampaignCreateResponse: + return CampaignCreateResponse.from_dict(self._send(Request( + method="POST", + uri="/campaigns", + body=json.dumps(body) if body else None, + ))) + + def get_campaign_by_id(self, id: int) -> CampaignDetails: + return CampaignDetails.from_dict(self._send(Request( + method="GET", + uri=f"/campaigns/{id}", + ))) + + def update_campaign(self, id: int, body: dict[str, Any] | list[Any] | None = None) -> ResultTrueWithId: + return ResultTrueWithId.from_dict(self._send(Request( + method="PATCH", + uri=f"/campaigns/{id}", + body=json.dumps(body) if body else None, + ))) + + def cancel_campaign(self, id: int) -> ResultTrue: + return ResultTrue.from_dict(self._send(Request( + method="DELETE", + uri=f"/campaigns/{id}", + ))) + + def get_campaign_country_stats(self, id: int) -> dict[str, Any]: + return self._send(Request( + method="GET", + uri=f"/campaigns/{id}/countries", + )) + + def get_campaign_referral_stats(self, id: int) -> list[ReferralStat]: + return [ReferralStat.from_dict(i) for i in self._send_list(Request( + method="GET", + uri=f"/campaigns/{id}/referrals", + ))] + + def get_campaigns_by_list(self, id: int, limit: int | None = None, offset: int | None = None) -> list[CampaignByListSummary]: + params = {k: v for k, v in {"limit": limit, "offset": offset}.items() if v is not None} + return [CampaignByListSummary.from_dict(i) for i in self._send_list(Request( + method="GET", + uri=f"/addressbooks/{id}/campaigns", + params=params or None, + ))] diff --git a/sendpulse/generated/email/service/email_address_resource.py b/sendpulse/generated/email/service/email_address_resource.py new file mode 100644 index 0000000..4b0f0c7 --- /dev/null +++ b/sendpulse/generated/email/service/email_address_resource.py @@ -0,0 +1,72 @@ +from __future__ import annotations + +import json +from typing import Any + +from sendpulse.generated.email.model.EmailAcrossBooks import EmailAcrossBooks +from sendpulse.generated.email.model.EmailCampaignDeliveryInfo import EmailCampaignDeliveryInfo +from sendpulse.generated.email.model.EmailDetails import EmailDetails +from sendpulse.generated.email.model.ListContactInfo import ListContactInfo +from sendpulse.generated.email.model.ResultTrue import ResultTrue +from sendpulse.generated.email.model.SubscriberStats import SubscriberStats +from sendpulse.http.request import Request +from sendpulse.service.abstract import AbstractService + + +class EmailAddressResource(AbstractService): + def update_contact_variables(self, id: int, body: dict[str, Any] | list[Any] | None = None) -> ResultTrue: + return ResultTrue.from_dict(self._send(Request( + method="POST", + uri=f"/addressbooks/{id}/emails/variable", + body=json.dumps(body) if body else None, + ))) + + def get_email_info(self, email: str) -> list[EmailAcrossBooks]: + return [EmailAcrossBooks.from_dict(i) for i in self._send_list(Request( + method="GET", + uri=f"/emails/{email}", + ))] + + def delete_email_globally(self, email: str) -> ResultTrue: + return ResultTrue.from_dict(self._send(Request( + method="DELETE", + uri=f"/emails/{email}", + ))) + + def get_email_details(self, email: str) -> list[EmailDetails]: + return [EmailDetails.from_dict(i) for i in self._send_list(Request( + method="GET", + uri=f"/emails/{email}/details", + ))] + + def get_multiple_emails_info(self, body: dict[str, Any] | list[Any] | None = None) -> dict[str, Any]: + return self._send(Request( + method="POST", + uri="/emails", + body=json.dumps(body) if body else None, + )) + + def get_email_campaign_stats(self, email: str) -> SubscriberStats: + return SubscriberStats.from_dict(self._send(Request( + method="GET", + uri=f"/emails/{email}/campaigns", + ))) + + def get_email_campaign_info(self, id: int, email: str) -> EmailCampaignDeliveryInfo: + return EmailCampaignDeliveryInfo.from_dict(self._send(Request( + method="GET", + uri=f"/campaigns/{id}/email/{email}", + ))) + + def get_email_from_list(self, id: int, email: str) -> ListContactInfo: + return ListContactInfo.from_dict(self._send(Request( + method="GET", + uri=f"/addressbooks/{id}/emails/{email}", + ))) + + def get_multiple_emails_campaign_stats(self, body: dict[str, Any] | list[Any] | None = None) -> dict[str, Any]: + return self._send(Request( + method="POST", + uri="/emails/campaigns", + body=json.dumps(body) if body else None, + )) diff --git a/sendpulse/generated/email/service/email_service.py b/sendpulse/generated/email/service/email_service.py new file mode 100644 index 0000000..5ee00f9 --- /dev/null +++ b/sendpulse/generated/email/service/email_service.py @@ -0,0 +1,41 @@ +from __future__ import annotations + +from sendpulse.service.abstract import AbstractService +from sendpulse.generated.email.service.mailing_lists_resource import MailingListsResource +from sendpulse.generated.email.service.email_address_resource import EmailAddressResource +from sendpulse.generated.email.service.campaigns_resource import CampaignsResource +from sendpulse.generated.email.service.templates_resource import TemplatesResource +from sendpulse.generated.email.service.senders_resource import SendersResource +from sendpulse.generated.email.service.tags_resource import TagsResource +from sendpulse.generated.email.service.blacklist_resource import BlacklistResource +from sendpulse.generated.email.service.balance_resource import BalanceResource +from sendpulse.generated.email.service.webhooks_resource import WebhooksResource + + +class EmailService(AbstractService): + def mailing_lists(self) -> MailingListsResource: + return MailingListsResource(self._client) + + def email_address(self) -> EmailAddressResource: + return EmailAddressResource(self._client) + + def campaigns(self) -> CampaignsResource: + return CampaignsResource(self._client) + + def templates(self) -> TemplatesResource: + return TemplatesResource(self._client) + + def senders(self) -> SendersResource: + return SendersResource(self._client) + + def tags(self) -> TagsResource: + return TagsResource(self._client) + + def blacklist(self) -> BlacklistResource: + return BlacklistResource(self._client) + + def balance(self) -> BalanceResource: + return BalanceResource(self._client) + + def webhooks(self) -> WebhooksResource: + return WebhooksResource(self._client) diff --git a/sendpulse/generated/email/service/mailing_lists_resource.py b/sendpulse/generated/email/service/mailing_lists_resource.py new file mode 100644 index 0000000..1304bc7 --- /dev/null +++ b/sendpulse/generated/email/service/mailing_lists_resource.py @@ -0,0 +1,113 @@ +from __future__ import annotations + +import json +from typing import Any + +from sendpulse.generated.email.model.CampaignCost import CampaignCost +from sendpulse.generated.email.model.EmailContact import EmailContact +from sendpulse.generated.email.model.EmailContactBasic import EmailContactBasic +from sendpulse.generated.email.model.MailingList import MailingList +from sendpulse.generated.email.model.MailingListId import MailingListId +from sendpulse.generated.email.model.ResultTrue import ResultTrue +from sendpulse.generated.email.model.TotalCount import TotalCount +from sendpulse.generated.email.model.VariableDefinition import VariableDefinition +from sendpulse.http.request import Request +from sendpulse.service.abstract import AbstractService + + +class MailingListsResource(AbstractService): + def get_mailing_lists(self, limit: int | None = None, offset: int | None = None) -> list[MailingList]: + params = {k: v for k, v in {"limit": limit, "offset": offset}.items() if v is not None} + return [MailingList.from_dict(i) for i in self._send_list(Request( + method="GET", + uri="/addressbooks", + params=params or None, + ))] + + def create_mailing_list(self, body: dict[str, Any] | list[Any] | None = None) -> MailingListId: + return MailingListId.from_dict(self._send(Request( + method="POST", + uri="/addressbooks", + body=json.dumps(body) if body else None, + ))) + + def get_mailing_list_by_id(self, id: int) -> list[MailingList]: + return [MailingList.from_dict(i) for i in self._send_list(Request( + method="GET", + uri=f"/addressbooks/{id}", + ))] + + def update_mailing_list(self, id: int, body: dict[str, Any] | list[Any] | None = None) -> ResultTrue: + return ResultTrue.from_dict(self._send(Request( + method="PUT", + uri=f"/addressbooks/{id}", + body=json.dumps(body) if body else None, + ))) + + def delete_mailing_list(self, id: int) -> ResultTrue: + return ResultTrue.from_dict(self._send(Request( + method="DELETE", + uri=f"/addressbooks/{id}", + ))) + + def get_mailing_list_variables(self, id: int) -> list[VariableDefinition]: + return [VariableDefinition.from_dict(i) for i in self._send_list(Request( + method="GET", + uri=f"/addressbooks/{id}/variables", + ))] + + def get_emails_from_mailing_list(self, id: int, limit: int | None = None, offset: int | None = None, order: str | None = None, active: bool | None = None, not_active: bool | None = None) -> list[EmailContact]: + params = {k: v for k, v in {"limit": limit, "offset": offset, "order": order, "active": active, "not_active": not_active}.items() if v is not None} + return [EmailContact.from_dict(i) for i in self._send_list(Request( + method="GET", + uri=f"/addressbooks/{id}/emails", + params=params or None, + ))] + + def add_emails_to_mailing_list(self, id: int, body: dict[str, Any] | list[Any] | None = None) -> ResultTrue: + return ResultTrue.from_dict(self._send(Request( + method="POST", + uri=f"/addressbooks/{id}/emails", + body=json.dumps(body) if body else None, + ))) + + def delete_emails_from_mailing_list(self, id: int, body: dict[str, Any] | list[Any] | None = None) -> ResultTrue: + return ResultTrue.from_dict(self._send(Request( + method="DELETE", + uri=f"/addressbooks/{id}/emails", + body=json.dumps(body) if body else None, + ))) + + def get_emails_total_count(self, id: int, active: bool | None = None) -> TotalCount: + params = {k: v for k, v in {"active": active}.items() if v is not None} + return TotalCount.from_dict(self._send(Request( + method="GET", + uri=f"/addressbooks/{id}/emails/total", + params=params or None, + ))) + + def unsubscribe_emails_from_mailing_list(self, id: int, body: dict[str, Any] | list[Any] | None = None) -> ResultTrue: + return ResultTrue.from_dict(self._send(Request( + method="POST", + uri=f"/addressbooks/{id}/emails/unsubscribe", + body=json.dumps(body) if body else None, + ))) + + def get_contacts_by_variable(self, id: int, variableName: str, searchValue: str) -> list[EmailContactBasic]: + return [EmailContactBasic.from_dict(i) for i in self._send_list(Request( + method="GET", + uri=f"/addressbooks/{id}/variables/{variableName}/{searchValue}", + ))] + + def update_contact_phone(self, id: int, body: dict[str, Any] | list[Any] | None = None) -> ResultTrue: + return ResultTrue.from_dict(self._send(Request( + method="PUT", + uri=f"/addressbooks/{id}/phone", + body=json.dumps(body) if body else None, + ))) + + def get_campaign_cost_by_list(self, id: int) -> CampaignCost: + return CampaignCost.from_dict(self._send(Request( + method="GET", + uri=f"/addressbooks/{id}/cost", + ))) diff --git a/sendpulse/generated/email/service/senders_resource.py b/sendpulse/generated/email/service/senders_resource.py new file mode 100644 index 0000000..b92def3 --- /dev/null +++ b/sendpulse/generated/email/service/senders_resource.py @@ -0,0 +1,45 @@ +from __future__ import annotations + +import json +from typing import Any + +from sendpulse.generated.email.model.ResultTrue import ResultTrue +from sendpulse.generated.email.model.Sender import Sender +from sendpulse.generated.email.model.SenderActivationResponse import SenderActivationResponse +from sendpulse.http.request import Request +from sendpulse.service.abstract import AbstractService + + +class SendersResource(AbstractService): + def get_senders(self) -> list[Sender]: + return [Sender.from_dict(i) for i in self._send_list(Request( + method="GET", + uri="/senders", + ))] + + def add_sender(self, body: dict[str, Any] | list[Any] | None = None) -> ResultTrue: + return ResultTrue.from_dict(self._send(Request( + method="POST", + uri="/senders", + body=json.dumps(body) if body else None, + ))) + + def delete_sender(self, body: dict[str, Any] | list[Any] | None = None) -> ResultTrue: + return ResultTrue.from_dict(self._send(Request( + method="DELETE", + uri="/senders", + body=json.dumps(body) if body else None, + ))) + + def request_sender_activation_code(self, email: str) -> SenderActivationResponse: + return SenderActivationResponse.from_dict(self._send(Request( + method="GET", + uri=f"/senders/{email}/code", + ))) + + def activate_sender(self, email: str, body: dict[str, Any] | list[Any] | None = None) -> SenderActivationResponse: + return SenderActivationResponse.from_dict(self._send(Request( + method="POST", + uri=f"/senders/{email}/code", + body=json.dumps(body) if body else None, + ))) diff --git a/sendpulse/generated/email/service/tags_resource.py b/sendpulse/generated/email/service/tags_resource.py new file mode 100644 index 0000000..d6c4aa7 --- /dev/null +++ b/sendpulse/generated/email/service/tags_resource.py @@ -0,0 +1,65 @@ +from __future__ import annotations + +import json +from typing import Any + +from sendpulse.generated.email.model.TagListResponse import TagListResponse +from sendpulse.generated.email.model.TagQueueResponse import TagQueueResponse +from sendpulse.http.request import Request +from sendpulse.service.abstract import AbstractService + + +class TagsResource(AbstractService): + def get_tags(self) -> TagListResponse: + return TagListResponse.from_dict(self._send(Request( + method="GET", + uri="/tags", + ))) + + def create_tag(self, body: dict[str, Any] | list[Any] | None = None) -> TagQueueResponse: + return TagQueueResponse.from_dict(self._send(Request( + method="POST", + uri="/tags", + body=json.dumps(body) if body else None, + ))) + + def update_tag(self, id: int, body: dict[str, Any] | list[Any] | None = None) -> TagQueueResponse: + return TagQueueResponse.from_dict(self._send(Request( + method="PUT", + uri=f"/tags/{id}", + body=json.dumps(body) if body else None, + ))) + + def delete_tag(self, id: int) -> TagQueueResponse: + return TagQueueResponse.from_dict(self._send(Request( + method="DELETE", + uri=f"/tags/{id}", + ))) + + def pin_tag_to_email(self, body: dict[str, Any] | list[Any] | None = None) -> TagQueueResponse: + return TagQueueResponse.from_dict(self._send(Request( + method="POST", + uri="/tags/pin/email", + body=json.dumps(body) if body else None, + ))) + + def pin_tag_to_phone(self, body: dict[str, Any] | list[Any] | None = None) -> TagQueueResponse: + return TagQueueResponse.from_dict(self._send(Request( + method="POST", + uri="/tags/pin/phone", + body=json.dumps(body) if body else None, + ))) + + def unpin_tag_from_email(self, body: dict[str, Any] | list[Any] | None = None) -> TagQueueResponse: + return TagQueueResponse.from_dict(self._send(Request( + method="POST", + uri="/tags/unpin/email", + body=json.dumps(body) if body else None, + ))) + + def unpin_tag_from_phone(self, body: dict[str, Any] | list[Any] | None = None) -> TagQueueResponse: + return TagQueueResponse.from_dict(self._send(Request( + method="POST", + uri="/tags/unpin/phone", + body=json.dumps(body) if body else None, + ))) diff --git a/sendpulse/generated/email/service/templates_resource.py b/sendpulse/generated/email/service/templates_resource.py new file mode 100644 index 0000000..2d606cc --- /dev/null +++ b/sendpulse/generated/email/service/templates_resource.py @@ -0,0 +1,51 @@ +from __future__ import annotations + +import json +from typing import Any + +from sendpulse.generated.email.model.ResultTrue import ResultTrue +from sendpulse.generated.email.model.TemplateCreation import TemplateCreation +from sendpulse.generated.email.model.TemplateDetails import TemplateDetails +from sendpulse.generated.email.model.TemplateSummary import TemplateSummary +from sendpulse.http.request import Request +from sendpulse.service.abstract import AbstractService + + +class TemplatesResource(AbstractService): + def create_template(self, body: dict[str, Any] | list[Any] | None = None) -> TemplateCreation: + return TemplateCreation.from_dict(self._send(Request( + method="POST", + uri="/template", + body=json.dumps(body) if body else None, + ))) + + def update_template(self, id: int, body: dict[str, Any] | list[Any] | None = None) -> ResultTrue: + return ResultTrue.from_dict(self._send(Request( + method="POST", + uri=f"/template/edit/{id}", + body=json.dumps(body) if body else None, + ))) + + def get_template_by_id(self, template_id: str, owner: str | None = None, lang: str | None = None) -> TemplateDetails: + params = {k: v for k, v in {"owner": owner, "lang": lang}.items() if v is not None} + return TemplateDetails.from_dict(self._send(Request( + method="GET", + uri=f"/template/{template_id}", + params=params or None, + ))) + + def get_template_by_slug(self, name_slug: str, owner: str | None = None, lang: str | None = None) -> TemplateDetails: + params = {k: v for k, v in {"owner": owner, "lang": lang}.items() if v is not None} + return TemplateDetails.from_dict(self._send(Request( + method="GET", + uri=f"/template/slug/{name_slug}", + params=params or None, + ))) + + def get_templates(self, owner: str | None = None, only_active: bool | None = None, limit: int | None = None, offset: int | None = None) -> list[TemplateSummary]: + params = {k: v for k, v in {"owner": owner, "only_active": only_active, "limit": limit, "offset": offset}.items() if v is not None} + return [TemplateSummary.from_dict(i) for i in self._send_list(Request( + method="GET", + uri="/templates", + params=params or None, + ))] diff --git a/sendpulse/generated/email/service/webhooks_resource.py b/sendpulse/generated/email/service/webhooks_resource.py new file mode 100644 index 0000000..c1c10f8 --- /dev/null +++ b/sendpulse/generated/email/service/webhooks_resource.py @@ -0,0 +1,44 @@ +from __future__ import annotations + +import json +from typing import Any + +from sendpulse.generated.email.model.WebhookListResponse import WebhookListResponse +from sendpulse.generated.email.model.WebhookResponse import WebhookResponse +from sendpulse.generated.email.model.WebhookResult import WebhookResult +from sendpulse.http.request import Request +from sendpulse.service.abstract import AbstractService + + +class WebhooksResource(AbstractService): + def get_webhooks(self) -> WebhookListResponse: + return WebhookListResponse.from_dict(self._send(Request( + method="GET", + uri="/v2/email-service/webhook", + ))) + + def create_webhook(self, body: dict[str, Any] | list[Any] | None = None) -> WebhookListResponse: + return WebhookListResponse.from_dict(self._send(Request( + method="POST", + uri="/v2/email-service/webhook", + body=json.dumps(body) if body else None, + ))) + + def get_webhook_by_id(self, id: int) -> WebhookResponse: + return WebhookResponse.from_dict(self._send(Request( + method="GET", + uri=f"/v2/email-service/webhook/{id}", + ))) + + def update_webhook(self, id: int, body: dict[str, Any] | list[Any] | None = None) -> WebhookResult: + return WebhookResult.from_dict(self._send(Request( + method="PUT", + uri=f"/v2/email-service/webhook/{id}", + body=json.dumps(body) if body else None, + ))) + + def delete_webhook(self, id: int) -> WebhookResult: + return WebhookResult.from_dict(self._send(Request( + method="DELETE", + uri=f"/v2/email-service/webhook/{id}", + ))) diff --git a/sendpulse/generated/sms/__init__.py b/sendpulse/generated/sms/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/sendpulse/generated/sms/model/CampaignCreation.py b/sendpulse/generated/sms/model/CampaignCreation.py new file mode 100644 index 0000000..1dc6f10 --- /dev/null +++ b/sendpulse/generated/sms/model/CampaignCreation.py @@ -0,0 +1,17 @@ +from __future__ import annotations + +from dataclasses import dataclass +from typing import Any + + +@dataclass(slots=True) +class CampaignCreation: + result: bool | None = None + campaign_id: int | None = None + + @classmethod + def from_dict(cls, data: dict[str, Any]) -> CampaignCreation: + return cls( + result=data.get("result"), + campaign_id=data.get("campaign_id"), + ) diff --git a/sendpulse/generated/sms/model/CampaignDelivery.py b/sendpulse/generated/sms/model/CampaignDelivery.py new file mode 100644 index 0000000..47c7b8b --- /dev/null +++ b/sendpulse/generated/sms/model/CampaignDelivery.py @@ -0,0 +1,19 @@ +from __future__ import annotations + +from dataclasses import dataclass +from typing import Any + + +@dataclass(slots=True) +class CampaignDelivery: + result: bool | None = None + campaign_id: int | None = None + counters: dict[str, Any] | None = None + + @classmethod + def from_dict(cls, data: dict[str, Any]) -> CampaignDelivery: + return cls( + result=data.get("result"), + campaign_id=data.get("campaign_id"), + counters=data.get("counters"), + ) diff --git a/sendpulse/generated/sms/model/CostData.py b/sendpulse/generated/sms/model/CostData.py new file mode 100644 index 0000000..c961a53 --- /dev/null +++ b/sendpulse/generated/sms/model/CostData.py @@ -0,0 +1,17 @@ +from __future__ import annotations + +from dataclasses import dataclass +from typing import Any + + +@dataclass(slots=True) +class CostData: + price: float | None = None + currency: str | None = None + + @classmethod + def from_dict(cls, data: dict[str, Any]) -> CostData: + return cls( + price=data.get("price"), + currency=data.get("currency"), + ) diff --git a/sendpulse/generated/sms/model/CostEstimate.py b/sendpulse/generated/sms/model/CostEstimate.py new file mode 100644 index 0000000..012d814 --- /dev/null +++ b/sendpulse/generated/sms/model/CostEstimate.py @@ -0,0 +1,19 @@ +from __future__ import annotations + +from dataclasses import dataclass +from typing import Any + +from sendpulse.generated.sms.model.CostData import CostData + + +@dataclass(slots=True) +class CostEstimate: + result: bool | None = None + data: CostData | None = None + + @classmethod + def from_dict(cls, data: dict[str, Any]) -> CostEstimate: + return cls( + result=data.get("result"), + data=CostData.from_dict(data["data"]) if isinstance(data.get("data"), dict) else None, + ) diff --git a/sendpulse/generated/sms/model/NumbersImport.py b/sendpulse/generated/sms/model/NumbersImport.py new file mode 100644 index 0000000..1ade3b6 --- /dev/null +++ b/sendpulse/generated/sms/model/NumbersImport.py @@ -0,0 +1,17 @@ +from __future__ import annotations + +from dataclasses import dataclass +from typing import Any + + +@dataclass(slots=True) +class NumbersImport: + result: bool | None = None + counters: dict[str, Any] | None = None + + @classmethod + def from_dict(cls, data: dict[str, Any]) -> NumbersImport: + return cls( + result=data.get("result"), + counters=data.get("counters"), + ) diff --git a/sendpulse/generated/sms/model/NumbersRemoval.py b/sendpulse/generated/sms/model/NumbersRemoval.py new file mode 100644 index 0000000..b27b4ec --- /dev/null +++ b/sendpulse/generated/sms/model/NumbersRemoval.py @@ -0,0 +1,17 @@ +from __future__ import annotations + +from dataclasses import dataclass +from typing import Any + + +@dataclass(slots=True) +class NumbersRemoval: + result: bool | None = None + counters: dict[str, Any] | None = None + + @classmethod + def from_dict(cls, data: dict[str, Any]) -> NumbersRemoval: + return cls( + result=data.get("result"), + counters=data.get("counters"), + ) diff --git a/sendpulse/generated/sms/model/ResultTrue.py b/sendpulse/generated/sms/model/ResultTrue.py new file mode 100644 index 0000000..368a726 --- /dev/null +++ b/sendpulse/generated/sms/model/ResultTrue.py @@ -0,0 +1,15 @@ +from __future__ import annotations + +from dataclasses import dataclass +from typing import Any + + +@dataclass(slots=True) +class ResultTrue: + result: bool | None = None + + @classmethod + def from_dict(cls, data: dict[str, Any]) -> ResultTrue: + return cls( + result=data.get("result"), + ) diff --git a/sendpulse/generated/sms/model/__init__.py b/sendpulse/generated/sms/model/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/sendpulse/generated/sms/service/__init__.py b/sendpulse/generated/sms/service/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/sendpulse/generated/sms/service/campaigns_resource.py b/sendpulse/generated/sms/service/campaigns_resource.py new file mode 100644 index 0000000..fc1c712 --- /dev/null +++ b/sendpulse/generated/sms/service/campaigns_resource.py @@ -0,0 +1,62 @@ +from __future__ import annotations + +import json +from typing import Any + +from sendpulse.generated.sms.model.CampaignCreation import CampaignCreation +from sendpulse.generated.sms.model.CampaignDelivery import CampaignDelivery +from sendpulse.generated.sms.model.CostEstimate import CostEstimate +from sendpulse.generated.sms.model.ResultTrue import ResultTrue +from sendpulse.http.request import Request +from sendpulse.service.abstract import AbstractService + + +class CampaignsResource(AbstractService): + def create_sms_campaign(self, body: dict[str, Any] | list[Any] | None = None) -> CampaignCreation: + return CampaignCreation.from_dict(self._send(Request( + method="POST", + uri="/sms/campaigns", + body=json.dumps(body) if body else None, + ))) + + def delete_sms_campaign(self, body: dict[str, Any] | list[Any] | None = None) -> ResultTrue: + return ResultTrue.from_dict(self._send(Request( + method="DELETE", + uri="/sms/campaigns", + body=json.dumps(body) if body else None, + ))) + + def send_sms_to_numbers(self, body: dict[str, Any] | list[Any] | None = None) -> CampaignDelivery: + return CampaignDelivery.from_dict(self._send(Request( + method="POST", + uri="/sms/send", + body=json.dumps(body) if body else None, + ))) + + def get_sms_campaigns(self, dateFrom: str | None = None, dateTo: str | None = None) -> dict[str, Any]: + params = {k: v for k, v in {"dateFrom": dateFrom, "dateTo": dateTo}.items() if v is not None} + return self._send(Request( + method="GET", + uri="/sms/campaigns/list", + params=params or None, + )) + + def get_sms_campaign_info(self, id: int) -> dict[str, Any]: + return self._send(Request( + method="GET", + uri=f"/sms/campaigns/info/{id}", + )) + + def cancel_sms_campaign(self, id: int) -> ResultTrue: + return ResultTrue.from_dict(self._send(Request( + method="PUT", + uri=f"/sms/campaigns/cancel/{id}", + ))) + + def calculate_sms_cost(self, body: str, sender: str, addressBookId: int | None = None, phones: list[Any] | None = None, route: dict[str, Any] | None = None) -> CostEstimate: + params = {k: v for k, v in {"addressBookId": addressBookId, "phones": phones, "body": body, "sender": sender, "route": route}.items() if v is not None} + return CostEstimate.from_dict(self._send(Request( + method="GET", + uri="/sms/campaigns/cost", + params=params or None, + ))) diff --git a/sendpulse/generated/sms/service/compliance_resource.py b/sendpulse/generated/sms/service/compliance_resource.py new file mode 100644 index 0000000..648eec3 --- /dev/null +++ b/sendpulse/generated/sms/service/compliance_resource.py @@ -0,0 +1,29 @@ +from __future__ import annotations + +import json +from typing import Any + +from sendpulse.http.request import Request +from sendpulse.service.abstract import AbstractService + + +class ComplianceResource(AbstractService): + def get_sms_blacklist(self) -> dict[str, Any]: + return self._send(Request( + method="GET", + uri="/sms/black_list", + )) + + def add_sms_blacklist(self, body: dict[str, Any] | list[Any] | None = None) -> dict[str, Any]: + return self._send(Request( + method="POST", + uri="/sms/black_list", + body=json.dumps(body) if body else None, + )) + + def remove_sms_blacklist(self, body: dict[str, Any] | list[Any] | None = None) -> dict[str, Any]: + return self._send(Request( + method="DELETE", + uri="/sms/black_list", + body=json.dumps(body) if body else None, + )) diff --git a/sendpulse/generated/sms/service/configuration_resource.py b/sendpulse/generated/sms/service/configuration_resource.py new file mode 100644 index 0000000..0398714 --- /dev/null +++ b/sendpulse/generated/sms/service/configuration_resource.py @@ -0,0 +1,14 @@ +from __future__ import annotations + +from typing import Any + +from sendpulse.http.request import Request +from sendpulse.service.abstract import AbstractService + + +class ConfigurationResource(AbstractService): + def get_sms_senders(self) -> dict[str, Any]: + return self._send(Request( + method="GET", + uri="/sms/senders", + )) diff --git a/sendpulse/generated/sms/service/contacts_resource.py b/sendpulse/generated/sms/service/contacts_resource.py new file mode 100644 index 0000000..88af6dc --- /dev/null +++ b/sendpulse/generated/sms/service/contacts_resource.py @@ -0,0 +1,60 @@ +from __future__ import annotations + +import json +from typing import Any + +from sendpulse.generated.sms.model.NumbersImport import NumbersImport +from sendpulse.generated.sms.model.NumbersRemoval import NumbersRemoval +from sendpulse.generated.sms.model.ResultTrue import ResultTrue +from sendpulse.http.request import Request +from sendpulse.service.abstract import AbstractService + + +class ContactsResource(AbstractService): + def add_sms_numbers(self, body: dict[str, Any] | list[Any] | None = None) -> NumbersImport: + return NumbersImport.from_dict(self._send(Request( + method="POST", + uri="/sms/numbers", + body=json.dumps(body) if body else None, + ))) + + def update_sms_variables_batch(self, body: dict[str, Any] | list[Any] | None = None) -> dict[str, Any]: + return self._send(Request( + method="PUT", + uri="/sms/numbers", + body=json.dumps(body) if body else None, + )) + + def remove_sms_numbers(self, body: dict[str, Any] | list[Any] | None = None) -> NumbersRemoval: + return NumbersRemoval.from_dict(self._send(Request( + method="DELETE", + uri="/sms/numbers", + body=json.dumps(body) if body else None, + ))) + + def add_sms_numbers_with_variables(self, body: dict[str, Any] | list[Any] | None = None) -> dict[str, Any]: + return self._send(Request( + method="POST", + uri="/sms/numbers/variables", + body=json.dumps(body) if body else None, + )) + + def update_contact_phone(self, id: int, body: dict[str, Any] | list[Any] | None = None) -> ResultTrue: + return ResultTrue.from_dict(self._send(Request( + method="PUT", + uri=f"/addressbooks/{id}/phone", + body=json.dumps(body) if body else None, + ))) + + def update_sms_variables_single(self, id: int, body: dict[str, Any] | list[Any] | None = None) -> ResultTrue: + return ResultTrue.from_dict(self._send(Request( + method="POST", + uri=f"/addressbooks/{id}/phones/variable", + body=json.dumps(body) if body else None, + ))) + + def get_sms_number_info(self, addressBookId: int, phoneNumber: str) -> dict[str, Any]: + return self._send(Request( + method="GET", + uri=f"/sms/numbers/info/{addressBookId}/{phoneNumber}", + )) diff --git a/sendpulse/generated/sms/service/sms_service.py b/sendpulse/generated/sms/service/sms_service.py new file mode 100644 index 0000000..433bbe8 --- /dev/null +++ b/sendpulse/generated/sms/service/sms_service.py @@ -0,0 +1,21 @@ +from __future__ import annotations + +from sendpulse.service.abstract import AbstractService +from sendpulse.generated.sms.service.contacts_resource import ContactsResource +from sendpulse.generated.sms.service.compliance_resource import ComplianceResource +from sendpulse.generated.sms.service.campaigns_resource import CampaignsResource +from sendpulse.generated.sms.service.configuration_resource import ConfigurationResource + + +class SmsService(AbstractService): + def contacts(self) -> ContactsResource: + return ContactsResource(self._client) + + def compliance(self) -> ComplianceResource: + return ComplianceResource(self._client) + + def campaigns(self) -> CampaignsResource: + return CampaignsResource(self._client) + + def configuration(self) -> ConfigurationResource: + return ConfigurationResource(self._client) diff --git a/sendpulse/generated/smtp/__init__.py b/sendpulse/generated/smtp/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/sendpulse/generated/smtp/model/BounceEmail.py b/sendpulse/generated/smtp/model/BounceEmail.py new file mode 100644 index 0000000..5f16303 --- /dev/null +++ b/sendpulse/generated/smtp/model/BounceEmail.py @@ -0,0 +1,27 @@ +from __future__ import annotations + +from dataclasses import dataclass +from typing import Any + + +@dataclass(slots=True) +class BounceEmail: + email_to: str | None = None + sender: str | None = None + send_date: str | None = None + subject: str | None = None + smtp_answer_code: int | None = None + smtp_answer_subcode: str | None = None + smtp_answer_data: str | None = None + + @classmethod + def from_dict(cls, data: dict[str, Any]) -> BounceEmail: + return cls( + email_to=data.get("email_to"), + sender=data.get("sender"), + send_date=data.get("send_date"), + subject=data.get("subject"), + smtp_answer_code=data.get("smtp_answer_code"), + smtp_answer_subcode=data.get("smtp_answer_subcode"), + smtp_answer_data=data.get("smtp_answer_data"), + ) diff --git a/sendpulse/generated/smtp/model/BounceReport.py b/sendpulse/generated/smtp/model/BounceReport.py new file mode 100644 index 0000000..7c72799 --- /dev/null +++ b/sendpulse/generated/smtp/model/BounceReport.py @@ -0,0 +1,23 @@ +from __future__ import annotations + +from dataclasses import dataclass +from typing import Any + +from sendpulse.generated.smtp.model.BounceEmail import BounceEmail + + +@dataclass(slots=True) +class BounceReport: + total: int | None = None + emails: list[BounceEmail] | None = None + request_limit: int | None = None + found: int | None = None + + @classmethod + def from_dict(cls, data: dict[str, Any]) -> BounceReport: + return cls( + total=data.get("total"), + emails=[BounceEmail.from_dict(i) for i in data["emails"]] if isinstance(data.get("emails"), list) else None, + request_limit=data.get("request_limit"), + found=data.get("found"), + ) diff --git a/sendpulse/generated/smtp/model/DomainList.py b/sendpulse/generated/smtp/model/DomainList.py new file mode 100644 index 0000000..9f89fd4 --- /dev/null +++ b/sendpulse/generated/smtp/model/DomainList.py @@ -0,0 +1,17 @@ +from __future__ import annotations + +from dataclasses import dataclass +from typing import Any + +from sendpulse.generated.smtp.model.DomainListData import DomainListData + + +@dataclass(slots=True) +class DomainList: + data: DomainListData | None = None + + @classmethod + def from_dict(cls, data: dict[str, Any]) -> DomainList: + return cls( + data=DomainListData.from_dict(data["data"]) if isinstance(data.get("data"), dict) else None, + ) diff --git a/sendpulse/generated/smtp/model/DomainListData.py b/sendpulse/generated/smtp/model/DomainListData.py new file mode 100644 index 0000000..201dc0e --- /dev/null +++ b/sendpulse/generated/smtp/model/DomainListData.py @@ -0,0 +1,19 @@ +from __future__ import annotations + +from dataclasses import dataclass +from typing import Any + +from sendpulse.generated.smtp.model.DomainRecord import DomainRecord + + +@dataclass(slots=True) +class DomainListData: + result: bool | None = None + data: list[DomainRecord] | None = None + + @classmethod + def from_dict(cls, data: dict[str, Any]) -> DomainListData: + return cls( + result=data.get("result"), + data=[DomainRecord.from_dict(i) for i in data["data"]] if isinstance(data.get("data"), list) else None, + ) diff --git a/sendpulse/generated/smtp/model/DomainRecord.py b/sendpulse/generated/smtp/model/DomainRecord.py new file mode 100644 index 0000000..869bf6c --- /dev/null +++ b/sendpulse/generated/smtp/model/DomainRecord.py @@ -0,0 +1,29 @@ +from __future__ import annotations + +from dataclasses import dataclass +from typing import Any + + +@dataclass(slots=True) +class DomainRecord: + id: int | None = None + user_id: int | None = None + service_type: int | None = None + service_value: str | None = None + status: int | None = None + is_default: bool | None = None + ssl_type: int | None = None + checks: dict[str, Any] | None = None + + @classmethod + def from_dict(cls, data: dict[str, Any]) -> DomainRecord: + return cls( + id=data.get("id"), + user_id=data.get("user_id"), + service_type=data.get("service_type"), + service_value=data.get("service_value"), + status=data.get("status"), + is_default=data.get("is_default"), + ssl_type=data.get("ssl_type"), + checks=data.get("checks"), + ) diff --git a/sendpulse/generated/smtp/model/DomainResult.py b/sendpulse/generated/smtp/model/DomainResult.py new file mode 100644 index 0000000..dffac09 --- /dev/null +++ b/sendpulse/generated/smtp/model/DomainResult.py @@ -0,0 +1,17 @@ +from __future__ import annotations + +from dataclasses import dataclass +from typing import Any + +from sendpulse.generated.smtp.model.ResultTrue import ResultTrue + + +@dataclass(slots=True) +class DomainResult: + data: ResultTrue | None = None + + @classmethod + def from_dict(cls, data: dict[str, Any]) -> DomainResult: + return cls( + data=ResultTrue.from_dict(data["data"]) if isinstance(data.get("data"), dict) else None, + ) diff --git a/sendpulse/generated/smtp/model/EmailRecord.py b/sendpulse/generated/smtp/model/EmailRecord.py new file mode 100644 index 0000000..bcaaf61 --- /dev/null +++ b/sendpulse/generated/smtp/model/EmailRecord.py @@ -0,0 +1,37 @@ +from __future__ import annotations + +from dataclasses import dataclass +from typing import Any + + +@dataclass(slots=True) +class EmailRecord: + id: str | None = None + sender: str | None = None + total_size: int | None = None + sender_ip: str | None = None + smtp_answer_code: int | None = None + smtp_answer_subcode: str | None = None + smtp_answer_data: str | None = None + used_ip: str | None = None + recipient: str | None = None + subject: str | None = None + send_date: str | None = None + tracking: dict[str, Any] | None = None + + @classmethod + def from_dict(cls, data: dict[str, Any]) -> EmailRecord: + return cls( + id=data.get("id"), + sender=data.get("sender"), + total_size=data.get("total_size"), + sender_ip=data.get("sender_ip"), + smtp_answer_code=data.get("smtp_answer_code"), + smtp_answer_subcode=data.get("smtp_answer_subcode"), + smtp_answer_data=data.get("smtp_answer_data"), + used_ip=data.get("used_ip"), + recipient=data.get("recipient"), + subject=data.get("subject"), + send_date=data.get("send_date"), + tracking=data.get("tracking"), + ) diff --git a/sendpulse/generated/smtp/model/Error.py b/sendpulse/generated/smtp/model/Error.py new file mode 100644 index 0000000..8d62660 --- /dev/null +++ b/sendpulse/generated/smtp/model/Error.py @@ -0,0 +1,17 @@ +from __future__ import annotations + +from dataclasses import dataclass +from typing import Any + + +@dataclass(slots=True) +class Error: + message: str | None = None + error_code: int | None = None + + @classmethod + def from_dict(cls, data: dict[str, Any]) -> Error: + return cls( + message=data.get("message"), + error_code=data.get("error_code"), + ) diff --git a/sendpulse/generated/smtp/model/ResultTrue.py b/sendpulse/generated/smtp/model/ResultTrue.py new file mode 100644 index 0000000..368a726 --- /dev/null +++ b/sendpulse/generated/smtp/model/ResultTrue.py @@ -0,0 +1,15 @@ +from __future__ import annotations + +from dataclasses import dataclass +from typing import Any + + +@dataclass(slots=True) +class ResultTrue: + result: bool | None = None + + @classmethod + def from_dict(cls, data: dict[str, Any]) -> ResultTrue: + return cls( + result=data.get("result"), + ) diff --git a/sendpulse/generated/smtp/model/SentEmail.py b/sendpulse/generated/smtp/model/SentEmail.py new file mode 100644 index 0000000..13db871 --- /dev/null +++ b/sendpulse/generated/smtp/model/SentEmail.py @@ -0,0 +1,17 @@ +from __future__ import annotations + +from dataclasses import dataclass +from typing import Any + + +@dataclass(slots=True) +class SentEmail: + result: bool | None = None + id: str | None = None + + @classmethod + def from_dict(cls, data: dict[str, Any]) -> SentEmail: + return cls( + result=data.get("result"), + id=data.get("id"), + ) diff --git a/sendpulse/generated/smtp/model/TotalCount.py b/sendpulse/generated/smtp/model/TotalCount.py new file mode 100644 index 0000000..8059d15 --- /dev/null +++ b/sendpulse/generated/smtp/model/TotalCount.py @@ -0,0 +1,15 @@ +from __future__ import annotations + +from dataclasses import dataclass +from typing import Any + + +@dataclass(slots=True) +class TotalCount: + total: int | None = None + + @classmethod + def from_dict(cls, data: dict[str, Any]) -> TotalCount: + return cls( + total=data.get("total"), + ) diff --git a/sendpulse/generated/smtp/model/__init__.py b/sendpulse/generated/smtp/model/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/sendpulse/generated/smtp/service/__init__.py b/sendpulse/generated/smtp/service/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/sendpulse/generated/smtp/service/bounces_resource.py b/sendpulse/generated/smtp/service/bounces_resource.py new file mode 100644 index 0000000..e87a65c --- /dev/null +++ b/sendpulse/generated/smtp/service/bounces_resource.py @@ -0,0 +1,24 @@ +from __future__ import annotations + +from typing import Any + +from sendpulse.generated.smtp.model.BounceReport import BounceReport +from sendpulse.generated.smtp.model.TotalCount import TotalCount +from sendpulse.http.request import Request +from sendpulse.service.abstract import AbstractService + + +class BouncesResource(AbstractService): + def get_bounce_report(self, date: str | None = None, limit: int | None = None, offset: int | None = None) -> BounceReport: + params = {k: v for k, v in {"date": date, "limit": limit, "offset": offset}.items() if v is not None} + return BounceReport.from_dict(self._send(Request( + method="GET", + uri="/smtp/bounces/day", + params=params or None, + ))) + + def get_smtp_bounces_total(self) -> TotalCount: + return TotalCount.from_dict(self._send(Request( + method="GET", + uri="/smtp/bounces/day/total", + ))) diff --git a/sendpulse/generated/smtp/service/configuration_resource.py b/sendpulse/generated/smtp/service/configuration_resource.py new file mode 100644 index 0000000..d9243ef --- /dev/null +++ b/sendpulse/generated/smtp/service/configuration_resource.py @@ -0,0 +1,43 @@ +from __future__ import annotations + +import json +from typing import Any + +from sendpulse.generated.smtp.model.DomainList import DomainList +from sendpulse.generated.smtp.model.DomainResult import DomainResult +from sendpulse.generated.smtp.model.ResultTrue import ResultTrue +from sendpulse.http.request import Request +from sendpulse.service.abstract import AbstractService + + +class ConfigurationResource(AbstractService): + def get_smtp_ips(self) -> dict[str, Any]: + return self._send(Request( + method="GET", + uri="/smtp/ips", + )) + + def get_smtp_senders(self) -> dict[str, Any]: + return self._send(Request( + method="GET", + uri="/smtp/senders", + )) + + def get_smtp_allowed_domains(self) -> DomainList: + return DomainList.from_dict(self._send(Request( + method="GET", + uri="/v2/email-service/smtp/sender_domains", + ))) + + def add_smtp_sender(self, body: dict[str, Any] | list[Any] | None = None) -> ResultTrue: + return ResultTrue.from_dict(self._send(Request( + method="POST", + uri="/senders", + body=json.dumps(body) if body else None, + ))) + + def add_smtp_domain(self, domain: str) -> DomainResult: + return DomainResult.from_dict(self._send(Request( + method="POST", + uri=f"/v2/email-service/smtp/sender_domains/{domain}", + ))) diff --git a/sendpulse/generated/smtp/service/emails_resource.py b/sendpulse/generated/smtp/service/emails_resource.py new file mode 100644 index 0000000..0bdc5bc --- /dev/null +++ b/sendpulse/generated/smtp/service/emails_resource.py @@ -0,0 +1,46 @@ +from __future__ import annotations + +import json +from typing import Any + +from sendpulse.generated.smtp.model.EmailRecord import EmailRecord +from sendpulse.generated.smtp.model.SentEmail import SentEmail +from sendpulse.generated.smtp.model.TotalCount import TotalCount +from sendpulse.http.request import Request +from sendpulse.service.abstract import AbstractService + + +class EmailsResource(AbstractService): + def get_smtp_emails(self, limit: int | None = None, offset: int | None = None, from_: str | None = None, to: str | None = None, sender: str | None = None, recipient: str | None = None, country: str | None = None) -> list[EmailRecord]: + params = {k: v for k, v in {"limit": limit, "offset": offset, "from": from_, "to": to, "sender": sender, "recipient": recipient, "country": country}.items() if v is not None} + return [EmailRecord.from_dict(i) for i in self._send_list(Request( + method="GET", + uri="/smtp/emails", + params=params or None, + ))] + + def send_smtp_email(self, body: dict[str, Any] | list[Any] | None = None) -> SentEmail: + return SentEmail.from_dict(self._send(Request( + method="POST", + uri="/smtp/emails", + body=json.dumps(body) if body else None, + ))) + + def get_smtp_emails_total(self) -> TotalCount: + return TotalCount.from_dict(self._send(Request( + method="GET", + uri="/smtp/emails/total", + ))) + + def get_smtp_email_info(self, id: str) -> EmailRecord: + return EmailRecord.from_dict(self._send(Request( + method="GET", + uri=f"/smtp/emails/{id}", + ))) + + def get_smtp_emails_batch_info(self, body: dict[str, Any] | list[Any] | None = None) -> list[EmailRecord]: + return [EmailRecord.from_dict(i) for i in self._send_list(Request( + method="POST", + uri="/smtp/emails/info", + body=json.dumps(body) if body else None, + ))] diff --git a/sendpulse/generated/smtp/service/smtp_service.py b/sendpulse/generated/smtp/service/smtp_service.py new file mode 100644 index 0000000..29ace29 --- /dev/null +++ b/sendpulse/generated/smtp/service/smtp_service.py @@ -0,0 +1,21 @@ +from __future__ import annotations + +from sendpulse.service.abstract import AbstractService +from sendpulse.generated.smtp.service.emails_resource import EmailsResource +from sendpulse.generated.smtp.service.bounces_resource import BouncesResource +from sendpulse.generated.smtp.service.unsubscribe_resource import UnsubscribeResource +from sendpulse.generated.smtp.service.configuration_resource import ConfigurationResource + + +class SmtpService(AbstractService): + def emails(self) -> EmailsResource: + return EmailsResource(self._client) + + def bounces(self) -> BouncesResource: + return BouncesResource(self._client) + + def unsubscribe(self) -> UnsubscribeResource: + return UnsubscribeResource(self._client) + + def configuration(self) -> ConfigurationResource: + return ConfigurationResource(self._client) diff --git a/sendpulse/generated/smtp/service/unsubscribe_resource.py b/sendpulse/generated/smtp/service/unsubscribe_resource.py new file mode 100644 index 0000000..d5cc7de --- /dev/null +++ b/sendpulse/generated/smtp/service/unsubscribe_resource.py @@ -0,0 +1,48 @@ +from __future__ import annotations + +import json +from typing import Any + +from sendpulse.generated.smtp.model.ResultTrue import ResultTrue +from sendpulse.generated.smtp.model.SentEmail import SentEmail +from sendpulse.http.request import Request +from sendpulse.service.abstract import AbstractService + + +class UnsubscribeResource(AbstractService): + def get_smtp_unsubscribed(self, date: str | None = None, limit: int | None = None, offset: int | None = None) -> dict[str, Any]: + params = {k: v for k, v in {"date": date, "limit": limit, "offset": offset}.items() if v is not None} + return self._send(Request( + method="GET", + uri="/smtp/unsubscribe", + params=params or None, + )) + + def unsubscribe_smtp_recipients(self, body: dict[str, Any] | list[Any] | None = None) -> ResultTrue: + return ResultTrue.from_dict(self._send(Request( + method="POST", + uri="/smtp/unsubscribe", + body=json.dumps(body) if body else None, + ))) + + def remove_smtp_unsubscribe(self, body: dict[str, Any] | list[Any] | None = None) -> ResultTrue: + return ResultTrue.from_dict(self._send(Request( + method="DELETE", + uri="/smtp/unsubscribe", + body=json.dumps(body) if body else None, + ))) + + def search_smtp_unsubscribe(self, email: str) -> ResultTrue: + params = {k: v for k, v in {"email": email}.items() if v is not None} + return ResultTrue.from_dict(self._send(Request( + method="GET", + uri="/smtp/unsubscribe/search", + params=params or None, + ))) + + def resubscribe_smtp_recipient(self, body: dict[str, Any] | list[Any] | None = None) -> SentEmail: + return SentEmail.from_dict(self._send(Request( + method="POST", + uri="/smtp/resubscribe", + body=json.dumps(body) if body else None, + ))) diff --git a/sendpulse/http/__init__.py b/sendpulse/http/__init__.py new file mode 100644 index 0000000..a86b0ba --- /dev/null +++ b/sendpulse/http/__init__.py @@ -0,0 +1,5 @@ +from sendpulse.http.httpx_client import HttpxClient +from sendpulse.http.request import Request +from sendpulse.http.response import Response + +__all__ = ["Request", "Response", "HttpxClient"] diff --git a/sendpulse/http/httpx_client.py b/sendpulse/http/httpx_client.py new file mode 100644 index 0000000..8e96f0c --- /dev/null +++ b/sendpulse/http/httpx_client.py @@ -0,0 +1,47 @@ +from __future__ import annotations + +import httpx + +from sendpulse.exception.exceptions import NetworkException +from sendpulse.http.request import Request +from sendpulse.http.response import Response + + +class HttpxClient: + def __init__( + self, + connect_timeout: float = 10.0, + request_timeout: float = 30.0, + ) -> None: + self._client = httpx.Client( + timeout=httpx.Timeout(request_timeout, connect=connect_timeout), + verify=True, + follow_redirects=False, + ) + + def send(self, request: Request) -> Response: + try: + r = self._client.request( + method=request.method, + url=request.uri, + headers=request.headers, + content=request.body, + params=request.params, + ) + except httpx.TransportError as e: + raise NetworkException(str(e)) from e + + return Response( + status_code=r.status_code, + headers=dict(r.headers), + body=r.text, + ) + + def close(self) -> None: + self._client.close() + + def __enter__(self) -> HttpxClient: + return self + + def __exit__(self, *args: object) -> None: + self.close() diff --git a/sendpulse/http/protocol.py b/sendpulse/http/protocol.py new file mode 100644 index 0000000..6553885 --- /dev/null +++ b/sendpulse/http/protocol.py @@ -0,0 +1,10 @@ +from __future__ import annotations + +from typing import Protocol + +from sendpulse.http.request import Request +from sendpulse.http.response import Response + + +class HttpClient(Protocol): + def send(self, request: Request) -> Response: ... diff --git a/sendpulse/http/request.py b/sendpulse/http/request.py new file mode 100644 index 0000000..cfc6ddc --- /dev/null +++ b/sendpulse/http/request.py @@ -0,0 +1,13 @@ +from __future__ import annotations + +from dataclasses import dataclass, field +from typing import Any + + +@dataclass(slots=True) +class Request: + method: str + uri: str + headers: dict[str, str] = field(default_factory=dict) + body: str | None = None + params: dict[str, Any] | None = None diff --git a/sendpulse/http/response.py b/sendpulse/http/response.py new file mode 100644 index 0000000..743a3b2 --- /dev/null +++ b/sendpulse/http/response.py @@ -0,0 +1,10 @@ +from __future__ import annotations + +from dataclasses import dataclass + + +@dataclass(slots=True) +class Response: + status_code: int + headers: dict[str, str] + body: str diff --git a/sendpulse/response/__init__.py b/sendpulse/response/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/sendpulse/response/validator.py b/sendpulse/response/validator.py new file mode 100644 index 0000000..96a34e0 --- /dev/null +++ b/sendpulse/response/validator.py @@ -0,0 +1,41 @@ +from __future__ import annotations + +import json +from typing import Any + +from sendpulse.exception.exceptions import ( + ApiException, + AuthException, + ProtocolException, + RateLimitException, +) +from sendpulse.http.response import Response + + +class ResponseValidator: + def validate(self, response: Response) -> dict[str, Any] | list[Any]: + status = response.status_code + + if 200 <= status < 300: + return self._decode(response) + + if status in (401, 403): + raise AuthException(status, response.body) + + if status == 429: + raise RateLimitException(status, response.body) + + raise ApiException(status, response.body) + + def _decode(self, response: Response) -> dict[str, Any] | list[Any]: + if not response.body: + return {} + + try: + data = json.loads(response.body) + except json.JSONDecodeError as e: + raise ProtocolException(f"Failed to decode response body: {e}") from e + + if isinstance(data, (dict, list)): + return data + return {} diff --git a/sendpulse/service/__init__.py b/sendpulse/service/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/sendpulse/service/abstract.py b/sendpulse/service/abstract.py new file mode 100644 index 0000000..4098659 --- /dev/null +++ b/sendpulse/service/abstract.py @@ -0,0 +1,20 @@ +from __future__ import annotations + +from typing import TYPE_CHECKING, Any + +if TYPE_CHECKING: + from sendpulse.client import Client + from sendpulse.http.request import Request + + +class AbstractService: + def __init__(self, client: Client) -> None: + self._client = client + + def _send(self, request: Request) -> dict[str, Any]: + result = self._client.send(request) + return result if isinstance(result, dict) else {} + + def _send_list(self, request: Request) -> list[Any]: + result = self._client.send(request) + return result if isinstance(result, list) else [] diff --git a/setup.py b/setup.py deleted file mode 100644 index a9fd64c..0000000 --- a/setup.py +++ /dev/null @@ -1,28 +0,0 @@ -from setuptools import setup, find_packages -from sys import version_info -from pysendpulse import ( - __author__, - __author_email__, - __version__ -) - -install_requires = ['python3-memcached', 'requests', 'deprecated'] - -if version_info.major == 2: - install_requires = ['python-memcached', 'requests', 'simplejson'] - -with open("README.md", "r") as fh: - long_description = fh.read() - -setup( - name='pysendpulse', - version=__version__, - packages=find_packages(), - description='A simple SendPulse REST client library and example for Python', - long_description=long_description, - long_description_content_type="text/markdown", - author=__author__, - author_email=__author_email__, - url='https://github.com/sendpulse/sendpulse-rest-api-python', - install_requires=install_requires -) diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/fixture/__init__.py b/tests/fixture/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/fixture/fake_http_client.py b/tests/fixture/fake_http_client.py new file mode 100644 index 0000000..9b89f64 --- /dev/null +++ b/tests/fixture/fake_http_client.py @@ -0,0 +1,30 @@ +from __future__ import annotations + +from sendpulse.http.request import Request +from sendpulse.http.response import Response + + +class FakeHttpClient: + def __init__(self) -> None: + self._queue: list[Response] = [] + self._sent: list[Request] = [] + + def queue(self, *responses: Response) -> None: + self._queue.extend(responses) + + def send(self, request: Request) -> Response: + self._sent.append(request) + if not self._queue: + raise RuntimeError("FakeHttpClient: no response queued") + return self._queue.pop(0) + + def last_request(self) -> Request: + if not self._sent: + raise RuntimeError("FakeHttpClient: no request sent") + return self._sent[-1] + + def all_requests(self) -> list[Request]: + return list(self._sent) + + def call_count(self) -> int: + return len(self._sent) diff --git a/tests/test_client.py b/tests/test_client.py new file mode 100644 index 0000000..933d7e3 --- /dev/null +++ b/tests/test_client.py @@ -0,0 +1,169 @@ +from __future__ import annotations + +import json + +import pytest + +from sendpulse.auth.token_storage import InMemoryTokenStorage +from sendpulse.client import Client +from sendpulse.exception.exceptions import ApiException, AuthException +from sendpulse.http.request import Request +from sendpulse.http.response import Response +from tests.fixture.fake_http_client import FakeHttpClient + + +def _ok(body: dict[str, object]) -> Response: + return Response(200, {}, json.dumps(body)) + + +def _make_client(http: FakeHttpClient, *, oauth: bool = False) -> Client: + if oauth: + storage = InMemoryTokenStorage() + http.queue(Response(200, {}, json.dumps({ + "access_token": "tok", "token_type": "Bearer", "expires_in": 3600, + }))) + return Client( + client_id="id", client_secret="secret", + http_client=http, token_storage=storage, + ) + return Client(api_key="test-key", http_client=http) + + +# ── Authorization header ────────────────────────────────────────────────────── + +def test_api_key_sets_bearer_header() -> None: + http = FakeHttpClient() + http.queue(_ok({})) + client = _make_client(http) + + client.send(Request(method="GET", uri="/test")) + + assert http.last_request().headers["Authorization"] == "Bearer test-key" + + +def test_oauth_sets_bearer_header() -> None: + http = FakeHttpClient() + client = _make_client(http, oauth=True) # queues token fetch first + http.queue(_ok({})) + + client.send(Request(method="GET", uri="/test")) + + req = http.last_request() + assert req.headers["Authorization"] == "Bearer tok" + + +# ── URI construction ────────────────────────────────────────────────────────── + +def test_base_url_is_prepended_to_uri() -> None: + http = FakeHttpClient() + http.queue(_ok({})) + client = Client(api_key="key", http_client=http) + + client.send(Request(method="GET", uri="/smtp/emails")) + + assert http.last_request().uri == "https://api.sendpulse.com/smtp/emails" + + +def test_leading_slash_on_uri_is_not_doubled() -> None: + http = FakeHttpClient() + http.queue(_ok({})) + client = Client(api_key="key", http_client=http) + + client.send(Request(method="GET", uri="/smtp/emails")) + + assert "//smtp" not in http.last_request().uri + + +# ── Default headers ─────────────────────────────────────────────────────────── + +def test_content_type_and_accept_headers_are_set() -> None: + http = FakeHttpClient() + http.queue(_ok({})) + client = _make_client(http) + + client.send(Request(method="POST", uri="/test")) + + req = http.last_request() + assert req.headers["Content-Type"] == "application/json" + assert req.headers["Accept"] == "application/json" + + +def test_caller_headers_override_defaults() -> None: + http = FakeHttpClient() + http.queue(_ok({})) + client = _make_client(http) + + client.send(Request(method="GET", uri="/test", headers={"Accept": "text/plain"})) + + assert http.last_request().headers["Accept"] == "text/plain" + + +# ── 401 retry with OAuth ────────────────────────────────────────────────────── + +def test_oauth_retries_on_401() -> None: + http = FakeHttpClient() + def _token(t: str) -> Response: + return Response(200, {}, json.dumps( + {"access_token": t, "token_type": "Bearer", "expires_in": 3600} + )) + + # token fetch → 401 on first call → token refresh → success + http.queue( + _token("tok1"), + Response(401, {}, "Unauthorized"), + _token("tok2"), + _ok({"ok": True}), + ) + storage = InMemoryTokenStorage() + client = Client(client_id="id", client_secret="secret", http_client=http, token_storage=storage) + + result = client.send(Request(method="GET", uri="/test")) + + assert result == {"ok": True} + assert http.call_count() == 4 # token + original + token refresh + retry + + +def test_api_key_does_not_retry_on_401() -> None: + http = FakeHttpClient() + http.queue(Response(401, {}, "Unauthorized")) + client = _make_client(http) + + with pytest.raises(AuthException): + client.send(Request(method="GET", uri="/test")) + + assert http.call_count() == 1 + + +# ── Response parsing ────────────────────────────────────────────────────────── + +def test_returns_parsed_json_body() -> None: + http = FakeHttpClient() + http.queue(_ok({"id": 42, "name": "test"})) + client = _make_client(http) + + result = client.send(Request(method="GET", uri="/test")) + + assert result == {"id": 42, "name": "test"} + + +def test_raises_api_exception_on_500() -> None: + http = FakeHttpClient() + http.queue(Response(500, {}, "Internal Server Error")) + client = _make_client(http) + + with pytest.raises(ApiException) as exc_info: + client.send(Request(method="GET", uri="/test")) + + assert exc_info.value.http_status == 500 + + +# ── Service lazy caching ────────────────────────────────────────────────────── + +def test_email_service_returns_same_instance() -> None: + client = Client(api_key="key", http_client=FakeHttpClient()) + assert client.email_service() is client.email_service() + + +def test_smtp_service_returns_same_instance() -> None: + client = Client(api_key="key", http_client=FakeHttpClient()) + assert client.smtp_service() is client.smtp_service() diff --git a/tests/test_config.py b/tests/test_config.py new file mode 100644 index 0000000..8ab5ebf --- /dev/null +++ b/tests/test_config.py @@ -0,0 +1,41 @@ +import pytest + +from sendpulse.config import Config + + +def test_api_key_only_is_valid() -> None: + config = Config(api_key="my-key") + assert config.api_key == "my-key" + assert not config.is_oauth + + +def test_oauth_credentials_are_valid() -> None: + config = Config(client_id="id", client_secret="secret") + assert config.is_oauth + + +def test_raises_when_no_credentials() -> None: + with pytest.raises(ValueError, match="api_key"): + Config() + + +def test_raises_when_both_api_key_and_oauth() -> None: + with pytest.raises(ValueError, match="not both"): + Config(api_key="key", client_id="id", client_secret="secret") + + +def test_raises_when_partial_oauth() -> None: + with pytest.raises(ValueError): + Config(client_id="id") + + +def test_default_timeouts() -> None: + config = Config(api_key="key") + assert config.connect_timeout == 10.0 + assert config.request_timeout == 30.0 + + +def test_custom_timeouts() -> None: + config = Config(api_key="key", connect_timeout=5.0, request_timeout=15.0) + assert config.connect_timeout == 5.0 + assert config.request_timeout == 15.0 diff --git a/tests/test_response_validator.py b/tests/test_response_validator.py new file mode 100644 index 0000000..e7861a8 --- /dev/null +++ b/tests/test_response_validator.py @@ -0,0 +1,77 @@ +import pytest + +from sendpulse.exception.exceptions import ( + ApiException, + AuthException, + ProtocolException, + RateLimitException, +) +from sendpulse.http.response import Response +from sendpulse.response.validator import ResponseValidator + + +@pytest.fixture +def validator() -> ResponseValidator: + return ResponseValidator() + + +def test_returns_decoded_dict_on_200(validator: ResponseValidator) -> None: + response = Response(200, {}, '{"id": 1, "name": "test"}') + assert validator.validate(response) == {"id": 1, "name": "test"} + + +def test_returns_empty_dict_on_empty_body(validator: ResponseValidator) -> None: + response = Response(200, {}, "") + assert validator.validate(response) == {} + + +def test_returns_empty_dict_on_non_dict_json(validator: ResponseValidator) -> None: + response = Response(200, {}, '"just-a-string"') + assert validator.validate(response) == {} + + +def test_201_is_treated_as_success(validator: ResponseValidator) -> None: + response = Response(201, {}, '{"created": true}') + assert validator.validate(response) == {"created": True} + + +def test_401_raises_auth_exception(validator: ResponseValidator) -> None: + with pytest.raises(AuthException) as exc_info: + validator.validate(Response(401, {}, '{"error": "Unauthorized"}')) + assert exc_info.value.http_status == 401 + + +def test_403_raises_auth_exception(validator: ResponseValidator) -> None: + with pytest.raises(AuthException) as exc_info: + validator.validate(Response(403, {}, '{"error": "Forbidden"}')) + assert exc_info.value.http_status == 403 + + +def test_429_raises_rate_limit_exception(validator: ResponseValidator) -> None: + with pytest.raises(RateLimitException) as exc_info: + validator.validate(Response(429, {}, '{"error": "Too Many Requests"}')) + assert exc_info.value.http_status == 429 + + +def test_400_raises_api_exception(validator: ResponseValidator) -> None: + with pytest.raises(ApiException) as exc_info: + validator.validate(Response(400, {}, '{"error": "Bad Request"}')) + assert exc_info.value.http_status == 400 + + +def test_500_raises_api_exception(validator: ResponseValidator) -> None: + with pytest.raises(ApiException) as exc_info: + validator.validate(Response(500, {}, "Internal Server Error")) + assert exc_info.value.http_status == 500 + + +def test_invalid_json_raises_protocol_exception(validator: ResponseValidator) -> None: + with pytest.raises(ProtocolException, match="Failed to decode"): + validator.validate(Response(200, {}, "not-json{")) + + +def test_raw_body_preserved_on_api_exception(validator: ResponseValidator) -> None: + body = '{"message": "validation failed"}' + with pytest.raises(ApiException) as exc_info: + validator.validate(Response(422, {}, body)) + assert exc_info.value.raw_body == body diff --git a/tests/test_token_manager.py b/tests/test_token_manager.py new file mode 100644 index 0000000..448bb41 --- /dev/null +++ b/tests/test_token_manager.py @@ -0,0 +1,143 @@ +from __future__ import annotations + +import json +import time + +import pytest + +from sendpulse.auth.token_manager import TokenManager +from sendpulse.auth.token_storage import InMemoryTokenStorage, TokenData +from sendpulse.exception.exceptions import AuthException, ProtocolException +from sendpulse.http.response import Response +from tests.fixture.fake_http_client import FakeHttpClient + + +def _make_token_response(access_token: str = "tok123", expires_in: int = 3600) -> Response: + body = json.dumps( + {"access_token": access_token, "token_type": "Bearer", "expires_in": expires_in} + ) + return Response(200, {}, body) + + +def _make_manager( + http_client: FakeHttpClient, + storage: InMemoryTokenStorage | None = None, +) -> TokenManager: + return TokenManager( + http_client=http_client, + client_id="id123", + client_secret="secret456", + base_url="https://api.sendpulse.com", + storage=storage or InMemoryTokenStorage(), + ) + + +def test_fetches_token_on_first_call() -> None: + client = FakeHttpClient() + client.queue(_make_token_response("first-token")) + manager = _make_manager(client) + + token = manager.get_token() + + assert token == "first-token" + assert client.call_count() == 1 + + +def test_returns_cached_token_without_network_call() -> None: + client = FakeHttpClient() + client.queue(_make_token_response("cached-token")) + manager = _make_manager(client) + + first = manager.get_token() + second = manager.get_token() + + assert first == "cached-token" + assert second == "cached-token" + assert client.call_count() == 1 + + +def test_refetches_when_token_expired() -> None: + client = FakeHttpClient() + storage = InMemoryTokenStorage() + manager = _make_manager(client, storage) + + expired: TokenData = { + "access_token": "old-token", + "token_type": "Bearer", + "expires_at": int(time.time()) - 1, + } + import hashlib + key = hashlib.sha256(b"id123").hexdigest() + storage.set(key, expired) + + client.queue(_make_token_response("new-token")) + token = manager.get_token() + + assert token == "new-token" + assert client.call_count() == 1 + + +def test_refetches_after_invalidate() -> None: + client = FakeHttpClient() + client.queue(_make_token_response("first-token")) + client.queue(_make_token_response("refreshed-token")) + manager = _make_manager(client) + + manager.get_token() + manager.invalidate() + token = manager.get_token() + + assert token == "refreshed-token" + assert client.call_count() == 2 + + +def test_raises_auth_exception_on_non_200() -> None: + client = FakeHttpClient() + client.queue(Response(401, {}, '{"error": "invalid_client"}')) + manager = _make_manager(client) + + with pytest.raises(AuthException) as exc_info: + manager.get_token() + + assert exc_info.value.http_status == 401 + + +def test_raises_protocol_exception_on_invalid_json() -> None: + client = FakeHttpClient() + client.queue(Response(200, {}, "not-json")) + manager = _make_manager(client) + + with pytest.raises(ProtocolException, match="Failed to decode"): + manager.get_token() + + +def test_raises_protocol_exception_on_missing_access_token() -> None: + client = FakeHttpClient() + client.queue(Response(200, {}, '{"token_type": "Bearer", "expires_in": 3600}')) + manager = _make_manager(client) + + with pytest.raises(ProtocolException, match="Invalid OAuth"): + manager.get_token() + + +def test_raises_protocol_exception_on_missing_expires_in() -> None: + client = FakeHttpClient() + client.queue(Response(200, {}, '{"access_token": "tok", "token_type": "Bearer"}')) + manager = _make_manager(client) + + with pytest.raises(ProtocolException, match="Invalid OAuth"): + manager.get_token() + + +def test_sends_correct_oauth_request() -> None: + client = FakeHttpClient() + client.queue(_make_token_response()) + manager = _make_manager(client) + + manager.get_token() + + req = client.last_request() + assert req.method == "POST" + assert req.uri == "https://api.sendpulse.com/oauth/access_token" + assert "client_credentials" in (req.body or "") + assert "id123" in (req.body or "")