kuhl-haus-mdp 0.1.5__tar.gz → 0.1.6__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 (46) hide show
  1. {kuhl_haus_mdp-0.1.5 → kuhl_haus_mdp-0.1.6}/PKG-INFO +2 -2
  2. {kuhl_haus_mdp-0.1.5 → kuhl_haus_mdp-0.1.6}/README.md +1 -1
  3. {kuhl_haus_mdp-0.1.5 → kuhl_haus_mdp-0.1.6}/pyproject.toml +1 -1
  4. {kuhl_haus_mdp-0.1.5 → kuhl_haus_mdp-0.1.6}/src/kuhl_haus/mdp/analyzers/analyzer.py +5 -4
  5. {kuhl_haus_mdp-0.1.5 → kuhl_haus_mdp-0.1.6}/src/kuhl_haus/mdp/analyzers/top_stocks.py +8 -9
  6. {kuhl_haus_mdp-0.1.5 → kuhl_haus_mdp-0.1.6}/src/kuhl_haus/mdp/components/market_data_scanner.py +21 -5
  7. {kuhl_haus_mdp-0.1.5 → kuhl_haus_mdp-0.1.6}/tests/analyzers/test_top_stocks_rehydrate.py +18 -9
  8. {kuhl_haus_mdp-0.1.5 → kuhl_haus_mdp-0.1.6}/tests/components/test_market_data_scanner.py +8 -18
  9. {kuhl_haus_mdp-0.1.5 → kuhl_haus_mdp-0.1.6}/LICENSE.txt +0 -0
  10. {kuhl_haus_mdp-0.1.5 → kuhl_haus_mdp-0.1.6}/src/kuhl_haus/mdp/__init__.py +0 -0
  11. {kuhl_haus_mdp-0.1.5 → kuhl_haus_mdp-0.1.6}/src/kuhl_haus/mdp/analyzers/__init__.py +0 -0
  12. {kuhl_haus_mdp-0.1.5 → kuhl_haus_mdp-0.1.6}/src/kuhl_haus/mdp/analyzers/massive_data_analyzer.py +0 -0
  13. {kuhl_haus_mdp-0.1.5 → kuhl_haus_mdp-0.1.6}/src/kuhl_haus/mdp/components/__init__.py +0 -0
  14. {kuhl_haus_mdp-0.1.5 → kuhl_haus_mdp-0.1.6}/src/kuhl_haus/mdp/components/market_data_cache.py +0 -0
  15. {kuhl_haus_mdp-0.1.5 → kuhl_haus_mdp-0.1.6}/src/kuhl_haus/mdp/components/widget_data_service.py +0 -0
  16. {kuhl_haus_mdp-0.1.5 → kuhl_haus_mdp-0.1.6}/src/kuhl_haus/mdp/helpers/__init__.py +0 -0
  17. {kuhl_haus_mdp-0.1.5 → kuhl_haus_mdp-0.1.6}/src/kuhl_haus/mdp/helpers/process_manager.py +0 -0
  18. {kuhl_haus_mdp-0.1.5 → kuhl_haus_mdp-0.1.6}/src/kuhl_haus/mdp/helpers/queue_name_resolver.py +0 -0
  19. {kuhl_haus_mdp-0.1.5 → kuhl_haus_mdp-0.1.6}/src/kuhl_haus/mdp/helpers/utils.py +0 -0
  20. {kuhl_haus_mdp-0.1.5 → kuhl_haus_mdp-0.1.6}/src/kuhl_haus/mdp/integ/__init__.py +0 -0
  21. {kuhl_haus_mdp-0.1.5 → kuhl_haus_mdp-0.1.6}/src/kuhl_haus/mdp/integ/massive_data_listener.py +0 -0
  22. {kuhl_haus_mdp-0.1.5 → kuhl_haus_mdp-0.1.6}/src/kuhl_haus/mdp/integ/massive_data_processor.py +0 -0
  23. {kuhl_haus_mdp-0.1.5 → kuhl_haus_mdp-0.1.6}/src/kuhl_haus/mdp/integ/massive_data_queues.py +0 -0
  24. {kuhl_haus_mdp-0.1.5 → kuhl_haus_mdp-0.1.6}/src/kuhl_haus/mdp/integ/web_socket_message_serde.py +0 -0
  25. {kuhl_haus_mdp-0.1.5 → kuhl_haus_mdp-0.1.6}/src/kuhl_haus/mdp/models/__init__.py +0 -0
  26. {kuhl_haus_mdp-0.1.5 → kuhl_haus_mdp-0.1.6}/src/kuhl_haus/mdp/models/market_data_analyzer_result.py +0 -0
  27. {kuhl_haus_mdp-0.1.5 → kuhl_haus_mdp-0.1.6}/src/kuhl_haus/mdp/models/market_data_cache_keys.py +0 -0
  28. {kuhl_haus_mdp-0.1.5 → kuhl_haus_mdp-0.1.6}/src/kuhl_haus/mdp/models/market_data_cache_ttl.py +0 -0
  29. {kuhl_haus_mdp-0.1.5 → kuhl_haus_mdp-0.1.6}/src/kuhl_haus/mdp/models/market_data_pubsub_keys.py +0 -0
  30. {kuhl_haus_mdp-0.1.5 → kuhl_haus_mdp-0.1.6}/src/kuhl_haus/mdp/models/market_data_scanner_names.py +0 -0
  31. {kuhl_haus_mdp-0.1.5 → kuhl_haus_mdp-0.1.6}/src/kuhl_haus/mdp/models/massive_data_queue.py +0 -0
  32. {kuhl_haus_mdp-0.1.5 → kuhl_haus_mdp-0.1.6}/src/kuhl_haus/mdp/models/top_stocks_cache_item.py +0 -0
  33. {kuhl_haus_mdp-0.1.5 → kuhl_haus_mdp-0.1.6}/tests/__init__.py +0 -0
  34. {kuhl_haus_mdp-0.1.5 → kuhl_haus_mdp-0.1.6}/tests/analyzers/__init__.py +0 -0
  35. {kuhl_haus_mdp-0.1.5 → kuhl_haus_mdp-0.1.6}/tests/analyzers/test_massive_data_analyzer.py +0 -0
  36. {kuhl_haus_mdp-0.1.5 → kuhl_haus_mdp-0.1.6}/tests/components/__init__.py +0 -0
  37. {kuhl_haus_mdp-0.1.5 → kuhl_haus_mdp-0.1.6}/tests/components/test_market_data_cache.py +0 -0
  38. {kuhl_haus_mdp-0.1.5 → kuhl_haus_mdp-0.1.6}/tests/components/test_widget_data_service.py +0 -0
  39. {kuhl_haus_mdp-0.1.5 → kuhl_haus_mdp-0.1.6}/tests/helpers/__init__.py +0 -0
  40. {kuhl_haus_mdp-0.1.5 → kuhl_haus_mdp-0.1.6}/tests/helpers/test_process_manager.py +0 -0
  41. {kuhl_haus_mdp-0.1.5 → kuhl_haus_mdp-0.1.6}/tests/helpers/test_queue_name_resolver.py +0 -0
  42. {kuhl_haus_mdp-0.1.5 → kuhl_haus_mdp-0.1.6}/tests/helpers/test_utils.py +0 -0
  43. {kuhl_haus_mdp-0.1.5 → kuhl_haus_mdp-0.1.6}/tests/integ/__init__.py +0 -0
  44. {kuhl_haus_mdp-0.1.5 → kuhl_haus_mdp-0.1.6}/tests/integ/test_web_socket_message_serde.py +0 -0
  45. {kuhl_haus_mdp-0.1.5 → kuhl_haus_mdp-0.1.6}/tests/models/__init__.py +0 -0
  46. {kuhl_haus_mdp-0.1.5 → kuhl_haus_mdp-0.1.6}/tests/models/test_top_stocks_cache_item.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: kuhl-haus-mdp
3
- Version: 0.1.5
3
+ Version: 0.1.6
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)
@@ -68,7 +68,7 @@ Description-Content-Type: text/markdown
68
68
  [![codecov](https://codecov.io/gh/kuhl-haus/kuhl-haus-mdp/branch/mainline/graph/badge.svg)](https://codecov.io/gh/kuhl-haus/kuhl-haus-mdp)
69
69
  [![GitHub issues](https://img.shields.io/github/issues/kuhl-haus/kuhl-haus-mdp)](https://github.com/kuhl-haus/kuhl-haus-mdp/issues)
70
70
  [![GitHub pull requests](https://img.shields.io/github/issues-pr/kuhl-haus/kuhl-haus-mdp)](https://github.com/kuhl-haus/kuhl-haus-mdp/pulls)
71
-
71
+ [![Documentation](https://readthedocs.org/projects/kuhl-haus-mdp/badge/?version=latest)](https://kuhl-haus-mdp.readthedocs.io/en/latest/)
72
72
 
73
73
  # kuhl-haus-mdp
74
74
 
@@ -15,7 +15,7 @@
15
15
  [![codecov](https://codecov.io/gh/kuhl-haus/kuhl-haus-mdp/branch/mainline/graph/badge.svg)](https://codecov.io/gh/kuhl-haus/kuhl-haus-mdp)
16
16
  [![GitHub issues](https://img.shields.io/github/issues/kuhl-haus/kuhl-haus-mdp)](https://github.com/kuhl-haus/kuhl-haus-mdp/issues)
17
17
  [![GitHub pull requests](https://img.shields.io/github/issues-pr/kuhl-haus/kuhl-haus-mdp)](https://github.com/kuhl-haus/kuhl-haus-mdp/pulls)
18
-
18
+ [![Documentation](https://readthedocs.org/projects/kuhl-haus-mdp/badge/?version=latest)](https://kuhl-haus-mdp.readthedocs.io/en/latest/)
19
19
 
20
20
  # kuhl-haus-mdp
21
21
 
@@ -30,7 +30,7 @@ dependencies = [
30
30
  "uvicorn[standard]",
31
31
  "websockets",
32
32
  ]
33
- version = "0.1.5"
33
+ version = "0.1.6"
34
34
 
35
35
  [project.license]
36
36
  file = "LICENSE.txt"
@@ -1,14 +1,15 @@
1
1
  from typing import Optional, List
2
2
  from kuhl_haus.mdp.models.market_data_analyzer_result import MarketDataAnalyzerResult
3
+ from kuhl_haus.mdp.components.market_data_cache import MarketDataCache
3
4
 
4
5
 
5
6
  class Analyzer:
6
- cache_key: str
7
+ cache: MarketDataCache
7
8
 
8
- def __init__(self, cache_key: str, **kwargs):
9
- self.cache_key = cache_key
9
+ def __init__(self, cache: MarketDataCache, **kwargs):
10
+ self.cache = cache
10
11
 
11
- async def rehydrate(self, data: dict):
12
+ async def rehydrate(self):
12
13
  pass
13
14
 
14
15
  async def analyze_data(self, data: dict) -> Optional[List[MarketDataAnalyzerResult]]:
@@ -26,21 +26,15 @@ from kuhl_haus.mdp.models.top_stocks_cache_item import TopStocksCacheItem
26
26
  class TopStocksAnalyzer(Analyzer):
27
27
 
28
28
  def __init__(self, cache: MarketDataCache, **kwargs):
29
- if "cache_key" not in kwargs:
30
- kwargs["cache_key"] = MarketDataCacheKeys.TOP_STOCKS_SCANNER.value
31
- super().__init__(**kwargs)
29
+ super().__init__(cache=cache, **kwargs)
32
30
  self.cache = cache
31
+ self.cache_key = MarketDataCacheKeys.TOP_STOCKS_SCANNER.value
33
32
  self.logger = logging.getLogger(__name__)
34
33
  self.cache_item = TopStocksCacheItem()
35
34
  self.last_update_time = 0
36
35
  self.pre_market_reset = False
37
36
 
38
- async def rehydrate(self, data: dict):
39
- if not data:
40
- self.cache_item = TopStocksCacheItem()
41
- self.logger.info("No data to rehydrate TopStocksCacheItem.")
42
- return
43
-
37
+ async def rehydrate(self):
44
38
  # Get current time in UTC, then convert to Eastern Time
45
39
  utc_now = datetime.now(timezone.utc)
46
40
  et_now = utc_now.astimezone(ZoneInfo("America/New_York"))
@@ -52,6 +46,11 @@ class TopStocksAnalyzer(Analyzer):
52
46
  self.cache_item = TopStocksCacheItem()
53
47
  self.logger.info(f"Outside market hours ({et_now.strftime('%H:%M:%S %Z')}), clearing cache.")
54
48
  return
49
+ data = await self.cache.get_cache(self.cache_key)
50
+ if not data:
51
+ self.cache_item = TopStocksCacheItem()
52
+ self.logger.info("No data to rehydrate TopStocksCacheItem.")
53
+ return
55
54
  self.cache_item = TopStocksCacheItem(**data)
56
55
  self.logger.info("Rehydrated TopStocksCacheItem")
57
56
 
@@ -1,13 +1,16 @@
1
1
  import asyncio
2
2
  import json
3
3
  import logging
4
- from typing import Union, Optional, List
4
+ from typing import Any, Union, Optional, List
5
5
 
6
6
  import redis.asyncio as aioredis
7
7
  from redis.exceptions import ConnectionError
8
8
 
9
+ from massive.rest import RESTClient
10
+
9
11
  from kuhl_haus.mdp.analyzers.analyzer import Analyzer
10
12
  from kuhl_haus.mdp.models.market_data_analyzer_result import MarketDataAnalyzerResult
13
+ from kuhl_haus.mdp.components.market_data_cache import MarketDataCache
11
14
 
12
15
 
13
16
  class MarketDataScanner:
@@ -18,11 +21,14 @@ class MarketDataScanner:
18
21
  error: int
19
22
  restarts: int
20
23
 
21
- def __init__(self, redis_url: str, analyzer: Analyzer, subscriptions: List[str]):
24
+ def __init__(self, redis_url: str, massive_api_key: str, subscriptions: List[str], analyzer_class: Any):
22
25
  self.redis_url = redis_url
23
- self.analyzer = analyzer
26
+ self.massive_api_key = massive_api_key
24
27
  self.logger = logging.getLogger(__name__)
25
28
 
29
+ self.analyzer: Analyzer = None
30
+ self.analyzer_class = analyzer_class
31
+
26
32
  # Connection objects
27
33
  self.redis_client = None # : aioredis.Redis = None
28
34
  self.pubsub_client: Optional[aioredis.client.PubSub] = None
@@ -30,6 +36,7 @@ class MarketDataScanner:
30
36
  # State
31
37
  self.mdc_connected = False
32
38
  self.running = False
39
+ self.mdc: Optional[MarketDataCache] = None
33
40
 
34
41
  self.subscriptions: List[str] = subscriptions
35
42
  self._pubsub_task: Union[asyncio.Task, None] = None
@@ -48,9 +55,9 @@ class MarketDataScanner:
48
55
  await self.connect()
49
56
  self.pubsub_client = self.redis_client.pubsub()
50
57
 
51
- scanner_cache = await self.get_cache(self.analyzer.cache_key)
58
+ self.analyzer = self.analyzer_class(cache=self.mdc)
52
59
  self.logger.info(f"mds rehydrating from cache")
53
- await self.analyzer.rehydrate(scanner_cache)
60
+ await self.analyzer.rehydrate()
54
61
  self.logger.info("mds rehydration complete")
55
62
 
56
63
  for subscription in self.subscriptions:
@@ -73,6 +80,10 @@ class MarketDataScanner:
73
80
  pass
74
81
  self._pubsub_task = None
75
82
 
83
+ if self.mdc:
84
+ await self.mdc.close()
85
+ self.mdc = None
86
+
76
87
  if self.pubsub_client:
77
88
  for subscription in self.subscriptions:
78
89
  if subscription.endswith("*"):
@@ -104,6 +115,11 @@ class MarketDataScanner:
104
115
 
105
116
  # Test Redis connection
106
117
  await self.redis_client.ping()
118
+ self.mdc = MarketDataCache(
119
+ rest_client=RESTClient(api_key=self.massive_api_key),
120
+ redis_client=self.redis_client,
121
+ massive_api_key=self.massive_api_key
122
+ )
107
123
  self.mdc_connected = True
108
124
  self.logger.debug(f"Connected to Redis: {self.redis_url}")
109
125
  except Exception as e:
@@ -1,5 +1,6 @@
1
+
1
2
  from datetime import datetime, timezone
2
- from unittest.mock import patch, MagicMock
3
+ from unittest.mock import patch, MagicMock, AsyncMock
3
4
 
4
5
  import pytest
5
6
 
@@ -10,7 +11,9 @@ from kuhl_haus.mdp.components.market_data_cache import MarketDataCache
10
11
 
11
12
  @pytest.fixture
12
13
  def mock_market_data_cache():
13
- return MagicMock(spec=MarketDataCache)
14
+ mock = MagicMock(spec=MarketDataCache)
15
+ mock.get_cache = AsyncMock()
16
+ return mock
14
17
 
15
18
 
16
19
  @pytest.fixture
@@ -56,13 +59,16 @@ def outside_trading_hour_patch():
56
59
 
57
60
  @pytest.mark.asyncio
58
61
  @patch("kuhl_haus.mdp.analyzers.top_stocks.ZoneInfo")
59
- async def test_rehydrate_no_data(mock_zoneinfo, top_stocks_analyzer, mock_logger):
62
+ async def test_rehydrate_no_data(mock_zoneinfo, top_stocks_analyzer, mock_logger, trading_hour_patch, mock_market_data_cache):
60
63
  """Test rehydrate when no data is passed."""
61
64
  # Arrange
65
+ # Configure ZoneInfo mock to return timezone.utc so astimezone works properly
66
+ mock_zoneinfo.return_value = timezone.utc
62
67
  top_stocks_analyzer.logger = mock_logger
68
+ top_stocks_analyzer.cache.get_cache.return_value = None
63
69
 
64
70
  # Act
65
- await top_stocks_analyzer.rehydrate(None)
71
+ _ = await top_stocks_analyzer.rehydrate()
66
72
 
67
73
  # Assert
68
74
  assert isinstance(top_stocks_analyzer.cache_item, TopStocksCacheItem)
@@ -71,15 +77,17 @@ async def test_rehydrate_no_data(mock_zoneinfo, top_stocks_analyzer, mock_logger
71
77
 
72
78
  @pytest.mark.asyncio
73
79
  @patch("kuhl_haus.mdp.analyzers.top_stocks.ZoneInfo")
74
- async def test_rehydrate_outside_trading_hours(mock_zoneinfo, top_stocks_analyzer, outside_trading_hour_patch, mock_logger):
80
+ async def test_rehydrate_outside_trading_hours(mock_zoneinfo, top_stocks_analyzer, outside_trading_hour_patch, mock_logger, mock_market_data_cache):
75
81
  """Test rehydrate outside trading hours."""
76
82
  # Arrange
77
83
  # Configure ZoneInfo mock to return timezone.utc so astimezone works properly
78
84
  mock_zoneinfo.return_value = timezone.utc
79
85
  top_stocks_analyzer.logger = mock_logger
86
+ data = {"day_start_time": 1672531200}
87
+ top_stocks_analyzer.cache.get_cache.return_value = data
80
88
 
81
89
  # Act
82
- await top_stocks_analyzer.rehydrate({"day_start_time": 1672531200})
90
+ await top_stocks_analyzer.rehydrate()
83
91
 
84
92
  # Assert
85
93
  assert isinstance(top_stocks_analyzer.cache_item, TopStocksCacheItem)
@@ -91,18 +99,19 @@ async def test_rehydrate_outside_trading_hours(mock_zoneinfo, top_stocks_analyze
91
99
 
92
100
  @pytest.mark.asyncio
93
101
  @patch("kuhl_haus.mdp.analyzers.top_stocks.ZoneInfo")
94
- async def test_rehydrate_within_trading_hours(mock_zoneinfo, top_stocks_analyzer, trading_hour_patch, mock_logger):
102
+ async def test_rehydrate_within_trading_hours(mock_zoneinfo, top_stocks_analyzer, trading_hour_patch, mock_logger, mock_market_data_cache):
95
103
  """Test rehydrate within trading hours with valid data."""
96
104
  # Arrange
97
105
  # Configure ZoneInfo mock to return timezone.utc so astimezone works properly
98
106
  mock_zoneinfo.return_value = timezone.utc
99
107
  data = {"day_start_time": 1672531200}
108
+ top_stocks_analyzer.cache.get_cache.return_value = data
100
109
  top_stocks_analyzer.logger = mock_logger
101
110
 
102
111
  # Act
103
- await top_stocks_analyzer.rehydrate(data)
112
+ await top_stocks_analyzer.rehydrate()
104
113
 
105
114
  # Assert
106
115
  assert isinstance(top_stocks_analyzer.cache_item, TopStocksCacheItem)
107
116
  assert top_stocks_analyzer.cache_item.day_start_time == 1672531200
108
- mock_logger.info.assert_called_once_with("Rehydrated TopStocksCacheItem")
117
+ mock_logger.info.assert_called_once_with("Rehydrated TopStocksCacheItem")
@@ -1,10 +1,9 @@
1
1
  # tests/test_market_data_scanner.py
2
- import asyncio
3
- import json
4
2
  import unittest
5
3
  from unittest.mock import AsyncMock, patch, MagicMock
6
4
 
7
5
  from kuhl_haus.mdp.analyzers.analyzer import Analyzer
6
+ from kuhl_haus.mdp.analyzers.top_stocks import TopStocksAnalyzer
8
7
  from kuhl_haus.mdp.components.market_data_scanner import MarketDataScanner
9
8
 
10
9
 
@@ -14,22 +13,26 @@ class TestMarketDataScanner(unittest.IsolatedAsyncioTestCase):
14
13
  def setUp(self):
15
14
  """Set up a MarketDataScanner instance for testing."""
16
15
  self.redis_url = "redis://localhost:6379/0"
17
- self.analyzer = MagicMock(spec=Analyzer)
16
+ self.analyzer = MagicMock(spec=TopStocksAnalyzer)
18
17
  self.analyzer.cache_key = MagicMock()
19
18
  self.analyzer.rehydrate = AsyncMock()
20
19
  self.analyzer.analyze_data = AsyncMock()
21
20
  self.subscriptions = ["channel_1"]
22
21
  self.scanner = MarketDataScanner(
23
22
  redis_url=self.redis_url,
24
- analyzer=self.analyzer,
23
+ massive_api_key="test_key",
25
24
  subscriptions=self.subscriptions,
25
+ analyzer_class=Analyzer
26
26
  )
27
+ self.scanner.start()
27
28
 
29
+ @patch("kuhl_haus.mdp.analyzers.analyzer.Analyzer")
28
30
  @patch("kuhl_haus.mdp.components.market_data_scanner.asyncio.sleep", new_callable=AsyncMock)
29
31
  @patch("kuhl_haus.mdp.components.market_data_scanner.MarketDataScanner.start", new_callable=AsyncMock)
30
32
  @patch("kuhl_haus.mdp.components.market_data_scanner.MarketDataScanner.stop", new_callable=AsyncMock)
31
- async def test_restart(self, mock_stop, mock_start, mock_sleep):
33
+ async def test_restart(self, mock_stop, mock_start, mock_sleep, mock_analyzer):
32
34
  """Test the restart method stops and starts the scanner."""
35
+ self.analyzer.return_value = mock_analyzer
33
36
  self.scanner.start = mock_start
34
37
  self.scanner.stop = mock_stop
35
38
  mock_stop.return_value = None
@@ -40,16 +43,3 @@ class TestMarketDataScanner(unittest.IsolatedAsyncioTestCase):
40
43
  mock_start.assert_called_once()
41
44
  self.assertEqual(self.scanner.restarts, 1)
42
45
 
43
- async def test_process_message_success(self):
44
- """Test _process_message handles and processes valid data."""
45
- valid_data = {"key": "value"}
46
- analyzer_results = [MagicMock(), MagicMock()]
47
- self.analyzer.analyze_data = AsyncMock(return_value=analyzer_results)
48
- self.scanner.cache_result = AsyncMock()
49
-
50
- await self.scanner._process_message(valid_data)
51
-
52
- self.analyzer.analyze_data.assert_called_once_with(valid_data)
53
- self.scanner.cache_result.assert_any_call(analyzer_results[0])
54
- self.scanner.cache_result.assert_any_call(analyzer_results[1])
55
- self.assertEqual(self.scanner.processed, 1)
File without changes