recurvedata-lib 0.1.487__py2.py3-none-any.whl
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.
Potentially problematic release.
This version of recurvedata-lib might be problematic. Click here for more details.
- recurvedata/__init__.py +0 -0
- recurvedata/__version__.py +1 -0
- recurvedata/client/__init__.py +3 -0
- recurvedata/client/client.py +150 -0
- recurvedata/client/server_client.py +91 -0
- recurvedata/config.py +99 -0
- recurvedata/connectors/__init__.py +20 -0
- recurvedata/connectors/_register.py +46 -0
- recurvedata/connectors/base.py +111 -0
- recurvedata/connectors/config_schema.py +1575 -0
- recurvedata/connectors/connectors/__init__.py +0 -0
- recurvedata/connectors/connectors/aliyun_access_key.py +30 -0
- recurvedata/connectors/connectors/auth.py +44 -0
- recurvedata/connectors/connectors/azure_blob.py +89 -0
- recurvedata/connectors/connectors/azure_synapse.py +79 -0
- recurvedata/connectors/connectors/bigquery.py +359 -0
- recurvedata/connectors/connectors/clickhouse.py +219 -0
- recurvedata/connectors/connectors/dingtalk.py +61 -0
- recurvedata/connectors/connectors/doris.py +215 -0
- recurvedata/connectors/connectors/es.py +62 -0
- recurvedata/connectors/connectors/feishu.py +65 -0
- recurvedata/connectors/connectors/ftp.py +50 -0
- recurvedata/connectors/connectors/generic.py +49 -0
- recurvedata/connectors/connectors/google_cloud_storage.py +115 -0
- recurvedata/connectors/connectors/google_service_account.py +225 -0
- recurvedata/connectors/connectors/hive.py +207 -0
- recurvedata/connectors/connectors/impala.py +210 -0
- recurvedata/connectors/connectors/jenkins.py +51 -0
- recurvedata/connectors/connectors/mail.py +89 -0
- recurvedata/connectors/connectors/microsoft_fabric.py +284 -0
- recurvedata/connectors/connectors/mongo.py +79 -0
- recurvedata/connectors/connectors/mssql.py +131 -0
- recurvedata/connectors/connectors/mysql.py +191 -0
- recurvedata/connectors/connectors/n8n.py +141 -0
- recurvedata/connectors/connectors/oss.py +74 -0
- recurvedata/connectors/connectors/owncloud.py +36 -0
- recurvedata/connectors/connectors/phoenix.py +36 -0
- recurvedata/connectors/connectors/postgres.py +230 -0
- recurvedata/connectors/connectors/python.py +50 -0
- recurvedata/connectors/connectors/redshift.py +187 -0
- recurvedata/connectors/connectors/s3.py +93 -0
- recurvedata/connectors/connectors/sftp.py +87 -0
- recurvedata/connectors/connectors/slack.py +35 -0
- recurvedata/connectors/connectors/spark.py +99 -0
- recurvedata/connectors/connectors/starrocks.py +175 -0
- recurvedata/connectors/connectors/tencent_cos.py +40 -0
- recurvedata/connectors/connectors/tidb.py +49 -0
- recurvedata/connectors/const.py +315 -0
- recurvedata/connectors/datasource.py +189 -0
- recurvedata/connectors/dbapi.py +469 -0
- recurvedata/connectors/fs.py +66 -0
- recurvedata/connectors/ftp.py +40 -0
- recurvedata/connectors/object_store.py +60 -0
- recurvedata/connectors/pigeon.py +172 -0
- recurvedata/connectors/proxy.py +104 -0
- recurvedata/connectors/service.py +223 -0
- recurvedata/connectors/utils.py +47 -0
- recurvedata/consts.py +49 -0
- recurvedata/core/__init__.py +0 -0
- recurvedata/core/config.py +46 -0
- recurvedata/core/configurable.py +27 -0
- recurvedata/core/consts.py +2 -0
- recurvedata/core/templating.py +206 -0
- recurvedata/core/tracing.py +223 -0
- recurvedata/core/transformer.py +186 -0
- recurvedata/core/translation.py +91 -0
- recurvedata/dbt/client.py +97 -0
- recurvedata/dbt/consts.py +99 -0
- recurvedata/dbt/cosmos_utils.py +275 -0
- recurvedata/dbt/error_codes.py +18 -0
- recurvedata/dbt/schemas.py +98 -0
- recurvedata/dbt/service.py +451 -0
- recurvedata/dbt/utils.py +246 -0
- recurvedata/error_codes.py +71 -0
- recurvedata/exceptions.py +72 -0
- recurvedata/executors/__init__.py +4 -0
- recurvedata/executors/cli/__init__.py +7 -0
- recurvedata/executors/cli/connector.py +117 -0
- recurvedata/executors/cli/dbt.py +118 -0
- recurvedata/executors/cli/main.py +82 -0
- recurvedata/executors/cli/parameters.py +18 -0
- recurvedata/executors/client.py +190 -0
- recurvedata/executors/consts.py +50 -0
- recurvedata/executors/debug_executor.py +100 -0
- recurvedata/executors/executor.py +300 -0
- recurvedata/executors/link_executor.py +189 -0
- recurvedata/executors/models.py +34 -0
- recurvedata/executors/schemas.py +222 -0
- recurvedata/executors/service/__init__.py +0 -0
- recurvedata/executors/service/connector.py +380 -0
- recurvedata/executors/utils.py +172 -0
- recurvedata/filestorage/__init__.py +11 -0
- recurvedata/filestorage/_factory.py +33 -0
- recurvedata/filestorage/backends/__init__.py +0 -0
- recurvedata/filestorage/backends/fsspec.py +45 -0
- recurvedata/filestorage/backends/local.py +67 -0
- recurvedata/filestorage/backends/oss.py +56 -0
- recurvedata/filestorage/interface.py +84 -0
- recurvedata/operators/__init__.py +10 -0
- recurvedata/operators/base.py +28 -0
- recurvedata/operators/config.py +21 -0
- recurvedata/operators/context.py +255 -0
- recurvedata/operators/dbt_operator/__init__.py +2 -0
- recurvedata/operators/dbt_operator/model_pipeline_link_operator.py +55 -0
- recurvedata/operators/dbt_operator/operator.py +353 -0
- recurvedata/operators/link_operator/__init__.py +1 -0
- recurvedata/operators/link_operator/operator.py +120 -0
- recurvedata/operators/models.py +55 -0
- recurvedata/operators/notify_operator/__init__.py +1 -0
- recurvedata/operators/notify_operator/operator.py +180 -0
- recurvedata/operators/operator.py +119 -0
- recurvedata/operators/python_operator/__init__.py +1 -0
- recurvedata/operators/python_operator/operator.py +132 -0
- recurvedata/operators/sensor_operator/__init__.py +1 -0
- recurvedata/operators/sensor_operator/airflow_utils.py +63 -0
- recurvedata/operators/sensor_operator/operator.py +172 -0
- recurvedata/operators/spark_operator/__init__.py +1 -0
- recurvedata/operators/spark_operator/operator.py +200 -0
- recurvedata/operators/spark_operator/spark_sample.py +47 -0
- recurvedata/operators/sql_operator/__init__.py +1 -0
- recurvedata/operators/sql_operator/operator.py +90 -0
- recurvedata/operators/task.py +211 -0
- recurvedata/operators/transfer_operator/__init__.py +40 -0
- recurvedata/operators/transfer_operator/const.py +10 -0
- recurvedata/operators/transfer_operator/dump_aliyun_sls.py +82 -0
- recurvedata/operators/transfer_operator/dump_sheet_task_base.py +292 -0
- recurvedata/operators/transfer_operator/dump_task_cass.py +155 -0
- recurvedata/operators/transfer_operator/dump_task_dbapi.py +209 -0
- recurvedata/operators/transfer_operator/dump_task_es.py +113 -0
- recurvedata/operators/transfer_operator/dump_task_feishu_sheet.py +114 -0
- recurvedata/operators/transfer_operator/dump_task_ftp.py +234 -0
- recurvedata/operators/transfer_operator/dump_task_google_sheet.py +66 -0
- recurvedata/operators/transfer_operator/dump_task_mongodb.py +168 -0
- recurvedata/operators/transfer_operator/dump_task_oss.py +285 -0
- recurvedata/operators/transfer_operator/dump_task_python.py +212 -0
- recurvedata/operators/transfer_operator/dump_task_s3.py +270 -0
- recurvedata/operators/transfer_operator/dump_task_sftp.py +229 -0
- recurvedata/operators/transfer_operator/load_task_aliyun_oss.py +107 -0
- recurvedata/operators/transfer_operator/load_task_azure_blob.py +115 -0
- recurvedata/operators/transfer_operator/load_task_azure_synapse.py +90 -0
- recurvedata/operators/transfer_operator/load_task_clickhouse.py +167 -0
- recurvedata/operators/transfer_operator/load_task_doris.py +164 -0
- recurvedata/operators/transfer_operator/load_task_email.py +188 -0
- recurvedata/operators/transfer_operator/load_task_es.py +86 -0
- recurvedata/operators/transfer_operator/load_task_filebrowser.py +151 -0
- recurvedata/operators/transfer_operator/load_task_ftp.py +19 -0
- recurvedata/operators/transfer_operator/load_task_google_bigquery.py +90 -0
- recurvedata/operators/transfer_operator/load_task_google_cloud_storage.py +127 -0
- recurvedata/operators/transfer_operator/load_task_google_sheet.py +130 -0
- recurvedata/operators/transfer_operator/load_task_hive.py +158 -0
- recurvedata/operators/transfer_operator/load_task_microsoft_fabric.py +105 -0
- recurvedata/operators/transfer_operator/load_task_mssql.py +153 -0
- recurvedata/operators/transfer_operator/load_task_mysql.py +157 -0
- recurvedata/operators/transfer_operator/load_task_owncloud.py +135 -0
- recurvedata/operators/transfer_operator/load_task_postgresql.py +109 -0
- recurvedata/operators/transfer_operator/load_task_qcloud_cos.py +119 -0
- recurvedata/operators/transfer_operator/load_task_recurve_data_prep.py +75 -0
- recurvedata/operators/transfer_operator/load_task_redshift.py +95 -0
- recurvedata/operators/transfer_operator/load_task_s3.py +150 -0
- recurvedata/operators/transfer_operator/load_task_sftp.py +90 -0
- recurvedata/operators/transfer_operator/load_task_starrocks.py +169 -0
- recurvedata/operators/transfer_operator/load_task_yicrowds.py +97 -0
- recurvedata/operators/transfer_operator/mixin.py +31 -0
- recurvedata/operators/transfer_operator/operator.py +231 -0
- recurvedata/operators/transfer_operator/task.py +223 -0
- recurvedata/operators/transfer_operator/utils.py +134 -0
- recurvedata/operators/ui.py +80 -0
- recurvedata/operators/utils/__init__.py +51 -0
- recurvedata/operators/utils/file_factory.py +150 -0
- recurvedata/operators/utils/fs.py +10 -0
- recurvedata/operators/utils/lineage.py +265 -0
- recurvedata/operators/web_init.py +15 -0
- recurvedata/pigeon/connector/__init__.py +294 -0
- recurvedata/pigeon/connector/_registry.py +17 -0
- recurvedata/pigeon/connector/aliyun_oss.py +80 -0
- recurvedata/pigeon/connector/awss3.py +123 -0
- recurvedata/pigeon/connector/azure_blob.py +176 -0
- recurvedata/pigeon/connector/azure_synapse.py +51 -0
- recurvedata/pigeon/connector/cass.py +151 -0
- recurvedata/pigeon/connector/clickhouse.py +403 -0
- recurvedata/pigeon/connector/clickhouse_native.py +351 -0
- recurvedata/pigeon/connector/dbapi.py +571 -0
- recurvedata/pigeon/connector/doris.py +166 -0
- recurvedata/pigeon/connector/es.py +176 -0
- recurvedata/pigeon/connector/feishu.py +1135 -0
- recurvedata/pigeon/connector/ftp.py +163 -0
- recurvedata/pigeon/connector/google_bigquery.py +283 -0
- recurvedata/pigeon/connector/google_cloud_storage.py +130 -0
- recurvedata/pigeon/connector/hbase_phoenix.py +108 -0
- recurvedata/pigeon/connector/hdfs.py +204 -0
- recurvedata/pigeon/connector/hive_impala.py +383 -0
- recurvedata/pigeon/connector/microsoft_fabric.py +95 -0
- recurvedata/pigeon/connector/mongodb.py +56 -0
- recurvedata/pigeon/connector/mssql.py +467 -0
- recurvedata/pigeon/connector/mysql.py +175 -0
- recurvedata/pigeon/connector/owncloud.py +92 -0
- recurvedata/pigeon/connector/postgresql.py +267 -0
- recurvedata/pigeon/connector/power_bi.py +179 -0
- recurvedata/pigeon/connector/qcloud_cos.py +79 -0
- recurvedata/pigeon/connector/redshift.py +123 -0
- recurvedata/pigeon/connector/sftp.py +73 -0
- recurvedata/pigeon/connector/sqlite.py +42 -0
- recurvedata/pigeon/connector/starrocks.py +144 -0
- recurvedata/pigeon/connector/tableau.py +162 -0
- recurvedata/pigeon/const.py +21 -0
- recurvedata/pigeon/csv.py +172 -0
- recurvedata/pigeon/docs/datasources-example.json +82 -0
- recurvedata/pigeon/docs/images/pigeon_design.png +0 -0
- recurvedata/pigeon/docs/lightweight-data-sync-solution.md +111 -0
- recurvedata/pigeon/dumper/__init__.py +171 -0
- recurvedata/pigeon/dumper/aliyun_sls.py +415 -0
- recurvedata/pigeon/dumper/base.py +141 -0
- recurvedata/pigeon/dumper/cass.py +213 -0
- recurvedata/pigeon/dumper/dbapi.py +346 -0
- recurvedata/pigeon/dumper/es.py +112 -0
- recurvedata/pigeon/dumper/ftp.py +64 -0
- recurvedata/pigeon/dumper/mongodb.py +103 -0
- recurvedata/pigeon/handler/__init__.py +4 -0
- recurvedata/pigeon/handler/base.py +153 -0
- recurvedata/pigeon/handler/csv_handler.py +290 -0
- recurvedata/pigeon/loader/__init__.py +87 -0
- recurvedata/pigeon/loader/base.py +83 -0
- recurvedata/pigeon/loader/csv_to_azure_synapse.py +214 -0
- recurvedata/pigeon/loader/csv_to_clickhouse.py +152 -0
- recurvedata/pigeon/loader/csv_to_doris.py +215 -0
- recurvedata/pigeon/loader/csv_to_es.py +51 -0
- recurvedata/pigeon/loader/csv_to_google_bigquery.py +169 -0
- recurvedata/pigeon/loader/csv_to_hive.py +468 -0
- recurvedata/pigeon/loader/csv_to_microsoft_fabric.py +242 -0
- recurvedata/pigeon/loader/csv_to_mssql.py +174 -0
- recurvedata/pigeon/loader/csv_to_mysql.py +180 -0
- recurvedata/pigeon/loader/csv_to_postgresql.py +248 -0
- recurvedata/pigeon/loader/csv_to_redshift.py +240 -0
- recurvedata/pigeon/loader/csv_to_starrocks.py +233 -0
- recurvedata/pigeon/meta.py +116 -0
- recurvedata/pigeon/row_factory.py +42 -0
- recurvedata/pigeon/schema/__init__.py +124 -0
- recurvedata/pigeon/schema/types.py +13 -0
- recurvedata/pigeon/sync.py +283 -0
- recurvedata/pigeon/transformer.py +146 -0
- recurvedata/pigeon/utils/__init__.py +134 -0
- recurvedata/pigeon/utils/bloomfilter.py +181 -0
- recurvedata/pigeon/utils/date_time.py +323 -0
- recurvedata/pigeon/utils/escape.py +15 -0
- recurvedata/pigeon/utils/fs.py +266 -0
- recurvedata/pigeon/utils/json.py +44 -0
- recurvedata/pigeon/utils/keyed_tuple.py +85 -0
- recurvedata/pigeon/utils/mp.py +156 -0
- recurvedata/pigeon/utils/sql.py +328 -0
- recurvedata/pigeon/utils/timing.py +155 -0
- recurvedata/provider_manager.py +0 -0
- recurvedata/providers/__init__.py +0 -0
- recurvedata/providers/dbapi/__init__.py +0 -0
- recurvedata/providers/flywheel/__init__.py +0 -0
- recurvedata/providers/mysql/__init__.py +0 -0
- recurvedata/schedulers/__init__.py +1 -0
- recurvedata/schedulers/airflow.py +974 -0
- recurvedata/schedulers/airflow_db_process.py +331 -0
- recurvedata/schedulers/airflow_operators.py +61 -0
- recurvedata/schedulers/airflow_plugin.py +9 -0
- recurvedata/schedulers/airflow_trigger_dag_patch.py +117 -0
- recurvedata/schedulers/base.py +99 -0
- recurvedata/schedulers/cli.py +228 -0
- recurvedata/schedulers/client.py +56 -0
- recurvedata/schedulers/consts.py +52 -0
- recurvedata/schedulers/debug_celery.py +62 -0
- recurvedata/schedulers/model.py +63 -0
- recurvedata/schedulers/schemas.py +97 -0
- recurvedata/schedulers/service.py +20 -0
- recurvedata/schedulers/system_dags.py +59 -0
- recurvedata/schedulers/task_status.py +279 -0
- recurvedata/schedulers/utils.py +73 -0
- recurvedata/schema/__init__.py +0 -0
- recurvedata/schema/field.py +88 -0
- recurvedata/schema/schema.py +55 -0
- recurvedata/schema/types.py +17 -0
- recurvedata/schema.py +0 -0
- recurvedata/server/__init__.py +0 -0
- recurvedata/server/app.py +7 -0
- recurvedata/server/connector/__init__.py +0 -0
- recurvedata/server/connector/api.py +79 -0
- recurvedata/server/connector/schemas.py +28 -0
- recurvedata/server/data_service/__init__.py +0 -0
- recurvedata/server/data_service/api.py +126 -0
- recurvedata/server/data_service/client.py +18 -0
- recurvedata/server/data_service/consts.py +1 -0
- recurvedata/server/data_service/schemas.py +68 -0
- recurvedata/server/data_service/service.py +218 -0
- recurvedata/server/dbt/__init__.py +0 -0
- recurvedata/server/dbt/api.py +116 -0
- recurvedata/server/error_code.py +49 -0
- recurvedata/server/exceptions.py +19 -0
- recurvedata/server/executor/__init__.py +0 -0
- recurvedata/server/executor/api.py +37 -0
- recurvedata/server/executor/schemas.py +30 -0
- recurvedata/server/executor/service.py +220 -0
- recurvedata/server/main.py +32 -0
- recurvedata/server/schedulers/__init__.py +0 -0
- recurvedata/server/schedulers/api.py +252 -0
- recurvedata/server/schedulers/schemas.py +50 -0
- recurvedata/server/schemas.py +50 -0
- recurvedata/utils/__init__.py +15 -0
- recurvedata/utils/_typer.py +61 -0
- recurvedata/utils/attrdict.py +19 -0
- recurvedata/utils/command_helper.py +20 -0
- recurvedata/utils/compat.py +12 -0
- recurvedata/utils/compression.py +203 -0
- recurvedata/utils/crontab.py +42 -0
- recurvedata/utils/crypto_util.py +305 -0
- recurvedata/utils/dataclass.py +11 -0
- recurvedata/utils/date_time.py +464 -0
- recurvedata/utils/dispatch.py +114 -0
- recurvedata/utils/email_util.py +104 -0
- recurvedata/utils/files.py +386 -0
- recurvedata/utils/helpers.py +170 -0
- recurvedata/utils/httputil.py +117 -0
- recurvedata/utils/imports.py +132 -0
- recurvedata/utils/json.py +80 -0
- recurvedata/utils/log.py +117 -0
- recurvedata/utils/log_capture.py +153 -0
- recurvedata/utils/mp.py +178 -0
- recurvedata/utils/normalizer.py +102 -0
- recurvedata/utils/redis_lock.py +474 -0
- recurvedata/utils/registry.py +54 -0
- recurvedata/utils/shell.py +15 -0
- recurvedata/utils/singleton.py +33 -0
- recurvedata/utils/sql.py +6 -0
- recurvedata/utils/timeout.py +28 -0
- recurvedata/utils/tracing.py +14 -0
- recurvedata_lib-0.1.487.dist-info/METADATA +605 -0
- recurvedata_lib-0.1.487.dist-info/RECORD +333 -0
- recurvedata_lib-0.1.487.dist-info/WHEEL +5 -0
- recurvedata_lib-0.1.487.dist-info/entry_points.txt +6 -0
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
import copy
|
|
2
|
+
import datetime
|
|
3
|
+
import inspect
|
|
4
|
+
import re
|
|
5
|
+
import types
|
|
6
|
+
from typing import Any, Callable, TypeVar
|
|
7
|
+
|
|
8
|
+
import jinja2.nodes
|
|
9
|
+
from jinja2 import Environment, TemplateSyntaxError, meta, pass_context
|
|
10
|
+
from jinja2.runtime import Context
|
|
11
|
+
|
|
12
|
+
from recurvedata.utils.crontab import previous_schedule
|
|
13
|
+
from recurvedata.utils.registry import jinja2_template_funcs_registry
|
|
14
|
+
|
|
15
|
+
T = TypeVar("T")
|
|
16
|
+
|
|
17
|
+
# {% set navigation = [('index.html', 'Index'), ('about.html', 'About')] %} -> navigation
|
|
18
|
+
# {% set key, value = call_something() %} -> key, value
|
|
19
|
+
_jinja2_set_p = re.compile(r"\{%\s*set\s([\w,\s]+?)\s*=.*")
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def get_template_env() -> Environment:
|
|
23
|
+
env = Environment(
|
|
24
|
+
cache_size=0,
|
|
25
|
+
trim_blocks=True,
|
|
26
|
+
lstrip_blocks=True,
|
|
27
|
+
)
|
|
28
|
+
for func_name, func in jinja2_template_funcs_registry.items():
|
|
29
|
+
env.globals[func_name] = func
|
|
30
|
+
return env
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def extract_vars_from_template_code(template_code: str) -> list[str]:
|
|
34
|
+
"""
|
|
35
|
+
This function is copied from recurve-server recurve.library.jinja_utils
|
|
36
|
+
"""
|
|
37
|
+
env = Environment(autoescape=True)
|
|
38
|
+
ast = env.parse(template_code)
|
|
39
|
+
|
|
40
|
+
extracted_var_names = []
|
|
41
|
+
|
|
42
|
+
# Helper function to recursively walk through nodes
|
|
43
|
+
def visit_node(node: jinja2.nodes.Node):
|
|
44
|
+
# We're looking for Call nodes where the function is 'var'
|
|
45
|
+
if isinstance(node, jinja2.nodes.Call) and isinstance(node.node, jinja2.nodes.Name) and node.node.name == "var":
|
|
46
|
+
# The first argument of the Call node is the variable name
|
|
47
|
+
arguments = [arg.value for arg in node.args if isinstance(arg, jinja2.nodes.Const)]
|
|
48
|
+
extracted_var_names.append(arguments[0])
|
|
49
|
+
|
|
50
|
+
# Recursively visit child nodes
|
|
51
|
+
for child_node in node.iter_child_nodes():
|
|
52
|
+
visit_node(child_node)
|
|
53
|
+
|
|
54
|
+
# Start the traversal
|
|
55
|
+
visit_node(ast)
|
|
56
|
+
|
|
57
|
+
return extracted_var_names
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
@pass_context
|
|
61
|
+
def var_function(context: Context, name: str, default: Any = None) -> Any:
|
|
62
|
+
return context.get(name, default)
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
class Renderer(object):
|
|
66
|
+
def __init__(self):
|
|
67
|
+
self.env = get_template_env()
|
|
68
|
+
self.env.globals["var"] = var_function
|
|
69
|
+
|
|
70
|
+
@staticmethod
|
|
71
|
+
def init_context(execution_date: datetime.datetime, schedule_interval: str):
|
|
72
|
+
yesterday_dttm = execution_date - datetime.timedelta(days=1)
|
|
73
|
+
tomorrow_dttm = execution_date + datetime.timedelta(days=1)
|
|
74
|
+
data_interval_start = previous_schedule(schedule_interval, execution_date)
|
|
75
|
+
|
|
76
|
+
template_context = {
|
|
77
|
+
"dt": execution_date.date(),
|
|
78
|
+
"yesterday": yesterday_dttm,
|
|
79
|
+
"yesterday_dt": yesterday_dttm.date(),
|
|
80
|
+
"tomorrow": tomorrow_dttm,
|
|
81
|
+
"tomorrow_dt": tomorrow_dttm.date(),
|
|
82
|
+
# "execution_date": execution_date,
|
|
83
|
+
"logical_date": execution_date,
|
|
84
|
+
"data_interval_start": data_interval_start,
|
|
85
|
+
"data_interval_end": execution_date,
|
|
86
|
+
"data_interval_start_dt": data_interval_start and data_interval_start.date(),
|
|
87
|
+
"data_interval_end_dt": execution_date.date(),
|
|
88
|
+
}
|
|
89
|
+
return template_context
|
|
90
|
+
|
|
91
|
+
@staticmethod
|
|
92
|
+
def get_functions() -> dict[str, Callable]:
|
|
93
|
+
return dict(jinja2_template_funcs_registry.items())
|
|
94
|
+
|
|
95
|
+
def render_template(self, tmpl: T, context: dict) -> T:
|
|
96
|
+
if isinstance(tmpl, str):
|
|
97
|
+
result = self.env.from_string(tmpl).render(**context)
|
|
98
|
+
elif isinstance(tmpl, (tuple, list)):
|
|
99
|
+
result = [self.render_template(x, context) for x in tmpl]
|
|
100
|
+
elif isinstance(tmpl, dict):
|
|
101
|
+
result = {k: self.render_template(v, context) for k, v in tmpl.items()}
|
|
102
|
+
else:
|
|
103
|
+
# raise TypeError(f'Type {type(tmpl)} is not supported for templating')
|
|
104
|
+
result = tmpl
|
|
105
|
+
return result
|
|
106
|
+
|
|
107
|
+
def extract_variables(self, tmpl: str) -> list[str]:
|
|
108
|
+
ast = self.env.parse(tmpl)
|
|
109
|
+
var_variables: list[str] = extract_vars_from_template_code(tmpl)
|
|
110
|
+
variables: set[str] = meta.find_undeclared_variables(ast)
|
|
111
|
+
variables.update(var_variables)
|
|
112
|
+
|
|
113
|
+
# exclude assignments within if blocks.
|
|
114
|
+
# e.g. for this template, the undefined variables are ['yesterday_ds']
|
|
115
|
+
# WITHOUT 'dedup_order', which is a local variable defined by set
|
|
116
|
+
# {% if yesterday_ds <= '2020-09-25' %}
|
|
117
|
+
# {% set dedup_order = "snapshot_time ASC" %}
|
|
118
|
+
# {% else %}
|
|
119
|
+
# {% set dedup_order = "sell_count DESC NULLS LAST" %}
|
|
120
|
+
# {% endif %}
|
|
121
|
+
assignments: list[str] = _jinja2_set_p.findall(tmpl)
|
|
122
|
+
for vs in set(assignments):
|
|
123
|
+
for v in vs.split(","):
|
|
124
|
+
v = v.strip()
|
|
125
|
+
if v in variables:
|
|
126
|
+
variables.remove(v)
|
|
127
|
+
|
|
128
|
+
return sorted(variables)
|
|
129
|
+
|
|
130
|
+
def _prepare_jinja_context(
|
|
131
|
+
self, exist_variables: dict, execution_date: datetime.datetime, schedule_interval: str
|
|
132
|
+
) -> dict:
|
|
133
|
+
template_var_dct = self.init_context(execution_date, schedule_interval)
|
|
134
|
+
context = copy.copy(template_var_dct) # shallow copy
|
|
135
|
+
context.update(exist_variables) # python code may use exist_variables
|
|
136
|
+
for func_name, func in self.get_functions().items():
|
|
137
|
+
context.setdefault(func_name, func)
|
|
138
|
+
|
|
139
|
+
return context
|
|
140
|
+
|
|
141
|
+
def render_variables(
|
|
142
|
+
self, variables: dict[str, Any], execution_date: datetime.datetime, schedule_interval: str
|
|
143
|
+
) -> dict:
|
|
144
|
+
"""
|
|
145
|
+
Renders variables in a dictionary using Jinja2 templating.
|
|
146
|
+
|
|
147
|
+
This method processes each string value in the input dictionary,
|
|
148
|
+
rendering it with Jinja2 if it contains template variables. The
|
|
149
|
+
rendered values are then updated in the original dictionary.
|
|
150
|
+
|
|
151
|
+
Args:
|
|
152
|
+
variables: A dictionary of variables to render.
|
|
153
|
+
execution_date: a given date that some date function can be used to calculate
|
|
154
|
+
schedule_interval: A string representing the schedule interval (crontab expression).
|
|
155
|
+
Returns:
|
|
156
|
+
A dictionary with the same keys as the input, but with rendered values.
|
|
157
|
+
"""
|
|
158
|
+
context = self._prepare_jinja_context(variables, execution_date, schedule_interval)
|
|
159
|
+
update_dct = {}
|
|
160
|
+
for name, val in variables.items():
|
|
161
|
+
if not isinstance(val, str):
|
|
162
|
+
# Process only the Jinja variables within the string.
|
|
163
|
+
continue
|
|
164
|
+
try:
|
|
165
|
+
jinja_variables = self.extract_variables(val)
|
|
166
|
+
except TemplateSyntaxError:
|
|
167
|
+
# invalid jinja, leave it unrendered
|
|
168
|
+
continue
|
|
169
|
+
if not jinja_variables:
|
|
170
|
+
continue
|
|
171
|
+
|
|
172
|
+
rendered_val = self.render_template(val, context)
|
|
173
|
+
if rendered_val != val:
|
|
174
|
+
update_dct[name] = rendered_val
|
|
175
|
+
context[name] = rendered_val
|
|
176
|
+
variables.update(update_dct)
|
|
177
|
+
return variables
|
|
178
|
+
|
|
179
|
+
def extract_python_code_variable(
|
|
180
|
+
self, python_code: str, exist_variables: dict, execution_date: datetime.datetime, schedule_interval: str
|
|
181
|
+
) -> dict:
|
|
182
|
+
result = {}
|
|
183
|
+
name_space = self._prepare_jinja_context(exist_variables, execution_date, schedule_interval)
|
|
184
|
+
orig_name_space = copy.copy(name_space)
|
|
185
|
+
rendered_code = self.render_template(python_code, name_space)
|
|
186
|
+
|
|
187
|
+
compiled_code = compile(rendered_code, "", "exec")
|
|
188
|
+
exec(compiled_code, name_space)
|
|
189
|
+
|
|
190
|
+
for key2, value2 in name_space.items():
|
|
191
|
+
if key2 == "__builtins__":
|
|
192
|
+
continue
|
|
193
|
+
if isinstance(value2, types.ModuleType):
|
|
194
|
+
continue
|
|
195
|
+
if inspect.isclass(value2):
|
|
196
|
+
continue
|
|
197
|
+
if key2 in orig_name_space and orig_name_space[key2] == value2: # only return defined/updated var
|
|
198
|
+
continue
|
|
199
|
+
result[key2] = value2
|
|
200
|
+
return result
|
|
201
|
+
|
|
202
|
+
|
|
203
|
+
if __name__ == "__main__":
|
|
204
|
+
import doctest
|
|
205
|
+
|
|
206
|
+
doctest.testmod()
|
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
import functools
|
|
3
|
+
import inspect
|
|
4
|
+
import json
|
|
5
|
+
import random
|
|
6
|
+
import re
|
|
7
|
+
from typing import Any, Dict, overload
|
|
8
|
+
|
|
9
|
+
from opentelemetry import context, trace
|
|
10
|
+
from opentelemetry.context import Context
|
|
11
|
+
from opentelemetry.exporter.otlp.proto.http.trace_exporter import OTLPSpanExporter
|
|
12
|
+
from opentelemetry.sdk.resources import SERVICE_NAME, Resource
|
|
13
|
+
from opentelemetry.sdk.trace import Span, TracerProvider
|
|
14
|
+
from opentelemetry.sdk.trace.export import BatchSpanProcessor, SpanExporter
|
|
15
|
+
from opentelemetry.trace.propagation.tracecontext import TraceContextTextMapPropagator
|
|
16
|
+
from pydantic import BaseModel
|
|
17
|
+
|
|
18
|
+
from .consts import TRACE_KEY, TRACING_CONTEXT_KEY
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class _OTLPSpanExporter(OTLPSpanExporter):
|
|
22
|
+
def update_auth_headers(self, headers: Dict[str, str]):
|
|
23
|
+
if not headers:
|
|
24
|
+
return
|
|
25
|
+
self._session.headers.update(headers)
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class FilteringSpanProcessor(BatchSpanProcessor):
|
|
29
|
+
"""A span processor that filters out unwanted spans before they are created."""
|
|
30
|
+
|
|
31
|
+
def __init__(
|
|
32
|
+
self,
|
|
33
|
+
span_exporter: SpanExporter,
|
|
34
|
+
max_queue_size: int = None,
|
|
35
|
+
schedule_delay_millis: float = None,
|
|
36
|
+
max_export_batch_size: int = None,
|
|
37
|
+
export_timeout_millis: float = None,
|
|
38
|
+
):
|
|
39
|
+
super().__init__(
|
|
40
|
+
span_exporter,
|
|
41
|
+
schedule_delay_millis=schedule_delay_millis,
|
|
42
|
+
export_timeout_millis=export_timeout_millis,
|
|
43
|
+
max_export_batch_size=max_export_batch_size,
|
|
44
|
+
max_queue_size=max_queue_size,
|
|
45
|
+
)
|
|
46
|
+
# Single compiled pattern with OR operator for better performance
|
|
47
|
+
self._fastapi_pattern = re.compile(r"^(?:handling\s+event|Event\s+.*\s+dispatched$)")
|
|
48
|
+
|
|
49
|
+
def _should_filter_span(self, span: Span) -> bool:
|
|
50
|
+
"""Determine if a span should be filtered out using regex pattern."""
|
|
51
|
+
if not span.name:
|
|
52
|
+
return False
|
|
53
|
+
return bool(self._fastapi_pattern.match(span.name))
|
|
54
|
+
|
|
55
|
+
def on_end(self, span: Span) -> None:
|
|
56
|
+
"""Called when a span is ended."""
|
|
57
|
+
if not self._should_filter_span(span):
|
|
58
|
+
super().on_end(span)
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
class Singleton(type):
|
|
62
|
+
_instances = {}
|
|
63
|
+
|
|
64
|
+
def __call__(cls, *args, **kwargs):
|
|
65
|
+
if cls not in cls._instances:
|
|
66
|
+
instance = super().__call__(*args, **kwargs)
|
|
67
|
+
cls._instances[cls] = instance
|
|
68
|
+
return cls._instances[cls]
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
class Tracing(metaclass=Singleton):
|
|
72
|
+
@classmethod
|
|
73
|
+
def is_instantiated(cls):
|
|
74
|
+
"""
|
|
75
|
+
Returns the valid singleton instance if it exists, otherwise None.
|
|
76
|
+
"""
|
|
77
|
+
instance: "Tracing" = cls._instances.get(cls, None)
|
|
78
|
+
if not instance:
|
|
79
|
+
return False
|
|
80
|
+
if not instance.tracer:
|
|
81
|
+
return False
|
|
82
|
+
return True
|
|
83
|
+
|
|
84
|
+
def __init__(
|
|
85
|
+
self,
|
|
86
|
+
endpoint: str = None,
|
|
87
|
+
service: str = None,
|
|
88
|
+
export_timeout_millis: int = 1000,
|
|
89
|
+
schedule_delay_millis: int = 1000,
|
|
90
|
+
):
|
|
91
|
+
if endpoint and service:
|
|
92
|
+
self.init(endpoint, service, export_timeout_millis, schedule_delay_millis)
|
|
93
|
+
else:
|
|
94
|
+
self.exporter = None
|
|
95
|
+
self.tracer = None
|
|
96
|
+
|
|
97
|
+
def init(
|
|
98
|
+
self,
|
|
99
|
+
endpoint: str,
|
|
100
|
+
service: str,
|
|
101
|
+
export_timeout_millis: int = 1000,
|
|
102
|
+
schedule_delay_millis: int = 1000,
|
|
103
|
+
):
|
|
104
|
+
self.exporter = _OTLPSpanExporter(endpoint=endpoint)
|
|
105
|
+
processor = FilteringSpanProcessor(
|
|
106
|
+
self.exporter, schedule_delay_millis=schedule_delay_millis, export_timeout_millis=export_timeout_millis
|
|
107
|
+
)
|
|
108
|
+
resource = Resource.create(attributes={SERVICE_NAME: service})
|
|
109
|
+
provider = TracerProvider(resource=resource)
|
|
110
|
+
provider.add_span_processor(processor)
|
|
111
|
+
trace.set_tracer_provider(provider)
|
|
112
|
+
self.tracer = trace.get_tracer(service, tracer_provider=provider)
|
|
113
|
+
|
|
114
|
+
def set_auth_headers(self, headers: Dict[str, str]):
|
|
115
|
+
self.exporter.update_auth_headers(headers)
|
|
116
|
+
|
|
117
|
+
def dump_context(self) -> Dict[str, str]:
|
|
118
|
+
carrier = {}
|
|
119
|
+
TraceContextTextMapPropagator().inject(carrier)
|
|
120
|
+
return carrier
|
|
121
|
+
|
|
122
|
+
def load_context(self, ctx_json: Dict[str, str] | None = None) -> Context:
|
|
123
|
+
ctx = TraceContextTextMapPropagator().extract(ctx_json or {})
|
|
124
|
+
context.attach(ctx)
|
|
125
|
+
|
|
126
|
+
def send_context(self, payload: dict):
|
|
127
|
+
payload[TRACE_KEY] = self.dump_context()
|
|
128
|
+
return payload
|
|
129
|
+
|
|
130
|
+
@overload
|
|
131
|
+
def receive_context(self, payload: dict):
|
|
132
|
+
...
|
|
133
|
+
|
|
134
|
+
@overload
|
|
135
|
+
def receive_context(self, payload: str):
|
|
136
|
+
...
|
|
137
|
+
|
|
138
|
+
@overload
|
|
139
|
+
def receive_context(self, payload: Any):
|
|
140
|
+
...
|
|
141
|
+
|
|
142
|
+
def receive_context(self, payload: str | dict | BaseModel | Any):
|
|
143
|
+
if isinstance(payload, str):
|
|
144
|
+
try:
|
|
145
|
+
payload = json.loads(payload)
|
|
146
|
+
except Exception:
|
|
147
|
+
return
|
|
148
|
+
elif isinstance(payload, BaseModel):
|
|
149
|
+
payload = payload.model_dump(by_alias=True)
|
|
150
|
+
if not payload.get(TRACING_CONTEXT_KEY, None):
|
|
151
|
+
return
|
|
152
|
+
payload = payload.get(TRACING_CONTEXT_KEY)
|
|
153
|
+
try:
|
|
154
|
+
payload = json.loads(payload)
|
|
155
|
+
except Exception:
|
|
156
|
+
return
|
|
157
|
+
elif isinstance(payload, dict):
|
|
158
|
+
pass
|
|
159
|
+
else:
|
|
160
|
+
from fastapi import Request
|
|
161
|
+
|
|
162
|
+
if isinstance(payload, Request):
|
|
163
|
+
payload = payload.headers
|
|
164
|
+
else:
|
|
165
|
+
return
|
|
166
|
+
|
|
167
|
+
if TRACE_KEY in payload:
|
|
168
|
+
self.load_context(payload.get(TRACE_KEY))
|
|
169
|
+
|
|
170
|
+
def create_span(self, name: str = None, sampling_rate: float = 0.0, context_payload_name: str = None):
|
|
171
|
+
"""
|
|
172
|
+
Decorator to create a span for a function.
|
|
173
|
+
|
|
174
|
+
Args:
|
|
175
|
+
name: The name of the span. If not provided, the function name will be used.
|
|
176
|
+
sampling_rate: The rate at which to sample the span. Default is 0.0.
|
|
177
|
+
context_payload_name: The name of the argument that contains the context payload."""
|
|
178
|
+
|
|
179
|
+
def decorator(func):
|
|
180
|
+
def get_all_args_as_dict(func, args, kwargs):
|
|
181
|
+
"""Helper to get all arguments as a dictionary."""
|
|
182
|
+
sig = inspect.signature(func)
|
|
183
|
+
bound_args = sig.bind(*args, **kwargs)
|
|
184
|
+
bound_args.apply_defaults()
|
|
185
|
+
return dict(bound_args.arguments)
|
|
186
|
+
|
|
187
|
+
@functools.wraps(func)
|
|
188
|
+
async def async_wrapper(*args, **kwargs):
|
|
189
|
+
all_args = get_all_args_as_dict(func, args, kwargs)
|
|
190
|
+
if not self.tracer:
|
|
191
|
+
return await func(*args, **kwargs)
|
|
192
|
+
if context_payload_name:
|
|
193
|
+
if all_args.get(context_payload_name, None):
|
|
194
|
+
self.receive_context(all_args[context_payload_name])
|
|
195
|
+
span_name = name or f"{func.__module__}.{func.__name__}"
|
|
196
|
+
if not context.get_current():
|
|
197
|
+
if random.random() > sampling_rate:
|
|
198
|
+
return await func(*args, **kwargs)
|
|
199
|
+
with self.tracer.start_as_current_span(span_name):
|
|
200
|
+
return await func(*args, **kwargs)
|
|
201
|
+
|
|
202
|
+
@functools.wraps(func)
|
|
203
|
+
def wrapper(*args, **kwargs):
|
|
204
|
+
all_args = get_all_args_as_dict(func, args, kwargs)
|
|
205
|
+
if not self.tracer:
|
|
206
|
+
return func(*args, **kwargs)
|
|
207
|
+
if context_payload_name:
|
|
208
|
+
if all_args.get(context_payload_name, None):
|
|
209
|
+
self.receive_context(all_args[context_payload_name])
|
|
210
|
+
span_name = name or f"{func.__module__}.{func.__name__}"
|
|
211
|
+
if not context.get_current():
|
|
212
|
+
if random.random() > sampling_rate:
|
|
213
|
+
return func(*args, **kwargs)
|
|
214
|
+
with self.tracer.start_as_current_span(span_name):
|
|
215
|
+
return func(*args, **kwargs)
|
|
216
|
+
|
|
217
|
+
return async_wrapper if asyncio.iscoroutinefunction(func) else wrapper
|
|
218
|
+
|
|
219
|
+
return decorator
|
|
220
|
+
|
|
221
|
+
@property
|
|
222
|
+
def current_span(self) -> Span:
|
|
223
|
+
return trace.get_current_span()
|
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
import json
|
|
2
|
+
import struct
|
|
3
|
+
import zlib
|
|
4
|
+
from typing import Literal
|
|
5
|
+
|
|
6
|
+
from recurvedata.pigeon.schema import Schema
|
|
7
|
+
from recurvedata.utils.crypto_util import CryptoUtil
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class Transformer:
|
|
11
|
+
@property
|
|
12
|
+
def input_schema(self):
|
|
13
|
+
"""Returns the schema of input data"""
|
|
14
|
+
return getattr(self, "_input_schema", None)
|
|
15
|
+
|
|
16
|
+
@input_schema.setter
|
|
17
|
+
def input_schema(self, schema):
|
|
18
|
+
"""Should be called by the handler"""
|
|
19
|
+
assert isinstance(schema, Schema)
|
|
20
|
+
setattr(self, "_input_schema", schema)
|
|
21
|
+
|
|
22
|
+
@property
|
|
23
|
+
def output_schema(self):
|
|
24
|
+
"""Subclasses that change the rows schema should provide the output schema.
|
|
25
|
+
|
|
26
|
+
These operations will change the output schema:
|
|
27
|
+
- Add or remove fields
|
|
28
|
+
- Change the name of fields
|
|
29
|
+
- Change the type of fields
|
|
30
|
+
|
|
31
|
+
An example of valid schema:
|
|
32
|
+
|
|
33
|
+
from recurvedata.pigeon.schema import Schema, Field, types
|
|
34
|
+
|
|
35
|
+
Schema([
|
|
36
|
+
Field(name='id', type=types.INT32),
|
|
37
|
+
Field(name='name', type=types.STRING, size=64),
|
|
38
|
+
Field(name='snapshot_time', type=types.DATETIME),
|
|
39
|
+
Field(name='is_active', type=types.BOOLEAN)
|
|
40
|
+
])
|
|
41
|
+
|
|
42
|
+
Allowed types:
|
|
43
|
+
|
|
44
|
+
- INT8 = 'INT8' # 1-byte (8-bit) signed integers
|
|
45
|
+
- INT16 = 'INT16' # 2-byte (16-bit) signed integers
|
|
46
|
+
- INT32 = 'INT32' # 4-byte (32-bit) signed integers
|
|
47
|
+
- INT64 = 'INT64' # 8-byte (64-bit) signed integers
|
|
48
|
+
- FLOAT32 = 'FLOAT32' # 4-byte (32-bit) single-precision floating
|
|
49
|
+
- FLOAT64 = 'FLOAT64' # 8-byte (64-bit) double-precision floating
|
|
50
|
+
- BOOLEAN = 'BOOLEAN'
|
|
51
|
+
- DATETIME = 'DATETIME'
|
|
52
|
+
- DATE = 'DATE'
|
|
53
|
+
- STRING = 'STRING'
|
|
54
|
+
"""
|
|
55
|
+
return None
|
|
56
|
+
|
|
57
|
+
def transform(self, row: dict, *args, **kwargs) -> dict | list[dict] | None:
|
|
58
|
+
"""This is the method called by Handler.
|
|
59
|
+
|
|
60
|
+
It internally calls `transform_impl` to do the real transform logic.
|
|
61
|
+
Subclasses should implement `transform_impl` but not this method.
|
|
62
|
+
|
|
63
|
+
:param row: a Row (namedtuple) object contains a row record fetched from database
|
|
64
|
+
:type row: collection.namedtuple
|
|
65
|
+
:returns: returns one (tuple) or multiple (list of tuple) rows
|
|
66
|
+
"""
|
|
67
|
+
return self.transform_impl(row, *args, **kwargs)
|
|
68
|
+
|
|
69
|
+
def transform_impl(self, row: dict, *args, **kwargs) -> dict | list[dict] | None:
|
|
70
|
+
"""subclass should override this method to implement the custom transform operations"""
|
|
71
|
+
return row
|
|
72
|
+
|
|
73
|
+
@staticmethod
|
|
74
|
+
def convert_json_to_hive_map(data: str | dict) -> str:
|
|
75
|
+
from recurvedata.connectors.connectors.hive import ( # lazy import
|
|
76
|
+
HIVE_MAP_ITEM_DELIMITER,
|
|
77
|
+
HIVE_MAP_KV_DELIMITER,
|
|
78
|
+
HIVE_NULL,
|
|
79
|
+
)
|
|
80
|
+
|
|
81
|
+
if not data:
|
|
82
|
+
return HIVE_NULL
|
|
83
|
+
|
|
84
|
+
if isinstance(data, str):
|
|
85
|
+
d = json.loads(data)
|
|
86
|
+
else:
|
|
87
|
+
d = data
|
|
88
|
+
|
|
89
|
+
items = []
|
|
90
|
+
for key, value in d.items():
|
|
91
|
+
key = str(key).strip()
|
|
92
|
+
value = str(value).strip()
|
|
93
|
+
item = f"{key}{HIVE_MAP_KV_DELIMITER}{value}"
|
|
94
|
+
items.append(item)
|
|
95
|
+
return HIVE_MAP_ITEM_DELIMITER.join(items)
|
|
96
|
+
|
|
97
|
+
@staticmethod
|
|
98
|
+
def convert_json_to_hive_array(data: str | list) -> str:
|
|
99
|
+
from recurvedata.connectors.connectors.hive import HIVE_ARRAY_DELIMITER, HIVE_NULL
|
|
100
|
+
|
|
101
|
+
if not data:
|
|
102
|
+
return HIVE_NULL
|
|
103
|
+
|
|
104
|
+
if isinstance(data, str):
|
|
105
|
+
items = json.loads(data)
|
|
106
|
+
else:
|
|
107
|
+
items = data
|
|
108
|
+
|
|
109
|
+
return HIVE_ARRAY_DELIMITER.join(items)
|
|
110
|
+
|
|
111
|
+
@staticmethod
|
|
112
|
+
def mysql_uncompress(value: bytes, return_str=False) -> str | bytes:
|
|
113
|
+
"""An Python implementation of UNCOMPRESS function of MySQL.
|
|
114
|
+
|
|
115
|
+
Used to decompress result of COMPRESS function.
|
|
116
|
+
|
|
117
|
+
https://dev.mysql.com/doc/refman/5.7/en/encryption-functions.html#function_compress
|
|
118
|
+
|
|
119
|
+
:param value: the compressed data in bytes
|
|
120
|
+
:type value: bytes
|
|
121
|
+
:param return_str: the return value should be unicode
|
|
122
|
+
:type return_str: bool
|
|
123
|
+
:rtype: bytes | str
|
|
124
|
+
"""
|
|
125
|
+
|
|
126
|
+
# Empty strings are stored as empty strings.
|
|
127
|
+
# Nonempty strings are stored as a 4-byte length of the uncompressed string
|
|
128
|
+
if not value or len(value) < 4:
|
|
129
|
+
return value
|
|
130
|
+
|
|
131
|
+
rv = zlib.decompress(value[4:])
|
|
132
|
+
|
|
133
|
+
if return_str:
|
|
134
|
+
rv = rv.decode()
|
|
135
|
+
return rv
|
|
136
|
+
|
|
137
|
+
@staticmethod
|
|
138
|
+
def mysql_compress(value: str) -> bytes | None:
|
|
139
|
+
if value is None:
|
|
140
|
+
return None
|
|
141
|
+
if value == "":
|
|
142
|
+
return b""
|
|
143
|
+
size = struct.pack("I", len(value))
|
|
144
|
+
data = zlib.compress(value.encode())
|
|
145
|
+
return size + data
|
|
146
|
+
|
|
147
|
+
@staticmethod
|
|
148
|
+
def json_loads(*args, **kwargs):
|
|
149
|
+
return json.loads(*args, **kwargs)
|
|
150
|
+
|
|
151
|
+
@staticmethod
|
|
152
|
+
def json_dumps(*args, **kwargs) -> str:
|
|
153
|
+
return json.dumps(*args, **kwargs)
|
|
154
|
+
|
|
155
|
+
def aes_encrypt(
|
|
156
|
+
self, key_name: str, data: str | bytes, mode: Literal["ECB", "CBC"] = "ECB", iv: str | bytes = None
|
|
157
|
+
) -> str:
|
|
158
|
+
return CryptoUtil.base64_encode(CryptoUtil.aes_encrypt(key_name, data, mode, iv))
|
|
159
|
+
|
|
160
|
+
def aes_decrypt(self, key_name: str, data: bytes | str) -> str:
|
|
161
|
+
if isinstance(data, str):
|
|
162
|
+
data = CryptoUtil.base64_decode(data)
|
|
163
|
+
return CryptoUtil.aes_decrypt(key_name, data)
|
|
164
|
+
|
|
165
|
+
def rsa_encrypt(self, key_name: str, data: str | bytes) -> str:
|
|
166
|
+
return CryptoUtil.base64_encode(CryptoUtil.rsa_encrypt(key_name, data))
|
|
167
|
+
|
|
168
|
+
def rsa_decrypt(self, key_name: str, data: bytes | str) -> str:
|
|
169
|
+
if isinstance(data, str):
|
|
170
|
+
data = CryptoUtil.base64_decode(data)
|
|
171
|
+
return CryptoUtil.rsa_decrypt(key_name, data)
|
|
172
|
+
|
|
173
|
+
def base64_encode(self, data: str | bytes) -> str:
|
|
174
|
+
return CryptoUtil.base64_encode(data)
|
|
175
|
+
|
|
176
|
+
def base64_decode(self, data: str | bytes) -> str:
|
|
177
|
+
return CryptoUtil.base64_decode(data)
|
|
178
|
+
|
|
179
|
+
def md5(self, data: str | bytes) -> str:
|
|
180
|
+
return CryptoUtil.md5(data)
|
|
181
|
+
|
|
182
|
+
def sha1(self, data: str | bytes) -> str:
|
|
183
|
+
return CryptoUtil.sha1(data)
|
|
184
|
+
|
|
185
|
+
def sha256(self, data: str | bytes) -> str:
|
|
186
|
+
return CryptoUtil.sha256(data)
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
from contextvars import ContextVar
|
|
2
|
+
from gettext import NullTranslations
|
|
3
|
+
from typing import Any, Union
|
|
4
|
+
|
|
5
|
+
_current_translations: ContextVar[NullTranslations] = ContextVar("_current_translations", default=None)
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class Translator:
|
|
9
|
+
_translations_cache: dict[str, NullTranslations] = {}
|
|
10
|
+
_translations_dir: str = "locale"
|
|
11
|
+
|
|
12
|
+
@classmethod
|
|
13
|
+
def set_translations_dir(cls, translations_dir: str):
|
|
14
|
+
cls._translations_dir = translations_dir
|
|
15
|
+
|
|
16
|
+
@classmethod
|
|
17
|
+
def _get_translations(cls, locale: str) -> NullTranslations:
|
|
18
|
+
"""Get translations for the given locale."""
|
|
19
|
+
from babel.support import Translations
|
|
20
|
+
|
|
21
|
+
if locale not in cls._translations_cache:
|
|
22
|
+
try:
|
|
23
|
+
translations = Translations.load(dirname=cls._translations_dir, locales=[locale])
|
|
24
|
+
cls._translations_cache[locale] = translations
|
|
25
|
+
except FileNotFoundError:
|
|
26
|
+
return None
|
|
27
|
+
return cls._translations_cache[locale]
|
|
28
|
+
|
|
29
|
+
@classmethod
|
|
30
|
+
def set_locale(cls, locale: str):
|
|
31
|
+
"""Set locale for the current context."""
|
|
32
|
+
translations = cls._get_translations(locale)
|
|
33
|
+
if translations:
|
|
34
|
+
translations.install()
|
|
35
|
+
_current_translations.set(translations)
|
|
36
|
+
|
|
37
|
+
@classmethod
|
|
38
|
+
def gettext(cls, message: str) -> str:
|
|
39
|
+
"""Translate a message based on the current locale."""
|
|
40
|
+
translations = _current_translations.get()
|
|
41
|
+
if translations:
|
|
42
|
+
return translations.gettext(message)
|
|
43
|
+
return message
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
class LazyString:
|
|
47
|
+
def __init__(self, message: str):
|
|
48
|
+
self.message = message
|
|
49
|
+
|
|
50
|
+
def __str__(self) -> str:
|
|
51
|
+
return Translator.gettext(self.message)
|
|
52
|
+
|
|
53
|
+
def __hash__(self):
|
|
54
|
+
return hash(str(self))
|
|
55
|
+
|
|
56
|
+
def __eq__(self, other: Union["LazyString", str]):
|
|
57
|
+
if isinstance(other, LazyString):
|
|
58
|
+
return str(self) == str(other)
|
|
59
|
+
return str(self) == other
|
|
60
|
+
|
|
61
|
+
def format(self, *args, **kwargs):
|
|
62
|
+
return str(self).format(*args, **kwargs)
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def convert_lazy_string(v: Any) -> Any:
|
|
66
|
+
"""Recursively convert LazyString instances to str in nested data structures.
|
|
67
|
+
|
|
68
|
+
This function traverses dictionaries, lists and other data structures recursively,
|
|
69
|
+
converting any LazyString instances to regular strings. This is needed because
|
|
70
|
+
JsonSchema and Pydantic (see Pydantic issue #8439) do not support LazyString.
|
|
71
|
+
|
|
72
|
+
Args:
|
|
73
|
+
v: The value to convert. Can be a LazyString, dict, list, or any other type.
|
|
74
|
+
|
|
75
|
+
Returns:
|
|
76
|
+
The input value with all LazyString instances converted to str. The structure
|
|
77
|
+
of the input (dict/list/etc) is preserved.
|
|
78
|
+
"""
|
|
79
|
+
if isinstance(v, dict):
|
|
80
|
+
return {k: convert_lazy_string(v) for k, v in v.items()}
|
|
81
|
+
if isinstance(v, list):
|
|
82
|
+
return [convert_lazy_string(v) for v in v]
|
|
83
|
+
if isinstance(v, LazyString):
|
|
84
|
+
return str(v)
|
|
85
|
+
return v
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
gettext = Translator.gettext
|
|
89
|
+
lazy_gettext = LazyString
|
|
90
|
+
_ = Translator.gettext
|
|
91
|
+
_l = LazyString
|