cryptodatapy 0.2.5__py3-none-any.whl → 0.2.7__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.
Files changed (42) hide show
  1. cryptodatapy/conf/fields.csv +1 -1
  2. cryptodatapy/conf/tickers.csv +0 -1
  3. cryptodatapy/extract/data_vendors/CoinMetrics.ipynb +747 -0
  4. cryptodatapy/extract/data_vendors/coinmetrics_api.py +279 -209
  5. cryptodatapy/extract/data_vendors/cryptocompare_api.py +3 -5
  6. cryptodatapy/extract/data_vendors/datavendor.py +32 -12
  7. cryptodatapy/extract/data_vendors/glassnode_api.py +3 -2
  8. cryptodatapy/extract/data_vendors/tiingo_api.py +3 -2
  9. cryptodatapy/extract/datarequest.py +197 -36
  10. cryptodatapy/extract/libraries/Untitled.ipynb +33 -0
  11. cryptodatapy/extract/libraries/ccxt.ipynb +628 -754
  12. cryptodatapy/extract/libraries/ccxt_api.py +630 -346
  13. cryptodatapy/extract/libraries/pandasdr_api.py +13 -12
  14. cryptodatapy/extract/libraries/yfinance_api.py +511 -0
  15. cryptodatapy/transform/cc_onchain_data.csv +118423 -0
  16. cryptodatapy/transform/clean.py +17 -15
  17. cryptodatapy/transform/clean_onchain_data.ipynb +4750 -0
  18. cryptodatapy/transform/clean_perp_futures_ohlcv.ipynb +1712 -1097
  19. cryptodatapy/transform/cmdty_data.ipynb +402 -0
  20. cryptodatapy/transform/convertparams.py +139 -181
  21. cryptodatapy/transform/credit_data.ipynb +291 -0
  22. cryptodatapy/transform/eqty_data.ipynb +836 -0
  23. cryptodatapy/transform/filter.py +13 -10
  24. cryptodatapy/transform/global_credit_data_daily.parquet +0 -0
  25. cryptodatapy/transform/od.py +1 -0
  26. cryptodatapy/transform/rates_data.ipynb +465 -0
  27. cryptodatapy/transform/us_rates_daily.csv +227752 -0
  28. cryptodatapy/transform/wrangle.py +109 -20
  29. cryptodatapy/util/datacredentials.py +28 -7
  30. {cryptodatapy-0.2.5.dist-info → cryptodatapy-0.2.7.dist-info}/METADATA +10 -7
  31. {cryptodatapy-0.2.5.dist-info → cryptodatapy-0.2.7.dist-info}/RECORD +33 -31
  32. {cryptodatapy-0.2.5.dist-info → cryptodatapy-0.2.7.dist-info}/WHEEL +1 -1
  33. cryptodatapy/.DS_Store +0 -0
  34. cryptodatapy/.idea/.gitignore +0 -3
  35. cryptodatapy/.idea/cryptodatapy.iml +0 -12
  36. cryptodatapy/.idea/csv-plugin.xml +0 -16
  37. cryptodatapy/.idea/inspectionProfiles/Project_Default.xml +0 -6
  38. cryptodatapy/.idea/inspectionProfiles/profiles_settings.xml +0 -6
  39. cryptodatapy/.idea/misc.xml +0 -4
  40. cryptodatapy/.idea/modules.xml +0 -8
  41. cryptodatapy/.idea/vcs.xml +0 -6
  42. {cryptodatapy-0.2.5.dist-info → cryptodatapy-0.2.7.dist-info}/LICENSE +0 -0
@@ -84,8 +84,9 @@ class CryptoCompare(DataVendor):
84
84
  if categories is None:
85
85
  self.categories = ['crypto']
86
86
  if api_key is None:
87
- raise TypeError("Set your api key. We recommend setting your api key in environment variables as"
88
- "'CRYPTOCOMPARE_API_KEY', will allow DataCredentials to automatically load it.")
87
+ raise TypeError("Set your CryptoCompare api key in environment variables as 'CRYPTOCOMPARE_API_KEY' or "
88
+ "add it as an argument when instantiating the class. To get an api key, visit: "
89
+ "https://min-api.cryptocompare.com/")
89
90
  if exchanges is None:
90
91
  self.exchanges = self.get_exchanges_info(as_list=True)
91
92
  if indexes is None:
@@ -467,9 +468,6 @@ class CryptoCompare(DataVendor):
467
468
  urls_params = self.set_urls_params(data_req, data_type, ticker)
468
469
  url, params = urls_params['url'], urls_params['params']
469
470
 
470
- # # data req
471
- # data_resp = DataRequest().get_req(url=url, params=params)
472
-
473
471
  # data req
474
472
  data_resp = DataRequest().get_req(url=url, params=params)
475
473
 
@@ -92,7 +92,7 @@ class DataVendor(ABC):
92
92
 
93
93
  @exchanges.setter
94
94
  def exchanges(
95
- self, exchanges: Optional[Union[str, List[str], Dict[str, List[str]]]]
95
+ self, exchanges: Optional[Union[str, List[str], Dict[str, List[str]], pd.DataFrame]]
96
96
  ):
97
97
  """
98
98
  Sets a list of available exchanges for the data vendor.
@@ -101,6 +101,7 @@ class DataVendor(ABC):
101
101
  exchanges is None
102
102
  or isinstance(exchanges, list)
103
103
  or isinstance(exchanges, dict)
104
+ or isinstance(exchanges, pd.DataFrame)
104
105
  ):
105
106
  self._exchanges = exchanges
106
107
  elif isinstance(exchanges, str):
@@ -126,11 +127,16 @@ class DataVendor(ABC):
126
127
  return self._indexes
127
128
 
128
129
  @indexes.setter
129
- def indexes(self, indexes: Optional[Union[str, List[str], Dict[str, List[str]]]]):
130
+ def indexes(self, indexes: Optional[Union[str, List[str], Dict[str, List[str]], pd.DataFrame]]):
130
131
  """
131
132
  Sets a list of available indexes for the data vendor.
132
133
  """
133
- if indexes is None or isinstance(indexes, list) or isinstance(indexes, dict):
134
+ if (
135
+ indexes is None
136
+ or isinstance(indexes, list)
137
+ or isinstance(indexes, dict)
138
+ or isinstance(indexes, pd.DataFrame)
139
+ ):
134
140
  self._indexes = indexes
135
141
  elif isinstance(indexes, str):
136
142
  self._indexes = [indexes]
@@ -155,11 +161,16 @@ class DataVendor(ABC):
155
161
  return self._assets
156
162
 
157
163
  @assets.setter
158
- def assets(self, assets: Optional[Union[str, List[str], Dict[str, List[str]]]]):
164
+ def assets(self, assets: Optional[Union[str, List[str], Dict[str, List[str]], pd.DataFrame]]):
159
165
  """
160
166
  Sets a list of available assets for the data vendor.
161
167
  """
162
- if assets is None or isinstance(assets, list) or isinstance(assets, dict):
168
+ if (
169
+ assets is None
170
+ or isinstance(assets, list)
171
+ or isinstance(assets, dict)
172
+ or isinstance(assets, pd.DataFrame)
173
+ ):
163
174
  self._assets = assets
164
175
  elif isinstance(assets, str):
165
176
  self._assets = [assets]
@@ -184,11 +195,16 @@ class DataVendor(ABC):
184
195
  return self._markets
185
196
 
186
197
  @markets.setter
187
- def markets(self, markets: Optional[Union[str, List[str], Dict[str, List[str]]]]):
198
+ def markets(self, markets: Optional[Union[str, List[str], Dict[str, List[str]], pd.DataFrame]]):
188
199
  """
189
200
  Sets a list of available markets for the data vendor.
190
201
  """
191
- if markets is None or isinstance(markets, list) or isinstance(markets, dict):
202
+ if (
203
+ markets is None
204
+ or isinstance(markets, list)
205
+ or isinstance(markets, dict)
206
+ or isinstance(markets, pd.DataFrame)
207
+ ):
192
208
  self._markets = markets
193
209
  elif isinstance(markets, str):
194
210
  self._markets = [markets]
@@ -251,11 +267,16 @@ class DataVendor(ABC):
251
267
  return self._fields
252
268
 
253
269
  @fields.setter
254
- def fields(self, fields: Optional[Union[str, List[str], Dict[str, List[str]]]]):
270
+ def fields(self, fields: Optional[Union[str, List[str], Dict[str, List[str]], pd.DataFrame]]):
255
271
  """
256
272
  Sets a list of available fields for the data vendor.
257
273
  """
258
- if fields is None or isinstance(fields, list) or isinstance(fields, dict):
274
+ if (
275
+ fields is None
276
+ or isinstance(fields, list)
277
+ or isinstance(fields, dict)
278
+ or isinstance(fields, pd.DataFrame)
279
+ ):
259
280
  self._fields = fields
260
281
  elif isinstance(fields, str):
261
282
  self._fields = [fields]
@@ -280,9 +301,7 @@ class DataVendor(ABC):
280
301
  return self._frequencies
281
302
 
282
303
  @frequencies.setter
283
- def frequencies(
284
- self, frequencies: Optional[Union[str, List[str], Dict[str, List[str]]]]
285
- ):
304
+ def frequencies(self, frequencies: Optional[Union[str, List[str], Dict[str, List[str]], pd.DataFrame]]):
286
305
  """
287
306
  Sets a list of available data frequencies for the data vendor.
288
307
  """
@@ -290,6 +309,7 @@ class DataVendor(ABC):
290
309
  frequencies is None
291
310
  or isinstance(frequencies, list)
292
311
  or isinstance(frequencies, dict)
312
+ or isinstance(frequencies, pd.DataFrame)
293
313
  ):
294
314
  self._frequencies = frequencies
295
315
  elif isinstance(frequencies, str):
@@ -79,8 +79,9 @@ class Glassnode(DataVendor):
79
79
  if categories is None:
80
80
  self.categories = ['crypto']
81
81
  if api_key is None:
82
- raise TypeError("Set your api key. We recommend setting your api key in environment variables as"
83
- "'GLASSNODE_API_KEY', will allow DataCredentials to automatically load it.")
82
+ raise TypeError("Set your Glassnode api key in environment variables as 'GLASSNODE_API_KEY' or "
83
+ "add it as an argument when instantiating the class. To get an api key, visit: "
84
+ "https://docs.glassnode.com/basic-api/api-key")
84
85
  if assets is None:
85
86
  self.assets = self.get_assets_info(as_list=True)
86
87
  if fields is None:
@@ -109,8 +109,9 @@ class Tiingo(DataVendor):
109
109
  if categories is None:
110
110
  self.categories = ["crypto", "fx", "eqty"]
111
111
  if api_key is None:
112
- raise TypeError("Set your api key. We recommend setting your api key in environment variables as"
113
- "'TIINGO_API_KEY', will allow DataCredentials to automatically load it.")
112
+ raise TypeError("Set your Tiingo api key in environment variables as 'TIINGO_API_KEY' or "
113
+ "add it as an argument when instantiating the class. To get an api key, visit: "
114
+ "https://www.tiingo.com/")
114
115
  if exchanges is None:
115
116
  self.exchanges = self.get_exchanges_info()
116
117
  if assets is None:
@@ -17,8 +17,9 @@ class DataRequest:
17
17
  self,
18
18
  source: str = "ccxt",
19
19
  tickers: Union[str, List[str]] = "btc",
20
- freq: str = "d",
21
20
  quote_ccy: Optional[str] = None,
21
+ markets: Optional[Union[str, List[str]]] = None,
22
+ freq: str = "d",
22
23
  exch: Optional[str] = None,
23
24
  mkt_type: Optional[str] = "spot",
24
25
  start_date: Optional[Union[str, datetime, pd.Timestamp]] = None,
@@ -30,8 +31,11 @@ class DataRequest:
30
31
  trials: Optional[int] = 3,
31
32
  pause: Optional[float] = 0.1,
32
33
  source_tickers: Optional[Union[str, List[str]]] = None,
34
+ source_markets: Optional[Union[str, List[str]]] = None,
33
35
  source_freq: Optional[str] = None,
34
- source_fields: Optional[Union[str, List[str]]] = None,
36
+ source_start_date: Optional[Union[str, int, datetime, pd.Timestamp]] = None,
37
+ source_end_date: Optional[Union[str, int, datetime, pd.Timestamp]] = None,
38
+ source_fields: Optional[Union[str, List[str]]] = None
35
39
  ):
36
40
  """
37
41
  Constructor
@@ -41,12 +45,14 @@ class DataRequest:
41
45
  source: str, default 'ccxt'
42
46
  Name of data source.
43
47
  tickers: list or str, default 'btc'
44
- Ticker symbols for assets or time series.
45
- e.g. 'BTC', 'EURUSD', 'SPY', 'US_Manuf_PMI', 'EZ_Rates_10Y', etc.
48
+ Ticker symbols for base assets.
49
+ e.g. 'BTC', 'EUR', 'SPY', 'US_Manuf_PMI', 'EZ_Rates_10Y', etc.
50
+ quote_ccy: str, optional, default None
51
+ Ticker symbol for quote asset, e.g. 'USDT' for BTCUSDT (bitcoin in Tether USD), 'GBP' for EURGBP, etc.
52
+ markets: list or str, optional, default None
53
+ Markets/traded pairs of base assets vs quote assets, e.g. 'BTC/USDT', 'EUR/USD', 'SPY/USD', etc.
46
54
  freq: str, default 'd'
47
55
  Frequency of data observations. Defaults to daily 'd' which includes weekends for cryptoassets.
48
- quote_ccy: str, optional, default None
49
- Quote currency for base asset, e.g. 'GBP' for EURGBP, 'USD' for BTCUSD (bitcoin in dollars), etc.
50
56
  exch: str, optional, default None
51
57
  Name of asset exchange, e.g. 'Binance', 'FTX', 'IEX', 'Nasdaq', etc.
52
58
  mkt_type: str, optional, default 'spot'
@@ -72,18 +78,26 @@ class DataRequest:
72
78
  source_tickers: list or str, optional, default None
73
79
  List or string of ticker symbols for assets or time series in the format used by the
74
80
  data source. If None, tickers will be converted from CryptoDataPy to data source format.
81
+ source_markets: list or str, optional, default None
82
+ List or string of markets/traded pairs of base assets vs quote assets in the format used by the
83
+ data source. If None, markets will be converted from CryptoDataPy to data source format.
75
84
  source_freq: str, optional, default None
76
85
  Frequency of observations for assets or time series in format used by data source. If None,
77
86
  frequency will be converted from CryptoDataPy to data source format.
87
+ source_start_date: str, int, datetime or pd.Timestamp, optional, default None
88
+ Start date for data request in format used by data source.
89
+ source_end_date: str, int, datetime or pd.Timestamp, optional, default None
90
+ End date for data request in format used by data source.
78
91
  source_fields: list or str, optional, default None
79
92
  List or string of fields for assets or time series in format used by data source. If None,
80
93
  fields will be converted from CryptoDataPy to data source format.
81
94
  """
82
95
  # params
83
- self.source = source # specific data source
96
+ self.source = source # name of data source
84
97
  self.tickers = tickers # tickers
85
- self.freq = freq # frequency
86
98
  self.quote_ccy = quote_ccy # quote ccy
99
+ self.markets = markets # markets
100
+ self.freq = freq # frequency
87
101
  self.exch = exch # exchange
88
102
  self.mkt_type = mkt_type # market type
89
103
  self.start_date = start_date # start date
@@ -95,7 +109,10 @@ class DataRequest:
95
109
  self.trials = trials # number of times to try query request
96
110
  self.pause = pause # number of seconds to pause between query request trials
97
111
  self.source_tickers = source_tickers # tickers used by data source
112
+ self.source_markets = source_markets
98
113
  self.source_freq = source_freq # frequency used by data source
114
+ self.source_start_date = source_start_date # start date used by data source
115
+ self.source_end_date = source_end_date # end date used by data source
99
116
  self.source_fields = source_fields # fields used by data source
100
117
 
101
118
  @property
@@ -151,6 +168,46 @@ class DataRequest:
151
168
  else:
152
169
  raise TypeError("Tickers must be a string or list of strings (tickers).")
153
170
 
171
+ @property
172
+ def quote_ccy(self):
173
+ """
174
+ Returns quote currency for data request.
175
+ """
176
+ return self._quote_ccy
177
+
178
+ @quote_ccy.setter
179
+ def quote_ccy(self, quote):
180
+ """
181
+ Sets quote currency for data request.
182
+ """
183
+ if quote is None:
184
+ self._quote_ccy = quote
185
+ elif isinstance(quote, str):
186
+ self._quote_ccy = quote
187
+ else:
188
+ raise TypeError("Quote currency must be a string.")
189
+
190
+ @property
191
+ def markets(self):
192
+ """
193
+ Returns markets for data request.
194
+ """
195
+ return self._markets
196
+
197
+ @markets.setter
198
+ def markets(self, markets):
199
+ """
200
+ Sets markets for data request.
201
+ """
202
+ if markets is None:
203
+ self._markets = markets
204
+ elif isinstance(markets, str):
205
+ self._markets = [markets]
206
+ elif isinstance(markets, list):
207
+ self._markets = markets
208
+ else:
209
+ raise TypeError("Markets must be a string or list of strings (markets).")
210
+
154
211
  @property
155
212
  def freq(self):
156
213
  """
@@ -197,32 +254,15 @@ class DataRequest:
197
254
  "y": "yearly",
198
255
  }
199
256
 
200
- if frequency not in list(freq_dict.keys()):
257
+ if frequency is None:
258
+ self._frequency = frequency
259
+ elif frequency not in list(freq_dict.keys()):
201
260
  raise ValueError(
202
261
  f"{frequency} is an invalid data frequency. Valid frequencies are: {freq_dict}"
203
262
  )
204
263
  else:
205
264
  self._frequency = frequency
206
265
 
207
- @property
208
- def quote_ccy(self):
209
- """
210
- Returns quote currency for data request.
211
- """
212
- return self._quote_ccy
213
-
214
- @quote_ccy.setter
215
- def quote_ccy(self, quote):
216
- """
217
- Sets quote currency for data request.
218
- """
219
- if quote is None:
220
- self._quote_ccy = quote
221
- elif isinstance(quote, str):
222
- self._quote_ccy = quote
223
- else:
224
- raise TypeError("Quote currency must be a string.")
225
-
226
266
  @property
227
267
  def exch(self):
228
268
  """
@@ -484,6 +524,29 @@ class DataRequest:
484
524
  "Source tickers must be a string or list of strings (tickers) in data source's format."
485
525
  )
486
526
 
527
+ @property
528
+ def source_markets(self):
529
+ """
530
+ Returns markets for data request in data source format.
531
+ """
532
+ return self._source_markets
533
+
534
+ @source_markets.setter
535
+ def source_markets(self, markets):
536
+ """
537
+ Sets markets for data request in data source format.
538
+ """
539
+ if markets is None:
540
+ self._source_markets = markets
541
+ elif isinstance(markets, str):
542
+ self._source_markets = [markets]
543
+ elif isinstance(markets, list):
544
+ self._source_markets = markets
545
+ else:
546
+ raise TypeError(
547
+ "Source markets must be a string or list of strings (markets) in data source's format."
548
+ )
549
+
487
550
  @property
488
551
  def source_freq(self):
489
552
  """
@@ -505,6 +568,60 @@ class DataRequest:
505
568
  "Source data frequency must be a string in data source's format."
506
569
  )
507
570
 
571
+ @property
572
+ def source_start_date(self):
573
+ """
574
+ Returns start date for data request in data source format.
575
+ """
576
+ return self._source_start_date
577
+
578
+ @source_start_date.setter
579
+ def source_start_date(self, start_date):
580
+ """
581
+ Sets start date for data request in data source format.
582
+ """
583
+ if start_date is None:
584
+ self._source_start_date = start_date
585
+ elif isinstance(start_date, str):
586
+ self._source_start_date = start_date
587
+ elif isinstance(start_date, int):
588
+ self._source_start_date = start_date
589
+ elif isinstance(start_date, datetime):
590
+ self._source_start_date = start_date
591
+ elif isinstance(start_date, pd.Timestamp):
592
+ self._source_start_date = start_date
593
+ else:
594
+ raise ValueError(
595
+ 'Start date must be in "YYYY-MM-DD" string, integer, datetime or pd.Timestamp format.'
596
+ )
597
+
598
+ @property
599
+ def source_end_date(self):
600
+ """
601
+ Returns end date for data request in data source format.
602
+ """
603
+ return self._source_end_date
604
+
605
+ @source_end_date.setter
606
+ def source_end_date(self, end_date):
607
+ """
608
+ Sets end date for data request in data source format.
609
+ """
610
+ if end_date is None:
611
+ self._source_end_date = end_date
612
+ elif isinstance(end_date, str):
613
+ self._source_end_date = end_date
614
+ elif isinstance(end_date, int):
615
+ self._source_end_date = end_date
616
+ elif isinstance(end_date, datetime):
617
+ self._source_end_date = end_date
618
+ elif isinstance(end_date, pd.Timestamp):
619
+ self._source_end_date = end_date
620
+ else:
621
+ raise ValueError(
622
+ 'End date must be in "YYYY-MM-DD" string, integer, datetime or pd.Timestamp format.'
623
+ )
624
+
508
625
  @property
509
626
  def source_fields(self):
510
627
  """
@@ -555,15 +672,59 @@ class DataRequest:
555
672
  # get request
556
673
  try:
557
674
  resp = requests.get(url, params=params, headers=headers)
558
- assert resp.status_code == 200
559
- # exception
560
- except AssertionError as e:
561
- logging.warning(e)
675
+ # check for status code
676
+ resp.raise_for_status()
677
+
678
+ return resp.json()
679
+
680
+ # handle HTTP errors
681
+ except requests.exceptions.HTTPError as http_err:
682
+ status_code = resp.status_code
683
+
684
+ # Tailored handling for different status codes
685
+ if status_code == 400:
686
+ logging.warning(f"Bad Request (400): {resp.text}")
687
+ elif status_code == 401:
688
+ logging.warning("Unauthorized (401): Check the authentication credentials.")
689
+ elif status_code == 403:
690
+ logging.warning("Forbidden (403): You do not have permission to access this resource.")
691
+ elif status_code == 404:
692
+ logging.warning("Not Found (404): The requested resource could not be found.")
693
+ elif status_code == 500:
694
+ logging.error("Internal Server Error (500): The server encountered an error.")
695
+ elif status_code == 503:
696
+ logging.error("Service Unavailable (503): The server is temporarily unavailable.")
697
+ else:
698
+ logging.error(f"HTTP error occurred: {http_err} (Status Code: {status_code})")
699
+ logging.error(f"Response Content: {resp.text}")
700
+
701
+ # Increment attempts and log warning
702
+ attempts += 1
703
+ logging.warning(f"Attempt #{attempts}: Failed to get data due to: {http_err}")
704
+ sleep(self.pause) # Pause before retrying
705
+ if attempts == self.trials:
706
+ logging.error("Max attempts reached. Unable to fetch data.")
707
+ break
708
+
709
+ # handle non-HTTP exceptions (e.g., network issues)
710
+ except requests.exceptions.RequestException as req_err:
562
711
  attempts += 1
563
- logging.warning(f"Failed to get data on attempt #{attempts}.")
712
+ logging.warning(f"Request error on attempt #{attempts}: {req_err}. "
713
+ f"Retrying after {self.pause} seconds...")
564
714
  sleep(self.pause)
565
- if attempts == 3:
715
+ if attempts == self.trials:
716
+ logging.error("Max attempts reached. Unable to fetch data due to request errors.")
566
717
  break
567
718
 
568
- else:
569
- return resp.json()
719
+ # handle other exceptions
720
+ except Exception as e:
721
+ attempts += 1
722
+ logging.warning(f"An unexpected error occurred: {e}. "
723
+ f"Retrying after {self.pause} seconds...")
724
+ sleep(self.pause)
725
+ if attempts == self.trials:
726
+ logging.error("Max attempts reached. Unable to fetch data due to request errors.")
727
+ break
728
+
729
+ # return None if the API call fails
730
+ return None
@@ -0,0 +1,33 @@
1
+ {
2
+ "cells": [
3
+ {
4
+ "cell_type": "code",
5
+ "execution_count": null,
6
+ "id": "89bd834b-aec0-45fe-82a0-c5d873c9518a",
7
+ "metadata": {},
8
+ "outputs": [],
9
+ "source": []
10
+ }
11
+ ],
12
+ "metadata": {
13
+ "kernelspec": {
14
+ "display_name": "cryptodatapy",
15
+ "language": "python",
16
+ "name": "cryptodatapy"
17
+ },
18
+ "language_info": {
19
+ "codemirror_mode": {
20
+ "name": "ipython",
21
+ "version": 3
22
+ },
23
+ "file_extension": ".py",
24
+ "mimetype": "text/x-python",
25
+ "name": "python",
26
+ "nbconvert_exporter": "python",
27
+ "pygments_lexer": "ipython3",
28
+ "version": "3.9.12"
29
+ }
30
+ },
31
+ "nbformat": 4,
32
+ "nbformat_minor": 5
33
+ }