kuhl-haus-mdp 0.1.6__py3-none-any.whl → 0.1.8__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.
@@ -126,6 +126,7 @@ class TopStocksAnalyzer(Analyzer):
126
126
  prev_day_close = cached_data["prev_day_close"]
127
127
  prev_day_volume = cached_data["prev_day_volume"]
128
128
  prev_day_vwap = cached_data["prev_day_vwap"]
129
+ free_float = cached_data["free_float"]
129
130
  else:
130
131
  # Get snapshot for previous day's data
131
132
  retry_count = 0
@@ -140,12 +141,10 @@ class TopStocksAnalyzer(Analyzer):
140
141
  prev_day_volume = snapshot.prev_day.volume
141
142
  prev_day_vwap = snapshot.prev_day.vwap
142
143
  break
143
- except BadResponse as e:
144
- self.logger.error(f"Error getting snapshot for {event.symbol}: {repr(e)}", exc_info=e, stack_info=True)
144
+ except Exception:
145
145
  retry_count += 1
146
146
  if retry_count == max_tries and prev_day_close == 0:
147
147
  self.logger.error(f"Failed to get snapshot for {event.symbol} after {max_tries} tries.")
148
- return
149
148
 
150
149
  # Get average volume
151
150
  retry_count = 0
@@ -155,12 +154,23 @@ class TopStocksAnalyzer(Analyzer):
155
154
  try:
156
155
  avg_volume = await self.cache.get_avg_volume(event.symbol)
157
156
  break
158
- except (BadResponse, ZeroDivisionError) as e:
159
- self.logger.error(f"Error getting average volume for {event.symbol}: {repr(e)}", exc_info=e, stack_info=True)
157
+ except Exception:
160
158
  retry_count += 1
161
159
  if retry_count == max_tries and avg_volume == 0:
162
160
  self.logger.error(f"Failed to get average volume for {event.symbol} after {max_tries} tries.")
163
- return
161
+
162
+ # Get free float - this uses an experimental API
163
+ retry_count = 0
164
+ max_tries = 2
165
+ free_float = 0
166
+ while retry_count < max_tries:
167
+ try:
168
+ free_float = await self.cache.get_free_float(event.symbol)
169
+ break
170
+ except Exception:
171
+ retry_count += 1
172
+ if retry_count == max_tries and free_float == 0:
173
+ self.logger.error(f"Failed to get free float for {event.symbol} after {max_tries} tries.")
164
174
 
165
175
  # Calculate relative volume
166
176
  if avg_volume == 0:
@@ -196,6 +206,7 @@ class TopStocksAnalyzer(Analyzer):
196
206
  self.cache_item.symbol_data_cache[event.symbol] = {
197
207
  "symbol": event.symbol,
198
208
  "volume": event.volume,
209
+ "free_float": free_float,
199
210
  "accumulated_volume": event.accumulated_volume,
200
211
  "relative_volume": relative_volume,
201
212
  "official_open_price": event.official_open_price,
@@ -1,6 +1,8 @@
1
1
  import json
2
2
  import logging
3
3
  from typing import Any, Optional, Iterator, List
4
+ from datetime import datetime, timezone, timedelta
5
+ from zoneinfo import ZoneInfo
4
6
 
5
7
  import aiohttp
6
8
  import redis.asyncio as aioredis
@@ -8,8 +10,10 @@ from massive.rest import RESTClient
8
10
  from massive.rest.models import (
9
11
  TickerSnapshot,
10
12
  FinancialRatio,
13
+ Agg,
11
14
  )
12
15
 
16
+ from kuhl_haus.mdp.helpers.utils import ticker_snapshot_to_dict
13
17
  from kuhl_haus.mdp.models.market_data_cache_keys import MarketDataCacheKeys
14
18
  from kuhl_haus.mdp.models.market_data_cache_ttl import MarketDataCacheTTL
15
19
 
@@ -34,49 +38,80 @@ class MarketDataCache:
34
38
  await self.redis_client.setex(cache_key, cache_ttl, json.dumps(data))
35
39
  else:
36
40
  await self.redis_client.set(cache_key, json.dumps(data))
37
- self.logger.debug(f"Cached data for {cache_key}")
41
+ self.logger.info(f"Cached data for {cache_key}")
38
42
 
39
43
  async def publish_data(self, data: Any, publish_key: str = None):
40
44
  await self.redis_client.publish(publish_key, json.dumps(data))
41
- self.logger.debug(f"Published data for {publish_key}")
45
+ self.logger.info(f"Published data for {publish_key}")
42
46
 
43
47
  async def get_ticker_snapshot(self, ticker: str) -> TickerSnapshot:
44
- self.logger.debug(f"Getting snapshot for {ticker}")
48
+ self.logger.info(f"Getting snapshot for {ticker}")
45
49
  cache_key = f"{MarketDataCacheKeys.TICKER_SNAPSHOTS.value}:{ticker}"
46
50
  result = await self.get_cache(cache_key=cache_key)
47
51
  if result:
48
- snapshot = TickerSnapshot.from_dict(**result)
52
+ self.logger.info(f"Returning cached snapshot for {ticker}")
53
+ snapshot = TickerSnapshot(**result)
49
54
  else:
50
55
  snapshot: TickerSnapshot = self.rest_client.get_snapshot_ticker(
51
56
  market_type="stocks",
52
57
  ticker=ticker
53
58
  )
54
- self.logger.debug(f"Snapshot result: {snapshot}")
59
+ self.logger.info(f"Snapshot result: {snapshot}")
60
+ data = ticker_snapshot_to_dict(snapshot)
55
61
  await self.cache_data(
56
- data=snapshot,
62
+ data=data,
57
63
  cache_key=cache_key,
58
64
  cache_ttl=MarketDataCacheTTL.EIGHT_HOURS.value
59
65
  )
60
66
  return snapshot
61
67
 
62
68
  async def get_avg_volume(self, ticker: str):
63
- self.logger.debug(f"Getting average volume for {ticker}")
69
+ self.logger.info(f"Getting average volume for {ticker}")
64
70
  cache_key = f"{MarketDataCacheKeys.TICKER_AVG_VOLUME.value}:{ticker}"
65
71
  avg_volume = await self.get_cache(cache_key=cache_key)
66
72
  if avg_volume:
67
- self.logger.debug(f"Returning cached value for {ticker}: {avg_volume}")
73
+ self.logger.info(f"Returning cached value for {ticker}: {avg_volume}")
68
74
  return avg_volume
69
75
 
76
+ # Experimental version - unreliable
70
77
  results: Iterator[FinancialRatio] = self.rest_client.list_financials_ratios(ticker=ticker)
71
78
  ratios: List[FinancialRatio] = []
72
79
  for financial_ratio in results:
73
80
  ratios.append(financial_ratio)
81
+
82
+ # If there is only one financial ratio, use it's average volume.
83
+ # Otherwise, calculate average volume from 30 trading sessions.'
74
84
  if len(ratios) == 1:
75
85
  avg_volume = ratios[0].average_volume
76
86
  else:
77
- raise Exception(f"Unexpected number of financial ratios for {ticker}: {len(ratios)}")
87
+ # Get date string in YYYY-MM-DD format
88
+ end_date = datetime.now(timezone.utc).strftime("%Y-%m-%d")
89
+ # Get date from 30 trading sessions ago in YYYY-MM-DD format
90
+ start_date = (datetime.now(timezone.utc) - timedelta(days=42)).strftime("%Y-%m-%d")
91
+
92
+ result: Iterator[Agg] = self.rest_client.list_aggs(
93
+ ticker=ticker,
94
+ multiplier=1,
95
+ timespan="day",
96
+ from_=start_date,
97
+ to=end_date,
98
+ adjusted=True,
99
+ sort="desc"
100
+ )
101
+ self.logger.info(f"average volume result: {result}")
102
+
103
+ total_volume = 0
104
+ max_periods = 30
105
+ periods_calculated = 0
106
+ for agg in result:
107
+ if periods_calculated < max_periods:
108
+ total_volume += agg.volume
109
+ periods_calculated += 1
110
+ else:
111
+ break
112
+ avg_volume = total_volume / periods_calculated
78
113
 
79
- self.logger.debug(f"average volume {ticker}: {avg_volume}")
114
+ self.logger.info(f"average volume {ticker}: {avg_volume}")
80
115
  await self.cache_data(
81
116
  data=avg_volume,
82
117
  cache_key=cache_key,
@@ -85,11 +120,11 @@ class MarketDataCache:
85
120
  return avg_volume
86
121
 
87
122
  async def get_free_float(self, ticker: str):
88
- self.logger.debug(f"Getting free float for {ticker}")
123
+ self.logger.info(f"Getting free float for {ticker}")
89
124
  cache_key = f"{MarketDataCacheKeys.TICKER_FREE_FLOAT.value}:{ticker}"
90
125
  free_float = await self.get_cache(cache_key=cache_key)
91
126
  if free_float:
92
- self.logger.debug(f"Returning cached value for {ticker}: {free_float}")
127
+ self.logger.info(f"Returning cached value for {ticker}: {free_float}")
93
128
  return free_float
94
129
 
95
130
  # NOTE: This endpoint is experimental and the interface may change.
@@ -123,7 +158,7 @@ class MarketDataCache:
123
158
  self.logger.error(f"Error fetching free float for {ticker}: {e}")
124
159
  raise
125
160
 
126
- self.logger.debug(f"free float {ticker}: {free_float}")
161
+ self.logger.info(f"free float {ticker}: {free_float}")
127
162
  await self.cache_data(
128
163
  data=free_float,
129
164
  cache_key=cache_key,
@@ -1,6 +1,8 @@
1
1
  import logging
2
2
  import os
3
+ from typing import Dict, Any
3
4
 
5
+ from massive.rest.models import TickerSnapshot
4
6
 
5
7
  logging.basicConfig(
6
8
  level=logging.INFO,
@@ -35,3 +37,101 @@ def get_massive_api_key():
35
37
  raise ValueError("MASSIVE_API_KEY environment variable not set")
36
38
  logger.info("Done.")
37
39
  return api_key
40
+
41
+
42
+ def ticker_snapshot_to_dict(snapshot: TickerSnapshot) -> Dict[str, Any]:
43
+ """
44
+ Convert a TickerSnapshot instance into a JSON-serializable dictionary.
45
+
46
+ Args:
47
+ snapshot: TickerSnapshot instance to convert
48
+
49
+ Returns:
50
+ Dictionary with keys matching the from_dict format (camelCase)
51
+ """
52
+ data = {
53
+ "ticker": snapshot.ticker,
54
+ "todays_change": snapshot.todays_change,
55
+ "todays_change_perc": snapshot.todays_change_percent,
56
+ "updated": snapshot.updated,
57
+ }
58
+
59
+ if snapshot.day is not None:
60
+ data["day"] = {
61
+ "open": snapshot.day.open,
62
+ "high": snapshot.day.high,
63
+ "low": snapshot.day.low,
64
+ "close": snapshot.day.close,
65
+ "volume": snapshot.day.volume,
66
+ "vwap": snapshot.day.vwap,
67
+ "timestamp": snapshot.day.timestamp,
68
+ "transactions": snapshot.day.transactions,
69
+ "otc": snapshot.day.otc,
70
+ }
71
+
72
+ if snapshot.last_quote is not None:
73
+ data["last_quote"] = {
74
+ "ticker": snapshot.last_quote.ticker,
75
+ "trf_timestamp": snapshot.last_quote.trf_timestamp,
76
+ "sequence_number": snapshot.last_quote.sequence_number,
77
+ "sip_timestamp": snapshot.last_quote.sip_timestamp,
78
+ "participant_timestamp": snapshot.last_quote.participant_timestamp,
79
+ "ask_price": snapshot.last_quote.ask_price,
80
+ "ask_size": snapshot.last_quote.ask_size,
81
+ "ask_exchange": snapshot.last_quote.ask_exchange,
82
+ "conditions": snapshot.last_quote.conditions,
83
+ "indicators": snapshot.last_quote.indicators,
84
+ "bid_price": snapshot.last_quote.bid_price,
85
+ "bid_size": snapshot.last_quote.bid_size,
86
+ "bid_exchange": snapshot.last_quote.bid_exchange,
87
+ "tape": snapshot.last_quote.tape,
88
+ }
89
+
90
+ if snapshot.last_trade is not None:
91
+ data["last_trade"] = {
92
+ "ticker": snapshot.last_trade.ticker,
93
+ "trf_timestamp": snapshot.last_trade.trf_timestamp,
94
+ "sequence_number": snapshot.last_trade.sequence_number,
95
+ "sip_timestamp": snapshot.last_trade.sip_timestamp,
96
+ "participant_timestamp": snapshot.last_trade.participant_timestamp,
97
+ "conditions": snapshot.last_trade.conditions,
98
+ "correction": snapshot.last_trade.correction,
99
+ "id": snapshot.last_trade.id,
100
+ "price": snapshot.last_trade.price,
101
+ "trf_id": snapshot.last_trade.trf_id,
102
+ "size": snapshot.last_trade.size,
103
+ "exchange": snapshot.last_trade.exchange,
104
+ "tape": snapshot.last_trade.tape,
105
+ }
106
+
107
+ if snapshot.min is not None:
108
+ data["min"] = {
109
+ "accumulated_volume": snapshot.min.accumulated_volume,
110
+ "open": snapshot.min.open,
111
+ "high": snapshot.min.high,
112
+ "low": snapshot.min.low,
113
+ "close": snapshot.min.close,
114
+ "volume": snapshot.min.volume,
115
+ "vwap": snapshot.min.vwap,
116
+ "otc": snapshot.min.otc,
117
+ "timestamp": snapshot.min.timestamp,
118
+ "transactions": snapshot.min.transactions,
119
+ }
120
+
121
+ if snapshot.prev_day is not None:
122
+ data["prev_day"] = {
123
+ "open": snapshot.prev_day.open,
124
+ "high": snapshot.prev_day.high,
125
+ "low": snapshot.prev_day.low,
126
+ "close": snapshot.prev_day.close,
127
+ "volume": snapshot.prev_day.volume,
128
+ "vwap": snapshot.prev_day.vwap,
129
+ "timestamp": snapshot.prev_day.timestamp,
130
+ "transactions": snapshot.prev_day.transactions,
131
+ "otc": snapshot.prev_day.otc,
132
+ }
133
+
134
+ if snapshot.fair_market_value is not None:
135
+ data["fmv"] = snapshot.fair_market_value
136
+
137
+ return data
@@ -15,10 +15,10 @@ class MarketDataCacheKeys(Enum):
15
15
  UNKNOWN = 'unknown'
16
16
 
17
17
  # MARKET DATA CACHE
18
- DAILY_AGGREGATES = 'aggregate:daily'
19
- TICKER_SNAPSHOTS = 'snapshots'
20
- TICKER_AVG_VOLUME = 'avg_volume'
21
- TICKER_FREE_FLOAT = 'free_float'
18
+ DAILY_AGGREGATES = 'mdc:aggregate:daily'
19
+ TICKER_SNAPSHOTS = 'mdc:snapshots'
20
+ TICKER_AVG_VOLUME = 'mdc:avg_volume'
21
+ TICKER_FREE_FLOAT = 'mdc:free_float'
22
22
 
23
23
  # MARKET DATA PROCESSOR CACHE
24
24
  TOP_TRADES_SCANNER = f'cache:{MarketDataScannerNames.TOP_TRADES.value}'
@@ -45,6 +45,7 @@ class TopStocksCacheItem:
45
45
  ret.append({
46
46
  "symbol": ticker,
47
47
  "volume": self.symbol_data_cache[ticker]["volume"],
48
+ "free_float": self.symbol_data_cache[ticker]["free_float"],
48
49
  "accumulated_volume": self.symbol_data_cache[ticker]["accumulated_volume"],
49
50
  "relative_volume": self.symbol_data_cache[ticker]["relative_volume"],
50
51
  "official_open_price": self.symbol_data_cache[ticker]["official_open_price"],
@@ -81,6 +82,7 @@ class TopStocksCacheItem:
81
82
  ret.append({
82
83
  "symbol": ticker,
83
84
  "volume": self.symbol_data_cache[ticker]["volume"],
85
+ "free_float": self.symbol_data_cache[ticker]["free_float"],
84
86
  "accumulated_volume": self.symbol_data_cache[ticker]["accumulated_volume"],
85
87
  "relative_volume": self.symbol_data_cache[ticker]["relative_volume"],
86
88
  "official_open_price": self.symbol_data_cache[ticker]["official_open_price"],
@@ -117,6 +119,7 @@ class TopStocksCacheItem:
117
119
  ret.append({
118
120
  "symbol": ticker,
119
121
  "volume": self.symbol_data_cache[ticker]["volume"],
122
+ "free_float": self.symbol_data_cache[ticker]["free_float"],
120
123
  "accumulated_volume": self.symbol_data_cache[ticker]["accumulated_volume"],
121
124
  "relative_volume": self.symbol_data_cache[ticker]["relative_volume"],
122
125
  "official_open_price": self.symbol_data_cache[ticker]["official_open_price"],
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: kuhl-haus-mdp
3
- Version: 0.1.6
3
+ Version: 0.1.8
4
4
  Summary: Market data processing pipeline for stock market scanner
5
5
  Author-Email: Tom Pounders <git@oldschool.engineer>
6
6
  License: The MIT License (MIT)
@@ -2,15 +2,15 @@ kuhl_haus/mdp/__init__.py,sha256=5dEpAdB3kypH8tCRECoXwbly1WV9kFU5kh8ldGSa0VI,349
2
2
  kuhl_haus/mdp/analyzers/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
3
3
  kuhl_haus/mdp/analyzers/analyzer.py,sha256=rIU1lcHwP2IBai0QLt0y-4ySg_ibWsutNU8JUgSxa1U,471
4
4
  kuhl_haus/mdp/analyzers/massive_data_analyzer.py,sha256=WSb7T8X4u2ue7Du7sf_fqxjgjEbR6ThllSNT1CncIM0,3866
5
- kuhl_haus/mdp/analyzers/top_stocks.py,sha256=GvNSa7yWZZ7WUgpaV2t1nVOxWA2R2qpISy16x2RGaQ8,9463
5
+ kuhl_haus/mdp/analyzers/top_stocks.py,sha256=Ms4TXCWCzooZdH7NFIiqMffjAPREDfw04jiWgpx53wQ,9768
6
6
  kuhl_haus/mdp/components/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
7
- kuhl_haus/mdp/components/market_data_cache.py,sha256=EmRDlh_GTKyYDvAbbAPQrE8n91JKNFKH8myAM4UTPLM,5835
7
+ kuhl_haus/mdp/components/market_data_cache.py,sha256=LT_yddNwB-l6skJgvDWHmUO6GjdBM0EmlMCGHX5kwKY,7209
8
8
  kuhl_haus/mdp/components/market_data_scanner.py,sha256=45MgprFlq03MvmIRYXENsrc7UlTcBE_hIsPyOvNs1zc,9745
9
9
  kuhl_haus/mdp/components/widget_data_service.py,sha256=ikygD9NRpidcXBEqft5Q11rHy_eUOwKGyOLEezo-Dd4,7439
10
10
  kuhl_haus/mdp/helpers/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
11
11
  kuhl_haus/mdp/helpers/process_manager.py,sha256=Is3Jx8nlBWvywQ1acdsdaSJTAG0olKskpPvrRB4VMDE,9024
12
12
  kuhl_haus/mdp/helpers/queue_name_resolver.py,sha256=l_zfRLxrjR9uwRCV2VDO4vPWLK_lj5KVG2p4Lh8xWiw,770
13
- kuhl_haus/mdp/helpers/utils.py,sha256=9JEpl2yr2LghOLrJUDxi-4dtDK3DZ1wBTZ1uxBJsFbQ,1309
13
+ kuhl_haus/mdp/helpers/utils.py,sha256=2jECmezuDBABcB1Fhe1_GPnlEAFnld2bZ1vSdaCZApg,5203
14
14
  kuhl_haus/mdp/integ/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
15
15
  kuhl_haus/mdp/integ/massive_data_listener.py,sha256=fPEYc6zZzHzFFjbP3zFInajKtEGInj8UQKKo3nKQEwQ,5098
16
16
  kuhl_haus/mdp/integ/massive_data_processor.py,sha256=H1WlbGtuSF45n7qLTLleuNlG-OlIXz4llJ7q3XRSS-s,8605
@@ -18,14 +18,14 @@ kuhl_haus/mdp/integ/massive_data_queues.py,sha256=zC_uV2vwZCMyVerDQ18RAQwIMMF75i
18
18
  kuhl_haus/mdp/integ/web_socket_message_serde.py,sha256=XdaoaByc7IhtzbPDXBtXKOTjyDzfPSDuZVCoHSIaTl4,5468
19
19
  kuhl_haus/mdp/models/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
20
20
  kuhl_haus/mdp/models/market_data_analyzer_result.py,sha256=iICb5GVCtuqARNbR1JNCAfbxMijM3uppDNdL8_FB3eI,422
21
- kuhl_haus/mdp/models/market_data_cache_keys.py,sha256=04nFRdNZtvEeKFnpjZ6CNSu-4MiUgifPXPHGAZhZRsE,1051
21
+ kuhl_haus/mdp/models/market_data_cache_keys.py,sha256=_CSPtwvZo5W6cuj9vb7lRTu6MMuRGASe_VigAaOQXwE,1067
22
22
  kuhl_haus/mdp/models/market_data_cache_ttl.py,sha256=a43ys3S61Y0ADdb03ThgrRd9x7B1EsI6FplCjecdNLY,373
23
23
  kuhl_haus/mdp/models/market_data_pubsub_keys.py,sha256=PEIPXK9jBehJB7G4pqoSuQZcfMZgOQq8Yho1itqv-1A,1306
24
24
  kuhl_haus/mdp/models/market_data_scanner_names.py,sha256=BYn1C0rYgGF1Sq583BkHADKUu-28ytNZQ-XgptuCH-Y,260
25
25
  kuhl_haus/mdp/models/massive_data_queue.py,sha256=MfYBcjVc4Fi61DWIvvhhWLUOiLmRpE9egtW-2KH6FTE,188
26
- kuhl_haus/mdp/models/top_stocks_cache_item.py,sha256=4vwwPTMkRRf1ct6iFInJnLSbBadM-tRk-zhqdD_ITE0,7676
27
- kuhl_haus_mdp-0.1.6.dist-info/METADATA,sha256=ecQ-j_82U-8JfxrpBMpJovbruVY2VSitRS-BgzG_cb8,8898
28
- kuhl_haus_mdp-0.1.6.dist-info/WHEEL,sha256=tsUv_t7BDeJeRHaSrczbGeuK-TtDpGsWi_JfpzD255I,90
29
- kuhl_haus_mdp-0.1.6.dist-info/entry_points.txt,sha256=6OYgBcLyFCUgeqLgnvMyOJxPCWzgy7se4rLPKtNonMs,34
30
- kuhl_haus_mdp-0.1.6.dist-info/licenses/LICENSE.txt,sha256=DRkJftAJcMqoTkQ_Y6-HtKj3nm4pZah_p8XBZiYnw-c,1079
31
- kuhl_haus_mdp-0.1.6.dist-info/RECORD,,
26
+ kuhl_haus/mdp/models/top_stocks_cache_item.py,sha256=Zn9LociBatP2a1OUfjXDFlWM7tCudO-CR4oj6jYfEEM,7916
27
+ kuhl_haus_mdp-0.1.8.dist-info/METADATA,sha256=tV0LyosGNJgB53oMi_HAJ2G2XOmu3oyLYaVICVZth8A,8898
28
+ kuhl_haus_mdp-0.1.8.dist-info/WHEEL,sha256=tsUv_t7BDeJeRHaSrczbGeuK-TtDpGsWi_JfpzD255I,90
29
+ kuhl_haus_mdp-0.1.8.dist-info/entry_points.txt,sha256=6OYgBcLyFCUgeqLgnvMyOJxPCWzgy7se4rLPKtNonMs,34
30
+ kuhl_haus_mdp-0.1.8.dist-info/licenses/LICENSE.txt,sha256=DRkJftAJcMqoTkQ_Y6-HtKj3nm4pZah_p8XBZiYnw-c,1079
31
+ kuhl_haus_mdp-0.1.8.dist-info/RECORD,,