datapungibea 2.0.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.
@@ -0,0 +1,19 @@
1
+ """Connect to Bureau of Economic Analysis (BEA) API and enrich data output.
2
+
3
+ Modernized version 2.0 with YAML-driven architecture.
4
+ """
5
+
6
+ import logging
7
+ from typing import Any, Dict, Optional
8
+
9
+ import pandas
10
+ import requests
11
+
12
+ from datapungibea.api import data
13
+ import datapungibea.tests as tests
14
+
15
+ __version__ = '2.0.0'
16
+ __all__ = ['data', 'tests', 'utils']
17
+
18
+ # Set up logging
19
+ logging.getLogger(__name__).addHandler(logging.NullHandler())
datapungibea/api.py ADDED
@@ -0,0 +1,616 @@
1
+ r'''
2
+ Main user-facing API for datapungibea.
3
+ Uses factory pattern from datapungi_fed architecture.
4
+ '''
5
+
6
+ import logging
7
+ from typing import Any, Dict, Optional
8
+
9
+ import pandas as pd
10
+
11
+ from datapungibea import generalSettings
12
+ from datapungibea.core import driverCore
13
+
14
+ logger = logging.getLogger(__name__)
15
+
16
+
17
+ class data:
18
+ r'''
19
+ Main API class for accessing BEA data.
20
+
21
+ Provides access to all BEA datasets through a unified interface.
22
+ Uses factory pattern to create query functions for each dataset group.
23
+
24
+ Example:
25
+ import datapungibea as dpb
26
+
27
+ # Initialize with API key
28
+ data = dpb.data("YOUR_API_KEY")
29
+
30
+ # Or use environment variable
31
+ data = dpb.data()
32
+
33
+ # Query datasets
34
+ data.nipa('T10101')
35
+ data.metadata() # list all datasets
36
+ '''
37
+
38
+ def __init__(
39
+ self,
40
+ key: Optional[str] = None,
41
+ keyfile: Optional[str] = None,
42
+ connectionParameters: Optional[Dict[str, Any]] = None,
43
+ ):
44
+ '''
45
+ Initialize the BEA data API.
46
+
47
+ Three ways to provide an API key:
48
+ 1. Environment variable API_KEY_BEA (default):
49
+ export API_KEY_BEA=your_key_here
50
+ data = dpb.data()
51
+
52
+ 2. Pass key directly:
53
+ data = dpb.data("YOUR_API_KEY")
54
+
55
+ 3. Point to a keys file (YAML or JSON):
56
+ data = dpb.data(keyfile="/path/to/keys.yaml")
57
+
58
+ Args:
59
+ key: API key string passed directly
60
+ keyfile: path to a YAML or JSON file containing API keys
61
+ connectionParameters: explicit dict with 'key' and 'url' (advanced)
62
+ '''
63
+ # Load general settings
64
+ self.__connectInfo = generalSettings.getGeneralSettings(
65
+ key=key,
66
+ keyfile=keyfile,
67
+ connectionParameters=connectionParameters,
68
+ )
69
+
70
+ self._metadata = self.__connectInfo.packageMetadata
71
+ self._help = self.__connectInfo.datasourceOverview
72
+
73
+ # Load parameters for driver creation
74
+ loadInfo = {
75
+ 'baseRequest': self.__connectInfo.baseRequest,
76
+ 'connectionParameters': self.__connectInfo.connectionParameters
77
+ }
78
+
79
+ # Initialize dataset groups using factory pattern
80
+ self._metadata_driver = driverCore('Metadata', **loadInfo)
81
+ self._nipa_driver = driverCore('NIPA', **loadInfo)
82
+ self._national_accounts_driver = driverCore('National Accounts', **loadInfo)
83
+ self._international_transactions_driver = driverCore('International Transactions', **loadInfo)
84
+ self._international_investment_driver = driverCore('International Investment Position', **loadInfo)
85
+ self._industry_driver = driverCore('Industry', **loadInfo)
86
+ self._regional_driver = driverCore('Regional', **loadInfo)
87
+ self._input_output_driver = driverCore('Input-Output', **loadInfo)
88
+ self._international_services_driver = driverCore('International Services Trade', **loadInfo)
89
+ self._multinational_driver = driverCore('Multinational Enterprises', **loadInfo)
90
+
91
+ # Store last loaded driver for clipcode
92
+ self._lastCalledDriver = None
93
+
94
+ def __str__(self) -> str:
95
+ '''List all available dataset groups.'''
96
+ groups = [
97
+ 'Metadata',
98
+ 'NIPA',
99
+ 'National Accounts',
100
+ 'International Transactions',
101
+ 'International Investment Position',
102
+ 'Industry',
103
+ 'Regional',
104
+ 'Input-Output',
105
+ 'International Services Trade',
106
+ 'Multinational Enterprises',
107
+ 'NIPA Vintage (not API)',
108
+ 'NIPA Summary (not API)'
109
+ ]
110
+
111
+ output = "BEA Dataset Groups:\n"
112
+ output += "=" * 50 + "\n"
113
+ for group in groups:
114
+ output += f" - {group}\n"
115
+ output += "\n"
116
+ output += "Usage: data.<group_name>() or data.<group_name>['<dataset>']()\n"
117
+ output += "Example: data.nipa('T10101')\n"
118
+ output += "\nRun data._help for more information.\n"
119
+
120
+ return output
121
+
122
+ # =========================================================================
123
+ # Metadata queries
124
+ # =========================================================================
125
+
126
+ def datasetlist(self, verbose: bool = False, **kwargs: Any) -> Any:
127
+ '''Get list of available BEA datasets.'''
128
+ self._lastCalledDriver = self._metadata_driver
129
+ return self._metadata_driver['datasetlist'](verbose=verbose, **kwargs)
130
+
131
+ def getParameterList(self, datasetname: str, verbose: bool = False, **kwargs: Any) -> Any:
132
+ '''Get parameters for a specific dataset.'''
133
+ self._lastCalledDriver = self._metadata_driver
134
+ return self._metadata_driver['parameterlist'](datasetname=datasetname, verbose=verbose, **kwargs)
135
+
136
+ def getParameterValues(self, datasetName: str, parameterName: str, verbose: bool = False, **kwargs: Any) -> Any:
137
+ '''Get available values for a dataset parameter.'''
138
+ self._lastCalledDriver = self._metadata_driver
139
+ return self._metadata_driver['parametervalues'](
140
+ datasetname=datasetName,
141
+ parametername=parameterName,
142
+ verbose=verbose,
143
+ **kwargs
144
+ )
145
+
146
+ # =========================================================================
147
+ # NIPA queries
148
+ # =========================================================================
149
+
150
+ def NIPA(
151
+ self,
152
+ TableName: str,
153
+ Frequency: str = 'Q',
154
+ Year: str = 'X',
155
+ verbose: bool = False,
156
+ includeIndentations: bool = True,
157
+ **kwargs: Any
158
+ ) -> Any:
159
+ '''
160
+ Query NIPA (National Income and Product Accounts) data.
161
+
162
+ Args:
163
+ TableName: NIPA table name (e.g., 'T10101')
164
+ Frequency: 'Q' (quarterly), 'A' (annual), or 'M' (monthly)
165
+ Year: specific year or 'X' for all years
166
+ verbose: if True, return dict with dataFrame, request, and code
167
+ includeIndentations: if True, add indentation markers to LineDescription
168
+
169
+ Returns:
170
+ pandas DataFrame or dict (if verbose=True)
171
+
172
+ Example:
173
+ data.NIPA('T10101')
174
+ data.NIPA('T10101', Frequency='A', Year='X', verbose=True)
175
+ '''
176
+ self._lastCalledDriver = self._nipa_driver
177
+ return self._nipa_driver(
178
+ TableName=TableName,
179
+ Frequency=Frequency,
180
+ Year=Year,
181
+ verbose=verbose,
182
+ includeIndentations=includeIndentations,
183
+ **kwargs
184
+ )
185
+
186
+ def NIUnderlyingDetail(
187
+ self,
188
+ TableName: str,
189
+ Frequency: str = 'Q',
190
+ Year: str = 'X',
191
+ verbose: bool = False,
192
+ **kwargs: Any
193
+ ) -> Any:
194
+ '''
195
+ Query National Income underlying detail tables.
196
+
197
+ Args:
198
+ TableName: table name
199
+ Frequency: 'Q', 'A', or 'M'
200
+ Year: specific year or 'X' for all years
201
+ verbose: if True, return dict with dataFrame, request, and code
202
+
203
+ Returns:
204
+ pandas DataFrame or dict (if verbose=True)
205
+ '''
206
+ self._lastCalledDriver = self._nipa_driver
207
+ return self._nipa_driver['NIUnderlyingDetail'](
208
+ TableName=TableName,
209
+ Frequency=Frequency,
210
+ Year=Year,
211
+ verbose=verbose,
212
+ **kwargs
213
+ )
214
+
215
+ # =========================================================================
216
+ # National Accounts queries
217
+ # =========================================================================
218
+
219
+ def fixedAssets(self, TableName: str, Year: str, verbose: bool = False, **kwargs: Any) -> Any:
220
+ '''Query Fixed Assets data.'''
221
+ self._lastCalledDriver = self._national_accounts_driver
222
+ return self._national_accounts_driver(
223
+ TableName=TableName,
224
+ Year=Year,
225
+ verbose=verbose,
226
+ **kwargs
227
+ )
228
+
229
+ # =========================================================================
230
+ # International Transactions queries
231
+ # =========================================================================
232
+
233
+ def ITA(
234
+ self,
235
+ Indicator: str,
236
+ AreaOrCountry: str,
237
+ Frequency: str,
238
+ Year: str,
239
+ verbose: bool = False,
240
+ **kwargs: Any
241
+ ) -> Any:
242
+ '''Query International Transactions Accounts.'''
243
+ self._lastCalledDriver = self._international_transactions_driver
244
+ return self._international_transactions_driver(
245
+ Indicator=Indicator,
246
+ AreaOrCountry=AreaOrCountry,
247
+ Frequency=Frequency,
248
+ Year=Year,
249
+ verbose=verbose,
250
+ **kwargs
251
+ )
252
+
253
+ # =========================================================================
254
+ # International Investment Position queries
255
+ # =========================================================================
256
+
257
+ def IIP(
258
+ self,
259
+ TypeOfInvestment: str,
260
+ Component: str,
261
+ Frequency: str,
262
+ Year: str,
263
+ verbose: bool = False,
264
+ **kwargs: Any
265
+ ) -> Any:
266
+ '''Query International Investment Position.'''
267
+ self._lastCalledDriver = self._international_investment_driver
268
+ return self._international_investment_driver(
269
+ TypeOfInvestment=TypeOfInvestment,
270
+ Component=Component,
271
+ Frequency=Frequency,
272
+ Year=Year,
273
+ verbose=verbose,
274
+ **kwargs
275
+ )
276
+
277
+ # =========================================================================
278
+ # Industry queries
279
+ # =========================================================================
280
+
281
+ def GDPbyIndustry(
282
+ self,
283
+ Industry: str,
284
+ TableID: str,
285
+ Frequency: str,
286
+ Year: str,
287
+ verbose: bool = False,
288
+ **kwargs: Any
289
+ ) -> Any:
290
+ '''Query GDP by Industry.'''
291
+ self._lastCalledDriver = self._industry_driver
292
+ return self._industry_driver['GDPbyIndustry'](
293
+ Industry=Industry,
294
+ TableID=TableID,
295
+ Frequency=Frequency,
296
+ Year=Year,
297
+ verbose=verbose,
298
+ **kwargs
299
+ )
300
+
301
+ def UnderlyingGDPbyIndustry(
302
+ self,
303
+ Industry: str,
304
+ TableID: str,
305
+ Frequency: str,
306
+ Year: str,
307
+ verbose: bool = False,
308
+ **kwargs: Any
309
+ ) -> Any:
310
+ '''Query Underlying GDP by Industry.'''
311
+ self._lastCalledDriver = self._industry_driver
312
+ return self._industry_driver['UnderlyingGDPbyIndustry'](
313
+ Industry=Industry,
314
+ TableID=TableID,
315
+ Frequency=Frequency,
316
+ Year=Year,
317
+ verbose=verbose,
318
+ **kwargs
319
+ )
320
+
321
+ # =========================================================================
322
+ # Regional queries
323
+ # =========================================================================
324
+
325
+ def Regional(
326
+ self,
327
+ GeoFips: str,
328
+ LineCode: str,
329
+ TableName: str,
330
+ Year: str,
331
+ verbose: bool = False,
332
+ **kwargs: Any
333
+ ) -> Any:
334
+ '''Query Regional economic data.'''
335
+ self._lastCalledDriver = self._regional_driver
336
+ return self._regional_driver(
337
+ GeoFips=GeoFips,
338
+ LineCode=LineCode,
339
+ TableName=TableName,
340
+ Year=Year,
341
+ verbose=verbose,
342
+ **kwargs
343
+ )
344
+
345
+ # =========================================================================
346
+ # Input-Output queries
347
+ # =========================================================================
348
+
349
+ def InputOutput(self, TableID: str, Year: str, verbose: bool = False, **kwargs: Any) -> Any:
350
+ '''Query Input-Output tables.'''
351
+ self._lastCalledDriver = self._input_output_driver
352
+ return self._input_output_driver(
353
+ TableID=TableID,
354
+ Year=Year,
355
+ verbose=verbose,
356
+ **kwargs
357
+ )
358
+
359
+ # =========================================================================
360
+ # International Services Trade queries
361
+ # =========================================================================
362
+
363
+ def IntlServTrade(
364
+ self,
365
+ TypeOfService: str,
366
+ TradeDirection: str,
367
+ Affiliation: str,
368
+ AreaOrCountry: str,
369
+ Year: str,
370
+ verbose: bool = False,
371
+ **kwargs: Any
372
+ ) -> Any:
373
+ '''Query International Services Trade.'''
374
+ self._lastCalledDriver = self._international_services_driver
375
+ return self._international_services_driver(
376
+ TypeOfService=TypeOfService,
377
+ TradeDirection=TradeDirection,
378
+ Affiliation=Affiliation,
379
+ AreaOrCountry=AreaOrCountry,
380
+ Year=Year,
381
+ verbose=verbose,
382
+ **kwargs
383
+ )
384
+
385
+ def IntlServSTA(
386
+ self,
387
+ Channel: str = 'ALL',
388
+ Destination: str = 'ALL',
389
+ Industry: str = 'ALL',
390
+ AreaOrCountry: str = 'AllCountries',
391
+ Year: str = 'ALL',
392
+ verbose: bool = False,
393
+ **kwargs: Any
394
+ ) -> Any:
395
+ '''
396
+ Query International Services Supplied Through Affiliates.
397
+
398
+ Args:
399
+ Channel: channel requested (default 'ALL')
400
+ Destination: destination requested (default 'ALL')
401
+ Industry: industry requested (default 'ALL')
402
+ AreaOrCountry: area or country requested (default 'AllCountries')
403
+ Year: year requested (default 'ALL')
404
+ verbose: if True, return dict with dataFrame, request, and code
405
+
406
+ Returns:
407
+ pandas DataFrame or dict (if verbose=True)
408
+ '''
409
+ self._lastCalledDriver = self._international_services_driver
410
+ return self._international_services_driver['IntlServSTA'](
411
+ Channel=Channel,
412
+ Destination=Destination,
413
+ Industry=Industry,
414
+ AreaOrCountry=AreaOrCountry,
415
+ Year=Year,
416
+ verbose=verbose,
417
+ **kwargs
418
+ )
419
+
420
+ # =========================================================================
421
+ # Multinational Enterprises queries
422
+ # =========================================================================
423
+
424
+ def MNE(
425
+ self,
426
+ Frequency: str,
427
+ TableID: str,
428
+ DirectionOfInvestment: str,
429
+ OwnershipLevel: str,
430
+ NonbankAffiliatesOnly: str,
431
+ Classification: str,
432
+ Country: str,
433
+ Industry: str,
434
+ Year: str,
435
+ State: str,
436
+ SeriesID: str,
437
+ GetFootnotes: str,
438
+ Investment: str,
439
+ ParentInvestment: str,
440
+ verbose: bool = False,
441
+ **kwargs: Any
442
+ ) -> Any:
443
+ '''Query Activities of U.S. Multinational Enterprises.'''
444
+ self._lastCalledDriver = self._multinational_driver
445
+ return self._multinational_driver(
446
+ Frequency=Frequency,
447
+ TableID=TableID,
448
+ DirectionOfInvestment=DirectionOfInvestment,
449
+ OwnershipLevel=OwnershipLevel,
450
+ NonbankAffiliatesOnly=NonbankAffiliatesOnly,
451
+ Classification=Classification,
452
+ Country=Country,
453
+ Industry=Industry,
454
+ Year=Year,
455
+ State=State,
456
+ SeriesID=SeriesID,
457
+ GetFootnotes=GetFootnotes,
458
+ Investment=Investment,
459
+ ParentInvestment=ParentInvestment,
460
+ verbose=verbose,
461
+ **kwargs
462
+ )
463
+
464
+ # =========================================================================
465
+
466
+ # =========================================================================
467
+ # NIPA Summary (not API - custom computation)
468
+ # =========================================================================
469
+
470
+ def NIPASummary(self, year: str, frequency: str, verbose: bool = False, **kwargs: Any) -> Any:
471
+ '''
472
+ Get NIPA Summary tables - overall view of NIPA accounts.
473
+
474
+ Queries 6 NIPA accounts (Domestic Income, Private Enterprise, Personal Income,
475
+ Government, Foreign Transactions, Domestic Capital) and returns source/uses tables.
476
+
477
+ Args:
478
+ year: year for summary (e.g., '2010')
479
+ frequency: 'Q' or 'A'
480
+ verbose: if True, return dict with dataFrame and request metadata
481
+
482
+ Returns:
483
+ List of DataFrames (one per account source/use) or dict (if verbose)
484
+
485
+ Example:
486
+ data.NIPASummary('2010', 'A')
487
+ '''
488
+ from datapungibea.config import CFGnipaSummary
489
+ import pandas as pd
490
+
491
+ logger.info(f"Generating NIPA Summary for year={year}, frequency={frequency}")
492
+
493
+ output = {'dataFrame': [], 'request': []}
494
+
495
+ for account_name, account_config in CFGnipaSummary.tabparams.items():
496
+ logger.debug(f"Processing {account_name}")
497
+
498
+ # Get source data
499
+ source_table = account_config['source']
500
+ source_df = self._getAccountUseOrSource(
501
+ tableName=source_table['tableName'],
502
+ year=year,
503
+ frequency=frequency if account_name != 'Account 2' else 'A', # Account 2 is annual only
504
+ tableEntries=source_table['tableEntries']
505
+ )
506
+ source_df.insert(0, 'Account', f"{account_name} sources")
507
+ source_df.insert(1, 'Heading', account_config['meta']['heading'])
508
+ source_df.attrs['meta'] = {'caption': account_config['meta']['caption'], 'type': 'sources'}
509
+
510
+ # Get uses data
511
+ uses_table = account_config['uses']
512
+ uses_df = self._getAccountUseOrSource(
513
+ tableName=uses_table['tableName'],
514
+ year=year,
515
+ frequency=frequency if account_name != 'Account 2' else 'A',
516
+ tableEntries=uses_table['tableEntries']
517
+ )
518
+ uses_df.insert(0, 'Account', f"{account_name} uses")
519
+ uses_df.insert(1, 'Heading', account_config['meta']['heading'])
520
+ uses_df.attrs['meta'] = {'caption': account_config['meta']['caption'], 'type': 'uses'}
521
+
522
+ output['dataFrame'].append(source_df)
523
+ output['dataFrame'].append(uses_df)
524
+
525
+ if verbose:
526
+ return output
527
+ else:
528
+ return output['dataFrame']
529
+
530
+ def _getAccountUseOrSource(
531
+ self,
532
+ tableName: str,
533
+ year: str,
534
+ frequency: str,
535
+ tableEntries: list
536
+ ) -> pd.DataFrame:
537
+ '''Helper method to get and filter NIPA table data for summary.'''
538
+ import pandas as pd
539
+
540
+ # Query the NIPA table without indentation (get clean LineDescription)
541
+ readTable = self.NIPA(TableName=tableName, Frequency=frequency, Year=year, includeIndentations=False)
542
+ readTable = readTable.reset_index()
543
+
544
+ # Create restriction DataFrame
545
+ restrict = pd.DataFrame(tableEntries)
546
+
547
+ # Merge to get only the rows we need
548
+ output = pd.merge(restrict, readTable, on='SeriesCode', how='left')
549
+
550
+ # Sometimes there are two lines with the same SeriesCode on readTable
551
+ # (Account 5 - Foreeign Transactions Current Account
552
+ # Balance on Current account: A124RC)
553
+ # this leads to sequential duplication of rows - removing them here
554
+ output = output.loc[output['SeriesCode'].shift() != output['SeriesCode']]
555
+
556
+ # Apply indentation from config by adding dashes to LineDescription
557
+ output['LineDescription'] = output.apply(
558
+ lambda x: '-' * int(x['indentation']) + str(x.get('LineDescription', '')),
559
+ axis=1
560
+ )
561
+
562
+ # Clean up columns
563
+ output = output.drop(['LineNumber'], axis=1, errors='ignore')
564
+
565
+ # Set index for sorting
566
+ output = output.set_index(['LineDescription', 'SeriesCode'])
567
+ output = output.reset_index()
568
+
569
+ return output
570
+
571
+ # =========================================================================
572
+ # Utility methods
573
+ # =========================================================================
574
+
575
+ def _clipcode(self) -> None:
576
+ '''Copy the last query's code to clipboard.'''
577
+ if self._lastCalledDriver and hasattr(self._lastCalledDriver, '_ETDB'):
578
+ from datapungibea.codegen import transformIncludeCodeSnippet
579
+ codegen = transformIncludeCodeSnippet()
580
+ codegen.clipcode(self._lastCalledDriver._lastLoad)
581
+ else:
582
+ logger.warning('Get data using a driver first, e.g., data.NIPA("T10101", verbose=True)')
583
+
584
+ def _docDriver(self, driverName: str) -> str:
585
+ '''Get documentation for a specific driver.'''
586
+ driver_map = {
587
+ 'NIPA': self._nipa_driver,
588
+ 'fixedAssets': self._national_accounts_driver,
589
+ 'ITA': self._international_transactions_driver,
590
+ 'IIP': self._international_investment_driver,
591
+ 'GDPbyIndustry': self._industry_driver,
592
+ 'Regional': self._regional_driver,
593
+ 'InputOutput': self._input_output_driver,
594
+ 'IntlServTrade': self._international_services_driver,
595
+ 'MNE': self._multinational_driver,
596
+ }
597
+
598
+ if driverName in driver_map:
599
+ return str(driver_map[driverName])
600
+ else:
601
+ return f"Driver '{driverName}' not found. Available: {list(driver_map.keys())}"
602
+
603
+
604
+ if __name__ == '__main__':
605
+ # Test basic functionality
606
+ import sys
607
+ logging.basicConfig(level=logging.INFO)
608
+
609
+ try:
610
+ d = data()
611
+ print(d)
612
+ print("\nTrying datasetlist...")
613
+ print(d.datasetlist())
614
+ except Exception as e:
615
+ print(f"Error: {e}")
616
+ sys.exit(1)