arcane-flask 2.0.7__tar.gz → 2.2.1__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.
@@ -0,0 +1,152 @@
1
+ Metadata-Version: 2.4
2
+ Name: arcane-flask
3
+ Version: 2.2.1
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.
@@ -5,3 +5,4 @@ from .exception_decorator import *
5
5
  from .log import *
6
6
  from .http_response import *
7
7
  from .types import *
8
+ __version__ = "2.1.0"
@@ -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
- func_response = job_func(*args, **kwargs)
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.info(message)
35
+ logging.error(message, exc_info=True)
35
36
  raise e
36
37
  return wrapper
37
38
  return catch_exceptions
@@ -0,0 +1,209 @@
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
+
205
+ root.addFilter(filter)
206
+ for handler in root.handlers:
207
+ handler.addFilter(filter)
208
+
209
+ return root
@@ -0,0 +1,57 @@
1
+ [tool.poetry]
2
+ name = "arcane-flask"
3
+
4
+ version = "2.2.1"
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
@@ -1,49 +0,0 @@
1
- Metadata-Version: 2.4
2
- Name: arcane-flask
3
- Version: 2.0.7
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
-
@@ -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.7"
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-core>=1.0.0"]
30
- build-backend = "poetry.core.masonry.api"