investify-utils 2.0.0a6__tar.gz → 2.0.0a7__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.

Potentially problematic release.


This version of investify-utils might be problematic. Click here for more details.

@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: investify-utils
3
- Version: 2.0.0a6
3
+ Version: 2.0.0a7
4
4
  Summary: Shared utilities for Investify services
5
5
  Author-Email: Investify <dev@investify.vn>
6
6
  License: MIT
@@ -24,6 +24,9 @@ Provides-Extra: kafka
24
24
  Requires-Dist: confluent-kafka[avro,schemaregistry]>=2.0; extra == "kafka"
25
25
  Provides-Extra: s3
26
26
  Requires-Dist: boto3>=1.34; extra == "s3"
27
+ Provides-Extra: helpers
28
+ Requires-Dist: pandas>=2.0; extra == "helpers"
29
+ Requires-Dist: numpy>=2.0; extra == "helpers"
27
30
  Provides-Extra: dev
28
31
  Requires-Dist: pytest; extra == "dev"
29
32
  Requires-Dist: pytest-asyncio; extra == "dev"
@@ -0,0 +1,28 @@
1
+ """
2
+ Investify Utils - Shared utilities for Investify services.
3
+
4
+ Install with optional dependencies:
5
+ pip install investify-utils[postgres] # Sync PostgreSQL client
6
+ pip install investify-utils[postgres-async] # Async PostgreSQL client
7
+ pip install investify-utils[kafka] # Kafka Avro producer/consumer
8
+ pip install investify-utils[s3] # S3 client
9
+ pip install investify-utils[helpers] # Timestamp/SQL utilities
10
+
11
+ Usage:
12
+ # Logging (no extra required)
13
+ from investify_utils.logging import setup_logging
14
+
15
+ # PostgreSQL
16
+ from investify_utils.postgres import PostgresClient, AsyncPostgresClient
17
+
18
+ # Kafka
19
+ from investify_utils.kafka import AvroProducer, AvroConsumer
20
+
21
+ # S3
22
+ from investify_utils.s3 import S3Client
23
+
24
+ # Helpers
25
+ from investify_utils.helpers import convert_to_pd_timestamp, create_sql_in_filter
26
+ """
27
+
28
+ __version__ = "2.0.0a7"
@@ -0,0 +1,154 @@
1
+ """
2
+ Common helper utilities for Investify services.
3
+
4
+ Usage:
5
+ from investify_utils.helpers import convert_to_pd_timestamp, create_sql_in_filter
6
+ """
7
+
8
+ import datetime as dt
9
+ import importlib.util
10
+ import logging
11
+ import sys
12
+ from numbers import Integral, Number, Real
13
+ from typing import Literal
14
+
15
+ import numpy as np
16
+ import pandas as pd
17
+
18
+ logger = logging.getLogger(__name__)
19
+
20
+
21
+ # =============================================================================
22
+ # Timestamp Utilities
23
+ # =============================================================================
24
+
25
+
26
+ def convert_to_pd_timestamp(timestamp) -> pd.Timestamp | None:
27
+ """
28
+ Convert various timestamp formats to pandas Timestamp.
29
+
30
+ Args:
31
+ timestamp: Can be None, pd.Timestamp, number (unix), string, datetime, or np.datetime64
32
+
33
+ Returns:
34
+ pd.Timestamp or None
35
+ """
36
+ if timestamp is None:
37
+ return None
38
+
39
+ if isinstance(timestamp, pd.Timestamp):
40
+ return timestamp
41
+
42
+ if isinstance(timestamp, Number):
43
+ return pd.Timestamp.fromtimestamp(float(timestamp), tz=dt.UTC)
44
+
45
+ if isinstance(timestamp, str | dt.datetime | np.datetime64):
46
+ try:
47
+ return pd.Timestamp(timestamp, tzinfo=dt.UTC)
48
+ except Exception as e:
49
+ logger.error(repr(e))
50
+ return timestamp
51
+
52
+ return timestamp
53
+
54
+
55
+ # =============================================================================
56
+ # SQL Utilities
57
+ # =============================================================================
58
+
59
+
60
+ def convert_to_sql_value(value: Integral | Real | str | dt.datetime | dt.date) -> str:
61
+ """
62
+ Convert Python value to SQL literal string.
63
+
64
+ Args:
65
+ value: Integer, float, string, datetime, or date
66
+
67
+ Returns:
68
+ SQL-safe string representation
69
+ """
70
+ if isinstance(value, Integral):
71
+ value = int(value)
72
+ elif isinstance(value, Real):
73
+ value = float(value)
74
+ elif isinstance(value, str):
75
+ value = f"'{value}'"
76
+ elif isinstance(value, dt.datetime):
77
+ value = value.isoformat(sep=" ")
78
+ value = f"'{value}'"
79
+ elif isinstance(value, dt.date):
80
+ value = value.isoformat()
81
+ value = f"'{value}'"
82
+ else:
83
+ raise ValueError(f"Not supported type={type(value)}")
84
+
85
+ return str(value)
86
+
87
+
88
+ def create_sql_in_filter(
89
+ col_name: str,
90
+ values: list[Integral | Real | str | dt.datetime | dt.date],
91
+ not_in: bool = False,
92
+ ) -> str:
93
+ """
94
+ Create SQL IN or NOT IN filter clause.
95
+
96
+ Args:
97
+ col_name: Column name
98
+ values: List of values
99
+ not_in: Use NOT IN instead of IN
100
+
101
+ Returns:
102
+ SQL filter string like "col IN (1, 2, 3)"
103
+ """
104
+ operator = "NOT IN" if not_in else "IN"
105
+ values_str = ", ".join([convert_to_sql_value(value) for value in values])
106
+ return f"{col_name} {operator} ({values_str})"
107
+
108
+
109
+ def create_sql_logical_filter(
110
+ filters: list[str],
111
+ operator: Literal["AND", "OR"],
112
+ inner_bracket: bool = False,
113
+ outer_bracket: bool = False,
114
+ ) -> str:
115
+ """
116
+ Combine multiple SQL filters with AND/OR.
117
+
118
+ Args:
119
+ filters: List of filter strings
120
+ operator: "AND" or "OR"
121
+ inner_bracket: Wrap each filter in parentheses
122
+ outer_bracket: Wrap result in parentheses
123
+
124
+ Returns:
125
+ Combined filter string
126
+ """
127
+ operator_sep = f" {operator} "
128
+ if inner_bracket:
129
+ filters = [f"({filter})" for filter in filters]
130
+ return f"({operator_sep.join(filters)})" if outer_bracket else operator_sep.join(filters)
131
+
132
+
133
+ # =============================================================================
134
+ # Module Utilities
135
+ # =============================================================================
136
+
137
+
138
+ def import_module_from_path(file_path: str, module_name: str):
139
+ """
140
+ Dynamically import a Python module from a file path.
141
+
142
+ Args:
143
+ file_path: Path to the Python file
144
+ module_name: Name to register the module as
145
+
146
+ Returns:
147
+ Imported module object
148
+ """
149
+ spec = importlib.util.spec_from_file_location(module_name, file_path)
150
+ logger.info(f"Loading `{spec.name}` from `{spec.origin}`")
151
+ module = importlib.util.module_from_spec(spec)
152
+ sys.modules[module_name] = module
153
+ spec.loader.exec_module(module)
154
+ return module
@@ -0,0 +1,81 @@
1
+ """
2
+ Logging utilities for Investify services.
3
+
4
+ Usage:
5
+ from investify_utils.logging import setup_logging
6
+
7
+ setup_logging()
8
+ logger = logging.getLogger(__name__)
9
+ """
10
+
11
+ import logging
12
+ import os
13
+ from enum import IntEnum, auto
14
+ from logging.handlers import RotatingFileHandler
15
+
16
+ old_factory = logging.getLogRecordFactory()
17
+ default_logging_fmt = "%(asctime)s - %(origin)-30s - %(levelname)s - %(message)s"
18
+
19
+
20
+ class TextColor(IntEnum):
21
+ """ANSI text colors for terminal output."""
22
+
23
+ BLACK = 0
24
+ RED = auto()
25
+ GREEN = auto()
26
+ YELLOW = auto()
27
+ BLUE = auto()
28
+ MAGENTA = auto()
29
+ CYAN = auto()
30
+ WHITE = auto()
31
+
32
+ @staticmethod
33
+ def colorize(text: str, color: "TextColor") -> str:
34
+ """Wrap text with ANSI color codes."""
35
+ return f"\033[0;{30 + color}m{text}\033[0m"
36
+
37
+
38
+ def record_factory(*args, **kwargs):
39
+ """Custom log record factory that adds origin (filename:lineno)."""
40
+ record = old_factory(*args, **kwargs)
41
+ record.origin = f"{record.filename}:{record.lineno}"
42
+ return record
43
+
44
+
45
+ def setup_logging(level=logging.INFO, logging_fmt=default_logging_fmt):
46
+ """
47
+ Configure logging with origin field (filename:lineno).
48
+
49
+ Args:
50
+ level: Logging level (default: INFO)
51
+ logging_fmt: Log format string
52
+ """
53
+ logging.setLogRecordFactory(record_factory)
54
+ logging.basicConfig(format=logging_fmt, level=level)
55
+
56
+
57
+ def setup_file_logging(
58
+ filename: str,
59
+ level=logging.INFO,
60
+ max_megabytes: int = 1,
61
+ backup_count: int = 3,
62
+ logging_fmt: str = default_logging_fmt,
63
+ ):
64
+ """
65
+ Configure rotating file logging.
66
+
67
+ Args:
68
+ filename: Log file path
69
+ level: Logging level
70
+ max_megabytes: Max file size before rotation
71
+ backup_count: Number of backup files to keep
72
+ logging_fmt: Log format string
73
+ """
74
+ filepath, _ = os.path.split(filename)
75
+ if filepath and not os.path.isdir(filepath):
76
+ os.makedirs(filepath)
77
+
78
+ max_log_size = int(max_megabytes * 1024 * 1024)
79
+ handler = RotatingFileHandler(filename=filename, maxBytes=max_log_size, backupCount=backup_count)
80
+ logging.setLogRecordFactory(record_factory)
81
+ logging.basicConfig(format=logging_fmt, level=level, handlers=[handler])
@@ -6,7 +6,7 @@ build-backend = "pdm.backend"
6
6
 
7
7
  [project]
8
8
  name = "investify-utils"
9
- version = "2.0.0a6"
9
+ version = "2.0.0a7"
10
10
  description = "Shared utilities for Investify services"
11
11
  readme = "README.md"
12
12
  requires-python = ">=3.12"
@@ -46,6 +46,10 @@ kafka = [
46
46
  s3 = [
47
47
  "boto3>=1.34",
48
48
  ]
49
+ helpers = [
50
+ "pandas>=2.0",
51
+ "numpy>=2.0",
52
+ ]
49
53
  dev = [
50
54
  "pytest",
51
55
  "pytest-asyncio",
@@ -1,13 +0,0 @@
1
- """
2
- Investify Utils - Shared utilities for Investify services.
3
-
4
- Install with optional dependencies:
5
- pip install investify-utils[postgres] # Sync PostgreSQL client
6
- pip install investify-utils[postgres-async] # Async PostgreSQL client
7
- pip install investify-utils[postgres-all] # Both clients
8
-
9
- Usage:
10
- from investify_utils.postgres import PostgresClient, AsyncPostgresClient
11
- """
12
-
13
- __version__ = "2.0.0a2"