tokenator 0.1.8__tar.gz → 0.1.10__tar.gz

Sign up to get free protection for your applications and to get access to all the features.
Files changed (22) hide show
  1. {tokenator-0.1.8 → tokenator-0.1.10}/PKG-INFO +40 -13
  2. {tokenator-0.1.8 → tokenator-0.1.10}/README.md +39 -12
  3. {tokenator-0.1.8 → tokenator-0.1.10}/pyproject.toml +4 -3
  4. {tokenator-0.1.8 → tokenator-0.1.10}/src/tokenator/__init__.py +3 -3
  5. tokenator-0.1.10/src/tokenator/anthropic/client_anthropic.py +155 -0
  6. tokenator-0.1.10/src/tokenator/anthropic/stream_interceptors.py +146 -0
  7. {tokenator-0.1.8 → tokenator-0.1.10}/src/tokenator/base_wrapper.py +26 -13
  8. {tokenator-0.1.8 → tokenator-0.1.10}/src/tokenator/create_migrations.py +6 -5
  9. {tokenator-0.1.8 → tokenator-0.1.10}/src/tokenator/migrations/env.py +5 -4
  10. tokenator-0.1.10/src/tokenator/migrations/versions/f6f1f2437513_initial_migration.py +51 -0
  11. {tokenator-0.1.8 → tokenator-0.1.10}/src/tokenator/migrations.py +9 -6
  12. {tokenator-0.1.8 → tokenator-0.1.10}/src/tokenator/models.py +15 -4
  13. tokenator-0.1.10/src/tokenator/openai/client_openai.py +163 -0
  14. tokenator-0.1.10/src/tokenator/openai/stream_interceptors.py +146 -0
  15. {tokenator-0.1.8 → tokenator-0.1.10}/src/tokenator/schemas.py +26 -27
  16. {tokenator-0.1.8 → tokenator-0.1.10}/src/tokenator/usage.py +114 -47
  17. {tokenator-0.1.8 → tokenator-0.1.10}/src/tokenator/utils.py +14 -9
  18. tokenator-0.1.8/src/tokenator/client_anthropic.py +0 -148
  19. tokenator-0.1.8/src/tokenator/client_openai.py +0 -151
  20. tokenator-0.1.8/src/tokenator/migrations/versions/f6f1f2437513_initial_migration.py +0 -49
  21. {tokenator-0.1.8 → tokenator-0.1.10}/LICENSE +0 -0
  22. {tokenator-0.1.8 → tokenator-0.1.10}/src/tokenator/migrations/script.py.mako +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: tokenator
3
- Version: 0.1.8
3
+ Version: 0.1.10
4
4
  Summary: Token usage tracking wrapper for LLMs
5
5
  License: MIT
6
6
  Author: Ujjwal Maheshwari
@@ -27,7 +27,7 @@ Have you ever wondered about :
27
27
  - How much does it cost to do run a complex AI workflow with multiple LLM providers?
28
28
  - How much money did I spent today on development?
29
29
 
30
- Afraid not, tokenator is here! With tokenator's easy to use API, you can start tracking LLM usage in a matter of minutes and track your LLM usage.
30
+ Afraid not, tokenator is here! With tokenator's easy to use API, you can start tracking LLM usage in a matter of minutes.
31
31
 
32
32
  Get started with just 3 lines of code!
33
33
 
@@ -60,27 +60,54 @@ response = client.chat.completions.create(
60
60
  ### Cost Analysis
61
61
 
62
62
  ```python
63
- from tokenator import cost
63
+ from tokenator import usage
64
64
 
65
65
  # Get usage for different time periods
66
- cost.last_hour()
67
- cost.last_day()
68
- cost.last_week()
69
- cost.last_month()
66
+ usage.last_hour()
67
+ usage.last_day()
68
+ usage.last_week()
69
+ usage.last_month()
70
70
 
71
71
  # Custom date range
72
- cost.between("2024-03-01", "2024-03-15")
72
+ usage.between("2024-03-01", "2024-03-15")
73
73
 
74
74
  # Get usage for different LLM providers
75
- cost.last_day("openai")
76
- cost.last_day("anthropic")
77
- cost.last_day("google")
75
+ usage.last_day("openai")
76
+ usage.last_day("anthropic")
77
+ usage.last_day("google")
78
78
  ```
79
79
 
80
- ### Example `cost` object
80
+ ### Example `usage` object
81
81
 
82
- ```json
82
+ ```python
83
+ print(cost.last_hour().model_dump_json(indent=4))
84
+ ```
83
85
 
86
+ ```json
87
+ {
88
+ "total_cost": 0.0004,
89
+ "total_tokens": 79,
90
+ "prompt_tokens": 52,
91
+ "completion_tokens": 27,
92
+ "providers": [
93
+ {
94
+ "total_cost": 0.0004,
95
+ "total_tokens": 79,
96
+ "prompt_tokens": 52,
97
+ "completion_tokens": 27,
98
+ "provider": "openai",
99
+ "models": [
100
+ {
101
+ "total_cost": 0.0004,
102
+ "total_tokens": 79,
103
+ "prompt_tokens": 52,
104
+ "completion_tokens": 27,
105
+ "model": "gpt-4o-2024-08-06"
106
+ }
107
+ ]
108
+ }
109
+ ]
110
+ }
84
111
  ```
85
112
 
86
113
  ## Features
@@ -5,7 +5,7 @@ Have you ever wondered about :
5
5
  - How much does it cost to do run a complex AI workflow with multiple LLM providers?
6
6
  - How much money did I spent today on development?
7
7
 
8
- Afraid not, tokenator is here! With tokenator's easy to use API, you can start tracking LLM usage in a matter of minutes and track your LLM usage.
8
+ Afraid not, tokenator is here! With tokenator's easy to use API, you can start tracking LLM usage in a matter of minutes.
9
9
 
10
10
  Get started with just 3 lines of code!
11
11
 
@@ -38,27 +38,54 @@ response = client.chat.completions.create(
38
38
  ### Cost Analysis
39
39
 
40
40
  ```python
41
- from tokenator import cost
41
+ from tokenator import usage
42
42
 
43
43
  # Get usage for different time periods
44
- cost.last_hour()
45
- cost.last_day()
46
- cost.last_week()
47
- cost.last_month()
44
+ usage.last_hour()
45
+ usage.last_day()
46
+ usage.last_week()
47
+ usage.last_month()
48
48
 
49
49
  # Custom date range
50
- cost.between("2024-03-01", "2024-03-15")
50
+ usage.between("2024-03-01", "2024-03-15")
51
51
 
52
52
  # Get usage for different LLM providers
53
- cost.last_day("openai")
54
- cost.last_day("anthropic")
55
- cost.last_day("google")
53
+ usage.last_day("openai")
54
+ usage.last_day("anthropic")
55
+ usage.last_day("google")
56
56
  ```
57
57
 
58
- ### Example `cost` object
58
+ ### Example `usage` object
59
59
 
60
- ```json
60
+ ```python
61
+ print(cost.last_hour().model_dump_json(indent=4))
62
+ ```
61
63
 
64
+ ```json
65
+ {
66
+ "total_cost": 0.0004,
67
+ "total_tokens": 79,
68
+ "prompt_tokens": 52,
69
+ "completion_tokens": 27,
70
+ "providers": [
71
+ {
72
+ "total_cost": 0.0004,
73
+ "total_tokens": 79,
74
+ "prompt_tokens": 52,
75
+ "completion_tokens": 27,
76
+ "provider": "openai",
77
+ "models": [
78
+ {
79
+ "total_cost": 0.0004,
80
+ "total_tokens": 79,
81
+ "prompt_tokens": 52,
82
+ "completion_tokens": 27,
83
+ "model": "gpt-4o-2024-08-06"
84
+ }
85
+ ]
86
+ }
87
+ ]
88
+ }
62
89
  ```
63
90
 
64
91
  ## Features
@@ -1,6 +1,6 @@
1
1
  [tool.poetry]
2
2
  name = "tokenator"
3
- version = "0.1.8"
3
+ version = "0.1.10"
4
4
  description = "Token usage tracking wrapper for LLMs"
5
5
  authors = ["Ujjwal Maheshwari <your.email@example.com>"]
6
6
  readme = "README.md"
@@ -19,11 +19,12 @@ anthropic = "^0.40.0"
19
19
  pytest = "^8.0.0"
20
20
  pytest-asyncio = "^0.23.0"
21
21
  pytest-cov = "^4.1.0"
22
+ ruff = "^0.8.4"
22
23
 
23
24
  [build-system]
24
25
  requires = ["poetry-core"]
25
26
  build-backend = "poetry.core.masonry.api"
26
27
 
27
28
  [tool.pytest.ini_options]
28
- pythonpath = ["src"]
29
- asyncio_mode = "auto"
29
+ testpaths = ["tests"]
30
+ pythonpath = "src"
@@ -1,8 +1,8 @@
1
1
  """Tokenator - Track and analyze your OpenAI API token usage and costs."""
2
2
 
3
3
  import logging
4
- from .client_openai import tokenator_openai
5
- from .client_anthropic import tokenator_anthropic
4
+ from .openai.client_openai import tokenator_openai
5
+ from .anthropic.client_anthropic import tokenator_anthropic
6
6
  from . import usage
7
7
  from .utils import get_default_db_path
8
8
  from .migrations import check_and_run_migrations
@@ -15,4 +15,4 @@ logger = logging.getLogger(__name__)
15
15
  try:
16
16
  check_and_run_migrations()
17
17
  except Exception as e:
18
- logger.warning(f"Failed to run migrations, but continuing anyway: {e}")
18
+ logger.warning(f"Failed to run migrations, but continuing anyway: {e}")
@@ -0,0 +1,155 @@
1
+ """Anthropic client wrapper with token usage tracking."""
2
+
3
+ from typing import Any, Optional, Union, overload, Iterator, AsyncIterator
4
+ import logging
5
+
6
+ from anthropic import Anthropic, AsyncAnthropic
7
+ from anthropic.types import Message, RawMessageStartEvent, RawMessageDeltaEvent
8
+
9
+ from ..models import Usage, TokenUsageStats
10
+ from ..base_wrapper import BaseWrapper, ResponseType
11
+ from .stream_interceptors import AnthropicAsyncStreamInterceptor, AnthropicSyncStreamInterceptor
12
+
13
+ logger = logging.getLogger(__name__)
14
+
15
+
16
+ class BaseAnthropicWrapper(BaseWrapper):
17
+ provider = "anthropic"
18
+
19
+ def _process_response_usage(
20
+ self, response: ResponseType
21
+ ) -> Optional[TokenUsageStats]:
22
+ """Process and log usage statistics from a response."""
23
+ try:
24
+ if isinstance(response, Message):
25
+ if not hasattr(response, "usage"):
26
+ return None
27
+ usage = Usage(
28
+ prompt_tokens=response.usage.input_tokens,
29
+ completion_tokens=response.usage.output_tokens,
30
+ total_tokens=response.usage.input_tokens
31
+ + response.usage.output_tokens,
32
+ )
33
+ return TokenUsageStats(model=response.model, usage=usage)
34
+ elif isinstance(response, dict):
35
+ usage_dict = response.get("usage")
36
+ if not usage_dict:
37
+ return None
38
+ usage = Usage(
39
+ prompt_tokens=usage_dict.get("input_tokens", 0),
40
+ completion_tokens=usage_dict.get("output_tokens", 0),
41
+ total_tokens=usage_dict.get("input_tokens", 0)
42
+ + usage_dict.get("output_tokens", 0),
43
+ )
44
+ return TokenUsageStats(
45
+ model=response.get("model", "unknown"), usage=usage
46
+ )
47
+ except Exception as e:
48
+ logger.warning("Failed to process usage stats: %s", str(e))
49
+ return None
50
+ return None
51
+
52
+ @property
53
+ def messages(self):
54
+ return self
55
+
56
+
57
+ def _create_usage_callback(execution_id, log_usage_fn):
58
+ """Creates a callback function for processing usage statistics from stream chunks."""
59
+ def usage_callback(chunks):
60
+ if not chunks:
61
+ return
62
+
63
+ usage_data = TokenUsageStats(
64
+ model=chunks[0].message.model if isinstance(chunks[0], RawMessageStartEvent) else "",
65
+ usage=Usage(),
66
+ )
67
+
68
+ for chunk in chunks:
69
+ if isinstance(chunk, RawMessageStartEvent):
70
+ usage_data.model = chunk.message.model
71
+ usage_data.usage.prompt_tokens += chunk.message.usage.input_tokens
72
+ usage_data.usage.completion_tokens += chunk.message.usage.output_tokens
73
+ elif isinstance(chunk, RawMessageDeltaEvent):
74
+ usage_data.usage.prompt_tokens += chunk.usage.input_tokens
75
+ usage_data.usage.completion_tokens += chunk.usage.output_tokens
76
+
77
+ usage_data.usage.total_tokens = usage_data.usage.prompt_tokens + usage_data.usage.completion_tokens
78
+ log_usage_fn(usage_data, execution_id=execution_id)
79
+
80
+ return usage_callback
81
+
82
+
83
+ class AnthropicWrapper(BaseAnthropicWrapper):
84
+ def create(
85
+ self, *args: Any, execution_id: Optional[str] = None, **kwargs: Any
86
+ ) -> Union[Message, Iterator[Message]]:
87
+ """Create a message completion and log token usage."""
88
+ logger.debug("Creating message completion with args: %s, kwargs: %s", args, kwargs)
89
+
90
+ if kwargs.get("stream", False):
91
+ base_stream = self.client.messages.create(*args, **kwargs)
92
+ return AnthropicSyncStreamInterceptor(
93
+ base_stream=base_stream,
94
+ usage_callback=_create_usage_callback(execution_id, self._log_usage),
95
+ )
96
+
97
+ response = self.client.messages.create(*args, **kwargs)
98
+ usage_data = self._process_response_usage(response)
99
+ if usage_data:
100
+ self._log_usage(usage_data, execution_id=execution_id)
101
+ return response
102
+
103
+
104
+ class AsyncAnthropicWrapper(BaseAnthropicWrapper):
105
+ async def create(
106
+ self, *args: Any, execution_id: Optional[str] = None, **kwargs: Any
107
+ ) -> Union[Message, AsyncIterator[Message]]:
108
+ """Create a message completion and log token usage."""
109
+ logger.debug("Creating message completion with args: %s, kwargs: %s", args, kwargs)
110
+
111
+ if kwargs.get("stream", False):
112
+ base_stream = await self.client.messages.create(*args, **kwargs)
113
+ return AnthropicAsyncStreamInterceptor(
114
+ base_stream=base_stream,
115
+ usage_callback=_create_usage_callback(execution_id, self._log_usage),
116
+ )
117
+
118
+ response = await self.client.messages.create(*args, **kwargs)
119
+ usage_data = self._process_response_usage(response)
120
+ if usage_data:
121
+ self._log_usage(usage_data, execution_id=execution_id)
122
+ return response
123
+
124
+
125
+ @overload
126
+ def tokenator_anthropic(
127
+ client: Anthropic,
128
+ db_path: Optional[str] = None,
129
+ ) -> AnthropicWrapper: ...
130
+
131
+
132
+ @overload
133
+ def tokenator_anthropic(
134
+ client: AsyncAnthropic,
135
+ db_path: Optional[str] = None,
136
+ ) -> AsyncAnthropicWrapper: ...
137
+
138
+
139
+ def tokenator_anthropic(
140
+ client: Union[Anthropic, AsyncAnthropic],
141
+ db_path: Optional[str] = None,
142
+ ) -> Union[AnthropicWrapper, AsyncAnthropicWrapper]:
143
+ """Create a token-tracking wrapper for an Anthropic client.
144
+
145
+ Args:
146
+ client: Anthropic or AsyncAnthropic client instance
147
+ db_path: Optional path to SQLite database for token tracking
148
+ """
149
+ if isinstance(client, Anthropic):
150
+ return AnthropicWrapper(client=client, db_path=db_path)
151
+
152
+ if isinstance(client, AsyncAnthropic):
153
+ return AsyncAnthropicWrapper(client=client, db_path=db_path)
154
+
155
+ raise ValueError("Client must be an instance of Anthropic or AsyncAnthropic")
@@ -0,0 +1,146 @@
1
+ import logging
2
+ from typing import AsyncIterator, Callable, List, Optional, TypeVar, Iterator
3
+
4
+ from anthropic import AsyncStream, Stream
5
+
6
+ logger = logging.getLogger(__name__)
7
+
8
+ _T = TypeVar("_T")
9
+
10
+
11
+ class AnthropicAsyncStreamInterceptor(AsyncStream[_T]):
12
+ """
13
+ A wrapper around anthropic.AsyncStream that delegates all functionality
14
+ to the 'base_stream' but intercepts each chunk to handle usage or
15
+ logging logic. This preserves .response and other methods.
16
+
17
+ You can store aggregated usage in a local list and process it when
18
+ the stream ends (StopAsyncIteration).
19
+ """
20
+
21
+ def __init__(
22
+ self,
23
+ base_stream: AsyncStream[_T],
24
+ usage_callback: Optional[Callable[[List[_T]], None]] = None,
25
+ ):
26
+ # We do NOT call super().__init__() because anthropic.AsyncStream
27
+ # expects constructor parameters we don't want to re-initialize.
28
+ # Instead, we just store the base_stream and delegate everything to it.
29
+ self._base_stream = base_stream
30
+ self._usage_callback = usage_callback
31
+ self._chunks: List[_T] = []
32
+
33
+ @property
34
+ def response(self):
35
+ """Expose the original stream's 'response' so user code can do stream.response, etc."""
36
+ return self._base_stream.response
37
+
38
+ def __aiter__(self) -> AsyncIterator[_T]:
39
+ """
40
+ Called when we do 'async for chunk in wrapped_stream:'
41
+ We simply return 'self'. Then __anext__ does the rest.
42
+ """
43
+ return self
44
+
45
+ async def __anext__(self) -> _T:
46
+ """
47
+ Intercept iteration. We pull the next chunk from the base_stream.
48
+ If it's the end, do any final usage logging, then raise StopAsyncIteration.
49
+ Otherwise, we can accumulate usage info or do whatever we need with the chunk.
50
+ """
51
+ try:
52
+ chunk = await self._base_stream.__anext__()
53
+ except StopAsyncIteration:
54
+ # Once the base stream is fully consumed, we can do final usage/logging.
55
+ if self._usage_callback and self._chunks:
56
+ self._usage_callback(self._chunks)
57
+ raise
58
+
59
+ # Intercept each chunk
60
+ self._chunks.append(chunk)
61
+ return chunk
62
+
63
+ async def __aenter__(self) -> "AnthropicAsyncStreamInterceptor[_T]":
64
+ """Support async with ... : usage."""
65
+ await self._base_stream.__aenter__()
66
+ return self
67
+
68
+ async def __aexit__(self, exc_type, exc_val, exc_tb):
69
+ """
70
+ Ensure we propagate __aexit__ to the base stream,
71
+ so connections are properly closed.
72
+ """
73
+ return await self._base_stream.__aexit__(exc_type, exc_val, exc_tb)
74
+
75
+ async def close(self) -> None:
76
+ """Delegate close to the base_stream."""
77
+ await self._base_stream.close()
78
+
79
+
80
+ class AnthropicSyncStreamInterceptor(Stream[_T]):
81
+ """
82
+ A wrapper around anthropic.Stream that delegates all functionality
83
+ to the 'base_stream' but intercepts each chunk to handle usage or
84
+ logging logic. This preserves .response and other methods.
85
+
86
+ You can store aggregated usage in a local list and process it when
87
+ the stream ends (StopIteration).
88
+ """
89
+
90
+ def __init__(
91
+ self,
92
+ base_stream: Stream[_T],
93
+ usage_callback: Optional[Callable[[List[_T]], None]] = None,
94
+ ):
95
+ # We do NOT call super().__init__() because openai.SyncStream
96
+ # expects constructor parameters we don't want to re-initialize.
97
+ # Instead, we just store the base_stream and delegate everything to it.
98
+ self._base_stream = base_stream
99
+ self._usage_callback = usage_callback
100
+ self._chunks: List[_T] = []
101
+
102
+ @property
103
+ def response(self):
104
+ """Expose the original stream's 'response' so user code can do stream.response, etc."""
105
+ return self._base_stream.response
106
+
107
+ def __iter__(self) -> Iterator[_T]:
108
+ """
109
+ Called when we do 'for chunk in wrapped_stream:'
110
+ We simply return 'self'. Then __next__ does the rest.
111
+ """
112
+ return self
113
+
114
+ def __next__(self) -> _T:
115
+ """
116
+ Intercept iteration. We pull the next chunk from the base_stream.
117
+ If it's the end, do any final usage logging, then raise StopIteration.
118
+ Otherwise, we can accumulate usage info or do whatever we need with the chunk.
119
+ """
120
+ try:
121
+ chunk = self._base_stream.__next__()
122
+ except StopIteration:
123
+ # Once the base stream is fully consumed, we can do final usage/logging.
124
+ if self._usage_callback and self._chunks:
125
+ self._usage_callback(self._chunks)
126
+ raise
127
+
128
+ # Intercept each chunk
129
+ self._chunks.append(chunk)
130
+ return chunk
131
+
132
+ def __enter__(self) -> "AnthropicSyncStreamInterceptor[_T]":
133
+ """Support with ... : usage."""
134
+ self._base_stream.__enter__()
135
+ return self
136
+
137
+ def __exit__(self, exc_type, exc_val, exc_tb):
138
+ """
139
+ Ensure we propagate __aexit__ to the base stream,
140
+ so connections are properly closed.
141
+ """
142
+ return self._base_stream.__exit__(exc_type, exc_val, exc_tb)
143
+
144
+ async def close(self) -> None:
145
+ """Delegate close to the base_stream."""
146
+ self._base_stream.close()
@@ -1,16 +1,17 @@
1
1
  """Base wrapper class for token usage tracking."""
2
2
 
3
3
  from pathlib import Path
4
- from typing import Any, Dict, Optional, TypeVar, Union
4
+ from typing import Any, Optional, TypeVar
5
5
  import logging
6
6
  import uuid
7
7
 
8
- from .models import Usage, TokenUsageStats
8
+ from .models import TokenUsageStats
9
9
  from .schemas import get_session, TokenUsage
10
10
 
11
11
  logger = logging.getLogger(__name__)
12
12
 
13
- ResponseType = TypeVar('ResponseType')
13
+ ResponseType = TypeVar("ResponseType")
14
+
14
15
 
15
16
  class BaseWrapper:
16
17
  def __init__(self, client: Any, db_path: Optional[str] = None):
@@ -22,13 +23,20 @@ class BaseWrapper:
22
23
  logger.info("Created database directory at: %s", Path(db_path).parent)
23
24
 
24
25
  self.Session = get_session(db_path)
25
-
26
- logger.debug("Initializing %s with db_path: %s",
27
- self.__class__.__name__, db_path)
28
26
 
29
- def _log_usage_impl(self, token_usage_stats: TokenUsageStats, session, execution_id: str) -> None:
27
+ logger.debug(
28
+ "Initializing %s with db_path: %s", self.__class__.__name__, db_path
29
+ )
30
+
31
+ def _log_usage_impl(
32
+ self, token_usage_stats: TokenUsageStats, session, execution_id: str
33
+ ) -> None:
30
34
  """Implementation of token usage logging."""
31
- logger.debug("Logging usage for model %s: %s", token_usage_stats.model, token_usage_stats.usage.model_dump())
35
+ logger.debug(
36
+ "Logging usage for model %s: %s",
37
+ token_usage_stats.model,
38
+ token_usage_stats.usage.model_dump(),
39
+ )
32
40
  try:
33
41
  token_usage = TokenUsage(
34
42
  execution_id=execution_id,
@@ -36,15 +44,20 @@ class BaseWrapper:
36
44
  model=token_usage_stats.model,
37
45
  prompt_tokens=token_usage_stats.usage.prompt_tokens,
38
46
  completion_tokens=token_usage_stats.usage.completion_tokens,
39
- total_tokens=token_usage_stats.usage.total_tokens
47
+ total_tokens=token_usage_stats.usage.total_tokens,
40
48
  )
41
49
  session.add(token_usage)
42
- logger.info("Logged token usage: model=%s, total_tokens=%d",
43
- token_usage_stats.model, token_usage_stats.usage.total_tokens)
50
+ logger.info(
51
+ "Logged token usage: model=%s, total_tokens=%d",
52
+ token_usage_stats.model,
53
+ token_usage_stats.usage.total_tokens,
54
+ )
44
55
  except Exception as e:
45
56
  logger.error("Failed to log token usage: %s", str(e))
46
57
 
47
- def _log_usage(self, token_usage_stats: TokenUsageStats, execution_id: Optional[str] = None):
58
+ def _log_usage(
59
+ self, token_usage_stats: TokenUsageStats, execution_id: Optional[str] = None
60
+ ):
48
61
  """Log token usage to database."""
49
62
  if not execution_id:
50
63
  execution_id = str(uuid.uuid4())
@@ -58,4 +71,4 @@ class BaseWrapper:
58
71
  logger.error("Failed to log token usage: %s", str(e))
59
72
  session.rollback()
60
73
  finally:
61
- session.close()
74
+ session.close()
@@ -1,25 +1,26 @@
1
1
  """Development utilities for tokenator."""
2
2
 
3
- import os
4
3
  import sys
5
4
  from pathlib import Path
6
5
  from alembic import command
7
6
  from tokenator.migrations import get_alembic_config
8
7
 
8
+
9
9
  def create_migration(message: str):
10
10
  """Create a new migration based on model changes."""
11
11
  config = get_alembic_config()
12
-
12
+
13
13
  # Get the migrations directory
14
14
  migrations_dir = Path(__file__).parent / "migrations" / "versions"
15
15
  migrations_dir.mkdir(parents=True, exist_ok=True)
16
-
16
+
17
17
  # Generate migration with custom message
18
- command.revision(config, autogenerate=True, message=message)
18
+ command.revision(config, autogenerate=True, message=message)
19
+
19
20
 
20
21
  if __name__ == "__main__":
21
22
  if len(sys.argv) > 1:
22
23
  msg = " ".join(sys.argv[1:])
23
24
  else:
24
25
  msg = "auto generated migration"
25
- create_migration(msg)
26
+ create_migration(msg)
@@ -18,6 +18,7 @@ if config.config_file_name is not None:
18
18
  # add your model's MetaData object here
19
19
  target_metadata = Base.metadata
20
20
 
21
+
21
22
  def run_migrations_offline() -> None:
22
23
  """Run migrations in 'offline' mode."""
23
24
  url = config.get_main_option("sqlalchemy.url")
@@ -31,6 +32,7 @@ def run_migrations_offline() -> None:
31
32
  with context.begin_transaction():
32
33
  context.run_migrations()
33
34
 
35
+
34
36
  def run_migrations_online() -> None:
35
37
  """Run migrations in 'online' mode."""
36
38
  connectable = engine_from_config(
@@ -40,14 +42,13 @@ def run_migrations_online() -> None:
40
42
  )
41
43
 
42
44
  with connectable.connect() as connection:
43
- context.configure(
44
- connection=connection, target_metadata=target_metadata
45
- )
45
+ context.configure(connection=connection, target_metadata=target_metadata)
46
46
 
47
47
  with context.begin_transaction():
48
48
  context.run_migrations()
49
49
 
50
+
50
51
  if context.is_offline_mode():
51
52
  run_migrations_offline()
52
53
  else:
53
- run_migrations_online()
54
+ run_migrations_online()