tina4-python 0.2.142__tar.gz → 0.2.144__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {tina4_python-0.2.142 → tina4_python-0.2.144}/.gitignore +40 -39
- {tina4_python-0.2.142 → tina4_python-0.2.144}/PKG-INFO +1 -1
- {tina4_python-0.2.142 → tina4_python-0.2.144}/README.md +92 -92
- {tina4_python-0.2.142 → tina4_python-0.2.144}/pyproject.toml +1 -1
- {tina4_python-0.2.142 → tina4_python-0.2.144}/tina4_python/Api.py +179 -179
- {tina4_python-0.2.142 → tina4_python-0.2.144}/tina4_python/Auth.py +322 -322
- {tina4_python-0.2.142 → tina4_python-0.2.144}/tina4_python/CRUD.py +387 -387
- {tina4_python-0.2.142 → tina4_python-0.2.144}/tina4_python/Constant.py +95 -95
- {tina4_python-0.2.142 → tina4_python-0.2.144}/tina4_python/Database.py +665 -665
- {tina4_python-0.2.142 → tina4_python-0.2.144}/tina4_python/DatabaseResult.py +88 -88
- {tina4_python-0.2.142 → tina4_python-0.2.144}/tina4_python/DatabaseTypes.py +15 -15
- {tina4_python-0.2.142 → tina4_python-0.2.144}/tina4_python/Debug.py +119 -119
- {tina4_python-0.2.142 → tina4_python-0.2.144}/tina4_python/Env.py +39 -39
- {tina4_python-0.2.142 → tina4_python-0.2.144}/tina4_python/FieldTypes.py +271 -271
- {tina4_python-0.2.142 → tina4_python-0.2.144}/tina4_python/HtmlElement.py +169 -169
- {tina4_python-0.2.142 → tina4_python-0.2.144}/tina4_python/Localization.py +42 -42
- {tina4_python-0.2.142 → tina4_python-0.2.144}/tina4_python/Messages.py +30 -30
- {tina4_python-0.2.142 → tina4_python-0.2.144}/tina4_python/MiddleWare.py +90 -90
- {tina4_python-0.2.142 → tina4_python-0.2.144}/tina4_python/Migration.py +107 -107
- {tina4_python-0.2.142 → tina4_python-0.2.144}/tina4_python/ORM.py +424 -424
- {tina4_python-0.2.142 → tina4_python-0.2.144}/tina4_python/Queue.py +221 -221
- {tina4_python-0.2.142 → tina4_python-0.2.144}/tina4_python/Request.py +21 -21
- {tina4_python-0.2.142 → tina4_python-0.2.144}/tina4_python/Response.py +212 -212
- {tina4_python-0.2.142 → tina4_python-0.2.144}/tina4_python/Router.py +672 -684
- {tina4_python-0.2.142 → tina4_python-0.2.144}/tina4_python/Session.py +342 -342
- {tina4_python-0.2.142 → tina4_python-0.2.144}/tina4_python/ShellColors.py +20 -20
- {tina4_python-0.2.142 → tina4_python-0.2.144}/tina4_python/Swagger.py +357 -357
- {tina4_python-0.2.142 → tina4_python-0.2.144}/tina4_python/Template.py +247 -247
- {tina4_python-0.2.142 → tina4_python-0.2.144}/tina4_python/Testing.py +118 -118
- {tina4_python-0.2.142 → tina4_python-0.2.144}/tina4_python/WSDL.py +445 -445
- {tina4_python-0.2.142 → tina4_python-0.2.144}/tina4_python/Webserver.py +594 -594
- {tina4_python-0.2.142 → tina4_python-0.2.144}/tina4_python/Websocket.py +47 -47
- {tina4_python-0.2.142 → tina4_python-0.2.144}/tina4_python/__init__.py +620 -607
- {tina4_python-0.2.142 → tina4_python-0.2.144}/tina4_python/cli.py +337 -337
- {tina4_python-0.2.142 → tina4_python-0.2.144}/tina4_python/messages.pot +83 -83
- {tina4_python-0.2.142 → tina4_python-0.2.144}/tina4_python/public/js/reconnecting-websocket.js +365 -365
- {tina4_python-0.2.142 → tina4_python-0.2.144}/tina4_python/public/js/tina4helper.js +361 -361
- {tina4_python-0.2.142 → tina4_python-0.2.144}/tina4_python/public/swagger/index.html +90 -90
- {tina4_python-0.2.142 → tina4_python-0.2.144}/tina4_python/public/swagger/oauth2-redirect.html +63 -63
- {tina4_python-0.2.142 → tina4_python-0.2.144}/tina4_python/templates/components/crud.twig +504 -504
- {tina4_python-0.2.142 → tina4_python-0.2.144}/tina4_python/templates/errors/403.twig +10 -10
- {tina4_python-0.2.142 → tina4_python-0.2.144}/tina4_python/templates/errors/404.twig +10 -10
- {tina4_python-0.2.142 → tina4_python-0.2.144}/tina4_python/templates/errors/500.twig +11 -11
- {tina4_python-0.2.142 → tina4_python-0.2.144}/tina4_python/translations/en/LC_MESSAGES/messages.po +80 -80
- {tina4_python-0.2.142 → tina4_python-0.2.144}/tina4_python/translations/fr/LC_MESSAGES/messages.po +84 -84
- {tina4_python-0.2.142 → tina4_python-0.2.144}/tina4_python/public/css/readme.md +0 -0
- {tina4_python-0.2.142 → tina4_python-0.2.144}/tina4_python/public/favicon.ico +0 -0
- {tina4_python-0.2.142 → tina4_python-0.2.144}/tina4_python/public/images/403.png +0 -0
- {tina4_python-0.2.142 → tina4_python-0.2.144}/tina4_python/public/images/404.png +0 -0
- {tina4_python-0.2.142 → tina4_python-0.2.144}/tina4_python/public/images/500.png +0 -0
- {tina4_python-0.2.142 → tina4_python-0.2.144}/tina4_python/public/images/logo.png +0 -0
- {tina4_python-0.2.142 → tina4_python-0.2.144}/tina4_python/public/images/readme.md +0 -0
- {tina4_python-0.2.142 → tina4_python-0.2.144}/tina4_python/public/js/readme.md +0 -0
- {tina4_python-0.2.142 → tina4_python-0.2.144}/tina4_python/templates/readme.md +0 -0
- {tina4_python-0.2.142 → tina4_python-0.2.144}/tina4_python/translations/en/LC_MESSAGES/messages.mo +0 -0
- {tina4_python-0.2.142 → tina4_python-0.2.144}/tina4_python/translations/fr/LC_MESSAGES/messages.mo +0 -0
|
@@ -1,39 +1,40 @@
|
|
|
1
|
-
/.idea/
|
|
2
|
-
/venv/
|
|
3
|
-
/dist/
|
|
4
|
-
/src/public/
|
|
5
|
-
/.pypirc
|
|
6
|
-
/src/templates/index.twig
|
|
7
|
-
/secrets/
|
|
8
|
-
/.env
|
|
9
|
-
/public/css/test.css
|
|
10
|
-
/sessions/
|
|
11
|
-
/tests/secrets/domain.cert
|
|
12
|
-
/tests/secrets/private.key
|
|
13
|
-
/tests/secrets/public.key
|
|
14
|
-
/tests/src/public/css/readme.md
|
|
15
|
-
/tests/src/public/images/403.png
|
|
16
|
-
/tests/src/public/images/404.png
|
|
17
|
-
/tests/src/public/images/logo.png
|
|
18
|
-
/tests/src/public/images/readme.md
|
|
19
|
-
/tests/src/public/js/readme.md
|
|
20
|
-
/tests/src/public/swagger/index.html
|
|
21
|
-
/tests/src/public/swagger/oauth2-redirect.html
|
|
22
|
-
/tests/src/public/favicon.ico
|
|
23
|
-
/tests/src/templates/errors/403.twig
|
|
24
|
-
/tests/src/templates/errors/404.twig
|
|
25
|
-
/tests/src/templates/readme.md
|
|
26
|
-
/tests/src/__init__.py
|
|
27
|
-
/tests/.env
|
|
28
|
-
/tests/app.py
|
|
29
|
-
/test.db
|
|
30
|
-
/logs/
|
|
31
|
-
/migrations/__test_user.sql
|
|
32
|
-
/migrations/__test_user_item.sql
|
|
33
|
-
/tina4_python.egg-info/
|
|
34
|
-
/src/templates/crud/
|
|
35
|
-
/.venv/
|
|
36
|
-
/publish.bat
|
|
37
|
-
/Dockerfile
|
|
38
|
-
/data.db
|
|
39
|
-
/test_queue.db-shm
|
|
1
|
+
/.idea/
|
|
2
|
+
/venv/
|
|
3
|
+
/dist/
|
|
4
|
+
/src/public/
|
|
5
|
+
/.pypirc
|
|
6
|
+
/src/templates/index.twig
|
|
7
|
+
/secrets/
|
|
8
|
+
/.env
|
|
9
|
+
/public/css/test.css
|
|
10
|
+
/sessions/
|
|
11
|
+
/tests/secrets/domain.cert
|
|
12
|
+
/tests/secrets/private.key
|
|
13
|
+
/tests/secrets/public.key
|
|
14
|
+
/tests/src/public/css/readme.md
|
|
15
|
+
/tests/src/public/images/403.png
|
|
16
|
+
/tests/src/public/images/404.png
|
|
17
|
+
/tests/src/public/images/logo.png
|
|
18
|
+
/tests/src/public/images/readme.md
|
|
19
|
+
/tests/src/public/js/readme.md
|
|
20
|
+
/tests/src/public/swagger/index.html
|
|
21
|
+
/tests/src/public/swagger/oauth2-redirect.html
|
|
22
|
+
/tests/src/public/favicon.ico
|
|
23
|
+
/tests/src/templates/errors/403.twig
|
|
24
|
+
/tests/src/templates/errors/404.twig
|
|
25
|
+
/tests/src/templates/readme.md
|
|
26
|
+
/tests/src/__init__.py
|
|
27
|
+
/tests/.env
|
|
28
|
+
/tests/app.py
|
|
29
|
+
/test.db
|
|
30
|
+
/logs/
|
|
31
|
+
/migrations/__test_user.sql
|
|
32
|
+
/migrations/__test_user_item.sql
|
|
33
|
+
/tina4_python.egg-info/
|
|
34
|
+
/src/templates/crud/
|
|
35
|
+
/.venv/
|
|
36
|
+
/publish.bat
|
|
37
|
+
/Dockerfile
|
|
38
|
+
/data.db
|
|
39
|
+
/test_queue.db-shm
|
|
40
|
+
/publish.sh
|
|
@@ -1,92 +1,92 @@
|
|
|
1
|
-
# Tina4 Python — This is not a framework
|
|
2
|
-
|
|
3
|
-
Laravel joy. Python speed. 10× less code.
|
|
4
|
-
|
|
5
|
-
## Quickstart
|
|
6
|
-
```bash
|
|
7
|
-
pip install tina4_python
|
|
8
|
-
tina4 init my_project
|
|
9
|
-
cd my_project
|
|
10
|
-
python app.py
|
|
11
|
-
```
|
|
12
|
-
|
|
13
|
-
You've just built your first Tina4 app — zero configuration, zero classes, zero boilerplate!
|
|
14
|
-
|
|
15
|
-
## Features
|
|
16
|
-
|
|
17
|
-
- Full ASGI compliance, use any ASGI compliant webserver
|
|
18
|
-
- Full async support out of the box
|
|
19
|
-
- Built-in JWT and Session handling
|
|
20
|
-
- Automatic Swagger docs at `/swagger`
|
|
21
|
-
- Instant CRUD interfaces with one line: `result.to_crud(request)`
|
|
22
|
-
- Built-in Twig templating, migrations, WebSockets, authentication and middleware
|
|
23
|
-
- Works with SQLite, PostgreSQL, MySQL, MariaDB, MSSQL, Firebird
|
|
24
|
-
- Hot reload in development (`uv run python -m jurigged app.py`)
|
|
25
|
-
|
|
26
|
-
## Install
|
|
27
|
-
|
|
28
|
-
```bash
|
|
29
|
-
pip install tina4-python
|
|
30
|
-
```
|
|
31
|
-
|
|
32
|
-
## Routing
|
|
33
|
-
|
|
34
|
-
Here are some basic GET routes
|
|
35
|
-
|
|
36
|
-
```python
|
|
37
|
-
# .src/__init__.py
|
|
38
|
-
from tina4_python import get
|
|
39
|
-
|
|
40
|
-
# simple get route
|
|
41
|
-
@get("/hello")
|
|
42
|
-
async def get_hello(request, response):
|
|
43
|
-
return response("Hello, Tina4 Python!")
|
|
44
|
-
|
|
45
|
-
# simple get route with inline params
|
|
46
|
-
@get("/hello/{world}")
|
|
47
|
-
async def get_hello_world(world, request, response):
|
|
48
|
-
return response(f"Hello, {world} ")
|
|
49
|
-
|
|
50
|
-
# simple route responding with json
|
|
51
|
-
@get("/hello/json")
|
|
52
|
-
async def get_hello_json(world, request, response):
|
|
53
|
-
cars = [{"brand": "BMW"}, {"brand": "Toyota"}]
|
|
54
|
-
|
|
55
|
-
return response(cars)
|
|
56
|
-
|
|
57
|
-
# respond with a file
|
|
58
|
-
@get("/hello/{filename}")
|
|
59
|
-
async def get_hello_file(filename, request, response):
|
|
60
|
-
|
|
61
|
-
return response.file(filename, './src/public')
|
|
62
|
-
|
|
63
|
-
# respond with a redirection to another location
|
|
64
|
-
@get("/hello/redirect")
|
|
65
|
-
async def get_hello_redirect(request, response):
|
|
66
|
-
|
|
67
|
-
return response.redirect("/hello/world")
|
|
68
|
-
|
|
69
|
-
@get("/hello/template")
|
|
70
|
-
async def get_hello_template(request, response):
|
|
71
|
-
|
|
72
|
-
# renders any template in .src/templates
|
|
73
|
-
return response.render("index.twig", {"data": request.params})
|
|
74
|
-
|
|
75
|
-
```
|
|
76
|
-
|
|
77
|
-
## Further Documentation
|
|
78
|
-
|
|
79
|
-
https://tina4.com/
|
|
80
|
-
|
|
81
|
-
## Community
|
|
82
|
-
|
|
83
|
-
- GitHub: https://github.com/tina4stack/tina4-python
|
|
84
|
-
|
|
85
|
-
## License
|
|
86
|
-
|
|
87
|
-
MIT © 2007 – 2025 Tina4 Stack
|
|
88
|
-
https://opensource.org/licenses/MIT
|
|
89
|
-
|
|
90
|
-
---
|
|
91
|
-
|
|
92
|
-
**Tina4** – The framework that keeps out of the way of your coding.
|
|
1
|
+
# Tina4 Python — This is not a framework
|
|
2
|
+
|
|
3
|
+
Laravel joy. Python speed. 10× less code.
|
|
4
|
+
|
|
5
|
+
## Quickstart
|
|
6
|
+
```bash
|
|
7
|
+
pip install tina4_python
|
|
8
|
+
tina4 init my_project
|
|
9
|
+
cd my_project
|
|
10
|
+
python app.py
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
You've just built your first Tina4 app — zero configuration, zero classes, zero boilerplate!
|
|
14
|
+
|
|
15
|
+
## Features
|
|
16
|
+
|
|
17
|
+
- Full ASGI compliance, use any ASGI compliant webserver
|
|
18
|
+
- Full async support out of the box
|
|
19
|
+
- Built-in JWT and Session handling
|
|
20
|
+
- Automatic Swagger docs at `/swagger`
|
|
21
|
+
- Instant CRUD interfaces with one line: `result.to_crud(request)`
|
|
22
|
+
- Built-in Twig templating, migrations, WebSockets, authentication and middleware
|
|
23
|
+
- Works with SQLite, PostgreSQL, MySQL, MariaDB, MSSQL, Firebird
|
|
24
|
+
- Hot reload in development (`uv run python -m jurigged app.py`)
|
|
25
|
+
|
|
26
|
+
## Install
|
|
27
|
+
|
|
28
|
+
```bash
|
|
29
|
+
pip install tina4-python
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
## Routing
|
|
33
|
+
|
|
34
|
+
Here are some basic GET routes
|
|
35
|
+
|
|
36
|
+
```python
|
|
37
|
+
# .src/__init__.py
|
|
38
|
+
from tina4_python import get
|
|
39
|
+
|
|
40
|
+
# simple get route
|
|
41
|
+
@get("/hello")
|
|
42
|
+
async def get_hello(request, response):
|
|
43
|
+
return response("Hello, Tina4 Python!")
|
|
44
|
+
|
|
45
|
+
# simple get route with inline params
|
|
46
|
+
@get("/hello/{world}")
|
|
47
|
+
async def get_hello_world(world, request, response):
|
|
48
|
+
return response(f"Hello, {world} ")
|
|
49
|
+
|
|
50
|
+
# simple route responding with json
|
|
51
|
+
@get("/hello/json")
|
|
52
|
+
async def get_hello_json(world, request, response):
|
|
53
|
+
cars = [{"brand": "BMW"}, {"brand": "Toyota"}]
|
|
54
|
+
|
|
55
|
+
return response(cars)
|
|
56
|
+
|
|
57
|
+
# respond with a file
|
|
58
|
+
@get("/hello/{filename}")
|
|
59
|
+
async def get_hello_file(filename, request, response):
|
|
60
|
+
|
|
61
|
+
return response.file(filename, './src/public')
|
|
62
|
+
|
|
63
|
+
# respond with a redirection to another location
|
|
64
|
+
@get("/hello/redirect")
|
|
65
|
+
async def get_hello_redirect(request, response):
|
|
66
|
+
|
|
67
|
+
return response.redirect("/hello/world")
|
|
68
|
+
|
|
69
|
+
@get("/hello/template")
|
|
70
|
+
async def get_hello_template(request, response):
|
|
71
|
+
|
|
72
|
+
# renders any template in .src/templates
|
|
73
|
+
return response.render("index.twig", {"data": request.params})
|
|
74
|
+
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
## Further Documentation
|
|
78
|
+
|
|
79
|
+
https://tina4.com/
|
|
80
|
+
|
|
81
|
+
## Community
|
|
82
|
+
|
|
83
|
+
- GitHub: https://github.com/tina4stack/tina4-python
|
|
84
|
+
|
|
85
|
+
## License
|
|
86
|
+
|
|
87
|
+
MIT © 2007 – 2025 Tina4 Stack
|
|
88
|
+
https://opensource.org/licenses/MIT
|
|
89
|
+
|
|
90
|
+
---
|
|
91
|
+
|
|
92
|
+
**Tina4** – The framework that keeps out of the way of your coding.
|
|
@@ -1,179 +1,179 @@
|
|
|
1
|
-
#
|
|
2
|
-
# Tina4 - This is not a 4ramework.
|
|
3
|
-
# Copy-right 2007 - current Tina4
|
|
4
|
-
# License: MIT https://opensource.org/licenses/MIT
|
|
5
|
-
#
|
|
6
|
-
# flake8: noqa: E501
|
|
7
|
-
|
|
8
|
-
import json
|
|
9
|
-
from typing import Optional, Dict, Any, Union, List
|
|
10
|
-
|
|
11
|
-
import requests
|
|
12
|
-
from requests.auth import HTTPBasicAuth
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
class Api:
|
|
16
|
-
"""
|
|
17
|
-
Consume REST API interfaces with minimal code - Tina4 style.
|
|
18
|
-
|
|
19
|
-
Lightweight wrapper around `requests` providing:
|
|
20
|
-
- Base URL handling
|
|
21
|
-
- Bearer token or custom auth header
|
|
22
|
-
- Basic authentication
|
|
23
|
-
- Persistent and per-request custom headers
|
|
24
|
-
- Automatic JSON serialization
|
|
25
|
-
- SSL verification control
|
|
26
|
-
- Consistent response dictionary
|
|
27
|
-
|
|
28
|
-
"""
|
|
29
|
-
|
|
30
|
-
def __init__(
|
|
31
|
-
self,
|
|
32
|
-
base_url: Optional[str] = None,
|
|
33
|
-
auth_header: str = "",
|
|
34
|
-
ignore_ssl_validation: bool = False,
|
|
35
|
-
):
|
|
36
|
-
"""
|
|
37
|
-
Initialize the API client.
|
|
38
|
-
|
|
39
|
-
Args:
|
|
40
|
-
base_url (str, optional): Root URL of the API (trailing slash is stripped)
|
|
41
|
-
auth_header (str): Full authentication header, e.g. "Authorization: Bearer xyz"
|
|
42
|
-
ignore_ssl_validation (bool): Disable SSL certificate verification when True
|
|
43
|
-
"""
|
|
44
|
-
self.base_url = base_url.rstrip("/") if base_url else None
|
|
45
|
-
self.auth_header = auth_header.strip()
|
|
46
|
-
self.ignore_ssl_validation = ignore_ssl_validation
|
|
47
|
-
|
|
48
|
-
self.custom_headers: List[str] = [] # persistent custom headers
|
|
49
|
-
self.username: Optional[str] = None # for Basic Auth
|
|
50
|
-
self.password: Optional[str] = None # for Basic Auth
|
|
51
|
-
|
|
52
|
-
def add_custom_headers(self, headers: Union[Dict[str, str], List[str]]) -> None:
|
|
53
|
-
"""
|
|
54
|
-
Add persistent headers that are sent with every request.
|
|
55
|
-
|
|
56
|
-
Args:
|
|
57
|
-
headers: Dictionary of headers or list of "Key: Value" strings
|
|
58
|
-
"""
|
|
59
|
-
if isinstance(headers, dict):
|
|
60
|
-
self.custom_headers.extend(f"{k}: {v}" for k, v in headers.items())
|
|
61
|
-
else:
|
|
62
|
-
self.custom_headers.extend(headers)
|
|
63
|
-
|
|
64
|
-
def set_username_password(self, username: str, password: str) -> None:
|
|
65
|
-
"""
|
|
66
|
-
Set credentials for HTTP Basic Authentication.
|
|
67
|
-
|
|
68
|
-
Args:
|
|
69
|
-
username: Username or client id
|
|
70
|
-
password: Password or secret
|
|
71
|
-
"""
|
|
72
|
-
self.username = username
|
|
73
|
-
self.password = password
|
|
74
|
-
|
|
75
|
-
def send_request(
|
|
76
|
-
self,
|
|
77
|
-
rest_service: str = "",
|
|
78
|
-
request_type: str = "GET",
|
|
79
|
-
body: Optional[Any] = None,
|
|
80
|
-
content_type: str = "application/json",
|
|
81
|
-
custom_headers: Optional[Dict[str, str]] = None,
|
|
82
|
-
timeout: int = 30,
|
|
83
|
-
) -> Dict[str, Any]:
|
|
84
|
-
"""
|
|
85
|
-
Execute an HTTP request against the API.
|
|
86
|
-
|
|
87
|
-
Args:
|
|
88
|
-
rest_service (str): Endpoint path (appended to base_url)
|
|
89
|
-
request_type (str): HTTP method - GET, POST, PUT, PATCH, DELETE, etc.
|
|
90
|
-
body (any, optional): Request payload (auto JSON-encoded when content_type is JSON)
|
|
91
|
-
content_type (str): Value for the Content-Type header
|
|
92
|
-
custom_headers (dict, optional): One-time headers for this request only
|
|
93
|
-
timeout (int): Request timeout in seconds
|
|
94
|
-
|
|
95
|
-
Returns:
|
|
96
|
-
dict containing:
|
|
97
|
-
- http_code (int): HTTP status code
|
|
98
|
-
- body (dict|list|str): Parsed JSON when possible, otherwise raw text
|
|
99
|
-
- headers (dict): Response headers
|
|
100
|
-
- error (str|None): Error message if the request failed
|
|
101
|
-
"""
|
|
102
|
-
if custom_headers is None:
|
|
103
|
-
custom_headers = {}
|
|
104
|
-
|
|
105
|
-
url = f"{self.base_url}{rest_service}" if self.base_url else rest_service
|
|
106
|
-
|
|
107
|
-
# Base headers
|
|
108
|
-
headers = {
|
|
109
|
-
"Accept": content_type,
|
|
110
|
-
"Accept-Charset": "utf-8, *;q=0.8",
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
# Persistent custom headers
|
|
114
|
-
for header in self.custom_headers:
|
|
115
|
-
if ":" in header:
|
|
116
|
-
key, value = header.split(":", 1)
|
|
117
|
-
headers[key.strip()] = value.strip()
|
|
118
|
-
|
|
119
|
-
# One-time custom headers
|
|
120
|
-
headers.update(custom_headers)
|
|
121
|
-
|
|
122
|
-
# Authentication
|
|
123
|
-
auth = None
|
|
124
|
-
if self.username and self.password:
|
|
125
|
-
auth = HTTPBasicAuth(self.username, self.password)
|
|
126
|
-
elif self.auth_header:
|
|
127
|
-
parts = self.auth_header.split(":", 1)
|
|
128
|
-
if len(parts) == 2:
|
|
129
|
-
key, val = parts
|
|
130
|
-
headers[key.strip()] = val.strip()
|
|
131
|
-
else:
|
|
132
|
-
# Fallback for "Bearer xyz" style
|
|
133
|
-
header_parts = self.auth_header.split()
|
|
134
|
-
if len(header_parts) >= 2:
|
|
135
|
-
headers[header_parts[0]] = " ".join(header_parts[1:])
|
|
136
|
-
|
|
137
|
-
# Payload handling
|
|
138
|
-
json_payload = None
|
|
139
|
-
data_payload = None
|
|
140
|
-
if body is not None:
|
|
141
|
-
if content_type == "application/json" and isinstance(body, (dict, list)):
|
|
142
|
-
json_payload = body
|
|
143
|
-
headers["Content-Type"] = content_type
|
|
144
|
-
else:
|
|
145
|
-
data_payload = body if isinstance(body, (str, bytes)) else str(body)
|
|
146
|
-
headers["Content-Type"] = content_type
|
|
147
|
-
|
|
148
|
-
try:
|
|
149
|
-
response = requests.request(
|
|
150
|
-
method=request_type.upper(),
|
|
151
|
-
url=url,
|
|
152
|
-
headers=headers,
|
|
153
|
-
json=json_payload,
|
|
154
|
-
data=data_payload,
|
|
155
|
-
auth=auth,
|
|
156
|
-
verify=not self.ignore_ssl_validation,
|
|
157
|
-
timeout=timeout,
|
|
158
|
-
)
|
|
159
|
-
|
|
160
|
-
# Parse JSON if possible
|
|
161
|
-
try:
|
|
162
|
-
parsed_body = response.json()
|
|
163
|
-
except json.JSONDecodeError:
|
|
164
|
-
parsed_body = response.text
|
|
165
|
-
|
|
166
|
-
return {
|
|
167
|
-
"http_code": response.status_code,
|
|
168
|
-
"body": parsed_body,
|
|
169
|
-
"headers": dict(response.headers),
|
|
170
|
-
"error": None,
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
except requests.RequestException as e:
|
|
174
|
-
return {
|
|
175
|
-
"http_code": None,
|
|
176
|
-
"body": None,
|
|
177
|
-
"headers": {},
|
|
178
|
-
"error": str(e),
|
|
179
|
-
}
|
|
1
|
+
#
|
|
2
|
+
# Tina4 - This is not a 4ramework.
|
|
3
|
+
# Copy-right 2007 - current Tina4
|
|
4
|
+
# License: MIT https://opensource.org/licenses/MIT
|
|
5
|
+
#
|
|
6
|
+
# flake8: noqa: E501
|
|
7
|
+
|
|
8
|
+
import json
|
|
9
|
+
from typing import Optional, Dict, Any, Union, List
|
|
10
|
+
|
|
11
|
+
import requests
|
|
12
|
+
from requests.auth import HTTPBasicAuth
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class Api:
|
|
16
|
+
"""
|
|
17
|
+
Consume REST API interfaces with minimal code - Tina4 style.
|
|
18
|
+
|
|
19
|
+
Lightweight wrapper around `requests` providing:
|
|
20
|
+
- Base URL handling
|
|
21
|
+
- Bearer token or custom auth header
|
|
22
|
+
- Basic authentication
|
|
23
|
+
- Persistent and per-request custom headers
|
|
24
|
+
- Automatic JSON serialization
|
|
25
|
+
- SSL verification control
|
|
26
|
+
- Consistent response dictionary
|
|
27
|
+
|
|
28
|
+
"""
|
|
29
|
+
|
|
30
|
+
def __init__(
|
|
31
|
+
self,
|
|
32
|
+
base_url: Optional[str] = None,
|
|
33
|
+
auth_header: str = "",
|
|
34
|
+
ignore_ssl_validation: bool = False,
|
|
35
|
+
):
|
|
36
|
+
"""
|
|
37
|
+
Initialize the API client.
|
|
38
|
+
|
|
39
|
+
Args:
|
|
40
|
+
base_url (str, optional): Root URL of the API (trailing slash is stripped)
|
|
41
|
+
auth_header (str): Full authentication header, e.g. "Authorization: Bearer xyz"
|
|
42
|
+
ignore_ssl_validation (bool): Disable SSL certificate verification when True
|
|
43
|
+
"""
|
|
44
|
+
self.base_url = base_url.rstrip("/") if base_url else None
|
|
45
|
+
self.auth_header = auth_header.strip()
|
|
46
|
+
self.ignore_ssl_validation = ignore_ssl_validation
|
|
47
|
+
|
|
48
|
+
self.custom_headers: List[str] = [] # persistent custom headers
|
|
49
|
+
self.username: Optional[str] = None # for Basic Auth
|
|
50
|
+
self.password: Optional[str] = None # for Basic Auth
|
|
51
|
+
|
|
52
|
+
def add_custom_headers(self, headers: Union[Dict[str, str], List[str]]) -> None:
|
|
53
|
+
"""
|
|
54
|
+
Add persistent headers that are sent with every request.
|
|
55
|
+
|
|
56
|
+
Args:
|
|
57
|
+
headers: Dictionary of headers or list of "Key: Value" strings
|
|
58
|
+
"""
|
|
59
|
+
if isinstance(headers, dict):
|
|
60
|
+
self.custom_headers.extend(f"{k}: {v}" for k, v in headers.items())
|
|
61
|
+
else:
|
|
62
|
+
self.custom_headers.extend(headers)
|
|
63
|
+
|
|
64
|
+
def set_username_password(self, username: str, password: str) -> None:
|
|
65
|
+
"""
|
|
66
|
+
Set credentials for HTTP Basic Authentication.
|
|
67
|
+
|
|
68
|
+
Args:
|
|
69
|
+
username: Username or client id
|
|
70
|
+
password: Password or secret
|
|
71
|
+
"""
|
|
72
|
+
self.username = username
|
|
73
|
+
self.password = password
|
|
74
|
+
|
|
75
|
+
def send_request(
|
|
76
|
+
self,
|
|
77
|
+
rest_service: str = "",
|
|
78
|
+
request_type: str = "GET",
|
|
79
|
+
body: Optional[Any] = None,
|
|
80
|
+
content_type: str = "application/json",
|
|
81
|
+
custom_headers: Optional[Dict[str, str]] = None,
|
|
82
|
+
timeout: int = 30,
|
|
83
|
+
) -> Dict[str, Any]:
|
|
84
|
+
"""
|
|
85
|
+
Execute an HTTP request against the API.
|
|
86
|
+
|
|
87
|
+
Args:
|
|
88
|
+
rest_service (str): Endpoint path (appended to base_url)
|
|
89
|
+
request_type (str): HTTP method - GET, POST, PUT, PATCH, DELETE, etc.
|
|
90
|
+
body (any, optional): Request payload (auto JSON-encoded when content_type is JSON)
|
|
91
|
+
content_type (str): Value for the Content-Type header
|
|
92
|
+
custom_headers (dict, optional): One-time headers for this request only
|
|
93
|
+
timeout (int): Request timeout in seconds
|
|
94
|
+
|
|
95
|
+
Returns:
|
|
96
|
+
dict containing:
|
|
97
|
+
- http_code (int): HTTP status code
|
|
98
|
+
- body (dict|list|str): Parsed JSON when possible, otherwise raw text
|
|
99
|
+
- headers (dict): Response headers
|
|
100
|
+
- error (str|None): Error message if the request failed
|
|
101
|
+
"""
|
|
102
|
+
if custom_headers is None:
|
|
103
|
+
custom_headers = {}
|
|
104
|
+
|
|
105
|
+
url = f"{self.base_url}{rest_service}" if self.base_url else rest_service
|
|
106
|
+
|
|
107
|
+
# Base headers
|
|
108
|
+
headers = {
|
|
109
|
+
"Accept": content_type,
|
|
110
|
+
"Accept-Charset": "utf-8, *;q=0.8",
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
# Persistent custom headers
|
|
114
|
+
for header in self.custom_headers:
|
|
115
|
+
if ":" in header:
|
|
116
|
+
key, value = header.split(":", 1)
|
|
117
|
+
headers[key.strip()] = value.strip()
|
|
118
|
+
|
|
119
|
+
# One-time custom headers
|
|
120
|
+
headers.update(custom_headers)
|
|
121
|
+
|
|
122
|
+
# Authentication
|
|
123
|
+
auth = None
|
|
124
|
+
if self.username and self.password:
|
|
125
|
+
auth = HTTPBasicAuth(self.username, self.password)
|
|
126
|
+
elif self.auth_header:
|
|
127
|
+
parts = self.auth_header.split(":", 1)
|
|
128
|
+
if len(parts) == 2:
|
|
129
|
+
key, val = parts
|
|
130
|
+
headers[key.strip()] = val.strip()
|
|
131
|
+
else:
|
|
132
|
+
# Fallback for "Bearer xyz" style
|
|
133
|
+
header_parts = self.auth_header.split()
|
|
134
|
+
if len(header_parts) >= 2:
|
|
135
|
+
headers[header_parts[0]] = " ".join(header_parts[1:])
|
|
136
|
+
|
|
137
|
+
# Payload handling
|
|
138
|
+
json_payload = None
|
|
139
|
+
data_payload = None
|
|
140
|
+
if body is not None:
|
|
141
|
+
if content_type == "application/json" and isinstance(body, (dict, list)):
|
|
142
|
+
json_payload = body
|
|
143
|
+
headers["Content-Type"] = content_type
|
|
144
|
+
else:
|
|
145
|
+
data_payload = body if isinstance(body, (str, bytes)) else str(body)
|
|
146
|
+
headers["Content-Type"] = content_type
|
|
147
|
+
|
|
148
|
+
try:
|
|
149
|
+
response = requests.request(
|
|
150
|
+
method=request_type.upper(),
|
|
151
|
+
url=url,
|
|
152
|
+
headers=headers,
|
|
153
|
+
json=json_payload,
|
|
154
|
+
data=data_payload,
|
|
155
|
+
auth=auth,
|
|
156
|
+
verify=not self.ignore_ssl_validation,
|
|
157
|
+
timeout=timeout,
|
|
158
|
+
)
|
|
159
|
+
|
|
160
|
+
# Parse JSON if possible
|
|
161
|
+
try:
|
|
162
|
+
parsed_body = response.json()
|
|
163
|
+
except json.JSONDecodeError:
|
|
164
|
+
parsed_body = response.text
|
|
165
|
+
|
|
166
|
+
return {
|
|
167
|
+
"http_code": response.status_code,
|
|
168
|
+
"body": parsed_body,
|
|
169
|
+
"headers": dict(response.headers),
|
|
170
|
+
"error": None,
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
except requests.RequestException as e:
|
|
174
|
+
return {
|
|
175
|
+
"http_code": None,
|
|
176
|
+
"body": None,
|
|
177
|
+
"headers": {},
|
|
178
|
+
"error": str(e),
|
|
179
|
+
}
|