Coverage for tests/test_update_prices.py: 96.64%

117 statements  

« prev     ^ index     » next       coverage.py v7.9.2, created at 2026-09-27 04:08 +0000

1import threading 

2from decimal import Decimal 

3 

4import httpx2 

5import pytest 

6from inline_snapshot import snapshot 

7 

8from voice_prices import ( 

9 UpdatePrices, 

10 Usage, 

11 calc_price, 

12 data_snapshot, 

13 wait_prices_updated_async, 

14 wait_prices_updated_sync, 

15) 

16 

17pytestmark = pytest.mark.anyio 

18 

19PROVIDER_ARRAY_PAYLOAD = ( 

20 b'[{"id":"openai","name":"OpenAI","api_pattern":"https://api\\\\.openai\\\\.com",' 

21 b'"models":[{"id":"gpt-4o","match":{"equals":"gpt-4o"},' 

22 b'"prices":{"input_mtok":2.5,"output_mtok":10}}]}]' 

23) 

24 

25 

26def _mock_update_prices_get(monkeypatch: pytest.MonkeyPatch, content: bytes = PROVIDER_ARRAY_PAYLOAD) -> None: 

27 class Response: 

28 def __init__(self, content: bytes) -> None: 

29 self.content = content 

30 

31 def raise_for_status(self) -> None: 

32 pass 

33 

34 def fake_get(url: str, timeout: httpx2.Timeout) -> Response: 

35 assert url in { 

36 'https://example.test/prices.json', 

37 'https://raw.githubusercontent.com/mahimailabs/voice-prices/refs/heads/main/prices/data.json', 

38 } 

39 assert timeout is not None 

40 return Response(content) 

41 

42 monkeypatch.setattr(httpx2, 'get', fake_get) 

43 

44 

45def test_update_prices_fetch_parses_provider_array(monkeypatch: pytest.MonkeyPatch): 

46 content = ( 

47 b'[{"id":"openai","name":"OpenAI","api_pattern":"https://api\\\\.openai\\\\.com",' 

48 b'"models":[{"id":"gpt-4o-mini","match":{"equals":"gpt-4o-mini"},' 

49 b'"prices":{"input_mtok":0.15,"output_mtok":0.6}}]}]' 

50 ) 

51 

52 _mock_update_prices_get(monkeypatch, content) 

53 

54 snapshot = UpdatePrices(url='https://example.test/prices.json').fetch() 

55 

56 assert snapshot is not None 

57 assert snapshot.from_auto_update is True 

58 provider, model = snapshot.find_provider_model('gpt-4o-mini', None, 'openai', None) 

59 assert provider.id == 'openai' 

60 assert model.id == 'gpt-4o-mini' 

61 

62 

63def test_update_prices_wait_on_start(monkeypatch: pytest.MonkeyPatch): 

64 _mock_update_prices_get(monkeypatch) 

65 assert data_snapshot._custom_snapshot is None 

66 with UpdatePrices() as update_prices: 

67 update_prices.wait() 

68 assert data_snapshot._custom_snapshot is not None 

69 price = calc_price(Usage(input_tokens=1000, output_tokens=100), model_ref='gpt-4o', provider_id='openai') 

70 assert price.input_price == snapshot(Decimal('0.0025')) 

71 assert price.output_price == snapshot(Decimal('0.001')) 

72 assert price.total_price == snapshot(Decimal('0.0035')) 

73 assert price.provider.id == snapshot('openai') 

74 assert price.auto_update_timestamp is not None 

75 

76 

77def test_wait_prices_updated_sync(monkeypatch: pytest.MonkeyPatch): 

78 _mock_update_prices_get(monkeypatch) 

79 assert data_snapshot._custom_snapshot is None 

80 with UpdatePrices(): 

81 wait_prices_updated_sync() 

82 assert data_snapshot._custom_snapshot is not None 

83 price = calc_price(Usage(input_tokens=1000, output_tokens=100), model_ref='gpt-4o', provider_id='openai') 

84 assert price.input_price == snapshot(Decimal('0.0025')) 

85 assert price.output_price == snapshot(Decimal('0.001')) 

86 assert price.total_price == snapshot(Decimal('0.0035')) 

87 assert price.provider.id == snapshot('openai') 

88 assert price.auto_update_timestamp is not None 

89 

90 

91async def test_wait_prices_updated_async(monkeypatch: pytest.MonkeyPatch): 

92 _mock_update_prices_get(monkeypatch) 

93 assert data_snapshot._custom_snapshot is None 

94 with UpdatePrices(): 

95 await wait_prices_updated_async() 

96 assert data_snapshot._custom_snapshot is not None 

97 price = calc_price(Usage(input_tokens=1000, output_tokens=100), model_ref='gpt-4o', provider_id='openai') 

98 assert price.input_price == snapshot(Decimal('0.0025')) 

99 assert price.output_price == snapshot(Decimal('0.001')) 

100 assert price.total_price == snapshot(Decimal('0.0035')) 

101 assert price.provider.id == snapshot('openai') 

102 assert price.auto_update_timestamp is not None 

103 

104 

105def test_update_prices_stop_clears_snapshot_after_in_flight_fetch(monkeypatch: pytest.MonkeyPatch) -> None: 

106 fetch_started = threading.Event() 

107 allow_fetch_return = threading.Event() 

108 stop_errors: list[BaseException] = [] 

109 

110 class Response: 

111 content = PROVIDER_ARRAY_PAYLOAD 

112 

113 def raise_for_status(self) -> None: 

114 pass 

115 

116 def fake_get(url: str, timeout: httpx2.Timeout) -> Response: 

117 assert url == 'https://example.test/prices.json' 

118 assert timeout is not None 

119 fetch_started.set() 

120 assert allow_fetch_return.wait(timeout=5) 

121 return Response() 

122 

123 def stop_update_prices(update_prices: UpdatePrices) -> None: 

124 try: 

125 update_prices.stop() 

126 except BaseException as exc: 

127 stop_errors.append(exc) 

128 

129 monkeypatch.setattr(httpx2, 'get', fake_get) 

130 update_prices = UpdatePrices(url='https://example.test/prices.json', update_interval=3600) 

131 update_prices.start() 

132 try: 

133 assert fetch_started.wait(timeout=5) 

134 

135 stop_thread = threading.Thread(target=stop_update_prices, args=(update_prices,)) 

136 stop_thread.start() 

137 assert update_prices._stop_event.wait(timeout=5) 

138 allow_fetch_return.set() 

139 stop_thread.join(timeout=5) 

140 

141 assert not stop_thread.is_alive() 

142 if stop_errors: 142 ↛ 143line 142 didn't jump to line 143 because the condition on line 142 was never true

143 raise stop_errors[0] 

144 assert data_snapshot._custom_snapshot is None 

145 finally: 

146 allow_fetch_return.set() 

147 update_prices.stop() 

148 data_snapshot.set_custom_snapshot(None) 

149 

150 

151@pytest.mark.default_cassette('fail.yaml') 

152@pytest.mark.vcr() 

153def test_update_prices_failed(): 

154 assert data_snapshot._custom_snapshot is None 

155 with UpdatePrices(url='https://demo-endpoints.pydantic.workers.dev/bin?status=404') as update_prices: 

156 with pytest.raises(httpx2.HTTPStatusError): 

157 update_prices.wait() 

158 assert data_snapshot._custom_snapshot is None 

159 

160 

161@pytest.mark.default_cassette('fail.yaml') 

162@pytest.mark.vcr() 

163def test_update_prices_failed_stop(): 

164 assert data_snapshot._custom_snapshot is None 

165 update_prices = UpdatePrices(url='https://demo-endpoints.pydantic.workers.dev/bin?status=404') 

166 update_prices.start() 

167 with pytest.raises(httpx2.HTTPStatusError): 

168 update_prices.stop() 

169 assert data_snapshot._custom_snapshot is None 

170 

171 

172def test_update_prices_multiple(monkeypatch: pytest.MonkeyPatch): 

173 _mock_update_prices_get(monkeypatch) 

174 with UpdatePrices(): 

175 with pytest.raises( 

176 RuntimeError, 

177 match='UpdatePrices global task already started, only one UpdatePrices can be active at a time', 

178 ): 

179 with UpdatePrices(): 

180 pass