rapidpro-api 0.1.0__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.
- rapidpro_api/__init__.py +0 -0
- rapidpro_api/contact_processors.py +343 -0
- rapidpro_api/contact_processors_pl.py +106 -0
- rapidpro_api/datasets/BaseDataset.py +551 -0
- rapidpro_api/datasets/ContactsDataset.py +31 -0
- rapidpro_api/datasets/DeltaLakeDataset.py +270 -0
- rapidpro_api/datasets/FieldsDataset.py +31 -0
- rapidpro_api/datasets/FlowsDataset.py +31 -0
- rapidpro_api/datasets/GroupsDataset.py +31 -0
- rapidpro_api/datasets/MessagesDataset.py +31 -0
- rapidpro_api/datasets/RawDataset.py +649 -0
- rapidpro_api/datasets/RunsDataset.py +32 -0
- rapidpro_api/datasets/__init__.py +8 -0
- rapidpro_api/field_processors.py +34 -0
- rapidpro_api/flow_processors.py +140 -0
- rapidpro_api/group_processors.py +101 -0
- rapidpro_api/message_processors.py +110 -0
- rapidpro_api/run_processors_pl.py +87 -0
- rapidpro_api/stats/__init__.py +5 -0
- rapidpro_api/stats/download_stats.py +112 -0
- rapidpro_api/stats/download_stats_list.py +115 -0
- rapidpro_api/stats/process_stats.py +318 -0
- rapidpro_api/stats/process_stats_list.py +266 -0
- rapidpro_api/stats/stats.py +643 -0
- rapidpro_api/telegram.py +37 -0
- rapidpro_api/time_utils.py +224 -0
- rapidpro_api/ureport_processors_pl.py +331 -0
- rapidpro_api/utils.py +72 -0
- rapidpro_api/validators.py +123 -0
- rapidpro_api/version.py +8 -0
- rapidpro_api/workspaces.py +278 -0
- rapidpro_api-0.1.0.dist-info/METADATA +95 -0
- rapidpro_api-0.1.0.dist-info/RECORD +65 -0
- rapidpro_api-0.1.0.dist-info/WHEEL +5 -0
- rapidpro_api-0.1.0.dist-info/top_level.txt +2 -0
- tests/__init__.py +0 -0
- tests/datasets/__init__.py +0 -0
- tests/datasets/test_base_dataset.py +528 -0
- tests/datasets/test_contacts_dataset.py +27 -0
- tests/datasets/test_fields_dataset.py +27 -0
- tests/datasets/test_flows_dataset.py +27 -0
- tests/datasets/test_groups_dataset.py +27 -0
- tests/datasets/test_messages_dataset.py +27 -0
- tests/datasets/test_raw_dataset.py +400 -0
- tests/datasets/test_runs_dataset.py +27 -0
- tests/stats/__init__.py +0 -0
- tests/stats/test_download_stats.py +186 -0
- tests/stats/test_download_stats_list.py +366 -0
- tests/stats/test_process_stats.py +0 -0
- tests/stats/test_process_stats_list.py +531 -0
- tests/stats/test_stats.py +711 -0
- tests/test_contact_processors.py +199 -0
- tests/test_contact_processors_pl.py +188 -0
- tests/test_field_processors.py +101 -0
- tests/test_flow_processor.py +90 -0
- tests/test_group_processors.py +190 -0
- tests/test_message_processors.py +129 -0
- tests/test_run_processors_pl.py +248 -0
- tests/test_telegram.py +61 -0
- tests/test_time_utils.py +318 -0
- tests/test_ureport_processors_pl.py +198 -0
- tests/test_utils.py +182 -0
- tests/test_validators.py +127 -0
- tests/test_version.py +20 -0
- tests/test_workspaces.py +576 -0
|
@@ -0,0 +1,270 @@
|
|
|
1
|
+
from deltalake import DeltaTable, write_deltalake
|
|
2
|
+
import polars as pl
|
|
3
|
+
from typing import Optional, Dict, Any, List, Union
|
|
4
|
+
import logging
|
|
5
|
+
|
|
6
|
+
from rapidpro_api.workspaces import Workspace
|
|
7
|
+
|
|
8
|
+
class DeltaLakeDataset:
|
|
9
|
+
"""Delta Lake manager with partition support.
|
|
10
|
+
This class provides methods to read and write Delta tables with partitioning support.
|
|
11
|
+
"""
|
|
12
|
+
def __init__(
|
|
13
|
+
self,
|
|
14
|
+
name: str,
|
|
15
|
+
table_name: str,
|
|
16
|
+
workspace: Workspace,
|
|
17
|
+
base_folder: str = "./datasets",
|
|
18
|
+
partition_cols: Optional[List[str]] = None,
|
|
19
|
+
storage_options: Optional[Dict[str, Any]] = None
|
|
20
|
+
|
|
21
|
+
):
|
|
22
|
+
"""
|
|
23
|
+
Initialize Delta Lake manager with partition support.
|
|
24
|
+
The resulting delta table path will be:
|
|
25
|
+
`<base_folder>/<name>/<table_name>`
|
|
26
|
+
Example for the dataset named contacts and table_name processed:
|
|
27
|
+
- Local: `./datasets/contacts/processed`
|
|
28
|
+
- S3: `s3://my-bucket/contacts/processed`
|
|
29
|
+
|
|
30
|
+
Args:
|
|
31
|
+
name: Name of the dataset (contacts, flows, etc.)
|
|
32
|
+
table_name: Name of the Delta table within the dataset (e.g., "processed", "aggregated")
|
|
33
|
+
workspace: Workspace object (e.g., RapidPro workspace)
|
|
34
|
+
base_folder: Path to Delta table. Defaults to "./datasets".
|
|
35
|
+
Examples:
|
|
36
|
+
- Local: "/tmp/my_table"
|
|
37
|
+
- S3: "s3://my-bucket/my-table"
|
|
38
|
+
- Azure: "az://container/my-table"
|
|
39
|
+
- Google Cloud: "gs://bucket/my-table"
|
|
40
|
+
partition_cols: List of column names to partition by. If None, defaults to workspace code.
|
|
41
|
+
Example: ["workspace_code", "created_on_year", "created_on_month"]
|
|
42
|
+
storage_options: Additional storage options (optional, overrides env vars)
|
|
43
|
+
Example:
|
|
44
|
+
- For S3: {"key": "my-access
|
|
45
|
+
- For Azure: {"az_storage_account_name": "myaccount", "az_storage_account_key": "mykey"}
|
|
46
|
+
- For Google Cloud: {"gcp_project": "my-project", "gcp_credentials": "/path/to/credentials.json"}
|
|
47
|
+
"""
|
|
48
|
+
self.name = name
|
|
49
|
+
self.workspace = workspace
|
|
50
|
+
self.table_name = table_name
|
|
51
|
+
self.base_folder = base_folder
|
|
52
|
+
self.table_path = f"{base_folder}/{name}/{table_name}"
|
|
53
|
+
self.partition_cols = partition_cols or [workspace.code]
|
|
54
|
+
self.storage_options = storage_options
|
|
55
|
+
self.table = None
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def get_table(self) -> Optional[DeltaTable]:
|
|
59
|
+
"""
|
|
60
|
+
Get whole dataset Delta table
|
|
61
|
+
Returns:
|
|
62
|
+
DeltaTable: Delta table object or empty if table doesn't exist
|
|
63
|
+
Raises:
|
|
64
|
+
Exception: If table cannot be loaded
|
|
65
|
+
"""
|
|
66
|
+
if self.table is None:
|
|
67
|
+
try:
|
|
68
|
+
if self.storage_options:
|
|
69
|
+
self.table = DeltaTable(self.table_path, storage_options=self.storage_options)
|
|
70
|
+
else:
|
|
71
|
+
self.table = DeltaTable(self.table_path)
|
|
72
|
+
except Exception as e:
|
|
73
|
+
logging.error(f"Table {self.table_path} doesn't exist yet or error: {e}")
|
|
74
|
+
return None
|
|
75
|
+
return self.table
|
|
76
|
+
|
|
77
|
+
def write_dataframe(
|
|
78
|
+
self,
|
|
79
|
+
df: pl.DataFrame,
|
|
80
|
+
mode: str = "overwrite",
|
|
81
|
+
partition_cols: Optional[List[str]] = None,
|
|
82
|
+
predicate: str = None,
|
|
83
|
+
**write_kwargs
|
|
84
|
+
):
|
|
85
|
+
"""
|
|
86
|
+
Write DataFrame to Delta table with partition support.
|
|
87
|
+
|
|
88
|
+
Args:
|
|
89
|
+
df: Polars DataFrame to write
|
|
90
|
+
mode: Write mode ('append', 'overwrite', 'error', 'ignore'). default is 'overwrite'.
|
|
91
|
+
- 'append': Add to existing data
|
|
92
|
+
- 'overwrite': Replace existing data
|
|
93
|
+
- 'error': Raise error if data exists
|
|
94
|
+
- 'ignore': Do nothing if data exists
|
|
95
|
+
partition_cols: List of columns to partition by. If None, uses instance partition columns.
|
|
96
|
+
Example: ["workspace_code", "created_on_year", "created_on_month"]
|
|
97
|
+
partition_cols: Override instance partition columns
|
|
98
|
+
**write_kwargs: Additional arguments for write_deltalake
|
|
99
|
+
"""
|
|
100
|
+
partition_columns = partition_cols or self.partition_cols
|
|
101
|
+
|
|
102
|
+
# Validate partition columns exist in DataFrame
|
|
103
|
+
if partition_columns:
|
|
104
|
+
missing_cols = [col for col in partition_columns if col not in df.columns]
|
|
105
|
+
if missing_cols:
|
|
106
|
+
raise ValueError(f"Partition columns {missing_cols} not found in DataFrame")
|
|
107
|
+
|
|
108
|
+
# Prepare write arguments
|
|
109
|
+
write_args = {
|
|
110
|
+
"table_or_uri": self.table_path,
|
|
111
|
+
"data": df,
|
|
112
|
+
"mode": mode,
|
|
113
|
+
"partition_by": partition_columns if partition_columns else None,
|
|
114
|
+
"storage_options": self.storage_options,
|
|
115
|
+
**write_kwargs
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
write_deltalake(**write_args)
|
|
119
|
+
self.table = None # Reset to reload
|
|
120
|
+
|
|
121
|
+
def read_as_polars(
|
|
122
|
+
self,
|
|
123
|
+
partition_filter: Optional[Union[List[tuple], Dict[str, Any]]] = None,
|
|
124
|
+
columns: Optional[List[str]] = None
|
|
125
|
+
) -> pl.DataFrame:
|
|
126
|
+
"""
|
|
127
|
+
Read Delta table as Polars DataFrame with partition filtering.
|
|
128
|
+
|
|
129
|
+
Args:
|
|
130
|
+
partition_filter: Filter partitions using various formats:
|
|
131
|
+
- List of tuples: [("year", "=", "2023"), ("month", "in", ["01", "02"])]
|
|
132
|
+
- Dict: {"year": "2023", "month": ["01", "02"]}
|
|
133
|
+
columns: List of columns to read (None for all)
|
|
134
|
+
|
|
135
|
+
Returns:
|
|
136
|
+
pl.DataFrame: Filtered data
|
|
137
|
+
"""
|
|
138
|
+
table = self.get_table()
|
|
139
|
+
if not table:
|
|
140
|
+
return pl.DataFrame()
|
|
141
|
+
|
|
142
|
+
try:
|
|
143
|
+
# Prepare filters
|
|
144
|
+
filters = None
|
|
145
|
+
if partition_filter:
|
|
146
|
+
if isinstance(partition_filter, dict):
|
|
147
|
+
filters = []
|
|
148
|
+
for col, values in partition_filter.items():
|
|
149
|
+
if isinstance(values, list):
|
|
150
|
+
filters.append((col, "in", values))
|
|
151
|
+
else:
|
|
152
|
+
filters.append((col, "=", str(values)))
|
|
153
|
+
else:
|
|
154
|
+
filters = partition_filter
|
|
155
|
+
|
|
156
|
+
# Try to use Polars' native DeltaLake scan if available (polars >=0.20.16)
|
|
157
|
+
try:
|
|
158
|
+
scan = pl.scan_deltalake(
|
|
159
|
+
self.table_path,
|
|
160
|
+
storage_options=self.storage_options,
|
|
161
|
+
filters=filters,
|
|
162
|
+
columns=columns,
|
|
163
|
+
)
|
|
164
|
+
return scan.collect()
|
|
165
|
+
except Exception as e:
|
|
166
|
+
logging.error(f"Polars native DeltaLake scan failed: {e}")
|
|
167
|
+
|
|
168
|
+
# Fallback to pandas
|
|
169
|
+
if filters:
|
|
170
|
+
pandas_df = table.to_pandas(filters=filters, columns=columns)
|
|
171
|
+
else:
|
|
172
|
+
pandas_df = table.to_pandas(columns=columns)
|
|
173
|
+
return pl.from_pandas(pandas_df)
|
|
174
|
+
|
|
175
|
+
except Exception as e:
|
|
176
|
+
logging.error(f"Error reading partitioned data: {e}")
|
|
177
|
+
return pl.DataFrame()
|
|
178
|
+
|
|
179
|
+
def list_partitions(self) -> List[Dict[str, str]]:
|
|
180
|
+
"""
|
|
181
|
+
List all partitions in the Delta table.
|
|
182
|
+
|
|
183
|
+
Returns:
|
|
184
|
+
List of partition dictionaries
|
|
185
|
+
"""
|
|
186
|
+
table = self.get_table()
|
|
187
|
+
if not table:
|
|
188
|
+
return []
|
|
189
|
+
|
|
190
|
+
try:
|
|
191
|
+
# Get partition columns
|
|
192
|
+
if hasattr(table, 'metadata') and table.metadata():
|
|
193
|
+
partition_cols = table.metadata().partition_columns
|
|
194
|
+
if not partition_cols:
|
|
195
|
+
return []
|
|
196
|
+
|
|
197
|
+
# Read distinct partition values
|
|
198
|
+
df = pl.from_pandas(table.to_pandas(columns=partition_cols))
|
|
199
|
+
partitions = df.select(partition_cols).unique().to_dicts()
|
|
200
|
+
return partitions
|
|
201
|
+
else:
|
|
202
|
+
return []
|
|
203
|
+
|
|
204
|
+
except Exception as e:
|
|
205
|
+
logging.error(f"Error listing partitions: {e}")
|
|
206
|
+
return []
|
|
207
|
+
|
|
208
|
+
def optimize_table(self, partition_filter: Optional[Dict[str, Any]] = None):
|
|
209
|
+
"""
|
|
210
|
+
Optimize Delta table (compact small files).
|
|
211
|
+
|
|
212
|
+
Args:
|
|
213
|
+
partition_filter: Only optimize specific partitions
|
|
214
|
+
"""
|
|
215
|
+
table = self.get_table()
|
|
216
|
+
if not table:
|
|
217
|
+
return
|
|
218
|
+
|
|
219
|
+
try:
|
|
220
|
+
if partition_filter:
|
|
221
|
+
# Convert filter to proper format and optimize specific partitions
|
|
222
|
+
filter_expr = " AND ".join([
|
|
223
|
+
f"{k} = '{v}'" if isinstance(v, str) else f"{k} = {v}"
|
|
224
|
+
for k, v in partition_filter.items()
|
|
225
|
+
])
|
|
226
|
+
table.optimize.compact(partition_filters=filter_expr)
|
|
227
|
+
else:
|
|
228
|
+
table.optimize.compact()
|
|
229
|
+
|
|
230
|
+
except Exception as e:
|
|
231
|
+
logging.error(f"Error optimizing table: {e}")
|
|
232
|
+
|
|
233
|
+
def vacuum_table(self, retention_hours: int = 168):
|
|
234
|
+
"""
|
|
235
|
+
Vacuum Delta table (remove old files).
|
|
236
|
+
|
|
237
|
+
Args:
|
|
238
|
+
retention_hours: Hours to retain (default 168 = 7 days)
|
|
239
|
+
"""
|
|
240
|
+
table = self.get_table()
|
|
241
|
+
if not table:
|
|
242
|
+
return
|
|
243
|
+
|
|
244
|
+
try:
|
|
245
|
+
table.vacuum(retention_hours)
|
|
246
|
+
except Exception as e:
|
|
247
|
+
logging.error(f"Error vacuuming table: {e}")
|
|
248
|
+
|
|
249
|
+
def get_partition_info(self) -> Dict[str, Any]:
|
|
250
|
+
"""
|
|
251
|
+
Get information about table partitioning.
|
|
252
|
+
|
|
253
|
+
Returns:
|
|
254
|
+
Dict with partition information
|
|
255
|
+
"""
|
|
256
|
+
table = self.get_table()
|
|
257
|
+
if not table:
|
|
258
|
+
return {}
|
|
259
|
+
|
|
260
|
+
try:
|
|
261
|
+
metadata = table.metadata()
|
|
262
|
+
return {
|
|
263
|
+
"partition_columns": metadata.partition_columns if metadata else [],
|
|
264
|
+
"total_files": len(table.files()),
|
|
265
|
+
"table_version": table.version(),
|
|
266
|
+
"schema": table.schema().to_pyarrow() if hasattr(table.schema(), 'to_pyarrow') else str(table.schema())
|
|
267
|
+
}
|
|
268
|
+
except Exception as e:
|
|
269
|
+
logging.error(f"Error getting partition info: {e}")
|
|
270
|
+
return {}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
from . import BaseDataset
|
|
2
|
+
from ..workspaces import Workspace
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
class FieldsDataset(BaseDataset):
|
|
6
|
+
"""
|
|
7
|
+
A class to handle the Fields dataset.
|
|
8
|
+
|
|
9
|
+
This class inherits from BaseDataset and is used to manage the Fields dataset.
|
|
10
|
+
It provides methods to download, process, and manage the dataset.
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
def __init__(self, workspace:Workspace = None,
|
|
14
|
+
base_folder: str = "./datasets",
|
|
15
|
+
storage_options: dict = None,
|
|
16
|
+
raw_dataset_batch_size: int = None):
|
|
17
|
+
"""
|
|
18
|
+
Initialize a Flow Dataset object.
|
|
19
|
+
|
|
20
|
+
Args:
|
|
21
|
+
workspace: The workspace object associated with the dataset (RapidPro workspace).
|
|
22
|
+
base_folder: The base folder where the dataset will be stored.
|
|
23
|
+
storage_options: Additional options for the storage backend (e.g., fsspec compatible).
|
|
24
|
+
raw_dataset_batch_size: The batch size for downloading the raw dataset.
|
|
25
|
+
"""
|
|
26
|
+
|
|
27
|
+
super().__init__(name="fields",
|
|
28
|
+
workspace=workspace,
|
|
29
|
+
base_folder=base_folder,
|
|
30
|
+
storage_options=storage_options,
|
|
31
|
+
raw_dataset_batch_size=raw_dataset_batch_size)
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
from . import BaseDataset
|
|
2
|
+
from ..workspaces import Workspace
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
class FlowsDataset(BaseDataset):
|
|
6
|
+
"""
|
|
7
|
+
A class to handle the Flows dataset.
|
|
8
|
+
|
|
9
|
+
This class inherits from BaseDataset and is used to manage the Flows dataset.
|
|
10
|
+
It provides methods to download, process, and manage the dataset.
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
def __init__(self, workspace:Workspace = None,
|
|
14
|
+
base_folder: str = "./datasets",
|
|
15
|
+
storage_options: dict = None,
|
|
16
|
+
raw_dataset_batch_size: int = None):
|
|
17
|
+
"""
|
|
18
|
+
Initialize a ContactsDataset object.
|
|
19
|
+
|
|
20
|
+
Args:
|
|
21
|
+
workspace: The workspace object associated with the dataset (RapidPro workspace).
|
|
22
|
+
base_folder: The base folder where the dataset will be stored.
|
|
23
|
+
storage_options: Additional options for the storage backend (e.g., fsspec compatible).
|
|
24
|
+
raw_dataset_batch_size: The batch size for downloading the raw dataset.
|
|
25
|
+
"""
|
|
26
|
+
|
|
27
|
+
super().__init__(name="flows",
|
|
28
|
+
workspace=workspace,
|
|
29
|
+
base_folder=base_folder,
|
|
30
|
+
storage_options=storage_options,
|
|
31
|
+
raw_dataset_batch_size=raw_dataset_batch_size)
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
from . import BaseDataset
|
|
2
|
+
from ..workspaces import Workspace
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
class GroupsDataset(BaseDataset):
|
|
6
|
+
"""
|
|
7
|
+
A class to handle the Groups dataset.
|
|
8
|
+
|
|
9
|
+
This class inherits from BaseDataset and is used to manage the Groups dataset.
|
|
10
|
+
It provides methods to download, process, and manage the dataset.
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
def __init__(self,
|
|
14
|
+
workspace:Workspace = None,
|
|
15
|
+
base_folder: str = "./datasets",
|
|
16
|
+
storage_options: dict = None,
|
|
17
|
+
raw_dataset_batch_size: int = None):
|
|
18
|
+
"""
|
|
19
|
+
Initialize a ContactsDataset object.
|
|
20
|
+
|
|
21
|
+
Args:
|
|
22
|
+
workspace: The workspace object associated with the dataset (RapidPro workspace).
|
|
23
|
+
base_folder: The base folder where the dataset will be stored.
|
|
24
|
+
fs: The file system object to use for file operations (fsspec compatible). Uses local file system by default.
|
|
25
|
+
"""
|
|
26
|
+
|
|
27
|
+
super().__init__(name="groups",
|
|
28
|
+
workspace=workspace,
|
|
29
|
+
base_folder=base_folder,
|
|
30
|
+
storage_options=storage_options,
|
|
31
|
+
raw_dataset_batch_size=raw_dataset_batch_size)
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
from . import BaseDataset
|
|
2
|
+
from ..workspaces import Workspace
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
class MessagesDataset(BaseDataset):
|
|
6
|
+
"""
|
|
7
|
+
A class to handle the Messages dataset.
|
|
8
|
+
|
|
9
|
+
This class inherits from BaseDataset and is used to manage the Messages dataset.
|
|
10
|
+
It provides methods to download, process, and manage the dataset.
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
def __init__(self, workspace:Workspace = None,
|
|
14
|
+
base_folder: str = "./datasets",
|
|
15
|
+
storage_options: dict = None,
|
|
16
|
+
raw_dataset_batch_size: int = None):
|
|
17
|
+
"""
|
|
18
|
+
Initialize a MessagesDataset object.
|
|
19
|
+
|
|
20
|
+
Args:
|
|
21
|
+
workspace: The workspace object associated with the dataset (RapidPro workspace).
|
|
22
|
+
base_folder: The base folder where the dataset will be stored.
|
|
23
|
+
Everything will be stored within the <base_folder>/messages folder.
|
|
24
|
+
fs: The file system object to use for file operations (fsspec compatible). Uses local file system by default.
|
|
25
|
+
"""
|
|
26
|
+
|
|
27
|
+
super().__init__(name="messages",
|
|
28
|
+
workspace=workspace,
|
|
29
|
+
base_folder=base_folder,
|
|
30
|
+
storage_options=storage_options,
|
|
31
|
+
raw_dataset_batch_size=raw_dataset_batch_size)
|