Metadata-Version: 2.3
Name: setech
Version: 1.8.0rc2
Summary: Setech utilities
Keywords: api-client,logging,setech,utility,utils
Author: Eriks Karls
Author-email: Eriks Karls <eriks.karls@sefinance.lv>
License: MIT License
         
         Copyright (c) 2026 "Sefinance"
         
         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.
Classifier: Development Status :: 5 - Production/Stable
Classifier: Intended Audience :: Developers
Classifier: License :: OSI Approved :: MIT License
Classifier: Operating System :: OS Independent
Classifier: Programming Language :: Python :: 3 :: Only
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Programming Language :: Python :: 3.14
Requires-Dist: httpx2[http2]~=2.3
Requires-Dist: num2words==0.5.14
Requires-Dist: phonenumberslite~=9.0
Requires-Dist: pydantic~=2.13
Requires-Dist: unidecode~=1.4
Requires-Dist: django>=5.2,<7.0 ; extra == 'django'
Requires-Dist: django-crum ; extra == 'django'
Requires-Dist: django-model-utils ; extra == 'django'
Requires-Dist: pillow ; extra == 'django'
Requires-Dist: dnspython[dnssec,doh,idna]~=2.8 ; extra == 'dns'
Requires-Dist: redis ; extra == 'dns-redis'
Requires-Dist: setech[dns] ; extra == 'dns-redis'
Requires-Dist: djangorestframework ; extra == 'drf'
Requires-Dist: setech[django] ; extra == 'drf'
Requires-Python: >=3.12
Project-URL: Homepage, https://pypi.org/project/setech/
Provides-Extra: django
Provides-Extra: dns
Provides-Extra: dns-redis
Provides-Extra: drf
Description-Content-Type: text/markdown

# Setech Utilities

Python utility library for API clients, logging, and various development tasks.

## Installation

Install the package using pip:
```bash
pip install setech
```

Or with virtualenv (recommended):

```bash 
python -m venv .venv 
source .venv/bin/activate # On Linux/macOS
```

## Extra Dependencies

The following optional dependencies are available:

| Extra       | Description                                                      |
|-------------|------------------------------------------------------------------|
| `django`    | Django support (`>=5.2`, `django-crum`, `django-model-utils`)    |
| `drf`       | Django REST Framework with full Django stack                     |
| `dns`       | DNS utilities (`dnspython` with `dnssec`, `doh`, `idna` support) |
| `dns-redis` | DNS cache using Redis backend                                    |

Install extras:
```bash
pip install "setech[django]"      # Django support
pip install "setech[drf]"         # Django REST Framework
pip install "setech[dns]"         # DNS utilities
pip install "setech[dns-redis]"   # DNS cache with Redis
```

## Core Dependencies

The package requires the following core dependencies:

- `httpx2[http2]`
- `num2words`
- `phonenumberslite`
- `pydantic`
- `unidecode`

## Development Setup

The repository uses **uv** as the package manager and dependency resolver.  
Follow these steps to get a fully‑featured development environment with one command.

```bash
# 1. Create and activate a virtual environment (recommended)
python -m venv .venv 
source .venv/bin/activate # On Windows use `.venv\Scripts\activate`
# 2. Install uv (if you don’t already have it)
curl -LsSf [https://astral.sh/uv/install.sh](https://astral.sh/uv/install.sh) | sh
# 3. Sync all base, dev and optional extras in a single command
uv sync --dev
# 4. Verify everything is installed
uv run python -m pip list | grep setech
```

### What the `--dev` flag does

* Installs the **core** dependencies listed under `[project]`.
* Adds the optional extras `django`, `drf` and `dns‑redis` (via `setech[... ]`).
* Installs the development tooling (`ipython`, `ruff`, `mypy`, test runners, etc.) from the `[dependency-groups] dev` section.
* Resolves transitive dependencies only once, so no duplication occurs.

> **Tip:** If you ever need to refresh the lockfile after a dependency bump, run:
> ```bash
> uv sync --dev --upgrade
> ```

# Example code

## API Clients
```python
# integration/client.py
from setech import SyncClient


class LocalClient(SyncClient):
    name = "local"
    _base_url = "https://obligari.serveo.net/ping/local"

    def __init__(self, nonce=None):
        super().__init__(nonce)
        self._session.headers.update(
            {"User-Agent": "Mozilla/5.0 (Windows NT 10.0; rv:123.0) Gecko/20100101 Firefox/123.0"}
        )

    def send_post_ping(self, var1: str, var2: int) -> bool:
        res = self.post("/some-post", json={"variable_one": var1, "second_variable": var2})
        return res.json().get("status")

    def send_put_ping(self, var1: str, var2: int) -> bool:
        res = self.put("/some-put", data={"variable_one": var1, "second_variable": var2})
        return res.json().get("status")

    def send_get_ping(self, var1: str, var2: int) -> bool:
        res = self.get("/some-get", params={"variable_one": var1, "second_variable": var2})
        return res.json().get("status")

    def send_patch_ping(self, var1: str, var2: int) -> bool:
        res = self.put("/some-patch", data=(("variable_one", var1), ("variable_one", var2)))
        return res.json().get("status")

    def send_trace_ping(self, var1: str, var2: int) -> bool:
        res = self.trace("/some-trace", params=(("variable_one", var1), ("variable_one", var2)))
        return res.json().get("status")
```

```python
# integration/main.py
from integration.client import LocalClient


client = LocalClient()
client.send_post_ping("asd", 123)
client.send_put_ping("asd", 123)
client.send_get_ping("asd", 123)
client.send_patch_ping("asd", 123)
client.send_trace_ping("asd", 123)
```

## Log output
### Simple
```text
[14d709e02c0c] Preparing POST request to "https://obligari.serveo.net/ping/local/some-post"
[14d709e02c0c] Sending request with payload=b'{"variable_one": "asd", "second_variable": 123}'
[14d709e02c0c] Response response.status_code=200 str_repr_content='{"status":true,"request_id":62}'
[14d709e02c0c] Preparing GET request to "https://obligari.serveo.net/ping/local/some-get"
[14d709e02c0c] Sending request with payload=None
[14d709e02c0c] Response response.status_code=200 str_repr_content='{"status":true,"request_id":63}'
```
### Structured
```text
{"app": "dev", "level": "DEBUG", "name": "APIClient", "date_time": "2024-03-09 22:59:24", "location": "api_client/client.py:_request:71", "message": "[cfbdadc56f53] Preparing POST request to \"https://obligari.serveo.net/ping/local/some-post\"", "extra_data": {"hooks": {"response": []}, "method": "POST", "url": "https://obligari.serveo.net/ping/local/some-post", "headers": {}, "files": [], "data": [], "json": {"variable_one": "asd", "second_variable": 123}, "params": {}, "auth": null, "cookies": null}}
{"app": "dev", "level": "INFO", "name": "APIClient", "date_time": "2024-03-09 22:59:24", "location": "api_client/client.py:_request:74", "message": "[cfbdadc56f53] Sending request with payload=b'{\"variable_one\": \"asd\", \"second_variable\": 123}'", "extra_data": {"payload": "{\"variable_one\": \"asd\", \"second_variable\": 123}"}}
{"app": "dev", "level": "INFO", "name": "APIClient", "date_time": "2024-03-09 22:59:25", "location": "api_client/client.py:_request:81", "message": "[cfbdadc56f53] Response response.status_code=200 str_repr_content='{\"status\":true,\"request_id\":72}'", "extra_data": {"status_code": 200, "content": "{\"status\":true,\"request_id\":72}"}}
{"app": "dev", "level": "DEBUG", "name": "APIClient", "date_time": "2024-03-09 22:59:25", "location": "api_client/client.py:_request:71", "message": "[cfbdadc56f53] Preparing GET request to \"https://obligari.serveo.net/ping/local/some-get\"", "extra_data": {"hooks": {"response": []}, "method": "GET", "url": "https://obligari.serveo.net/ping/local/some-get", "headers": {}, "files": [], "data": [], "json": null, "params": {"variable_one": "asd", "second_variable": 123}, "auth": null, "cookies": null}}
{"app": "dev", "level": "INFO", "name": "APIClient", "date_time": "2024-03-09 22:59:25", "location": "api_client/client.py:_request:74", "message": "[cfbdadc56f53] Sending request with payload=None", "extra_data": {"payload": "{}"}}
{"app": "dev", "level": "INFO", "name": "APIClient", "date_time": "2024-03-09 22:59:25", "location": "api_client/client.py:_request:81", "message": "[cfbdadc56f53] Response response.status_code=200 str_repr_content='{\"status\":true,\"request_id\":74}'", "extra_data": {"status_code": 200, "content": "{\"status\":true,\"request_id\":73}"}}
```

## django features

### Using the `setech.django` in a Django project

#### Models
The library ships two abstract base models that can be inherited to get timestamping, UUID and audit fields.
```python
# myapp/models.py
from django.db import models 
from setech.django.models import TimeStampedUUIDModel, UserTimeStampedUUIDModel

class Article(TimeStampedUUIDModel): 
    title = models.CharField()
    content = models.TextField()

class Comment(UserTimeStampedUUIDModel): 
    article = models.ForeignKey(Article, on_delete=models.CASCADE)
    text = models.TextField()
```

### What each base class provides

| Base model                 | Fields added                      | Notes                                                                     |
|----------------------------|-----------------------------------|---------------------------------------------------------------------------|
| `TimeStampedUUIDModel`     | `uid`, `created_at`, `updated_at` | UUID (v4 or v7 if available) and timestamps                               |
| `UserTimeStampedUUIDModel` | `created_by`, `modified_by`       | Populated automatically from the current request when using `Django‑CRUM` |


#### Validators

`setech.django.validators` contains a handful of reusable validator functions that can be dropped straight into Django model fields or form/serializer validations.  
They wrap common checks such as phone numbers, personal IDs and MX‑record validation for e‑mail addresses.

```python
# myapp/models.py
from django.db import models
from setech.django.models import TimeStampedUUIDModel
from setech.django.validators import django_phone_number_validator, django_personal_code_validator, django_validate_email_mx_domain, validate_image_and_svg_file_extension

class UserProfile(TimeStampedUUIDModel):
    # Phone number in international format, e.g. "+44 20 7946 0958" 
    phone = models.CharField(validators=[django_phone_number_validator])
    
    # Personal ID (SSN/NRIC/etc.) – the validator automatically uses your
    # `settings.COUNTRY_CODE` (e.g. "LV" for Latvia)
    personal_id = models.CharField(max_length=20, validators=[django_personal_code_validator])
    
    # Email address – ensures the domain can receive mail
    email = models.EmailField(validators=[django_validate_email_mx_domain])
    
    # Image file – ensures the file is either image or svg
    picture = models.FileField(validators=[validate_image_and_svg_file_extension])
```

### Customizing the country code

The validators rely on `settings.COUNTRY_CODE` to decide which format to expect.  
Add the following to your Django settings:
```python
# settings.py
from setech.django.constants import Country

...
COUNTRY_CODE = Country.LV # Latvia, change to your locale
```
