kuhl-haus-mdp 0.1.10__tar.gz → 0.1.11__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 (47) hide show
  1. {kuhl_haus_mdp-0.1.10 → kuhl_haus_mdp-0.1.11}/PKG-INFO +1 -1
  2. {kuhl_haus_mdp-0.1.10 → kuhl_haus_mdp-0.1.11}/pyproject.toml +1 -1
  3. {kuhl_haus_mdp-0.1.10 → kuhl_haus_mdp-0.1.11}/src/kuhl_haus/mdp/analyzers/top_stocks.py +3 -0
  4. {kuhl_haus_mdp-0.1.10 → kuhl_haus_mdp-0.1.11}/src/kuhl_haus/mdp/components/market_data_cache.py +22 -0
  5. {kuhl_haus_mdp-0.1.10 → kuhl_haus_mdp-0.1.11}/src/kuhl_haus/mdp/models/market_data_cache_ttl.py +0 -1
  6. {kuhl_haus_mdp-0.1.10 → kuhl_haus_mdp-0.1.11}/tests/components/test_market_data_cache.py +102 -1
  7. {kuhl_haus_mdp-0.1.10 → kuhl_haus_mdp-0.1.11}/LICENSE.txt +0 -0
  8. {kuhl_haus_mdp-0.1.10 → kuhl_haus_mdp-0.1.11}/README.md +0 -0
  9. {kuhl_haus_mdp-0.1.10 → kuhl_haus_mdp-0.1.11}/src/kuhl_haus/mdp/__init__.py +0 -0
  10. {kuhl_haus_mdp-0.1.10 → kuhl_haus_mdp-0.1.11}/src/kuhl_haus/mdp/analyzers/__init__.py +0 -0
  11. {kuhl_haus_mdp-0.1.10 → kuhl_haus_mdp-0.1.11}/src/kuhl_haus/mdp/analyzers/analyzer.py +0 -0
  12. {kuhl_haus_mdp-0.1.10 → kuhl_haus_mdp-0.1.11}/src/kuhl_haus/mdp/analyzers/massive_data_analyzer.py +0 -0
  13. {kuhl_haus_mdp-0.1.10 → kuhl_haus_mdp-0.1.11}/src/kuhl_haus/mdp/components/__init__.py +0 -0
  14. {kuhl_haus_mdp-0.1.10 → kuhl_haus_mdp-0.1.11}/src/kuhl_haus/mdp/components/market_data_scanner.py +0 -0
  15. {kuhl_haus_mdp-0.1.10 → kuhl_haus_mdp-0.1.11}/src/kuhl_haus/mdp/components/widget_data_service.py +0 -0
  16. {kuhl_haus_mdp-0.1.10 → kuhl_haus_mdp-0.1.11}/src/kuhl_haus/mdp/helpers/__init__.py +0 -0
  17. {kuhl_haus_mdp-0.1.10 → kuhl_haus_mdp-0.1.11}/src/kuhl_haus/mdp/helpers/process_manager.py +0 -0
  18. {kuhl_haus_mdp-0.1.10 → kuhl_haus_mdp-0.1.11}/src/kuhl_haus/mdp/helpers/queue_name_resolver.py +0 -0
  19. {kuhl_haus_mdp-0.1.10 → kuhl_haus_mdp-0.1.11}/src/kuhl_haus/mdp/helpers/utils.py +0 -0
  20. {kuhl_haus_mdp-0.1.10 → kuhl_haus_mdp-0.1.11}/src/kuhl_haus/mdp/integ/__init__.py +0 -0
  21. {kuhl_haus_mdp-0.1.10 → kuhl_haus_mdp-0.1.11}/src/kuhl_haus/mdp/integ/massive_data_listener.py +0 -0
  22. {kuhl_haus_mdp-0.1.10 → kuhl_haus_mdp-0.1.11}/src/kuhl_haus/mdp/integ/massive_data_processor.py +0 -0
  23. {kuhl_haus_mdp-0.1.10 → kuhl_haus_mdp-0.1.11}/src/kuhl_haus/mdp/integ/massive_data_queues.py +0 -0
  24. {kuhl_haus_mdp-0.1.10 → kuhl_haus_mdp-0.1.11}/src/kuhl_haus/mdp/integ/web_socket_message_serde.py +0 -0
  25. {kuhl_haus_mdp-0.1.10 → kuhl_haus_mdp-0.1.11}/src/kuhl_haus/mdp/models/__init__.py +0 -0
  26. {kuhl_haus_mdp-0.1.10 → kuhl_haus_mdp-0.1.11}/src/kuhl_haus/mdp/models/constants.py +0 -0
  27. {kuhl_haus_mdp-0.1.10 → kuhl_haus_mdp-0.1.11}/src/kuhl_haus/mdp/models/market_data_analyzer_result.py +0 -0
  28. {kuhl_haus_mdp-0.1.10 → kuhl_haus_mdp-0.1.11}/src/kuhl_haus/mdp/models/market_data_cache_keys.py +0 -0
  29. {kuhl_haus_mdp-0.1.10 → kuhl_haus_mdp-0.1.11}/src/kuhl_haus/mdp/models/market_data_pubsub_keys.py +0 -0
  30. {kuhl_haus_mdp-0.1.10 → kuhl_haus_mdp-0.1.11}/src/kuhl_haus/mdp/models/market_data_scanner_names.py +0 -0
  31. {kuhl_haus_mdp-0.1.10 → kuhl_haus_mdp-0.1.11}/src/kuhl_haus/mdp/models/massive_data_queue.py +0 -0
  32. {kuhl_haus_mdp-0.1.10 → kuhl_haus_mdp-0.1.11}/src/kuhl_haus/mdp/models/top_stocks_cache_item.py +0 -0
  33. {kuhl_haus_mdp-0.1.10 → kuhl_haus_mdp-0.1.11}/tests/__init__.py +0 -0
  34. {kuhl_haus_mdp-0.1.10 → kuhl_haus_mdp-0.1.11}/tests/analyzers/__init__.py +0 -0
  35. {kuhl_haus_mdp-0.1.10 → kuhl_haus_mdp-0.1.11}/tests/analyzers/test_massive_data_analyzer.py +0 -0
  36. {kuhl_haus_mdp-0.1.10 → kuhl_haus_mdp-0.1.11}/tests/analyzers/test_top_stocks_rehydrate.py +0 -0
  37. {kuhl_haus_mdp-0.1.10 → kuhl_haus_mdp-0.1.11}/tests/components/__init__.py +0 -0
  38. {kuhl_haus_mdp-0.1.10 → kuhl_haus_mdp-0.1.11}/tests/components/test_market_data_scanner.py +0 -0
  39. {kuhl_haus_mdp-0.1.10 → kuhl_haus_mdp-0.1.11}/tests/components/test_widget_data_service.py +0 -0
  40. {kuhl_haus_mdp-0.1.10 → kuhl_haus_mdp-0.1.11}/tests/helpers/__init__.py +0 -0
  41. {kuhl_haus_mdp-0.1.10 → kuhl_haus_mdp-0.1.11}/tests/helpers/test_process_manager.py +0 -0
  42. {kuhl_haus_mdp-0.1.10 → kuhl_haus_mdp-0.1.11}/tests/helpers/test_queue_name_resolver.py +0 -0
  43. {kuhl_haus_mdp-0.1.10 → kuhl_haus_mdp-0.1.11}/tests/helpers/test_utils.py +0 -0
  44. {kuhl_haus_mdp-0.1.10 → kuhl_haus_mdp-0.1.11}/tests/integ/__init__.py +0 -0
  45. {kuhl_haus_mdp-0.1.10 → kuhl_haus_mdp-0.1.11}/tests/integ/test_web_socket_message_serde.py +0 -0
  46. {kuhl_haus_mdp-0.1.10 → kuhl_haus_mdp-0.1.11}/tests/models/__init__.py +0 -0
  47. {kuhl_haus_mdp-0.1.10 → kuhl_haus_mdp-0.1.11}/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.10
3
+ Version: 0.1.11
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)
@@ -30,7 +30,7 @@ dependencies = [
30
30
  "uvicorn[standard]",
31
31
  "websockets",
32
32
  ]
33
- version = "0.1.10"
33
+ version = "0.1.11"
34
34
 
35
35
  [project.license]
36
36
  file = "LICENSE.txt"
@@ -136,6 +136,9 @@ class TopStocksAnalyzer(Analyzer):
136
136
  prev_day_volume = snapshot.prev_day.volume
137
137
  prev_day_vwap = snapshot.prev_day.vwap
138
138
  break
139
+ except KeyError:
140
+ retry_count += 1
141
+ await self.cache.delete_ticker_snapshot(event.symbol)
139
142
  except Exception as e:
140
143
  self.logger.error(f"Failed to get snapshot for {event.symbol}: {e}")
141
144
  retry_count += 1
@@ -26,6 +26,18 @@ class MarketDataCache:
26
26
  self.redis_client = redis_client
27
27
  self.http_session = None
28
28
 
29
+ async def delete_cache(self, cache_key: str):
30
+ """
31
+ Delete cache entry.
32
+
33
+ :arg cache_key: Cache key to delete
34
+ """
35
+ try:
36
+ await self.redis_client.delete(cache_key)
37
+ self.logger.info(f"Deleted cache entry: {cache_key}")
38
+ except Exception as e:
39
+ self.logger.error(f"Error deleting cache entry: {e}")
40
+
29
41
  async def get_cache(self, cache_key: str) -> Optional[dict]:
30
42
  """Fetch current value from Redis cache (for snapshot requests)."""
31
43
  value = await self.redis_client.get(cache_key)
@@ -44,6 +56,16 @@ class MarketDataCache:
44
56
  await self.redis_client.publish(publish_key, json.dumps(data))
45
57
  self.logger.info(f"Published data for {publish_key}")
46
58
 
59
+ async def delete_ticker_snapshot(self, ticker: str):
60
+ """
61
+ Delete ticker snapshot from cache.
62
+
63
+ :param ticker: symbol of ticker to delete snapshot for
64
+ :return: None
65
+ """
66
+ cache_key = f"{MarketDataCacheKeys.TICKER_SNAPSHOTS.value}:{ticker}"
67
+ await self.delete_cache(cache_key=cache_key)
68
+
47
69
  async def get_ticker_snapshot(self, ticker: str) -> TickerSnapshot:
48
70
  self.logger.info(f"Getting snapshot for {ticker}")
49
71
  cache_key = f"{MarketDataCacheKeys.TICKER_SNAPSHOTS.value}:{ticker}"
@@ -27,4 +27,3 @@ class MarketDataCacheTTL(Enum):
27
27
  TOP_VOLUME_SCANNER = THREE_DAYS
28
28
  TOP_GAINERS_SCANNER = THREE_DAYS
29
29
  TOP_GAPPERS_SCANNER = THREE_DAYS
30
-
@@ -781,4 +781,105 @@ async def test_get_http_session_singleton_behavior(mock_client_session):
781
781
  # Assert
782
782
  mock_client_session.assert_called_once() # Should only be called once
783
783
  assert result1 == result2 == result3 == mock_session # All should return same instance
784
- assert id(result1) == id(result2) == id(result3) # Verify same object in memory
784
+ assert id(result1) == id(result2) == id(result3) # Verify same object in memory
785
+
786
+
787
+ @pytest.mark.asyncio
788
+ async def test_delete_ticker_snapshot_with_valid_ticker_expect_cache_deleted():
789
+ # Arrange
790
+ mock_redis_client = AsyncMock()
791
+ mock_rest_client = MagicMock()
792
+ sut = MarketDataCache(rest_client=mock_rest_client, redis_client=mock_redis_client, massive_api_key="test_key")
793
+ ticker = "TEST"
794
+ cache_key = f"{MarketDataCacheKeys.TICKER_SNAPSHOTS.value}:{ticker}"
795
+
796
+ # Act
797
+ await sut.delete_ticker_snapshot(ticker)
798
+
799
+ # Assert
800
+ mock_redis_client.delete.assert_awaited_once_with(cache_key)
801
+
802
+
803
+ @pytest.mark.asyncio
804
+ async def test_delete_ticker_snapshot_with_empty_ticker_expect_no_side_effect():
805
+ # Arrange
806
+ mock_redis_client = AsyncMock()
807
+ mock_rest_client = MagicMock()
808
+ sut = MarketDataCache(rest_client=mock_rest_client, redis_client=mock_redis_client, massive_api_key="test_key")
809
+ ticker = ""
810
+ cache_key = f"{MarketDataCacheKeys.TICKER_SNAPSHOTS.value}:{ticker}"
811
+
812
+ # Act
813
+ await sut.delete_ticker_snapshot(ticker)
814
+
815
+ # Assert
816
+ mock_redis_client.delete.assert_awaited_once_with(cache_key)
817
+
818
+
819
+ @pytest.mark.asyncio
820
+ @patch("logging.Logger.error")
821
+ async def test_delete_ticker_snapshot_with_redis_error_expect_logged_error(mock_logger):
822
+ # Arrange
823
+ mock_redis_client = AsyncMock()
824
+ mock_redis_client.delete = AsyncMock(side_effect=Exception("Redis connection error"))
825
+ mock_rest_client = MagicMock()
826
+ sut = MarketDataCache(rest_client=mock_rest_client, redis_client=mock_redis_client, massive_api_key="test_key")
827
+ ticker = "TEST"
828
+ cache_key = f"{MarketDataCacheKeys.TICKER_SNAPSHOTS.value}:{ticker}"
829
+
830
+ # Act
831
+ await sut.delete_ticker_snapshot(ticker)
832
+
833
+ # Assert
834
+ mock_redis_client.delete.assert_awaited_once_with(cache_key)
835
+ mock_logger.assert_called_once_with("Error deleting cache entry: Redis connection error")
836
+
837
+
838
+ @pytest.mark.asyncio
839
+ async def test_delete_cache_with_existing_key_expect_cache_deleted():
840
+ # Arrange
841
+ mock_redis_client = AsyncMock()
842
+ mock_rest_client = MagicMock()
843
+ sut = MarketDataCache(rest_client=mock_rest_client, redis_client=mock_redis_client, massive_api_key="test_key")
844
+ test_cache_key = "test:cache:key"
845
+
846
+ # Act
847
+ await sut.delete_cache(test_cache_key)
848
+
849
+ # Assert
850
+ mock_redis_client.delete.assert_awaited_once_with(test_cache_key)
851
+
852
+
853
+ @pytest.mark.asyncio
854
+ @patch("logging.Logger.error")
855
+ async def test_delete_cache_with_redis_error_expect_error_logged(mock_logger):
856
+ # Arrange
857
+ mock_redis_client = AsyncMock()
858
+ mock_redis_client.delete = AsyncMock(side_effect=Exception("Redis connection error"))
859
+ mock_rest_client = MagicMock()
860
+ sut = MarketDataCache(rest_client=mock_rest_client, redis_client=mock_redis_client, massive_api_key="test_key")
861
+ test_cache_key = "test:cache:key"
862
+
863
+ # Act
864
+ await sut.delete_cache(test_cache_key)
865
+
866
+ # Assert
867
+ mock_redis_client.delete.assert_awaited_once_with(test_cache_key)
868
+ mock_logger.assert_called_once_with(f"Error deleting cache entry: Redis connection error")
869
+
870
+
871
+ @pytest.mark.asyncio
872
+ @patch("logging.Logger.info")
873
+ async def test_delete_cache_with_successful_deletion_expect_info_logged(mock_logger):
874
+ # Arrange
875
+ mock_redis_client = AsyncMock()
876
+ mock_rest_client = MagicMock()
877
+ sut = MarketDataCache(rest_client=mock_rest_client, redis_client=mock_redis_client, massive_api_key="test_key")
878
+ test_cache_key = "test:cache:key"
879
+
880
+ # Act
881
+ await sut.delete_cache(test_cache_key)
882
+
883
+ # Assert
884
+ mock_redis_client.delete.assert_awaited_once_with(test_cache_key)
885
+ mock_logger.assert_called_once_with(f"Deleted cache entry: {test_cache_key}")
File without changes