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.
Files changed (56) hide show
  1. {tina4_python-0.2.142 → tina4_python-0.2.144}/.gitignore +40 -39
  2. {tina4_python-0.2.142 → tina4_python-0.2.144}/PKG-INFO +1 -1
  3. {tina4_python-0.2.142 → tina4_python-0.2.144}/README.md +92 -92
  4. {tina4_python-0.2.142 → tina4_python-0.2.144}/pyproject.toml +1 -1
  5. {tina4_python-0.2.142 → tina4_python-0.2.144}/tina4_python/Api.py +179 -179
  6. {tina4_python-0.2.142 → tina4_python-0.2.144}/tina4_python/Auth.py +322 -322
  7. {tina4_python-0.2.142 → tina4_python-0.2.144}/tina4_python/CRUD.py +387 -387
  8. {tina4_python-0.2.142 → tina4_python-0.2.144}/tina4_python/Constant.py +95 -95
  9. {tina4_python-0.2.142 → tina4_python-0.2.144}/tina4_python/Database.py +665 -665
  10. {tina4_python-0.2.142 → tina4_python-0.2.144}/tina4_python/DatabaseResult.py +88 -88
  11. {tina4_python-0.2.142 → tina4_python-0.2.144}/tina4_python/DatabaseTypes.py +15 -15
  12. {tina4_python-0.2.142 → tina4_python-0.2.144}/tina4_python/Debug.py +119 -119
  13. {tina4_python-0.2.142 → tina4_python-0.2.144}/tina4_python/Env.py +39 -39
  14. {tina4_python-0.2.142 → tina4_python-0.2.144}/tina4_python/FieldTypes.py +271 -271
  15. {tina4_python-0.2.142 → tina4_python-0.2.144}/tina4_python/HtmlElement.py +169 -169
  16. {tina4_python-0.2.142 → tina4_python-0.2.144}/tina4_python/Localization.py +42 -42
  17. {tina4_python-0.2.142 → tina4_python-0.2.144}/tina4_python/Messages.py +30 -30
  18. {tina4_python-0.2.142 → tina4_python-0.2.144}/tina4_python/MiddleWare.py +90 -90
  19. {tina4_python-0.2.142 → tina4_python-0.2.144}/tina4_python/Migration.py +107 -107
  20. {tina4_python-0.2.142 → tina4_python-0.2.144}/tina4_python/ORM.py +424 -424
  21. {tina4_python-0.2.142 → tina4_python-0.2.144}/tina4_python/Queue.py +221 -221
  22. {tina4_python-0.2.142 → tina4_python-0.2.144}/tina4_python/Request.py +21 -21
  23. {tina4_python-0.2.142 → tina4_python-0.2.144}/tina4_python/Response.py +212 -212
  24. {tina4_python-0.2.142 → tina4_python-0.2.144}/tina4_python/Router.py +672 -684
  25. {tina4_python-0.2.142 → tina4_python-0.2.144}/tina4_python/Session.py +342 -342
  26. {tina4_python-0.2.142 → tina4_python-0.2.144}/tina4_python/ShellColors.py +20 -20
  27. {tina4_python-0.2.142 → tina4_python-0.2.144}/tina4_python/Swagger.py +357 -357
  28. {tina4_python-0.2.142 → tina4_python-0.2.144}/tina4_python/Template.py +247 -247
  29. {tina4_python-0.2.142 → tina4_python-0.2.144}/tina4_python/Testing.py +118 -118
  30. {tina4_python-0.2.142 → tina4_python-0.2.144}/tina4_python/WSDL.py +445 -445
  31. {tina4_python-0.2.142 → tina4_python-0.2.144}/tina4_python/Webserver.py +594 -594
  32. {tina4_python-0.2.142 → tina4_python-0.2.144}/tina4_python/Websocket.py +47 -47
  33. {tina4_python-0.2.142 → tina4_python-0.2.144}/tina4_python/__init__.py +620 -607
  34. {tina4_python-0.2.142 → tina4_python-0.2.144}/tina4_python/cli.py +337 -337
  35. {tina4_python-0.2.142 → tina4_python-0.2.144}/tina4_python/messages.pot +83 -83
  36. {tina4_python-0.2.142 → tina4_python-0.2.144}/tina4_python/public/js/reconnecting-websocket.js +365 -365
  37. {tina4_python-0.2.142 → tina4_python-0.2.144}/tina4_python/public/js/tina4helper.js +361 -361
  38. {tina4_python-0.2.142 → tina4_python-0.2.144}/tina4_python/public/swagger/index.html +90 -90
  39. {tina4_python-0.2.142 → tina4_python-0.2.144}/tina4_python/public/swagger/oauth2-redirect.html +63 -63
  40. {tina4_python-0.2.142 → tina4_python-0.2.144}/tina4_python/templates/components/crud.twig +504 -504
  41. {tina4_python-0.2.142 → tina4_python-0.2.144}/tina4_python/templates/errors/403.twig +10 -10
  42. {tina4_python-0.2.142 → tina4_python-0.2.144}/tina4_python/templates/errors/404.twig +10 -10
  43. {tina4_python-0.2.142 → tina4_python-0.2.144}/tina4_python/templates/errors/500.twig +11 -11
  44. {tina4_python-0.2.142 → tina4_python-0.2.144}/tina4_python/translations/en/LC_MESSAGES/messages.po +80 -80
  45. {tina4_python-0.2.142 → tina4_python-0.2.144}/tina4_python/translations/fr/LC_MESSAGES/messages.po +84 -84
  46. {tina4_python-0.2.142 → tina4_python-0.2.144}/tina4_python/public/css/readme.md +0 -0
  47. {tina4_python-0.2.142 → tina4_python-0.2.144}/tina4_python/public/favicon.ico +0 -0
  48. {tina4_python-0.2.142 → tina4_python-0.2.144}/tina4_python/public/images/403.png +0 -0
  49. {tina4_python-0.2.142 → tina4_python-0.2.144}/tina4_python/public/images/404.png +0 -0
  50. {tina4_python-0.2.142 → tina4_python-0.2.144}/tina4_python/public/images/500.png +0 -0
  51. {tina4_python-0.2.142 → tina4_python-0.2.144}/tina4_python/public/images/logo.png +0 -0
  52. {tina4_python-0.2.142 → tina4_python-0.2.144}/tina4_python/public/images/readme.md +0 -0
  53. {tina4_python-0.2.142 → tina4_python-0.2.144}/tina4_python/public/js/readme.md +0 -0
  54. {tina4_python-0.2.142 → tina4_python-0.2.144}/tina4_python/templates/readme.md +0 -0
  55. {tina4_python-0.2.142 → tina4_python-0.2.144}/tina4_python/translations/en/LC_MESSAGES/messages.mo +0 -0
  56. {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,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: tina4-python
3
- Version: 0.2.142
3
+ Version: 0.2.144
4
4
  Summary: Tina4Python - This is not another framework for Python
5
5
  Author-email: Andre van Zuydam <andrevanzuydam@gmail.com>
6
6
  Requires-Python: <4.0,>=3.12
@@ -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,6 +1,6 @@
1
1
  [project]
2
2
  name = "tina4-python"
3
- version = "0.2.142"
3
+ version = "0.2.144"
4
4
  description = "Tina4Python - This is not another framework for Python"
5
5
  authors = [
6
6
  {name = "Andre van Zuydam",email = "andrevanzuydam@gmail.com"}
@@ -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
+ }