sovai 0.2.38__tar.gz → 0.2.40__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.
- {sovai-0.2.38 → sovai-0.2.40}/PKG-INFO +1 -1
- {sovai-0.2.38 → sovai-0.2.40}/pyproject.toml +1 -1
- {sovai-0.2.38 → sovai-0.2.40}/sovai/get_data.py +16 -2
- {sovai-0.2.38 → sovai-0.2.40}/sovai/utils/client_side_s3_part_high.py +1 -1
- sovai-0.2.38/sovai/utils/client_side_s3_part_high copy 2.py +0 -432
- sovai-0.2.38/sovai/utils/client_side_s3_part_high copy.py +0 -407
- {sovai-0.2.38 → sovai-0.2.40}/LICENSE +0 -0
- {sovai-0.2.38 → sovai-0.2.40}/README.md +0 -0
- {sovai-0.2.38 → sovai-0.2.40}/data/codes.parq +0 -0
- {sovai-0.2.38 → sovai-0.2.40}/data/tickers.parq +0 -0
- {sovai-0.2.38 → sovai-0.2.40}/sovai/.DS_Store +0 -0
- {sovai-0.2.38 → sovai-0.2.40}/sovai/__init__.py +0 -0
- {sovai-0.2.38 → sovai-0.2.40}/sovai/api_config.py +0 -0
- {sovai-0.2.38 → sovai-0.2.40}/sovai/assets/dropdown.css +0 -0
- {sovai-0.2.38 → sovai-0.2.40}/sovai/assets/features_mapping.parq +0 -0
- {sovai-0.2.38 → sovai-0.2.40}/sovai/basic_auth.py +0 -0
- {sovai-0.2.38 → sovai-0.2.40}/sovai/computations/__init__.py +0 -0
- {sovai-0.2.38 → sovai-0.2.40}/sovai/computations/functions.py +0 -0
- {sovai-0.2.38 → sovai-0.2.40}/sovai/errors/__init__.py +0 -0
- {sovai-0.2.38 → sovai-0.2.40}/sovai/errors/sovai_errors.py +0 -0
- {sovai-0.2.38 → sovai-0.2.40}/sovai/extensions/__init__.py +0 -0
- {sovai-0.2.38 → sovai-0.2.40}/sovai/extensions/anomalies.py +0 -0
- {sovai-0.2.38 → sovai-0.2.40}/sovai/extensions/ask_df_llm.py +0 -0
- {sovai-0.2.38 → sovai-0.2.40}/sovai/extensions/change_point_generator.py +0 -0
- {sovai-0.2.38 → sovai-0.2.40}/sovai/extensions/clustering.py +0 -0
- {sovai-0.2.38 → sovai-0.2.40}/sovai/extensions/core_kshape.py +0 -0
- {sovai-0.2.38 → sovai-0.2.40}/sovai/extensions/cum_sum_torch.py +0 -0
- {sovai-0.2.38 → sovai-0.2.40}/sovai/extensions/cusum.py +0 -0
- {sovai-0.2.38 → sovai-0.2.40}/sovai/extensions/dimensionality_reduction.py +0 -0
- {sovai-0.2.38 → sovai-0.2.40}/sovai/extensions/feature_extraction.py +0 -0
- {sovai-0.2.38 → sovai-0.2.40}/sovai/extensions/feature_importance.py +0 -0
- {sovai-0.2.38 → sovai-0.2.40}/sovai/extensions/feature_neutralizer.py +0 -0
- {sovai-0.2.38 → sovai-0.2.40}/sovai/extensions/filter_df.py +0 -0
- {sovai-0.2.38 → sovai-0.2.40}/sovai/extensions/fractional_differencing.py +0 -0
- {sovai-0.2.38 → sovai-0.2.40}/sovai/extensions/nowcasting.py +0 -0
- {sovai-0.2.38 → sovai-0.2.40}/sovai/extensions/pairwise.py +0 -0
- {sovai-0.2.38 → sovai-0.2.40}/sovai/extensions/pandas_extensions.py +0 -0
- {sovai-0.2.38 → sovai-0.2.40}/sovai/extensions/parallel_functions.py +0 -0
- {sovai-0.2.38 → sovai-0.2.40}/sovai/extensions/pfa_feature_selector.py +0 -0
- {sovai-0.2.38 → sovai-0.2.40}/sovai/extensions/regime_change.py +0 -0
- {sovai-0.2.38 → sovai-0.2.40}/sovai/extensions/regime_change_pca.py +0 -0
- {sovai-0.2.38 → sovai-0.2.40}/sovai/extensions/shapley_global_importance.py +0 -0
- {sovai-0.2.38 → sovai-0.2.40}/sovai/extensions/shapley_importance.py +0 -0
- {sovai-0.2.38 → sovai-0.2.40}/sovai/extensions/signal_evaluation.py +0 -0
- {sovai-0.2.38 → sovai-0.2.40}/sovai/extensions/technical_indicators.py +0 -0
- {sovai-0.2.38 → sovai-0.2.40}/sovai/extensions/time_decomposition.py +0 -0
- {sovai-0.2.38 → sovai-0.2.40}/sovai/extensions/weight_optimization.py +0 -0
- {sovai-0.2.38 → sovai-0.2.40}/sovai/get_compute.py +0 -0
- {sovai-0.2.38 → sovai-0.2.40}/sovai/get_data copy.py +0 -0
- {sovai-0.2.38 → sovai-0.2.40}/sovai/get_plots.py +0 -0
- {sovai-0.2.38 → sovai-0.2.40}/sovai/get_reports.py +0 -0
- {sovai-0.2.38 → sovai-0.2.40}/sovai/get_tools.py +0 -0
- {sovai-0.2.38 → sovai-0.2.40}/sovai/plots/__init__.py +0 -0
- {sovai-0.2.38 → sovai-0.2.40}/sovai/plots/accounting/__init__.py +0 -0
- {sovai-0.2.38 → sovai-0.2.40}/sovai/plots/accounting/accounting_plots.py +0 -0
- {sovai-0.2.38 → sovai-0.2.40}/sovai/plots/allocation/__init__.py +0 -0
- {sovai-0.2.38 → sovai-0.2.40}/sovai/plots/allocation/allocation_plots.py +0 -0
- {sovai-0.2.38 → sovai-0.2.40}/sovai/plots/bankruptcy/__init__.py +0 -0
- {sovai-0.2.38 → sovai-0.2.40}/sovai/plots/bankruptcy/bankruptcy_plots.py +0 -0
- {sovai-0.2.38 → sovai-0.2.40}/sovai/plots/breakout/__init__.py +0 -0
- {sovai-0.2.38 → sovai-0.2.40}/sovai/plots/breakout/breakout_plots.py +0 -0
- {sovai-0.2.38 → sovai-0.2.40}/sovai/plots/corp_risk/__init__.py +0 -0
- {sovai-0.2.38 → sovai-0.2.40}/sovai/plots/corp_risk/corp_risk_plots.py +0 -0
- {sovai-0.2.38 → sovai-0.2.40}/sovai/plots/earnings_surprise/__init__.py +0 -0
- {sovai-0.2.38 → sovai-0.2.40}/sovai/plots/earnings_surprise/earnings_surprise_plots.py +0 -0
- {sovai-0.2.38 → sovai-0.2.40}/sovai/plots/insider/__init__.py +0 -0
- {sovai-0.2.38 → sovai-0.2.40}/sovai/plots/insider/insider_plots.py +0 -0
- {sovai-0.2.38 → sovai-0.2.40}/sovai/plots/institutional/__init__.py +0 -0
- {sovai-0.2.38 → sovai-0.2.40}/sovai/plots/institutional/institutional_plots.py +0 -0
- {sovai-0.2.38 → sovai-0.2.40}/sovai/plots/news/__init__.py +0 -0
- {sovai-0.2.38 → sovai-0.2.40}/sovai/plots/news/news_plots.py +0 -0
- {sovai-0.2.38 → sovai-0.2.40}/sovai/plots/ratios/__init__.py +0 -0
- {sovai-0.2.38 → sovai-0.2.40}/sovai/plots/ratios/ratios_plots.py +0 -0
- {sovai-0.2.38 → sovai-0.2.40}/sovai/reports/accounting/__init__.py +0 -0
- {sovai-0.2.38 → sovai-0.2.40}/sovai/reports/accounting/accounting_balance_sheet.py +0 -0
- {sovai-0.2.38 → sovai-0.2.40}/sovai/reports/bankruptcy/__init__.py +0 -0
- {sovai-0.2.38 → sovai-0.2.40}/sovai/reports/bankruptcy/bankruptcy_monthly_top.py +0 -0
- {sovai-0.2.38 → sovai-0.2.40}/sovai/reports/general/__init__.py +0 -0
- {sovai-0.2.38 → sovai-0.2.40}/sovai/reports/general/general_plots.py +0 -0
- {sovai-0.2.38 → sovai-0.2.40}/sovai/reports/news/__inti__.py +0 -0
- {sovai-0.2.38 → sovai-0.2.40}/sovai/reports/news/news_econometric_analysis.py +0 -0
- {sovai-0.2.38 → sovai-0.2.40}/sovai/studies/nowcasting.py +0 -0
- {sovai-0.2.38 → sovai-0.2.40}/sovai/token_auth.py +0 -0
- {sovai-0.2.38 → sovai-0.2.40}/sovai/tools/authentication/__init__.py +0 -0
- {sovai-0.2.38 → sovai-0.2.40}/sovai/tools/authentication/authentication.py +0 -0
- {sovai-0.2.38 → sovai-0.2.40}/sovai/tools/sec/__init__.py +0 -0
- {sovai-0.2.38 → sovai-0.2.40}/sovai/tools/sec/graphs.py +0 -0
- {sovai-0.2.38 → sovai-0.2.40}/sovai/tools/sec/llm_code_generator.py +0 -0
- {sovai-0.2.38 → sovai-0.2.40}/sovai/tools/sec/sec_10_k_8_k_filings.py +0 -0
- {sovai-0.2.38 → sovai-0.2.40}/sovai/tools/sec/sec_edgar_search.py +0 -0
- {sovai-0.2.38 → sovai-0.2.40}/sovai/utils/__init__.py +0 -0
- {sovai-0.2.38 → sovai-0.2.40}/sovai/utils/client_side.py +0 -0
- {sovai-0.2.38 → sovai-0.2.40}/sovai/utils/client_side_s3.py +0 -0
- {sovai-0.2.38 → sovai-0.2.40}/sovai/utils/converter.py +0 -0
- {sovai-0.2.38 → sovai-0.2.40}/sovai/utils/datetime_formats.py +0 -0
- {sovai-0.2.38 → sovai-0.2.40}/sovai/utils/file_management.py +0 -0
- {sovai-0.2.38 → sovai-0.2.40}/sovai/utils/get_tickers.py +0 -0
- {sovai-0.2.38 → sovai-0.2.40}/sovai/utils/helpers.py +0 -0
- {sovai-0.2.38 → sovai-0.2.40}/sovai/utils/pandas_extensions copy.py +0 -0
- {sovai-0.2.38 → sovai-0.2.40}/sovai/utils/plot.py +0 -0
- {sovai-0.2.38 → sovai-0.2.40}/sovai/utils/port_manager.py +0 -0
- {sovai-0.2.38 → sovai-0.2.40}/sovai/utils/stream.py +0 -0
|
@@ -339,7 +339,21 @@ ENDPOINT_TO_TICKER = {
|
|
|
339
339
|
"/trials/describe": "",
|
|
340
340
|
"/trials/all/predict": "",
|
|
341
341
|
"/trials/all/decribe": "",
|
|
342
|
-
"/trials/all": ""
|
|
342
|
+
"/trials/all": "",
|
|
343
|
+
|
|
344
|
+
"/patents/applications": "",
|
|
345
|
+
"/patents/grants": "",
|
|
346
|
+
"/clinical_trials": "",
|
|
347
|
+
"/spending/awards": "",
|
|
348
|
+
"/spending/compensation": "",
|
|
349
|
+
"/spending/competition": "",
|
|
350
|
+
"/spending/contracts": "",
|
|
351
|
+
"/spending/entities": "",
|
|
352
|
+
"/spending/location": "",
|
|
353
|
+
"/spending/product": "",
|
|
354
|
+
"/spending/transactions": "",
|
|
355
|
+
"/lobbying/data": ""
|
|
356
|
+
|
|
343
357
|
}
|
|
344
358
|
|
|
345
359
|
# Define endpoint sets for different processing methods
|
|
@@ -369,7 +383,7 @@ CLIENT_SIDE_ENDPOINTS_S3_PART_HIGH = {
|
|
|
369
383
|
"spending/awards",
|
|
370
384
|
"spending/compensation",
|
|
371
385
|
"spending/competition",
|
|
372
|
-
"spending/
|
|
386
|
+
"spending/contracts",
|
|
373
387
|
"spending/entities",
|
|
374
388
|
"spending/location",
|
|
375
389
|
"spending/product",
|
|
@@ -605,7 +605,7 @@ ENDPOINT_CONFIG = {
|
|
|
605
605
|
"date_path": "sovai/sovai-government/partitioned/competition/date",
|
|
606
606
|
"has_year": False
|
|
607
607
|
},
|
|
608
|
-
"spending/
|
|
608
|
+
"spending/contracts": {
|
|
609
609
|
"ticker_path": "sovai/sovai-government/partitioned/contract/ticker",
|
|
610
610
|
"date_path": "sovai/sovai-government/partitioned/contract/date",
|
|
611
611
|
"has_year": False
|
|
@@ -1,432 +0,0 @@
|
|
|
1
|
-
import pyarrow.dataset as ds
|
|
2
|
-
from pyarrow.fs import S3FileSystem
|
|
3
|
-
import pyarrow as pa
|
|
4
|
-
import pandas as pd
|
|
5
|
-
from functools import lru_cache
|
|
6
|
-
from concurrent.futures import ThreadPoolExecutor, as_completed
|
|
7
|
-
from sovai.tools.authentication import authentication
|
|
8
|
-
import os
|
|
9
|
-
import datetime
|
|
10
|
-
import s3fs
|
|
11
|
-
import logging
|
|
12
|
-
from tqdm import tqdm
|
|
13
|
-
|
|
14
|
-
# -------------------------------
|
|
15
|
-
# Cached Filesystem Accessors
|
|
16
|
-
# -------------------------------
|
|
17
|
-
|
|
18
|
-
@lru_cache(maxsize=2)
|
|
19
|
-
def get_cached_s3_filesystem(storage_provider):
|
|
20
|
-
return authentication.get_s3_filesystem_pickle(storage_provider, verbose=True)
|
|
21
|
-
|
|
22
|
-
@lru_cache(maxsize=2)
|
|
23
|
-
def get_cached_s3fs_filesystem(storage_provider):
|
|
24
|
-
return authentication.get_s3fs_filesystem_json(storage_provider, verbose=True)
|
|
25
|
-
|
|
26
|
-
# -------------------------------
|
|
27
|
-
# Logging Configuration
|
|
28
|
-
# -------------------------------
|
|
29
|
-
|
|
30
|
-
logging.basicConfig(
|
|
31
|
-
level=logging.INFO,
|
|
32
|
-
format='%(asctime)s - %(levelname)s - %(message)s',
|
|
33
|
-
handlers=[
|
|
34
|
-
logging.FileHandler("data_operations.log"),
|
|
35
|
-
logging.StreamHandler()
|
|
36
|
-
]
|
|
37
|
-
)
|
|
38
|
-
logger = logging.getLogger(__name__)
|
|
39
|
-
|
|
40
|
-
# -------------------------------
|
|
41
|
-
# Helper Functions
|
|
42
|
-
# -------------------------------
|
|
43
|
-
|
|
44
|
-
def construct_s3_path(bucket, type_name, provider, partition_type, ticker=None, publish_date=None, year=None, all_dates_after=False, has_year_subdir=True):
|
|
45
|
-
"""
|
|
46
|
-
Construct the S3 path based on the partitioning scheme.
|
|
47
|
-
|
|
48
|
-
Parameters:
|
|
49
|
-
- bucket (str): Bucket name/path prefix.
|
|
50
|
-
- type_name (str): Type of data (e.g., 'applications')
|
|
51
|
-
- provider (str): Storage provider (e.g., 'wasabi' or 'digitalocean')
|
|
52
|
-
- partition_type (str): 'date' or 'ticker'
|
|
53
|
-
- ticker (str, optional): Ticker symbol (required for ticker partitioning)
|
|
54
|
-
- publish_date (str, optional): Publish date in 'YYYY-MM-DD' format (required for date partitioning)
|
|
55
|
-
- year (int, optional): Year (required for ticker partitioning when has_year_subdir is True)
|
|
56
|
-
- all_dates_after (bool, optional): If True, returns the parent directory for scanning all date partitions
|
|
57
|
-
- has_year_subdir (bool, optional): Indicates whether the ticker partition includes a year subdirectory.
|
|
58
|
-
|
|
59
|
-
Returns:
|
|
60
|
-
- str: Constructed S3 path.
|
|
61
|
-
"""
|
|
62
|
-
if partition_type == "date":
|
|
63
|
-
if all_dates_after:
|
|
64
|
-
path = f"{type_name}/date/"
|
|
65
|
-
else:
|
|
66
|
-
if not publish_date:
|
|
67
|
-
raise ValueError("publish_date must be provided for date partitioning.")
|
|
68
|
-
path = f"{type_name}/date/date/date_partitioned={publish_date}/"
|
|
69
|
-
elif partition_type == "ticker":
|
|
70
|
-
if not ticker:
|
|
71
|
-
raise ValueError("Ticker must be provided for ticker partitioning.")
|
|
72
|
-
if has_year_subdir:
|
|
73
|
-
if not year:
|
|
74
|
-
raise ValueError("Year must be provided when has_year_subdir is True for ticker partitioning.")
|
|
75
|
-
path = f"{type_name}/ticker/ticker/ticker_partitioned={ticker}/year={year}/"
|
|
76
|
-
else:
|
|
77
|
-
path = f"{type_name}/ticker/ticker/ticker_partitioned={ticker}/"
|
|
78
|
-
else:
|
|
79
|
-
raise ValueError("Invalid partition_type. Choose 'date' or 'ticker'.")
|
|
80
|
-
|
|
81
|
-
return f"{bucket}/{path}"
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
def list_ticker_years(provider, bucket, type_name, ticker, has_year_subdir=True):
|
|
85
|
-
"""
|
|
86
|
-
List actual year subfolders for a given ticker in the S3 partition scheme.
|
|
87
|
-
|
|
88
|
-
If has_year_subdir is False, simply returns a single tuple with None as the year.
|
|
89
|
-
|
|
90
|
-
Returns:
|
|
91
|
-
- list of tuples: [(year, path), ...] where year is an integer if available, or None.
|
|
92
|
-
"""
|
|
93
|
-
ticker_base_path = f"{bucket}/{type_name}/ticker/ticker/ticker_partitioned={ticker}/"
|
|
94
|
-
if not has_year_subdir:
|
|
95
|
-
return [(None, ticker_base_path)]
|
|
96
|
-
|
|
97
|
-
fs = get_cached_s3fs_filesystem(provider)
|
|
98
|
-
try:
|
|
99
|
-
subdirs = fs.ls(ticker_base_path)
|
|
100
|
-
except Exception as e:
|
|
101
|
-
logger.error(f"Error listing subfolders in {ticker_base_path} ({provider}): {e}")
|
|
102
|
-
return []
|
|
103
|
-
|
|
104
|
-
year_paths = []
|
|
105
|
-
for subdir in subdirs:
|
|
106
|
-
basename = os.path.basename(subdir.rstrip('/'))
|
|
107
|
-
if basename.startswith('year='):
|
|
108
|
-
year_str = basename.split('=')[1]
|
|
109
|
-
try:
|
|
110
|
-
year_int = int(year_str)
|
|
111
|
-
year_paths.append((year_int, subdir))
|
|
112
|
-
except ValueError:
|
|
113
|
-
logger.warning(f"Could not parse year from folder name: {basename}")
|
|
114
|
-
return year_paths
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
def list_date_partitions(provider, bucket, type_name):
|
|
118
|
-
"""
|
|
119
|
-
List date partitions in the S3 bucket.
|
|
120
|
-
|
|
121
|
-
Returns:
|
|
122
|
-
- list of tuples: [(date_obj, partition_path), ...]
|
|
123
|
-
"""
|
|
124
|
-
s3_path = construct_s3_path(bucket, type_name, provider, partition_type="date", all_dates_after=True)
|
|
125
|
-
fs = get_cached_s3fs_filesystem(provider)
|
|
126
|
-
try:
|
|
127
|
-
partitions = fs.ls(s3_path)
|
|
128
|
-
dates = []
|
|
129
|
-
for partition in partitions:
|
|
130
|
-
basename = os.path.basename(partition.rstrip('/'))
|
|
131
|
-
if basename.startswith('date_partitioned='):
|
|
132
|
-
date_str = basename.split('=')[1]
|
|
133
|
-
try:
|
|
134
|
-
date_obj = datetime.datetime.strptime(date_str, '%Y-%m-%d').date()
|
|
135
|
-
dates.append((date_obj, partition))
|
|
136
|
-
except ValueError:
|
|
137
|
-
logger.warning(f"Unable to parse date from partition {partition}")
|
|
138
|
-
return dates
|
|
139
|
-
except Exception as e:
|
|
140
|
-
logger.error(f"Error listing date partitions in {s3_path} ({provider}): {e}")
|
|
141
|
-
return []
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
def load_parquet_from_s3(s3_path, provider, columns=None, start_date=None, end_date=None):
|
|
145
|
-
"""
|
|
146
|
-
Load a Parquet file from S3 into a pandas DataFrame with optional date filtering.
|
|
147
|
-
|
|
148
|
-
Now supports filtering on both start_date and end_date.
|
|
149
|
-
"""
|
|
150
|
-
fs = get_cached_s3_filesystem(provider)
|
|
151
|
-
try:
|
|
152
|
-
dataset = ds.dataset(s3_path, filesystem=fs, format='parquet')
|
|
153
|
-
|
|
154
|
-
# Build filter expression if start_date and/or end_date are provided.
|
|
155
|
-
filter_expr = None
|
|
156
|
-
if start_date:
|
|
157
|
-
min_date_pa = pa.scalar(pd.to_datetime(start_date).date(), type=pa.date32())
|
|
158
|
-
filter_expr = ds.field('date') >= min_date_pa
|
|
159
|
-
if end_date:
|
|
160
|
-
max_date_pa = pa.scalar(pd.to_datetime(end_date).date(), type=pa.date32())
|
|
161
|
-
if filter_expr is not None:
|
|
162
|
-
filter_expr = filter_expr & (ds.field('date') <= max_date_pa)
|
|
163
|
-
else:
|
|
164
|
-
filter_expr = ds.field('date') <= max_date_pa
|
|
165
|
-
if filter_expr is not None:
|
|
166
|
-
dataset = dataset.filter(filter_expr)
|
|
167
|
-
|
|
168
|
-
table = dataset.to_table(columns=columns, use_threads=True)
|
|
169
|
-
logger.info(f"Schema for {s3_path} ({provider}): {table.schema}")
|
|
170
|
-
df = table.to_pandas()
|
|
171
|
-
|
|
172
|
-
# Optionally, reorder columns if partition columns exist
|
|
173
|
-
partition_cols = ['ticker', 'date']
|
|
174
|
-
existing_cols = [col for col in partition_cols if col in df.columns]
|
|
175
|
-
other_cols = [col for col in df.columns if col not in partition_cols]
|
|
176
|
-
df = df[existing_cols + other_cols]
|
|
177
|
-
return df
|
|
178
|
-
except Exception as e:
|
|
179
|
-
logger.error(f"Error loading data from {s3_path} ({provider}): {e}")
|
|
180
|
-
return pd.DataFrame()
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
# -------------------------------
|
|
184
|
-
# Loading Functions
|
|
185
|
-
# -------------------------------
|
|
186
|
-
|
|
187
|
-
def load_data_by_ticker(
|
|
188
|
-
type_name,
|
|
189
|
-
buckets, # Dictionary mapping provider -> bucket value.
|
|
190
|
-
providers=['wasabi', 'digitalocean'],
|
|
191
|
-
tickers=None,
|
|
192
|
-
start_date=None,
|
|
193
|
-
end_date=None,
|
|
194
|
-
columns=None,
|
|
195
|
-
max_workers=4,
|
|
196
|
-
has_year_subdir=True
|
|
197
|
-
):
|
|
198
|
-
"""
|
|
199
|
-
Load data for specified tickers and/or dates from S3 storage providers.
|
|
200
|
-
"""
|
|
201
|
-
date_tasks = []
|
|
202
|
-
ticker_tasks = []
|
|
203
|
-
results = []
|
|
204
|
-
|
|
205
|
-
# Normalize tickers to a list (if provided)
|
|
206
|
-
if tickers and isinstance(tickers, str):
|
|
207
|
-
tickers = [tickers]
|
|
208
|
-
if not tickers:
|
|
209
|
-
tickers = []
|
|
210
|
-
|
|
211
|
-
# Case 1: Tickers and a start_date provided.
|
|
212
|
-
if tickers and start_date:
|
|
213
|
-
try:
|
|
214
|
-
min_date_obj = datetime.datetime.strptime(start_date, '%Y-%m-%d').date()
|
|
215
|
-
logger.info(f"Minimum date provided: {min_date_obj}")
|
|
216
|
-
except ValueError:
|
|
217
|
-
logger.error(f"Invalid start_date format: {start_date}. Expected 'YYYY-MM-DD'.")
|
|
218
|
-
return pd.DataFrame()
|
|
219
|
-
|
|
220
|
-
# If end_date is provided, determine the maximum year to scan.
|
|
221
|
-
if end_date:
|
|
222
|
-
try:
|
|
223
|
-
max_date_obj = datetime.datetime.strptime(end_date, '%Y-%m-%d').date()
|
|
224
|
-
logger.info(f"End date provided: {max_date_obj}")
|
|
225
|
-
except ValueError:
|
|
226
|
-
logger.error(f"Invalid end_date format: {end_date}. Expected 'YYYY-MM-DD'.")
|
|
227
|
-
return pd.DataFrame()
|
|
228
|
-
else:
|
|
229
|
-
max_date_obj = None
|
|
230
|
-
|
|
231
|
-
for provider in providers:
|
|
232
|
-
for ticker in tickers:
|
|
233
|
-
if has_year_subdir:
|
|
234
|
-
# Determine the upper bound for years
|
|
235
|
-
current_year = datetime.datetime.now().year
|
|
236
|
-
max_year = max_date_obj.year if max_date_obj else current_year
|
|
237
|
-
# Create a task for each year from the minimum year up to the max_year.
|
|
238
|
-
for year in range(min_date_obj.year, max_year + 1):
|
|
239
|
-
s3_path = construct_s3_path(
|
|
240
|
-
bucket=buckets[provider],
|
|
241
|
-
type_name=type_name,
|
|
242
|
-
provider=provider,
|
|
243
|
-
partition_type="ticker",
|
|
244
|
-
ticker=ticker,
|
|
245
|
-
year=year,
|
|
246
|
-
has_year_subdir=True
|
|
247
|
-
)
|
|
248
|
-
ticker_tasks.append((s3_path, provider, columns, None))
|
|
249
|
-
else:
|
|
250
|
-
s3_path = construct_s3_path(
|
|
251
|
-
bucket=buckets[provider],
|
|
252
|
-
type_name=type_name,
|
|
253
|
-
provider=provider,
|
|
254
|
-
partition_type="ticker",
|
|
255
|
-
ticker=ticker,
|
|
256
|
-
has_year_subdir=False
|
|
257
|
-
)
|
|
258
|
-
ticker_tasks.append((s3_path, provider, columns, None))
|
|
259
|
-
|
|
260
|
-
# Case 2: Only dates provided (no tickers), with both start and end dates.
|
|
261
|
-
elif start_date and end_date and not tickers:
|
|
262
|
-
try:
|
|
263
|
-
min_date_obj = datetime.datetime.strptime(start_date, '%Y-%m-%d').date()
|
|
264
|
-
max_date_obj = datetime.datetime.strptime(end_date, '%Y-%m-%d').date()
|
|
265
|
-
logger.info(f"Date range provided: {min_date_obj} to {max_date_obj}")
|
|
266
|
-
except ValueError as e:
|
|
267
|
-
logger.error(f"Invalid date format. Expected 'YYYY-MM-DD': {e}")
|
|
268
|
-
return pd.DataFrame()
|
|
269
|
-
|
|
270
|
-
for provider in providers:
|
|
271
|
-
date_partitions = list_date_partitions(provider, buckets[provider], type_name)
|
|
272
|
-
for date_obj, partition_path in date_partitions:
|
|
273
|
-
if min_date_obj <= date_obj <= max_date_obj:
|
|
274
|
-
date_tasks.append((partition_path, provider, columns, None))
|
|
275
|
-
|
|
276
|
-
# Case 3: Only start_date provided (no tickers)
|
|
277
|
-
elif start_date and not tickers:
|
|
278
|
-
try:
|
|
279
|
-
min_date_obj = datetime.datetime.strptime(start_date, '%Y-%m-%d').date()
|
|
280
|
-
logger.info(f"Minimum date provided: {min_date_obj}")
|
|
281
|
-
except ValueError:
|
|
282
|
-
logger.error(f"Invalid start_date format: {start_date}. Expected 'YYYY-MM-DD'.")
|
|
283
|
-
return pd.DataFrame()
|
|
284
|
-
|
|
285
|
-
for provider in providers:
|
|
286
|
-
date_partitions = list_date_partitions(provider, buckets[provider], type_name)
|
|
287
|
-
for date_obj, partition_path in date_partitions:
|
|
288
|
-
if date_obj >= min_date_obj:
|
|
289
|
-
date_tasks.append((partition_path, provider, columns, None))
|
|
290
|
-
|
|
291
|
-
# Case 4: Only tickers provided.
|
|
292
|
-
elif tickers and not start_date:
|
|
293
|
-
for provider in providers:
|
|
294
|
-
for ticker in tickers:
|
|
295
|
-
print(f"DEBUG: Looking for ticker {ticker} in {buckets[provider]}/{type_name}")
|
|
296
|
-
ticker_years = list_ticker_years(provider, buckets[provider], type_name, ticker, has_year_subdir=has_year_subdir)
|
|
297
|
-
print(f"DEBUG: Found ticker years: {ticker_years}")
|
|
298
|
-
for year_val, path in ticker_years:
|
|
299
|
-
print(f"DEBUG: Adding task for path: {path}")
|
|
300
|
-
ticker_tasks.append((path, provider, columns, None))
|
|
301
|
-
else:
|
|
302
|
-
logger.warning("No tickers or start_date provided. Please provide at least one.")
|
|
303
|
-
return pd.DataFrame()
|
|
304
|
-
|
|
305
|
-
# Combine tasks from ticker and date sources.
|
|
306
|
-
all_tasks = ticker_tasks + date_tasks
|
|
307
|
-
if not all_tasks:
|
|
308
|
-
logger.warning("No tasks to process. Exiting.")
|
|
309
|
-
return pd.DataFrame()
|
|
310
|
-
|
|
311
|
-
def load_task(task):
|
|
312
|
-
s3_path, provider, columns, ticker_filter = task
|
|
313
|
-
# Pass both start_date and end_date to the loading function.
|
|
314
|
-
df = load_parquet_from_s3(s3_path, provider, columns, start_date=start_date, end_date=end_date)
|
|
315
|
-
if not df.empty and ticker_filter:
|
|
316
|
-
if 'ticker' in df.columns:
|
|
317
|
-
df = df[df['ticker'].isin(ticker_filter)]
|
|
318
|
-
else:
|
|
319
|
-
logger.warning(f"'ticker' column not found in data from {s3_path} ({provider}). Skipping ticker filter.")
|
|
320
|
-
return df
|
|
321
|
-
|
|
322
|
-
with ThreadPoolExecutor(max_workers=max_workers) as executor:
|
|
323
|
-
future_to_task = {executor.submit(load_task, task): task for task in all_tasks}
|
|
324
|
-
for future in tqdm(as_completed(future_to_task), total=len(all_tasks), desc="Loading data"):
|
|
325
|
-
df = future.result()
|
|
326
|
-
if not df.empty:
|
|
327
|
-
results.append(df)
|
|
328
|
-
|
|
329
|
-
if results:
|
|
330
|
-
final_df = pd.concat(results, ignore_index=True)
|
|
331
|
-
logger.info(f"Successfully loaded data with {len(final_df)} records.")
|
|
332
|
-
else:
|
|
333
|
-
final_df = pd.DataFrame()
|
|
334
|
-
logger.warning("No data found.")
|
|
335
|
-
|
|
336
|
-
return final_df
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
# -------------------------------
|
|
340
|
-
# Example Usage
|
|
341
|
-
# -------------------------------
|
|
342
|
-
|
|
343
|
-
# Try to import CustomDataFrame; if unavailable, fall back to pandas.DataFrame.
|
|
344
|
-
try:
|
|
345
|
-
from sovai.extensions.pandas_extensions import CustomDataFrame
|
|
346
|
-
HAS_CUSTOM_DATAFRAME = True
|
|
347
|
-
except ImportError:
|
|
348
|
-
HAS_CUSTOM_DATAFRAME = False
|
|
349
|
-
CustomDataFrame = pd.DataFrame
|
|
350
|
-
|
|
351
|
-
def load_frame_s3_partitioned_high(endpoint, tickers=None, columns=None, start_date=None, end_date=None):
|
|
352
|
-
"""
|
|
353
|
-
Load data for a given endpoint using the S3 partitioned scheme.
|
|
354
|
-
|
|
355
|
-
The endpoint configuration now contains the bucket mapping, data type, and a new parameter
|
|
356
|
-
'has_year_subdir' that indicates whether the ticker partition includes a year subdirectory.
|
|
357
|
-
"""
|
|
358
|
-
print(f"DEBUG: load_frame_s3_partitioned_high called with endpoint={endpoint}, tickers={tickers}")
|
|
359
|
-
endpoint_config = {
|
|
360
|
-
"patents/applications": {
|
|
361
|
-
"bucket": {
|
|
362
|
-
"digitalocean": "sovai/sovai-patents-bulk", # Use the "bulk" bucket for ticker partitions
|
|
363
|
-
"wasabi": "sovai-patents-export"
|
|
364
|
-
},
|
|
365
|
-
"type": "applications",
|
|
366
|
-
"has_year_subdir": True
|
|
367
|
-
},
|
|
368
|
-
"patents/grants": {
|
|
369
|
-
"bucket": {
|
|
370
|
-
"digitalocean": "sovai/sovai-patents-bulk",
|
|
371
|
-
"wasabi": "sovai-patents-export"
|
|
372
|
-
},
|
|
373
|
-
"type": "grants",
|
|
374
|
-
"has_year_subdir": True
|
|
375
|
-
},
|
|
376
|
-
"clinical_trials": {
|
|
377
|
-
"bucket": {
|
|
378
|
-
"digitalocean": "sovai/sovai-clinical-trials-export",
|
|
379
|
-
"wasabi": "sovai-clinical-trials-export" # Adjust as needed
|
|
380
|
-
},
|
|
381
|
-
"type": "partitioned",
|
|
382
|
-
"has_year_subdir": False
|
|
383
|
-
},
|
|
384
|
-
"clinical_trials": {
|
|
385
|
-
"bucket": {
|
|
386
|
-
"digitalocean": "sovai/sovai-clinical-trials-export",
|
|
387
|
-
"wasabi": "sovai-clinical-trials-export" # Adjust as needed
|
|
388
|
-
},
|
|
389
|
-
"type": "partitioned",
|
|
390
|
-
"has_year_subdir": False
|
|
391
|
-
},
|
|
392
|
-
"spending/awards": {
|
|
393
|
-
"bucket": {
|
|
394
|
-
"digitalocean": "sovai/sovai-spending-export",
|
|
395
|
-
"wasabi": "sovai-spending-export" # Adjust as needed
|
|
396
|
-
},
|
|
397
|
-
"type": "partitioned/awards",
|
|
398
|
-
"has_year_subdir": False
|
|
399
|
-
}
|
|
400
|
-
}
|
|
401
|
-
|
|
402
|
-
if endpoint not in endpoint_config:
|
|
403
|
-
raise ValueError(f"Invalid endpoint: {endpoint}")
|
|
404
|
-
|
|
405
|
-
config = endpoint_config[endpoint]
|
|
406
|
-
|
|
407
|
-
loaded_df_both = load_data_by_ticker(
|
|
408
|
-
type_name=config["type"],
|
|
409
|
-
buckets=config["bucket"],
|
|
410
|
-
providers=['digitalocean'],
|
|
411
|
-
tickers=tickers,
|
|
412
|
-
start_date=start_date,
|
|
413
|
-
end_date=end_date,
|
|
414
|
-
columns=columns,
|
|
415
|
-
max_workers=8, # Adjust based on your system's capabilities
|
|
416
|
-
has_year_subdir=config.get("has_year_subdir", True)
|
|
417
|
-
)
|
|
418
|
-
|
|
419
|
-
# Sort the dataframe if the required columns exist
|
|
420
|
-
sort_columns = []
|
|
421
|
-
if 'ticker' in loaded_df_both.columns:
|
|
422
|
-
sort_columns.append('ticker')
|
|
423
|
-
if 'date' in loaded_df_both.columns:
|
|
424
|
-
sort_columns.append('date')
|
|
425
|
-
|
|
426
|
-
if sort_columns:
|
|
427
|
-
loaded_df_both = loaded_df_both.sort_values(sort_columns)
|
|
428
|
-
|
|
429
|
-
if HAS_CUSTOM_DATAFRAME:
|
|
430
|
-
return CustomDataFrame(loaded_df_both)
|
|
431
|
-
else:
|
|
432
|
-
return loaded_df_both
|
|
@@ -1,407 +0,0 @@
|
|
|
1
|
-
import pyarrow.dataset as ds
|
|
2
|
-
from pyarrow.fs import S3FileSystem
|
|
3
|
-
import pyarrow as pa
|
|
4
|
-
import pandas as pd
|
|
5
|
-
from functools import lru_cache
|
|
6
|
-
from concurrent.futures import ThreadPoolExecutor, as_completed
|
|
7
|
-
from sovai.tools.authentication import authentication
|
|
8
|
-
import os
|
|
9
|
-
import pandas as pd
|
|
10
|
-
import pyarrow as pa
|
|
11
|
-
import pyarrow.dataset as ds
|
|
12
|
-
import s3fs
|
|
13
|
-
import logging
|
|
14
|
-
import datetime
|
|
15
|
-
from functools import lru_cache
|
|
16
|
-
from concurrent.futures import ThreadPoolExecutor, as_completed
|
|
17
|
-
from tqdm import tqdm
|
|
18
|
-
from pyarrow.fs import S3FileSystem
|
|
19
|
-
|
|
20
|
-
def list_ticker_years(provider, type_name, ticker):
|
|
21
|
-
"""
|
|
22
|
-
List actual year subfolders for a given ticker in the S3 partition scheme.
|
|
23
|
-
|
|
24
|
-
For example, if your S3 structure is:
|
|
25
|
-
s3://sovai-patents-export/applications/ticker/ticker/ticker_partitioned=IBM/year=2019/
|
|
26
|
-
s3://sovai-patents-export/applications/ticker/ticker/ticker_partitioned=IBM/year=2020/
|
|
27
|
-
...
|
|
28
|
-
this function will discover those subfolders and parse out the 'year=NNNN'
|
|
29
|
-
part, returning [(2019, path), (2020, path), ...].
|
|
30
|
-
"""
|
|
31
|
-
|
|
32
|
-
# 1) Build the base ticker path (without specifying year=)
|
|
33
|
-
# e.g.: sovai/sovai-patents-bulk/applications/ticker/ticker/ticker_partitioned=IBM/
|
|
34
|
-
if provider == "digitalocean":
|
|
35
|
-
bucket = "sovai/sovai-patents-bulk"
|
|
36
|
-
else:
|
|
37
|
-
bucket = "sovai-patents-export"
|
|
38
|
-
|
|
39
|
-
# We'll just reuse your pattern from construct_s3_path() but exclude the /year={year}/ part
|
|
40
|
-
ticker_base_path = f"{bucket}/{type_name}/ticker/ticker/ticker_partitioned={ticker}/"
|
|
41
|
-
|
|
42
|
-
# 2) List the subdirectories
|
|
43
|
-
fs = get_cached_s3fs_filesystem(provider)
|
|
44
|
-
try:
|
|
45
|
-
# Returns a list of file/directory paths
|
|
46
|
-
subdirs = fs.ls(ticker_base_path)
|
|
47
|
-
except Exception as e:
|
|
48
|
-
logger.error(f"Error listing subfolders in {ticker_base_path} ({provider}): {e}")
|
|
49
|
-
return []
|
|
50
|
-
|
|
51
|
-
# 3) Parse out the 'year=YYYY' subfolder from each path
|
|
52
|
-
year_paths = []
|
|
53
|
-
for subdir in subdirs:
|
|
54
|
-
# subdir might look like 'sovai/sovai-patents-bulk/applications/ticker/ticker/ticker_partitioned=IBM/year=2020/'
|
|
55
|
-
# so we just parse out the year from that string
|
|
56
|
-
basename = os.path.basename(subdir.rstrip('/'))
|
|
57
|
-
# If subdir ends with 'year=2020' or 'year=2020/', etc.
|
|
58
|
-
if basename.startswith('year='):
|
|
59
|
-
# The right side is the actual year number
|
|
60
|
-
year_str = basename.split('=')[1]
|
|
61
|
-
try:
|
|
62
|
-
year_int = int(year_str)
|
|
63
|
-
year_paths.append((year_int, subdir))
|
|
64
|
-
except ValueError:
|
|
65
|
-
logger.warning(f"Could not parse year from folder name: {basename}")
|
|
66
|
-
|
|
67
|
-
# 4) Return the discovered years and their paths
|
|
68
|
-
return year_paths
|
|
69
|
-
|
|
70
|
-
# Try to import CustomDataFrame, use regular DataFrame if not available
|
|
71
|
-
try:
|
|
72
|
-
from sovai.extensions.pandas_extensions import CustomDataFrame
|
|
73
|
-
HAS_CUSTOM_DATAFRAME = True
|
|
74
|
-
except ImportError:
|
|
75
|
-
HAS_CUSTOM_DATAFRAME = False
|
|
76
|
-
CustomDataFrame = pd.DataFrame # Fallback to regular DataFrame
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
@lru_cache(maxsize=2)
|
|
80
|
-
def get_cached_s3_filesystem(storage_provider):
|
|
81
|
-
return authentication.get_s3_filesystem_pickle(storage_provider, verbose=True)
|
|
82
|
-
|
|
83
|
-
@lru_cache(maxsize=2)
|
|
84
|
-
def get_cached_s3fs_filesystem(storage_provider):
|
|
85
|
-
return authentication.get_s3fs_filesystem_json(storage_provider, verbose=True)
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
# Configure logging to output to both console and a log file
|
|
89
|
-
logging.basicConfig(
|
|
90
|
-
level=logging.INFO,
|
|
91
|
-
format='%(asctime)s - %(levelname)s - %(message)s',
|
|
92
|
-
handlers=[
|
|
93
|
-
logging.FileHandler("data_operations.log"),
|
|
94
|
-
logging.StreamHandler()
|
|
95
|
-
]
|
|
96
|
-
)
|
|
97
|
-
logger = logging.getLogger(__name__)
|
|
98
|
-
|
|
99
|
-
# -------------------------------
|
|
100
|
-
# Helper Functions
|
|
101
|
-
# -------------------------------
|
|
102
|
-
|
|
103
|
-
def construct_s3_path(type_name, provider, partition_type, ticker=None, publish_date=None, year=None, all_dates_after=False):
|
|
104
|
-
"""
|
|
105
|
-
Construct the S3 path based on the partitioning scheme.
|
|
106
|
-
|
|
107
|
-
Parameters:
|
|
108
|
-
- type_name (str): Type of data (e.g., 'applications')
|
|
109
|
-
- provider (str): 'wasabi' or 'digitalocean'
|
|
110
|
-
- partition_type (str): 'date' or 'ticker'
|
|
111
|
-
- ticker (str, optional): Ticker symbol
|
|
112
|
-
- publish_date (str, optional): Publish date in 'YYYY-MM-DD' format
|
|
113
|
-
- year (int, optional): Year extracted from publish_date
|
|
114
|
-
- all_dates_after (bool, optional): If True, scan all date partitions after a certain date
|
|
115
|
-
|
|
116
|
-
Returns:
|
|
117
|
-
- str: Constructed S3 path
|
|
118
|
-
"""
|
|
119
|
-
if provider=="digitalocean":
|
|
120
|
-
bucket ="sovai/sovai-patents-bulk"
|
|
121
|
-
else:
|
|
122
|
-
bucket = "sovai-patents-export"
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
if partition_type == "date":
|
|
126
|
-
if all_dates_after:
|
|
127
|
-
# Return the parent directory to scan all date partitions
|
|
128
|
-
path = f"{type_name}/date/"
|
|
129
|
-
else:
|
|
130
|
-
if not publish_date:
|
|
131
|
-
raise ValueError("publish_date must be provided for date partitioning.")
|
|
132
|
-
path = f"{type_name}/date/date/date_partitioned={publish_date}/"
|
|
133
|
-
elif partition_type == "ticker":
|
|
134
|
-
if not ticker or not year:
|
|
135
|
-
raise ValueError("Both ticker and year must be provided for ticker partitioning.")
|
|
136
|
-
path = f"{type_name}/ticker/ticker/ticker_partitioned={ticker}/year={year}/"
|
|
137
|
-
else:
|
|
138
|
-
raise ValueError("Invalid partition_type. Choose 'date' or 'ticker'.")
|
|
139
|
-
|
|
140
|
-
return f"{bucket}/{path}"
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
def list_date_partitions(provider, type_name):
|
|
144
|
-
s3_path = construct_s3_path(type_name, provider, partition_type="date", all_dates_after=True)
|
|
145
|
-
fs = get_cached_s3fs_filesystem(provider)
|
|
146
|
-
try:
|
|
147
|
-
partitions = fs.ls(s3_path)
|
|
148
|
-
dates = []
|
|
149
|
-
for partition in partitions:
|
|
150
|
-
basename = os.path.basename(partition.rstrip('/'))
|
|
151
|
-
if basename.startswith('date_partitioned='):
|
|
152
|
-
date_str = basename.split('=')[1]
|
|
153
|
-
try:
|
|
154
|
-
date_obj = datetime.datetime.strptime(date_str, '%Y-%m-%d').date()
|
|
155
|
-
dates.append((date_obj, partition))
|
|
156
|
-
except ValueError:
|
|
157
|
-
logger.warning(f"Unable to parse date from partition {partition}")
|
|
158
|
-
return dates
|
|
159
|
-
except Exception as e:
|
|
160
|
-
logger.error(f"Error listing date partitions in {s3_path} ({provider}): {e}")
|
|
161
|
-
return []
|
|
162
|
-
|
|
163
|
-
def load_parquet_from_s3(s3_path, provider, columns=None, start_date=None):
|
|
164
|
-
"""
|
|
165
|
-
Load a Parquet file from S3 into a pandas DataFrame with optional date filtering.
|
|
166
|
-
|
|
167
|
-
Parameters:
|
|
168
|
-
- s3_path (str): S3 path to the Parquet file or directory
|
|
169
|
-
- provider (str): 'wasabi' or 'digitalocean'
|
|
170
|
-
- columns (list, optional): List of columns to select
|
|
171
|
-
- start_date (str, optional): Minimum date in 'YYYY-MM-DD' format to filter the data
|
|
172
|
-
|
|
173
|
-
Returns:
|
|
174
|
-
- pandas.DataFrame: Loaded DataFrame
|
|
175
|
-
"""
|
|
176
|
-
|
|
177
|
-
fs = get_cached_s3_filesystem(provider)
|
|
178
|
-
|
|
179
|
-
try:
|
|
180
|
-
dataset = ds.dataset(s3_path, filesystem=fs, format='parquet')
|
|
181
|
-
|
|
182
|
-
if start_date:
|
|
183
|
-
# Convert start_date string to pyarrow date32 scalar
|
|
184
|
-
min_date_pa = pa.scalar(pd.to_datetime(start_date).date(), type=pa.date32())
|
|
185
|
-
# Use ds.field to construct the filter expression
|
|
186
|
-
filter_expr = ds.field('date') >= min_date_pa
|
|
187
|
-
dataset = dataset.filter(filter_expr)
|
|
188
|
-
|
|
189
|
-
table = dataset.to_table(columns=columns, use_threads=True)
|
|
190
|
-
|
|
191
|
-
# Log the schema for debugging
|
|
192
|
-
logger.info(f"Schema for {s3_path} ({provider}):")
|
|
193
|
-
logger.info(table.schema)
|
|
194
|
-
|
|
195
|
-
df = table.to_pandas()
|
|
196
|
-
|
|
197
|
-
partition_cols = ['ticker', 'date']
|
|
198
|
-
existing_cols = [col for col in partition_cols if col in df.columns]
|
|
199
|
-
other_cols = [col for col in df.columns if col not in partition_cols]
|
|
200
|
-
df = df[existing_cols + other_cols]
|
|
201
|
-
|
|
202
|
-
return df
|
|
203
|
-
except Exception as e:
|
|
204
|
-
logger.error(f"Error loading data from {s3_path} ({provider}): {e}")
|
|
205
|
-
return pd.DataFrame()
|
|
206
|
-
|
|
207
|
-
# -------------------------------
|
|
208
|
-
# Loading Functions
|
|
209
|
-
# -------------------------------
|
|
210
|
-
|
|
211
|
-
def load_data_by_ticker(
|
|
212
|
-
type_name,
|
|
213
|
-
providers=['wasabi', 'digitalocean'],
|
|
214
|
-
tickers=None,
|
|
215
|
-
start_date=None,
|
|
216
|
-
end_date=None,
|
|
217
|
-
columns=None,
|
|
218
|
-
max_workers=4
|
|
219
|
-
):
|
|
220
|
-
"""
|
|
221
|
-
Load data for specified tickers and/or dates from S3 storage providers.
|
|
222
|
-
|
|
223
|
-
Parameters:
|
|
224
|
-
- type_name (str): Type of data (e.g., 'applications', 'prediction_all', etc.)
|
|
225
|
-
- providers (list, optional): List of storage providers to load from ('wasabi', 'digitalocean')
|
|
226
|
-
- tickers (str or list, optional): Ticker symbol(s) to load data for
|
|
227
|
-
- start_date (str, optional): Minimum publishDate to filter data by (format 'YYYY-MM-DD') (only for date partitioning)
|
|
228
|
-
- columns (list, optional): List of columns to select
|
|
229
|
-
- max_workers (int, optional): Number of parallel threads
|
|
230
|
-
|
|
231
|
-
Returns:
|
|
232
|
-
- pandas.DataFrame: Concatenated DataFrame containing the loaded data
|
|
233
|
-
"""
|
|
234
|
-
date_tasks = []
|
|
235
|
-
ticker_tasks = []
|
|
236
|
-
results = []
|
|
237
|
-
|
|
238
|
-
# Normalize input parameters
|
|
239
|
-
if tickers and isinstance(tickers, str):
|
|
240
|
-
tickers = [tickers]
|
|
241
|
-
if not tickers:
|
|
242
|
-
tickers = []
|
|
243
|
-
|
|
244
|
-
# If both tickers and min_date are provided
|
|
245
|
-
if tickers and start_date:
|
|
246
|
-
try:
|
|
247
|
-
min_date_obj = datetime.datetime.strptime(start_date, '%Y-%m-%d').date()
|
|
248
|
-
min_year = min_date_obj.year
|
|
249
|
-
logger.info(f"Minimum date provided: {min_date_obj} (Year: {min_year})")
|
|
250
|
-
except ValueError:
|
|
251
|
-
logger.error(f"Invalid min_date format: {start_date}. Expected 'YYYY-MM-DD'.")
|
|
252
|
-
return pd.DataFrame()
|
|
253
|
-
|
|
254
|
-
for provider in providers:
|
|
255
|
-
for ticker in tickers:
|
|
256
|
-
# Determine the range of years to load based on start_date
|
|
257
|
-
# Assuming data starts from 2011
|
|
258
|
-
start_year = min_year
|
|
259
|
-
current_year = datetime.datetime.now().year
|
|
260
|
-
for year in range(start_year, current_year + 1):
|
|
261
|
-
s3_path = construct_s3_path(
|
|
262
|
-
type_name, provider, partition_type="ticker",
|
|
263
|
-
ticker=ticker, year=year
|
|
264
|
-
)
|
|
265
|
-
ticker_tasks.append((s3_path, provider, columns, None)) # No additional filtering here
|
|
266
|
-
|
|
267
|
-
# If only both dates are provided (no ticker)
|
|
268
|
-
elif start_date and end_date and not tickers:
|
|
269
|
-
try:
|
|
270
|
-
min_date_obj = datetime.datetime.strptime(start_date, '%Y-%m-%d').date()
|
|
271
|
-
max_date_obj = datetime.datetime.strptime(end_date, '%Y-%m-%d').date()
|
|
272
|
-
logger.info(f"Date range provided: {min_date_obj} to {max_date_obj}")
|
|
273
|
-
except ValueError as e:
|
|
274
|
-
logger.error(f"Invalid date format. Expected 'YYYY-MM-DD': {e}")
|
|
275
|
-
return pd.DataFrame()
|
|
276
|
-
|
|
277
|
-
for provider in providers:
|
|
278
|
-
# List all date-based partitions
|
|
279
|
-
date_partitions = list_date_partitions(provider, type_name)
|
|
280
|
-
for date_obj, partition_path in date_partitions:
|
|
281
|
-
# print(date_obj)
|
|
282
|
-
# print(max_date_obj)
|
|
283
|
-
|
|
284
|
-
if min_date_obj <= date_obj <= max_date_obj:
|
|
285
|
-
date_tasks.append((partition_path, provider, columns, None))
|
|
286
|
-
|
|
287
|
-
print(date_tasks)
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
elif start_date and not tickers:
|
|
292
|
-
try:
|
|
293
|
-
min_date_obj = datetime.datetime.strptime(start_date, '%Y-%m-%d').date()
|
|
294
|
-
print(f"Minimum date provided: {min_date_obj}")
|
|
295
|
-
except ValueError:
|
|
296
|
-
logger.error(f"Invalid start_date format: {start_date}. Expected 'YYYY-MM-DD'.")
|
|
297
|
-
return pd.DataFrame()
|
|
298
|
-
|
|
299
|
-
for provider in providers:
|
|
300
|
-
# List all date-based partitions
|
|
301
|
-
date_partitions = list_date_partitions(provider, type_name)
|
|
302
|
-
for date_obj, partition_path in date_partitions:
|
|
303
|
-
if date_obj >= min_date_obj:
|
|
304
|
-
date_tasks.append((partition_path, provider, columns, None))
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
# If only tickers are provided
|
|
308
|
-
elif tickers and not start_date:
|
|
309
|
-
for provider in providers:
|
|
310
|
-
for ticker in tickers:
|
|
311
|
-
# 1) Dynamically list existing year subfolders for this ticker
|
|
312
|
-
ticker_years = list_ticker_years(provider, type_name, ticker)
|
|
313
|
-
|
|
314
|
-
# 2) For each discovered (year, path), schedule a load task
|
|
315
|
-
for year_int, path in ticker_years:
|
|
316
|
-
# If you need the full path, you can pass `path` directly to load_parquet_from_s3
|
|
317
|
-
# or if you prefer to use construct_s3_path, you can do so:
|
|
318
|
-
# But since `list_ticker_years` already returns the full path, you can just use:
|
|
319
|
-
ticker_tasks.append((path, provider, columns, None))
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
else:
|
|
323
|
-
logger.warning("No tickers or start_date provided. Please provide at least one.")
|
|
324
|
-
return pd.DataFrame()
|
|
325
|
-
|
|
326
|
-
# Combine all tasks
|
|
327
|
-
all_tasks = ticker_tasks + date_tasks
|
|
328
|
-
|
|
329
|
-
print(all_tasks)
|
|
330
|
-
|
|
331
|
-
if not all_tasks:
|
|
332
|
-
logger.warning("No tasks to process. Exiting.")
|
|
333
|
-
return pd.DataFrame()
|
|
334
|
-
|
|
335
|
-
def load_task(task):
|
|
336
|
-
s3_path, provider, columns, ticker_filter = task
|
|
337
|
-
df = load_parquet_from_s3(s3_path, provider, columns, start_date=start_date if ticker_filter is None else None)
|
|
338
|
-
if not df.empty and ticker_filter:
|
|
339
|
-
if 'ticker' in df.columns:
|
|
340
|
-
df = df[df['ticker'].isin(ticker_filter)]
|
|
341
|
-
else:
|
|
342
|
-
logger.warning(f"'ticker' column not found in data from {s3_path} ({provider}). Skipping ticker filter.")
|
|
343
|
-
return df
|
|
344
|
-
|
|
345
|
-
with ThreadPoolExecutor(max_workers=max_workers) as executor:
|
|
346
|
-
future_to_task = {executor.submit(load_task, task): task for task in all_tasks}
|
|
347
|
-
for future in tqdm(as_completed(future_to_task), total=len(all_tasks), desc="Loading data"):
|
|
348
|
-
df = future.result()
|
|
349
|
-
if not df.empty:
|
|
350
|
-
results.append(df)
|
|
351
|
-
|
|
352
|
-
print(len(results))
|
|
353
|
-
if results:
|
|
354
|
-
final_df = pd.concat(results, ignore_index=True)
|
|
355
|
-
logger.info(f"Successfully loaded data with {len(final_df)} records.")
|
|
356
|
-
else:
|
|
357
|
-
final_df = pd.DataFrame()
|
|
358
|
-
logger.warning("No data found.")
|
|
359
|
-
|
|
360
|
-
return final_df
|
|
361
|
-
|
|
362
|
-
# -------------------------------
|
|
363
|
-
# Example Usage
|
|
364
|
-
# -------------------------------
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
def load_frame_s3_partitioned_high(endpoint, tickers=None, columns=None, start_date=None, end_date=None):
|
|
368
|
-
|
|
369
|
-
endpoint_config = {
|
|
370
|
-
"patents/applications": {
|
|
371
|
-
"bucket": {
|
|
372
|
-
"digitalocean": "sovai/sovai-patents-export",
|
|
373
|
-
"wasabi": "sovai-patents-export"
|
|
374
|
-
},
|
|
375
|
-
"type": "applications"
|
|
376
|
-
},
|
|
377
|
-
"patents/grants": {
|
|
378
|
-
"bucket": {
|
|
379
|
-
"digitalocean": "sovai/sovai-patents-export",
|
|
380
|
-
"wasabi": "sovai-patents-export"
|
|
381
|
-
},
|
|
382
|
-
"type": "grants"
|
|
383
|
-
},
|
|
384
|
-
}
|
|
385
|
-
|
|
386
|
-
if endpoint not in endpoint_config:
|
|
387
|
-
raise ValueError(f"Invalid endpoint: {endpoint}")
|
|
388
|
-
|
|
389
|
-
config = endpoint_config[endpoint]
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
loaded_df_both = load_data_by_ticker(
|
|
393
|
-
type_name=config["type"],
|
|
394
|
-
providers=['digitalocean'],
|
|
395
|
-
tickers=tickers,
|
|
396
|
-
start_date=start_date,
|
|
397
|
-
end_date=end_date,
|
|
398
|
-
columns=columns,
|
|
399
|
-
max_workers=8 # Adjust based on your system's capabilities
|
|
400
|
-
).sort_values(["ticker","date"])
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
if HAS_CUSTOM_DATAFRAME:
|
|
404
|
-
return CustomDataFrame(loaded_df_both)
|
|
405
|
-
else:
|
|
406
|
-
return loaded_df_both # Returns a regular pandas DataFrame if CustomDataFrame is not available
|
|
407
|
-
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|