arcane-flask 2.0.6__tar.gz → 2.2.0__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.
- arcane_flask-2.2.0/PKG-INFO +152 -0
- arcane_flask-2.2.0/README.md +125 -0
- {arcane_flask-2.0.6 → arcane_flask-2.2.0}/arcane/flask/__init__.py +1 -0
- {arcane_flask-2.0.6 → arcane_flask-2.2.0}/arcane/flask/authentication.py +12 -3
- {arcane_flask-2.0.6 → arcane_flask-2.2.0}/arcane/flask/exception_decorator.py +3 -2
- arcane_flask-2.2.0/arcane/flask/log.py +205 -0
- arcane_flask-2.2.0/pyproject.toml +57 -0
- arcane_flask-2.0.6/PKG-INFO +0 -49
- arcane_flask-2.0.6/README.md +0 -22
- arcane_flask-2.0.6/arcane/flask/log.py +0 -96
- arcane_flask-2.0.6/pyproject.toml +0 -30
- {arcane_flask-2.0.6 → arcane_flask-2.2.0}/arcane/flask/http_response.py +0 -0
- {arcane_flask-2.0.6 → arcane_flask-2.2.0}/arcane/flask/services.py +0 -0
- {arcane_flask-2.0.6 → arcane_flask-2.2.0}/arcane/flask/tracking.py +0 -0
- {arcane_flask-2.0.6 → arcane_flask-2.2.0}/arcane/flask/types.py +0 -0
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: arcane-flask
|
|
3
|
+
Version: 2.2.0
|
|
4
|
+
Summary: Utility functions for flask apps.
|
|
5
|
+
Author: Arcane
|
|
6
|
+
Author-email: product@wearcane.com
|
|
7
|
+
Requires-Python: >=3.8,<4.0
|
|
8
|
+
Classifier: Programming Language :: Python :: 3
|
|
9
|
+
Classifier: Programming Language :: Python :: 3.8
|
|
10
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
11
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
12
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
16
|
+
Requires-Dist: arcane-core (>=1.6.0,<2.0.0)
|
|
17
|
+
Requires-Dist: arcane-datastore (>=1.1.0,<2.0.0)
|
|
18
|
+
Requires-Dist: arcane-pubsub (>=1.1.0,<2.0.0)
|
|
19
|
+
Requires-Dist: backoff (>=1.10.0)
|
|
20
|
+
Requires-Dist: firebase_admin (==5.2.0)
|
|
21
|
+
Requires-Dist: flask (>=2.2.0,<3.0.0)
|
|
22
|
+
Requires-Dist: flask_log_request_id (>=0.10.1,<0.11.0)
|
|
23
|
+
Requires-Dist: pydash (>=8.0.3,<9.0.0)
|
|
24
|
+
Requires-Dist: pytz (>=2024.2,<2025.0)
|
|
25
|
+
Description-Content-Type: text/markdown
|
|
26
|
+
|
|
27
|
+
# Arcane flask
|
|
28
|
+
|
|
29
|
+
This package help us authenticate users
|
|
30
|
+
|
|
31
|
+
## Get Started
|
|
32
|
+
|
|
33
|
+
```sh
|
|
34
|
+
pip install arcane-flask
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
## Example Usage
|
|
38
|
+
|
|
39
|
+
```python
|
|
40
|
+
from arcane.flask import check_access_rights
|
|
41
|
+
from arcane.core import RightsLevelEnum, UserRightsEnum
|
|
42
|
+
from arcane.datastore import Client as DatastoreClient
|
|
43
|
+
from arcane.pubsub import Client as PubSubClient
|
|
44
|
+
|
|
45
|
+
datastore_client = DatastoreClient()
|
|
46
|
+
pubsub_client = PubSubClient()
|
|
47
|
+
|
|
48
|
+
@check_access_rights(
|
|
49
|
+
service='my-service',
|
|
50
|
+
required_rights=RightsLevelEnum.VIEWER,
|
|
51
|
+
service_user_right=UserRightsEnum.MY_SERVICE,
|
|
52
|
+
datastore_client=datastore_client,
|
|
53
|
+
pubsub_client=pubsub_client,
|
|
54
|
+
receive_rights_per_client=True,
|
|
55
|
+
project='my-project',
|
|
56
|
+
timeout=30 # Timeout in seconds
|
|
57
|
+
)
|
|
58
|
+
def function(params):
|
|
59
|
+
pass
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
## Timeout Configuration
|
|
63
|
+
|
|
64
|
+
The `check_access_rights` decorator includes a built-in `timeout` parameter that allows you to set a maximum execution time for the decorated function. If the function exceeds this timeout, the decorator will automatically return a timeout response.
|
|
65
|
+
|
|
66
|
+
```python
|
|
67
|
+
import time
|
|
68
|
+
|
|
69
|
+
@check_access_rights(
|
|
70
|
+
service='my-service',
|
|
71
|
+
required_rights=RightsLevelEnum.VIEWER,
|
|
72
|
+
service_user_right=UserRightsEnum.MY_SERVICE,
|
|
73
|
+
datastore_client=datastore_client,
|
|
74
|
+
pubsub_client=pubsub_client,
|
|
75
|
+
project='my-project',
|
|
76
|
+
timeout=2 # 2 seconds timeout
|
|
77
|
+
)
|
|
78
|
+
def slow_function():
|
|
79
|
+
time.sleep(5) # This will exceed the timeout
|
|
80
|
+
return {'result': 'success'}
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
If the function exceeds the timeout, the decorator will return:
|
|
84
|
+
|
|
85
|
+
```python
|
|
86
|
+
({'detail': 'Request Timeout'}, 504)
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
**Note:** Set the `timeout` parameter to a value a few seconds less than your Cloud Run or server timeout configuration to ensure proper error handling.
|
|
90
|
+
|
|
91
|
+
## Structured logging labels
|
|
92
|
+
|
|
93
|
+
The `arcane.flask.logs` module lets you add structured labels to every log
|
|
94
|
+
entry in a request. This is useful for attaching metadata such as
|
|
95
|
+
`component`, `module`, `function`, or IDs like `optimization_id`.
|
|
96
|
+
|
|
97
|
+
First, set up logging once at application startup:
|
|
98
|
+
|
|
99
|
+
```python
|
|
100
|
+
from arcane.flask import logs
|
|
101
|
+
|
|
102
|
+
logs.setup_logging(gcp_project="my-gcp-project-id")
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
Then, register a teardown handler to clear labels at the end of each request:
|
|
106
|
+
|
|
107
|
+
```python
|
|
108
|
+
from arcane.flask import logs
|
|
109
|
+
|
|
110
|
+
@app.teardown_request
|
|
111
|
+
def clear_logging_labels(exc):
|
|
112
|
+
logs.clear_labels()
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
Inside your handlers you can either attach labels imperatively:
|
|
116
|
+
|
|
117
|
+
```python
|
|
118
|
+
from arcane.flask import logs
|
|
119
|
+
|
|
120
|
+
def validate_model_parallelism_post(optimization_id: str, job_prefix: str, ...):
|
|
121
|
+
logs.attach_labels(
|
|
122
|
+
component="feed-boost",
|
|
123
|
+
module="api",
|
|
124
|
+
function="validate_model_parallelism_post",
|
|
125
|
+
optimization_id=optimization_id,
|
|
126
|
+
job_prefix=job_prefix,
|
|
127
|
+
)
|
|
128
|
+
...
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
or use the `with_log_labels` decorator for static labels and still add dynamic
|
|
132
|
+
labels as needed:
|
|
133
|
+
|
|
134
|
+
```python
|
|
135
|
+
from arcane.flask import logs
|
|
136
|
+
|
|
137
|
+
@logs.with_log_labels(
|
|
138
|
+
component="feed-boost",
|
|
139
|
+
module="api",
|
|
140
|
+
function="validate_model_parallelism_post",
|
|
141
|
+
)
|
|
142
|
+
def validate_model_parallelism_post(optimization_id: str, job_prefix: str, ...):
|
|
143
|
+
logs.attach_labels(
|
|
144
|
+
optimization_id=optimization_id,
|
|
145
|
+
job_prefix=job_prefix,
|
|
146
|
+
)
|
|
147
|
+
...
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
All log records emitted during the request will then include a
|
|
151
|
+
`logging.googleapis.com/labels` field with the JSON-encoded labels.
|
|
152
|
+
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
# Arcane flask
|
|
2
|
+
|
|
3
|
+
This package help us authenticate users
|
|
4
|
+
|
|
5
|
+
## Get Started
|
|
6
|
+
|
|
7
|
+
```sh
|
|
8
|
+
pip install arcane-flask
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Example Usage
|
|
12
|
+
|
|
13
|
+
```python
|
|
14
|
+
from arcane.flask import check_access_rights
|
|
15
|
+
from arcane.core import RightsLevelEnum, UserRightsEnum
|
|
16
|
+
from arcane.datastore import Client as DatastoreClient
|
|
17
|
+
from arcane.pubsub import Client as PubSubClient
|
|
18
|
+
|
|
19
|
+
datastore_client = DatastoreClient()
|
|
20
|
+
pubsub_client = PubSubClient()
|
|
21
|
+
|
|
22
|
+
@check_access_rights(
|
|
23
|
+
service='my-service',
|
|
24
|
+
required_rights=RightsLevelEnum.VIEWER,
|
|
25
|
+
service_user_right=UserRightsEnum.MY_SERVICE,
|
|
26
|
+
datastore_client=datastore_client,
|
|
27
|
+
pubsub_client=pubsub_client,
|
|
28
|
+
receive_rights_per_client=True,
|
|
29
|
+
project='my-project',
|
|
30
|
+
timeout=30 # Timeout in seconds
|
|
31
|
+
)
|
|
32
|
+
def function(params):
|
|
33
|
+
pass
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
## Timeout Configuration
|
|
37
|
+
|
|
38
|
+
The `check_access_rights` decorator includes a built-in `timeout` parameter that allows you to set a maximum execution time for the decorated function. If the function exceeds this timeout, the decorator will automatically return a timeout response.
|
|
39
|
+
|
|
40
|
+
```python
|
|
41
|
+
import time
|
|
42
|
+
|
|
43
|
+
@check_access_rights(
|
|
44
|
+
service='my-service',
|
|
45
|
+
required_rights=RightsLevelEnum.VIEWER,
|
|
46
|
+
service_user_right=UserRightsEnum.MY_SERVICE,
|
|
47
|
+
datastore_client=datastore_client,
|
|
48
|
+
pubsub_client=pubsub_client,
|
|
49
|
+
project='my-project',
|
|
50
|
+
timeout=2 # 2 seconds timeout
|
|
51
|
+
)
|
|
52
|
+
def slow_function():
|
|
53
|
+
time.sleep(5) # This will exceed the timeout
|
|
54
|
+
return {'result': 'success'}
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
If the function exceeds the timeout, the decorator will return:
|
|
58
|
+
|
|
59
|
+
```python
|
|
60
|
+
({'detail': 'Request Timeout'}, 504)
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
**Note:** Set the `timeout` parameter to a value a few seconds less than your Cloud Run or server timeout configuration to ensure proper error handling.
|
|
64
|
+
|
|
65
|
+
## Structured logging labels
|
|
66
|
+
|
|
67
|
+
The `arcane.flask.logs` module lets you add structured labels to every log
|
|
68
|
+
entry in a request. This is useful for attaching metadata such as
|
|
69
|
+
`component`, `module`, `function`, or IDs like `optimization_id`.
|
|
70
|
+
|
|
71
|
+
First, set up logging once at application startup:
|
|
72
|
+
|
|
73
|
+
```python
|
|
74
|
+
from arcane.flask import logs
|
|
75
|
+
|
|
76
|
+
logs.setup_logging(gcp_project="my-gcp-project-id")
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
Then, register a teardown handler to clear labels at the end of each request:
|
|
80
|
+
|
|
81
|
+
```python
|
|
82
|
+
from arcane.flask import logs
|
|
83
|
+
|
|
84
|
+
@app.teardown_request
|
|
85
|
+
def clear_logging_labels(exc):
|
|
86
|
+
logs.clear_labels()
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
Inside your handlers you can either attach labels imperatively:
|
|
90
|
+
|
|
91
|
+
```python
|
|
92
|
+
from arcane.flask import logs
|
|
93
|
+
|
|
94
|
+
def validate_model_parallelism_post(optimization_id: str, job_prefix: str, ...):
|
|
95
|
+
logs.attach_labels(
|
|
96
|
+
component="feed-boost",
|
|
97
|
+
module="api",
|
|
98
|
+
function="validate_model_parallelism_post",
|
|
99
|
+
optimization_id=optimization_id,
|
|
100
|
+
job_prefix=job_prefix,
|
|
101
|
+
)
|
|
102
|
+
...
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
or use the `with_log_labels` decorator for static labels and still add dynamic
|
|
106
|
+
labels as needed:
|
|
107
|
+
|
|
108
|
+
```python
|
|
109
|
+
from arcane.flask import logs
|
|
110
|
+
|
|
111
|
+
@logs.with_log_labels(
|
|
112
|
+
component="feed-boost",
|
|
113
|
+
module="api",
|
|
114
|
+
function="validate_model_parallelism_post",
|
|
115
|
+
)
|
|
116
|
+
def validate_model_parallelism_post(optimization_id: str, job_prefix: str, ...):
|
|
117
|
+
logs.attach_labels(
|
|
118
|
+
optimization_id=optimization_id,
|
|
119
|
+
job_prefix=job_prefix,
|
|
120
|
+
)
|
|
121
|
+
...
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
All log records emitted during the request will then include a
|
|
125
|
+
`logging.googleapis.com/labels` field with the JSON-encoded labels.
|
|
@@ -14,7 +14,7 @@ from google.cloud.exceptions import ServiceUnavailable
|
|
|
14
14
|
from arcane.core import CLIENTS_PARAM, ALL_CLIENTS_RIGHTS, RightsLevelEnum, GOOGLE_EXCEPTIONS_TO_RETRY, BadRequestError
|
|
15
15
|
from arcane.datastore import Client as DatastoreClient
|
|
16
16
|
from arcane.pubsub import Client as PubSubClient
|
|
17
|
-
|
|
17
|
+
import concurrent.futures
|
|
18
18
|
|
|
19
19
|
from .tracking import send_tracking_information
|
|
20
20
|
|
|
@@ -39,7 +39,8 @@ def check_access_rights(service: str,
|
|
|
39
39
|
kwargs_name_and_path_to_get: Dict[str, str] = {},
|
|
40
40
|
receive_rights_per_client: bool=False,
|
|
41
41
|
auth_enabled: bool=True,
|
|
42
|
-
project: str = None
|
|
42
|
+
project: str = None,
|
|
43
|
+
timeout: Optional[int] = None,
|
|
43
44
|
) -> Callable[[Callable], Callable]:
|
|
44
45
|
"""
|
|
45
46
|
This functions checks the authorizations from the current HTTP call.
|
|
@@ -135,7 +136,15 @@ def check_access_rights(service: str,
|
|
|
135
136
|
authorized_clients = [client_entity.key.name for client_entity in clients_query.fetch()]
|
|
136
137
|
|
|
137
138
|
kwargs[CLIENTS_PARAM] = sorted(set(authorized_clients))
|
|
138
|
-
|
|
139
|
+
try:
|
|
140
|
+
with concurrent.futures.ThreadPoolExecutor() as executor:
|
|
141
|
+
future = executor.submit(job_func, *args, **kwargs)
|
|
142
|
+
func_response = future.result(timeout=timeout)
|
|
143
|
+
except concurrent.futures.TimeoutError:
|
|
144
|
+
func_response = {'detail': 'Request Timeout'}, 504
|
|
145
|
+
except Exception as e:
|
|
146
|
+
func_response = {'detail': f'Server Error: {str(e)}'}, 500
|
|
147
|
+
|
|
139
148
|
end = time.time()
|
|
140
149
|
|
|
141
150
|
additionnal_info = {}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import functools
|
|
2
|
+
import logging
|
|
2
3
|
from typing import Callable
|
|
3
4
|
import logging
|
|
4
5
|
from flask import request
|
|
@@ -16,7 +17,7 @@ def catch_exceptions_service(service: str, auth_enabled=True) -> Callable:
|
|
|
16
17
|
try:
|
|
17
18
|
return job_func(*args, **kwargs)
|
|
18
19
|
except Exception as e:
|
|
19
|
-
message = f"Error while executing method {job_func.__name__} in service {service} "
|
|
20
|
+
message = f"Error while executing method {job_func.__name__!r} in service {service!r} "
|
|
20
21
|
try:
|
|
21
22
|
message += f' for user {get_user_email(auth_enabled=auth_enabled)} '
|
|
22
23
|
except ValueError:
|
|
@@ -31,7 +32,7 @@ def catch_exceptions_service(service: str, auth_enabled=True) -> Callable:
|
|
|
31
32
|
|
|
32
33
|
message += f"Args : {str(args)} / kwargs : {str(kwargs)} / headers : {request.environ.get('HTTP_ORIGIN')}"
|
|
33
34
|
|
|
34
|
-
logging.
|
|
35
|
+
logging.error(message, exc_info=True)
|
|
35
36
|
raise e
|
|
36
37
|
return wrapper
|
|
37
38
|
return catch_exceptions
|
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
import json
|
|
2
|
+
import logging
|
|
3
|
+
import sys
|
|
4
|
+
from functools import wraps
|
|
5
|
+
from typing import Any, Callable, Literal, Optional
|
|
6
|
+
|
|
7
|
+
from flask import g, has_request_context, request
|
|
8
|
+
from flask_log_request_id import current_request_id
|
|
9
|
+
|
|
10
|
+
STANDARD_FORMAT = '{"severity": "%(levelname)s", "logging.googleapis.com/trace": "%(cloud_trace)s", "logging.googleapis.com/spanId": "%(span_id)s", "component": "arbitrary", "message": "%(message)s", "logging.googleapis.com/labels": %(labels)s}'
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class LabelFilter(logging.Filter):
|
|
14
|
+
"""
|
|
15
|
+
Logging filter that injects structured labels into each log record.
|
|
16
|
+
|
|
17
|
+
Labels are stored per-request on ``flask.g.logging_labels`` (a simple
|
|
18
|
+
dictionary) and JSON-encoded into the ``labels`` attribute of the
|
|
19
|
+
record. When there is no active request context, ``labels`` defaults
|
|
20
|
+
to the empty JSON object (``{}``).
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
def filter(self, record: logging.LogRecord) -> bool:
|
|
24
|
+
if not hasattr(record, "labels"):
|
|
25
|
+
record.labels = "{}"
|
|
26
|
+
if has_request_context():
|
|
27
|
+
labels = getattr(g, "logging_labels", {})
|
|
28
|
+
record.labels = json.dumps(labels)
|
|
29
|
+
return True
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def attach_labels(**labels: Any) -> None:
|
|
33
|
+
"""
|
|
34
|
+
Attach or update structured logging labels for the current request.
|
|
35
|
+
|
|
36
|
+
This stores key/value pairs on ``flask.g.logging_labels`` which are
|
|
37
|
+
then picked up by ``LabelFilter`` and emitted in every log record
|
|
38
|
+
for the remainder of the request.
|
|
39
|
+
|
|
40
|
+
Example
|
|
41
|
+
-------
|
|
42
|
+
>>> attach_labels(component="feed-boost", module="api")
|
|
43
|
+
|
|
44
|
+
Notes
|
|
45
|
+
-----
|
|
46
|
+
This function must be called inside an active Flask request (or
|
|
47
|
+
application) context; otherwise Flask will raise a
|
|
48
|
+
``RuntimeError`` about working outside of the application context.
|
|
49
|
+
"""
|
|
50
|
+
if not hasattr(g, "logging_labels"):
|
|
51
|
+
g.logging_labels = {}
|
|
52
|
+
g.logging_labels.update(labels)
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def clear_labels() -> None:
|
|
56
|
+
"""
|
|
57
|
+
Clear all structured logging labels for the current request.
|
|
58
|
+
|
|
59
|
+
In a typical Flask application you should call this from a
|
|
60
|
+
``teardown_request`` handler to ensure labels do not leak between
|
|
61
|
+
re-used contexts in tests or background processing:
|
|
62
|
+
|
|
63
|
+
@app.teardown_request
|
|
64
|
+
def _clear_labels(exc):
|
|
65
|
+
logs.clear_labels()
|
|
66
|
+
"""
|
|
67
|
+
if hasattr(g, "logging_labels"):
|
|
68
|
+
g.logging_labels = {}
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def with_log_labels(**static_labels: Any) -> Callable[[Callable[..., Any]], Callable[..., Any]]:
|
|
72
|
+
"""
|
|
73
|
+
Decorator that automatically attaches static logging labels
|
|
74
|
+
at the beginning of a request handler.
|
|
75
|
+
|
|
76
|
+
This is a convenience wrapper around :func:`attach_labels` for
|
|
77
|
+
common, mostly-static metadata such as ``component``, ``module``
|
|
78
|
+
or ``function``.
|
|
79
|
+
|
|
80
|
+
Example
|
|
81
|
+
-------
|
|
82
|
+
>>> @with_log_labels(component="feed-boost", module="api", function="validate_model_parallelism_post")
|
|
83
|
+
... def validate_model_parallelism_post(...):
|
|
84
|
+
... attach_labels(optimization_id=optimization_id, job_prefix=job_prefix)
|
|
85
|
+
... ...
|
|
86
|
+
"""
|
|
87
|
+
|
|
88
|
+
def decorator(func: Callable[..., Any]) -> Callable[..., Any]:
|
|
89
|
+
@wraps(func)
|
|
90
|
+
def wrapper(*args: Any, **kwargs: Any) -> Any:
|
|
91
|
+
# This will be a no-op outside a request context, but will
|
|
92
|
+
# raise the standard Flask RuntimeError if misused.
|
|
93
|
+
attach_labels(**static_labels)
|
|
94
|
+
return func(*args, **kwargs)
|
|
95
|
+
|
|
96
|
+
return wrapper
|
|
97
|
+
|
|
98
|
+
return decorator
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def adscale_log(
|
|
102
|
+
service: str, # should be ValueOf ServiceEnum
|
|
103
|
+
msg: str
|
|
104
|
+
):
|
|
105
|
+
"""Deprecated, should not be used anymore
|
|
106
|
+
"""
|
|
107
|
+
request_id = None
|
|
108
|
+
try:
|
|
109
|
+
request_id = current_request_id()
|
|
110
|
+
except KeyError:
|
|
111
|
+
pass
|
|
112
|
+
|
|
113
|
+
logging.info("[%s][%s] %s", service, request_id, msg)
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
class RequestFormatter(logging.Formatter):
|
|
117
|
+
"""Logging Formatter to add request id and trace id to log records
|
|
118
|
+
"""
|
|
119
|
+
def __init__(self,
|
|
120
|
+
fmt: Optional[str] = STANDARD_FORMAT,
|
|
121
|
+
datefmt: Optional[str] = None,
|
|
122
|
+
style: Literal["%", "{", "$"] = "%",
|
|
123
|
+
validate: bool = True,
|
|
124
|
+
gcp_project: Optional[str] = None,
|
|
125
|
+
span_id_header: str = "X-Span-Id"):
|
|
126
|
+
|
|
127
|
+
super().__init__(fmt, datefmt, style, validate)
|
|
128
|
+
self.gcp_project = gcp_project
|
|
129
|
+
self.span_id_header = span_id_header
|
|
130
|
+
|
|
131
|
+
def format(self, record):
|
|
132
|
+
if has_request_context():
|
|
133
|
+
trace_header: str = request.headers.get("X-Cloud-Trace-Context")
|
|
134
|
+
span_id : str = request.headers.get(self.span_id_header)
|
|
135
|
+
if trace_header:
|
|
136
|
+
trace = trace_header.split("/")
|
|
137
|
+
record.cloud_trace = f"projects/{self.gcp_project}/traces/{trace[0]}"
|
|
138
|
+
else:
|
|
139
|
+
record.cloud_trace = None
|
|
140
|
+
|
|
141
|
+
if span_id:
|
|
142
|
+
record.span_id = f"projects/{self.gcp_project}/spanId/{span_id}"
|
|
143
|
+
else:
|
|
144
|
+
record.span_id = None
|
|
145
|
+
else:
|
|
146
|
+
record.cloud_trace = None
|
|
147
|
+
record.span_id = None
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
# Handle exception information properly
|
|
151
|
+
if record.exc_info:
|
|
152
|
+
# Format the exception with stack trace as a single string
|
|
153
|
+
exc_text = self.formatException(record.exc_info)
|
|
154
|
+
# Combine the message with the exception info
|
|
155
|
+
full_message = f"{record.getMessage()}\n{exc_text}"
|
|
156
|
+
else:
|
|
157
|
+
full_message = record.getMessage()
|
|
158
|
+
|
|
159
|
+
# JSON encode the complete message to escape newlines and special characters
|
|
160
|
+
record.msg = json.dumps(full_message)
|
|
161
|
+
# Clear args to prevent double formatting
|
|
162
|
+
record.args = None
|
|
163
|
+
|
|
164
|
+
return super().format(record)
|
|
165
|
+
|
|
166
|
+
def setup_logging(
|
|
167
|
+
fmt : Optional[str] = STANDARD_FORMAT,
|
|
168
|
+
datefmt: Optional[str] = None,
|
|
169
|
+
style : Literal["%", "{", "$"] = "%",
|
|
170
|
+
validate: bool = True,
|
|
171
|
+
gcp_project: Optional[str] = None,
|
|
172
|
+
level = logging.INFO,
|
|
173
|
+
span_id_header: str = "X-Span-Id",
|
|
174
|
+
filter: Optional[logging.Filter] = None
|
|
175
|
+
):
|
|
176
|
+
"""
|
|
177
|
+
Setup logging for the application
|
|
178
|
+
"""
|
|
179
|
+
handler = logging.StreamHandler(
|
|
180
|
+
sys.stdout
|
|
181
|
+
)
|
|
182
|
+
|
|
183
|
+
handler.setFormatter(
|
|
184
|
+
RequestFormatter(
|
|
185
|
+
fmt,
|
|
186
|
+
datefmt,
|
|
187
|
+
style,
|
|
188
|
+
validate,
|
|
189
|
+
gcp_project,
|
|
190
|
+
span_id_header
|
|
191
|
+
)
|
|
192
|
+
)
|
|
193
|
+
|
|
194
|
+
logging.basicConfig(
|
|
195
|
+
level=level,
|
|
196
|
+
handlers=[handler]
|
|
197
|
+
)
|
|
198
|
+
|
|
199
|
+
root = logging.getLogger()
|
|
200
|
+
|
|
201
|
+
if not filter:
|
|
202
|
+
filter = LabelFilter()
|
|
203
|
+
|
|
204
|
+
root.addFilter(filter)
|
|
205
|
+
return root
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
[tool.poetry]
|
|
2
|
+
name = "arcane-flask"
|
|
3
|
+
|
|
4
|
+
version = "2.2.0"
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
description = "Utility functions for flask apps."
|
|
8
|
+
readme = "README.md"
|
|
9
|
+
authors = ["Arcane <product@wearcane.com>"]
|
|
10
|
+
packages = [
|
|
11
|
+
{ include = "arcane" }
|
|
12
|
+
]
|
|
13
|
+
|
|
14
|
+
[tool.poetry.dependencies]
|
|
15
|
+
python = "^3.8"
|
|
16
|
+
arcane-core = "^1.6.0"
|
|
17
|
+
flask = "^2.2.0"
|
|
18
|
+
flask_log_request_id = "^0.10.1"
|
|
19
|
+
firebase_admin = "5.2.0"
|
|
20
|
+
arcane-datastore = "^1.1.0"
|
|
21
|
+
arcane-pubsub = "^1.1.0"
|
|
22
|
+
pydash = "^8.0.3"
|
|
23
|
+
backoff = ">=1.10.0"
|
|
24
|
+
pytz = "^2024.2"
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
[tool.poetry.group.dev.dependencies]
|
|
28
|
+
pytest = "^8.3.4"
|
|
29
|
+
pytest-mock = "^3.14.0"
|
|
30
|
+
pytest-cov = "^4.1.0"
|
|
31
|
+
|
|
32
|
+
[build-system]
|
|
33
|
+
requires = ["poetry-core>=1.0.0"]
|
|
34
|
+
build-backend = "poetry.core.masonry.api"
|
|
35
|
+
|
|
36
|
+
[tool.coverage.run]
|
|
37
|
+
branch = true
|
|
38
|
+
|
|
39
|
+
[tool.coverage.report]
|
|
40
|
+
# Regexes for lines to exclude from consideration
|
|
41
|
+
exclude_also = [
|
|
42
|
+
# Don't complain about missing debug-only code:
|
|
43
|
+
"def __repr__",
|
|
44
|
+
"if self\\.debug",
|
|
45
|
+
|
|
46
|
+
# Don't complain if tests don't hit defensive assertion code:
|
|
47
|
+
"raise AssertionError",
|
|
48
|
+
"raise NotImplementedError",
|
|
49
|
+
|
|
50
|
+
# Don't complain if non-runnable code isn't run:
|
|
51
|
+
"if 0:",
|
|
52
|
+
"if __name__ == .__main__.:",
|
|
53
|
+
|
|
54
|
+
# Don't complain about abstract methods, they aren't run:
|
|
55
|
+
"@(abc\\.)?abstractmethod",
|
|
56
|
+
]
|
|
57
|
+
ignore_errors = true
|
arcane_flask-2.0.6/PKG-INFO
DELETED
|
@@ -1,49 +0,0 @@
|
|
|
1
|
-
Metadata-Version: 2.4
|
|
2
|
-
Name: arcane-flask
|
|
3
|
-
Version: 2.0.6
|
|
4
|
-
Summary: Utility functions for flask apps.
|
|
5
|
-
Author: Arcane
|
|
6
|
-
Author-email: product@wearcane.com
|
|
7
|
-
Requires-Python: >=3.8,<4.0
|
|
8
|
-
Classifier: Programming Language :: Python :: 3
|
|
9
|
-
Classifier: Programming Language :: Python :: 3.8
|
|
10
|
-
Classifier: Programming Language :: Python :: 3.9
|
|
11
|
-
Classifier: Programming Language :: Python :: 3.10
|
|
12
|
-
Classifier: Programming Language :: Python :: 3.11
|
|
13
|
-
Classifier: Programming Language :: Python :: 3.12
|
|
14
|
-
Classifier: Programming Language :: Python :: 3.13
|
|
15
|
-
Classifier: Programming Language :: Python :: 3.14
|
|
16
|
-
Requires-Dist: arcane-core (>=1.6.0,<2.0.0)
|
|
17
|
-
Requires-Dist: arcane-datastore (>=1.1.0,<2.0.0)
|
|
18
|
-
Requires-Dist: arcane-pubsub (>=1.1.0,<2.0.0)
|
|
19
|
-
Requires-Dist: backoff (>=1.10.0)
|
|
20
|
-
Requires-Dist: firebase_admin (==5.2.0)
|
|
21
|
-
Requires-Dist: flask (>=2.2.0,<3.0.0)
|
|
22
|
-
Requires-Dist: flask_log_request_id (>=0.10.1,<0.11.0)
|
|
23
|
-
Requires-Dist: pydash (>=8.0.3,<9.0.0)
|
|
24
|
-
Requires-Dist: pytz (>=2024.2,<2025.0)
|
|
25
|
-
Description-Content-Type: text/markdown
|
|
26
|
-
|
|
27
|
-
# Arcane flask
|
|
28
|
-
|
|
29
|
-
This package help us authenticate users
|
|
30
|
-
|
|
31
|
-
## Get Started
|
|
32
|
-
|
|
33
|
-
```sh
|
|
34
|
-
pip install arcane-flask
|
|
35
|
-
```
|
|
36
|
-
|
|
37
|
-
## Example Usage
|
|
38
|
-
|
|
39
|
-
```python
|
|
40
|
-
from arcane import flask
|
|
41
|
-
|
|
42
|
-
@check_access_rights(service='function', required_rights='Viewer',
|
|
43
|
-
receive_rights_per_client=True, project=Config.Project, adscale_key=Config.Key)
|
|
44
|
-
def function(params):
|
|
45
|
-
pass
|
|
46
|
-
|
|
47
|
-
```
|
|
48
|
-
|
|
49
|
-
|
arcane_flask-2.0.6/README.md
DELETED
|
@@ -1,22 +0,0 @@
|
|
|
1
|
-
# Arcane flask
|
|
2
|
-
|
|
3
|
-
This package help us authenticate users
|
|
4
|
-
|
|
5
|
-
## Get Started
|
|
6
|
-
|
|
7
|
-
```sh
|
|
8
|
-
pip install arcane-flask
|
|
9
|
-
```
|
|
10
|
-
|
|
11
|
-
## Example Usage
|
|
12
|
-
|
|
13
|
-
```python
|
|
14
|
-
from arcane import flask
|
|
15
|
-
|
|
16
|
-
@check_access_rights(service='function', required_rights='Viewer',
|
|
17
|
-
receive_rights_per_client=True, project=Config.Project, adscale_key=Config.Key)
|
|
18
|
-
def function(params):
|
|
19
|
-
pass
|
|
20
|
-
|
|
21
|
-
```
|
|
22
|
-
|
|
@@ -1,96 +0,0 @@
|
|
|
1
|
-
import json
|
|
2
|
-
import logging
|
|
3
|
-
from typing import Optional, Literal
|
|
4
|
-
import sys
|
|
5
|
-
|
|
6
|
-
from flask import has_request_context, request
|
|
7
|
-
from flask_log_request_id import current_request_id
|
|
8
|
-
|
|
9
|
-
STANDARD_FORMAT = '{"severity": "%(levelname)s", "logging.googleapis.com/trace": "%(cloud_trace)s", "component": "arbitrary", "message": %(message)s}'
|
|
10
|
-
def adscale_log(
|
|
11
|
-
service: str, # should be ValueOf ServiceEnum
|
|
12
|
-
msg: str
|
|
13
|
-
):
|
|
14
|
-
"""Deprecated, should not be used anymore
|
|
15
|
-
"""
|
|
16
|
-
request_id = None
|
|
17
|
-
try:
|
|
18
|
-
request_id = current_request_id()
|
|
19
|
-
except KeyError:
|
|
20
|
-
pass
|
|
21
|
-
|
|
22
|
-
logging.info(f"[{service}][{request_id}] {msg}")
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
class RequestFormatter(logging.Formatter):
|
|
26
|
-
"""Logging Formatter to add request id and trace id to log records
|
|
27
|
-
"""
|
|
28
|
-
def __init__(self,
|
|
29
|
-
fmt: Optional[str] = STANDARD_FORMAT,
|
|
30
|
-
datefmt: Optional[str] = None,
|
|
31
|
-
style: Literal["%", "{", "$"] = "%",
|
|
32
|
-
validate: bool = True,
|
|
33
|
-
gcp_project: Optional[str] = None):
|
|
34
|
-
|
|
35
|
-
super().__init__(fmt, datefmt, style, validate)
|
|
36
|
-
self.gcp_project = gcp_project
|
|
37
|
-
|
|
38
|
-
def format(self, record):
|
|
39
|
-
if has_request_context():
|
|
40
|
-
trace_header: str = request.headers.get("X-Cloud-Trace-Context")
|
|
41
|
-
if trace_header:
|
|
42
|
-
trace = trace_header.split("/")
|
|
43
|
-
record.cloud_trace = f"projects/{self.gcp_project}/traces/{trace[0]}"
|
|
44
|
-
else:
|
|
45
|
-
record.cloud_trace = None
|
|
46
|
-
else:
|
|
47
|
-
record.cloud_trace = None
|
|
48
|
-
|
|
49
|
-
# Handle exception information properly
|
|
50
|
-
if record.exc_info:
|
|
51
|
-
# Format the exception with stack trace as a single string
|
|
52
|
-
exc_text = self.formatException(record.exc_info)
|
|
53
|
-
# Combine the message with the exception info
|
|
54
|
-
full_message = f"{record.getMessage()}\n{exc_text}"
|
|
55
|
-
else:
|
|
56
|
-
full_message = record.getMessage()
|
|
57
|
-
|
|
58
|
-
# JSON encode the complete message to escape newlines and special characters
|
|
59
|
-
record.msg = json.dumps(full_message)
|
|
60
|
-
# Clear args to prevent double formatting
|
|
61
|
-
record.args = None
|
|
62
|
-
|
|
63
|
-
return super().format(record)
|
|
64
|
-
|
|
65
|
-
def setup_logging(
|
|
66
|
-
fmt : Optional[str] = STANDARD_FORMAT,
|
|
67
|
-
datefmt: Optional[str] = None,
|
|
68
|
-
style : Literal["%", "{", "$"] = "%",
|
|
69
|
-
validate: bool = True,
|
|
70
|
-
gcp_project: Optional[str] = None,
|
|
71
|
-
level = logging.INFO
|
|
72
|
-
):
|
|
73
|
-
"""
|
|
74
|
-
Setup logging for the application
|
|
75
|
-
"""
|
|
76
|
-
handler = logging.StreamHandler(
|
|
77
|
-
sys.stdout
|
|
78
|
-
)
|
|
79
|
-
|
|
80
|
-
handler.setFormatter(
|
|
81
|
-
RequestFormatter(
|
|
82
|
-
fmt,
|
|
83
|
-
datefmt,
|
|
84
|
-
style,
|
|
85
|
-
validate,
|
|
86
|
-
gcp_project
|
|
87
|
-
)
|
|
88
|
-
)
|
|
89
|
-
|
|
90
|
-
logging.basicConfig(
|
|
91
|
-
level=level,
|
|
92
|
-
handlers=[handler]
|
|
93
|
-
)
|
|
94
|
-
|
|
95
|
-
root = logging.getLogger()
|
|
96
|
-
return root
|
|
@@ -1,30 +0,0 @@
|
|
|
1
|
-
[tool.poetry]
|
|
2
|
-
name = "arcane-flask"
|
|
3
|
-
|
|
4
|
-
version = "2.0.6"
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
description = "Utility functions for flask apps."
|
|
8
|
-
readme = "README.md"
|
|
9
|
-
authors = ["Arcane <product@wearcane.com>"]
|
|
10
|
-
packages = [
|
|
11
|
-
{ include = "arcane" }
|
|
12
|
-
]
|
|
13
|
-
|
|
14
|
-
[tool.poetry.dependencies]
|
|
15
|
-
python = "^3.8"
|
|
16
|
-
arcane-core = "^1.6.0"
|
|
17
|
-
flask = "^2.2.0"
|
|
18
|
-
flask_log_request_id = "^0.10.1"
|
|
19
|
-
firebase_admin = "5.2.0"
|
|
20
|
-
arcane-datastore = "^1.1.0"
|
|
21
|
-
arcane-pubsub = "^1.1.0"
|
|
22
|
-
pydash = "^8.0.3"
|
|
23
|
-
backoff = ">=1.10.0"
|
|
24
|
-
pytz = "^2024.2"
|
|
25
|
-
|
|
26
|
-
[tool.poetry.group.dev.dependencies]
|
|
27
|
-
|
|
28
|
-
[build-system]
|
|
29
|
-
requires = ["poetry>=0.12"]
|
|
30
|
-
build-backend = "poetry.masonry.api"
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|