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.
Files changed (102) hide show
  1. {sovai-0.2.38 → sovai-0.2.40}/PKG-INFO +1 -1
  2. {sovai-0.2.38 → sovai-0.2.40}/pyproject.toml +1 -1
  3. {sovai-0.2.38 → sovai-0.2.40}/sovai/get_data.py +16 -2
  4. {sovai-0.2.38 → sovai-0.2.40}/sovai/utils/client_side_s3_part_high.py +1 -1
  5. sovai-0.2.38/sovai/utils/client_side_s3_part_high copy 2.py +0 -432
  6. sovai-0.2.38/sovai/utils/client_side_s3_part_high copy.py +0 -407
  7. {sovai-0.2.38 → sovai-0.2.40}/LICENSE +0 -0
  8. {sovai-0.2.38 → sovai-0.2.40}/README.md +0 -0
  9. {sovai-0.2.38 → sovai-0.2.40}/data/codes.parq +0 -0
  10. {sovai-0.2.38 → sovai-0.2.40}/data/tickers.parq +0 -0
  11. {sovai-0.2.38 → sovai-0.2.40}/sovai/.DS_Store +0 -0
  12. {sovai-0.2.38 → sovai-0.2.40}/sovai/__init__.py +0 -0
  13. {sovai-0.2.38 → sovai-0.2.40}/sovai/api_config.py +0 -0
  14. {sovai-0.2.38 → sovai-0.2.40}/sovai/assets/dropdown.css +0 -0
  15. {sovai-0.2.38 → sovai-0.2.40}/sovai/assets/features_mapping.parq +0 -0
  16. {sovai-0.2.38 → sovai-0.2.40}/sovai/basic_auth.py +0 -0
  17. {sovai-0.2.38 → sovai-0.2.40}/sovai/computations/__init__.py +0 -0
  18. {sovai-0.2.38 → sovai-0.2.40}/sovai/computations/functions.py +0 -0
  19. {sovai-0.2.38 → sovai-0.2.40}/sovai/errors/__init__.py +0 -0
  20. {sovai-0.2.38 → sovai-0.2.40}/sovai/errors/sovai_errors.py +0 -0
  21. {sovai-0.2.38 → sovai-0.2.40}/sovai/extensions/__init__.py +0 -0
  22. {sovai-0.2.38 → sovai-0.2.40}/sovai/extensions/anomalies.py +0 -0
  23. {sovai-0.2.38 → sovai-0.2.40}/sovai/extensions/ask_df_llm.py +0 -0
  24. {sovai-0.2.38 → sovai-0.2.40}/sovai/extensions/change_point_generator.py +0 -0
  25. {sovai-0.2.38 → sovai-0.2.40}/sovai/extensions/clustering.py +0 -0
  26. {sovai-0.2.38 → sovai-0.2.40}/sovai/extensions/core_kshape.py +0 -0
  27. {sovai-0.2.38 → sovai-0.2.40}/sovai/extensions/cum_sum_torch.py +0 -0
  28. {sovai-0.2.38 → sovai-0.2.40}/sovai/extensions/cusum.py +0 -0
  29. {sovai-0.2.38 → sovai-0.2.40}/sovai/extensions/dimensionality_reduction.py +0 -0
  30. {sovai-0.2.38 → sovai-0.2.40}/sovai/extensions/feature_extraction.py +0 -0
  31. {sovai-0.2.38 → sovai-0.2.40}/sovai/extensions/feature_importance.py +0 -0
  32. {sovai-0.2.38 → sovai-0.2.40}/sovai/extensions/feature_neutralizer.py +0 -0
  33. {sovai-0.2.38 → sovai-0.2.40}/sovai/extensions/filter_df.py +0 -0
  34. {sovai-0.2.38 → sovai-0.2.40}/sovai/extensions/fractional_differencing.py +0 -0
  35. {sovai-0.2.38 → sovai-0.2.40}/sovai/extensions/nowcasting.py +0 -0
  36. {sovai-0.2.38 → sovai-0.2.40}/sovai/extensions/pairwise.py +0 -0
  37. {sovai-0.2.38 → sovai-0.2.40}/sovai/extensions/pandas_extensions.py +0 -0
  38. {sovai-0.2.38 → sovai-0.2.40}/sovai/extensions/parallel_functions.py +0 -0
  39. {sovai-0.2.38 → sovai-0.2.40}/sovai/extensions/pfa_feature_selector.py +0 -0
  40. {sovai-0.2.38 → sovai-0.2.40}/sovai/extensions/regime_change.py +0 -0
  41. {sovai-0.2.38 → sovai-0.2.40}/sovai/extensions/regime_change_pca.py +0 -0
  42. {sovai-0.2.38 → sovai-0.2.40}/sovai/extensions/shapley_global_importance.py +0 -0
  43. {sovai-0.2.38 → sovai-0.2.40}/sovai/extensions/shapley_importance.py +0 -0
  44. {sovai-0.2.38 → sovai-0.2.40}/sovai/extensions/signal_evaluation.py +0 -0
  45. {sovai-0.2.38 → sovai-0.2.40}/sovai/extensions/technical_indicators.py +0 -0
  46. {sovai-0.2.38 → sovai-0.2.40}/sovai/extensions/time_decomposition.py +0 -0
  47. {sovai-0.2.38 → sovai-0.2.40}/sovai/extensions/weight_optimization.py +0 -0
  48. {sovai-0.2.38 → sovai-0.2.40}/sovai/get_compute.py +0 -0
  49. {sovai-0.2.38 → sovai-0.2.40}/sovai/get_data copy.py +0 -0
  50. {sovai-0.2.38 → sovai-0.2.40}/sovai/get_plots.py +0 -0
  51. {sovai-0.2.38 → sovai-0.2.40}/sovai/get_reports.py +0 -0
  52. {sovai-0.2.38 → sovai-0.2.40}/sovai/get_tools.py +0 -0
  53. {sovai-0.2.38 → sovai-0.2.40}/sovai/plots/__init__.py +0 -0
  54. {sovai-0.2.38 → sovai-0.2.40}/sovai/plots/accounting/__init__.py +0 -0
  55. {sovai-0.2.38 → sovai-0.2.40}/sovai/plots/accounting/accounting_plots.py +0 -0
  56. {sovai-0.2.38 → sovai-0.2.40}/sovai/plots/allocation/__init__.py +0 -0
  57. {sovai-0.2.38 → sovai-0.2.40}/sovai/plots/allocation/allocation_plots.py +0 -0
  58. {sovai-0.2.38 → sovai-0.2.40}/sovai/plots/bankruptcy/__init__.py +0 -0
  59. {sovai-0.2.38 → sovai-0.2.40}/sovai/plots/bankruptcy/bankruptcy_plots.py +0 -0
  60. {sovai-0.2.38 → sovai-0.2.40}/sovai/plots/breakout/__init__.py +0 -0
  61. {sovai-0.2.38 → sovai-0.2.40}/sovai/plots/breakout/breakout_plots.py +0 -0
  62. {sovai-0.2.38 → sovai-0.2.40}/sovai/plots/corp_risk/__init__.py +0 -0
  63. {sovai-0.2.38 → sovai-0.2.40}/sovai/plots/corp_risk/corp_risk_plots.py +0 -0
  64. {sovai-0.2.38 → sovai-0.2.40}/sovai/plots/earnings_surprise/__init__.py +0 -0
  65. {sovai-0.2.38 → sovai-0.2.40}/sovai/plots/earnings_surprise/earnings_surprise_plots.py +0 -0
  66. {sovai-0.2.38 → sovai-0.2.40}/sovai/plots/insider/__init__.py +0 -0
  67. {sovai-0.2.38 → sovai-0.2.40}/sovai/plots/insider/insider_plots.py +0 -0
  68. {sovai-0.2.38 → sovai-0.2.40}/sovai/plots/institutional/__init__.py +0 -0
  69. {sovai-0.2.38 → sovai-0.2.40}/sovai/plots/institutional/institutional_plots.py +0 -0
  70. {sovai-0.2.38 → sovai-0.2.40}/sovai/plots/news/__init__.py +0 -0
  71. {sovai-0.2.38 → sovai-0.2.40}/sovai/plots/news/news_plots.py +0 -0
  72. {sovai-0.2.38 → sovai-0.2.40}/sovai/plots/ratios/__init__.py +0 -0
  73. {sovai-0.2.38 → sovai-0.2.40}/sovai/plots/ratios/ratios_plots.py +0 -0
  74. {sovai-0.2.38 → sovai-0.2.40}/sovai/reports/accounting/__init__.py +0 -0
  75. {sovai-0.2.38 → sovai-0.2.40}/sovai/reports/accounting/accounting_balance_sheet.py +0 -0
  76. {sovai-0.2.38 → sovai-0.2.40}/sovai/reports/bankruptcy/__init__.py +0 -0
  77. {sovai-0.2.38 → sovai-0.2.40}/sovai/reports/bankruptcy/bankruptcy_monthly_top.py +0 -0
  78. {sovai-0.2.38 → sovai-0.2.40}/sovai/reports/general/__init__.py +0 -0
  79. {sovai-0.2.38 → sovai-0.2.40}/sovai/reports/general/general_plots.py +0 -0
  80. {sovai-0.2.38 → sovai-0.2.40}/sovai/reports/news/__inti__.py +0 -0
  81. {sovai-0.2.38 → sovai-0.2.40}/sovai/reports/news/news_econometric_analysis.py +0 -0
  82. {sovai-0.2.38 → sovai-0.2.40}/sovai/studies/nowcasting.py +0 -0
  83. {sovai-0.2.38 → sovai-0.2.40}/sovai/token_auth.py +0 -0
  84. {sovai-0.2.38 → sovai-0.2.40}/sovai/tools/authentication/__init__.py +0 -0
  85. {sovai-0.2.38 → sovai-0.2.40}/sovai/tools/authentication/authentication.py +0 -0
  86. {sovai-0.2.38 → sovai-0.2.40}/sovai/tools/sec/__init__.py +0 -0
  87. {sovai-0.2.38 → sovai-0.2.40}/sovai/tools/sec/graphs.py +0 -0
  88. {sovai-0.2.38 → sovai-0.2.40}/sovai/tools/sec/llm_code_generator.py +0 -0
  89. {sovai-0.2.38 → sovai-0.2.40}/sovai/tools/sec/sec_10_k_8_k_filings.py +0 -0
  90. {sovai-0.2.38 → sovai-0.2.40}/sovai/tools/sec/sec_edgar_search.py +0 -0
  91. {sovai-0.2.38 → sovai-0.2.40}/sovai/utils/__init__.py +0 -0
  92. {sovai-0.2.38 → sovai-0.2.40}/sovai/utils/client_side.py +0 -0
  93. {sovai-0.2.38 → sovai-0.2.40}/sovai/utils/client_side_s3.py +0 -0
  94. {sovai-0.2.38 → sovai-0.2.40}/sovai/utils/converter.py +0 -0
  95. {sovai-0.2.38 → sovai-0.2.40}/sovai/utils/datetime_formats.py +0 -0
  96. {sovai-0.2.38 → sovai-0.2.40}/sovai/utils/file_management.py +0 -0
  97. {sovai-0.2.38 → sovai-0.2.40}/sovai/utils/get_tickers.py +0 -0
  98. {sovai-0.2.38 → sovai-0.2.40}/sovai/utils/helpers.py +0 -0
  99. {sovai-0.2.38 → sovai-0.2.40}/sovai/utils/pandas_extensions copy.py +0 -0
  100. {sovai-0.2.38 → sovai-0.2.40}/sovai/utils/plot.py +0 -0
  101. {sovai-0.2.38 → sovai-0.2.40}/sovai/utils/port_manager.py +0 -0
  102. {sovai-0.2.38 → sovai-0.2.40}/sovai/utils/stream.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: sovai
3
- Version: 0.2.38
3
+ Version: 0.2.40
4
4
  Summary: python for asset management
5
5
  License: Proprietary License
6
6
  Author: Derek Snow
@@ -1,6 +1,6 @@
1
1
  [tool.poetry]
2
2
  name = "sovai"
3
- version = "0.2.38"
3
+ version = "0.2.40"
4
4
  description = "python for asset management"
5
5
  authors = ["Derek Snow <d.snow@outlook.com>"]
6
6
  license = "Proprietary License"
@@ -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/contract",
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/contract": {
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