tokenmeter-cli 0.2.0__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.
tokenmeter/__init__.py ADDED
@@ -0,0 +1,19 @@
1
+ """tokenmeter: count tokens and estimate cost for prompts before sending them."""
2
+
3
+ from tokenmeter.meter import Measurement, measure, over_budget, total_cost
4
+ from tokenmeter.pricing import ModelPrice, known_models, price_for
5
+ from tokenmeter.pricing import total_cost as cost
6
+
7
+ __version__ = "0.2.0"
8
+
9
+ __all__ = [
10
+ "Measurement",
11
+ "ModelPrice",
12
+ "__version__",
13
+ "cost",
14
+ "known_models",
15
+ "measure",
16
+ "over_budget",
17
+ "price_for",
18
+ "total_cost",
19
+ ]
tokenmeter/__main__.py ADDED
@@ -0,0 +1,4 @@
1
+ from tokenmeter.cli import entrypoint
2
+
3
+ if __name__ == "__main__":
4
+ entrypoint()
tokenmeter/cli.py ADDED
@@ -0,0 +1,135 @@
1
+ """Command-line interface for tokenmeter."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ import sys
7
+
8
+ import typer
9
+ from rich.console import Console
10
+ from rich.table import Table
11
+
12
+ from tokenmeter import __version__
13
+ from tokenmeter.encoder import EncoderError, encoder_for_model
14
+ from tokenmeter.inputs import read_inputs
15
+ from tokenmeter.meter import measure, over_budget, total_cost
16
+ from tokenmeter.pricing import (
17
+ PRICES_AS_OF,
18
+ UnknownModel,
19
+ known_models,
20
+ price_for,
21
+ )
22
+ from tokenmeter.render import measurements_to_json, render_table
23
+
24
+ app = typer.Typer(
25
+ add_completion=False,
26
+ no_args_is_help=True,
27
+ help="Count tokens and estimate cost for prompts before you send them.",
28
+ )
29
+ _out = Console()
30
+ _err = Console(stderr=True)
31
+
32
+ EXIT_OK = 0
33
+ EXIT_OVER_BUDGET = 1
34
+ EXIT_BAD_INPUT = 2
35
+
36
+
37
+ def _version_callback(value: bool) -> None:
38
+ if value:
39
+ _out.print(f"tokenmeter {__version__}")
40
+ raise typer.Exit()
41
+
42
+
43
+ @app.callback()
44
+ def main(
45
+ _version: bool = typer.Option(
46
+ False,
47
+ "--version",
48
+ callback=_version_callback,
49
+ is_eager=True,
50
+ help="Show the version and exit.",
51
+ ),
52
+ ) -> None:
53
+ """tokenmeter command-line interface."""
54
+
55
+
56
+ def _collect(paths, model, output_tokens):
57
+ inputs = read_inputs(paths)
58
+ encoder = encoder_for_model(model)
59
+ return [
60
+ measure(encoder, model, name, text, output_tokens=output_tokens) for name, text in inputs
61
+ ]
62
+
63
+
64
+ @app.command("count")
65
+ def count(
66
+ paths: list[str] = typer.Argument(..., help="Files, directories, or - for stdin."),
67
+ model: str = typer.Option("gpt-4o", "--model", "-m", help="Model to price against."),
68
+ output_tokens: int = typer.Option(
69
+ 0, "--output-tokens", help="Assumed completion tokens, for cost only."
70
+ ),
71
+ as_json: bool = typer.Option(False, "--json", help="Emit JSON."),
72
+ ) -> None:
73
+ """Count tokens and estimate cost for one or more inputs."""
74
+
75
+ try:
76
+ measurements = _collect(paths, model, output_tokens)
77
+ except UnknownModel as exc:
78
+ _err.print(f"tokenmeter: {exc}; try 'tokenmeter models'")
79
+ raise typer.Exit(EXIT_BAD_INPUT) from exc
80
+ except (OSError, EncoderError) as exc:
81
+ _err.print(f"tokenmeter: {exc}")
82
+ raise typer.Exit(EXIT_BAD_INPUT) from exc
83
+
84
+ if as_json:
85
+ _out.print_json(json.dumps(measurements_to_json(measurements)))
86
+ else:
87
+ _out.print(render_table(measurements))
88
+
89
+
90
+ @app.command("budget")
91
+ def budget(
92
+ paths: list[str] = typer.Argument(..., help="Files, directories, or - for stdin."),
93
+ max_cost: float = typer.Option(..., "--max-cost", help="Fail above this USD cost."),
94
+ model: str = typer.Option("gpt-4o", "--model", "-m", help="Model to price against."),
95
+ output_tokens: int = typer.Option(0, "--output-tokens", help="Assumed completion tokens."),
96
+ ) -> None:
97
+ """Fail when the estimated cost of the inputs exceeds a budget."""
98
+
99
+ try:
100
+ measurements = _collect(paths, model, output_tokens)
101
+ except UnknownModel as exc:
102
+ _err.print(f"tokenmeter: {exc}; try 'tokenmeter models'")
103
+ raise typer.Exit(EXIT_BAD_INPUT) from exc
104
+ except (OSError, EncoderError) as exc:
105
+ _err.print(f"tokenmeter: {exc}")
106
+ raise typer.Exit(EXIT_BAD_INPUT) from exc
107
+
108
+ cost = total_cost(measurements)
109
+ _err.print(f"tokenmeter: estimated ${cost:.6f} against a ${max_cost:.6f} budget")
110
+ if over_budget(measurements, max_cost):
111
+ raise typer.Exit(EXIT_OVER_BUDGET)
112
+
113
+
114
+ @app.command("models")
115
+ def models() -> None:
116
+ """List the known models and their prices."""
117
+
118
+ title = f"prices as of {PRICES_AS_OF} (USD per 1M tokens)"
119
+ table = Table(box=None, pad_edge=False, title=title)
120
+ table.add_column("model")
121
+ table.add_column("encoding")
122
+ table.add_column("input", justify="right")
123
+ table.add_column("output", justify="right")
124
+ for name in known_models():
125
+ p = price_for(name)
126
+ table.add_row(p.model, p.encoding, f"${p.input_per_mtok:g}", f"${p.output_per_mtok:g}")
127
+ _out.print(table)
128
+
129
+
130
+ def entrypoint() -> None:
131
+ try:
132
+ app()
133
+ except KeyboardInterrupt: # pragma: no cover - interactive only
134
+ print("tokenmeter: interrupted", file=sys.stderr)
135
+ raise SystemExit(130) from None
tokenmeter/encoder.py ADDED
@@ -0,0 +1,52 @@
1
+ """Token counting behind a small interface.
2
+
3
+ The real encoder uses ``tiktoken``, imported lazily so the package installs and
4
+ imports without it and so the test suite can run with a fake encoder and no
5
+ network access. Counting is therefore exact for the supported OpenAI encodings
6
+ at run time, and deterministic in tests.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ from typing import Protocol
12
+
13
+ from tokenmeter.pricing import price_for
14
+
15
+
16
+ class Encoder(Protocol):
17
+ def count(self, text: str) -> int: ...
18
+
19
+
20
+ class EncoderError(RuntimeError):
21
+ """Raised when an encoder cannot be constructed."""
22
+
23
+
24
+ class TiktokenEncoder:
25
+ """Count tokens with tiktoken for a given encoding name."""
26
+
27
+ def __init__(self, encoding: str) -> None:
28
+ self.encoding = encoding
29
+ self._enc = None
30
+
31
+ def _ensure(self):
32
+ if self._enc is not None:
33
+ return self._enc
34
+ try:
35
+ import tiktoken
36
+ except ImportError as exc: # pragma: no cover - import guard
37
+ raise EncoderError(
38
+ "tiktoken is not installed; install tokenmeter with its default "
39
+ "dependencies to count tokens"
40
+ ) from exc
41
+ try:
42
+ self._enc = tiktoken.get_encoding(self.encoding)
43
+ except Exception as exc: # pragma: no cover - needs network on first use
44
+ raise EncoderError(f"could not load encoding {self.encoding!r}") from exc
45
+ return self._enc
46
+
47
+ def count(self, text: str) -> int:
48
+ return len(self._ensure().encode(text))
49
+
50
+
51
+ def encoder_for_model(model: str) -> Encoder:
52
+ return TiktokenEncoder(price_for(model).encoding)
tokenmeter/inputs.py ADDED
@@ -0,0 +1,41 @@
1
+ """Gather text inputs from files, directories, or standard input."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import sys
6
+ from collections.abc import Iterable, Sequence
7
+ from pathlib import Path
8
+
9
+ TEXT_SUFFIXES = {".txt", ".md", ".prompt", ".jinja", ".j2", ".tmpl"}
10
+
11
+
12
+ def read_inputs(
13
+ paths: Sequence[str | Path],
14
+ *,
15
+ stdin_text: str | None = None,
16
+ ) -> list[tuple[str, str]]:
17
+ """Return ``(name, text)`` pairs for every requested input.
18
+
19
+ A path of ``-`` reads standard input. Directories are expanded to their
20
+ text-like files, sorted for stable output.
21
+ """
22
+
23
+ out: list[tuple[str, str]] = []
24
+ for raw in paths:
25
+ if str(raw) == "-":
26
+ text = stdin_text if stdin_text is not None else sys.stdin.read()
27
+ out.append(("<stdin>", text))
28
+ continue
29
+ path = Path(raw)
30
+ if path.is_dir():
31
+ for child in _text_files(path):
32
+ out.append((str(child), child.read_text(encoding="utf-8")))
33
+ else:
34
+ out.append((str(path), path.read_text(encoding="utf-8")))
35
+ return out
36
+
37
+
38
+ def _text_files(directory: Path) -> Iterable[Path]:
39
+ return sorted(
40
+ p for p in directory.rglob("*") if p.is_file() and p.suffix.lower() in TEXT_SUFFIXES
41
+ )
tokenmeter/meter.py ADDED
@@ -0,0 +1,57 @@
1
+ """Combine token counts with prices into measurements and a budget gate."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from dataclasses import dataclass
6
+
7
+ from tokenmeter.encoder import Encoder
8
+ from tokenmeter.pricing import input_cost, output_cost
9
+
10
+
11
+ @dataclass(frozen=True, slots=True)
12
+ class Measurement:
13
+ name: str
14
+ model: str
15
+ input_tokens: int
16
+ output_tokens: int
17
+ input_cost: float
18
+ output_cost: float
19
+
20
+ @property
21
+ def total_cost(self) -> float:
22
+ return self.input_cost + self.output_cost
23
+
24
+ @property
25
+ def total_tokens(self) -> int:
26
+ return self.input_tokens + self.output_tokens
27
+
28
+
29
+ def measure(
30
+ encoder: Encoder,
31
+ model: str,
32
+ name: str,
33
+ text: str,
34
+ *,
35
+ output_tokens: int = 0,
36
+ ) -> Measurement:
37
+ input_tokens = encoder.count(text)
38
+ return Measurement(
39
+ name=name,
40
+ model=model,
41
+ input_tokens=input_tokens,
42
+ output_tokens=output_tokens,
43
+ input_cost=input_cost(model, input_tokens),
44
+ output_cost=output_cost(model, output_tokens),
45
+ )
46
+
47
+
48
+ def total_cost(measurements: list[Measurement]) -> float:
49
+ return sum(m.total_cost for m in measurements)
50
+
51
+
52
+ def total_tokens(measurements: list[Measurement]) -> int:
53
+ return sum(m.total_tokens for m in measurements)
54
+
55
+
56
+ def over_budget(measurements: list[Measurement], max_cost: float) -> bool:
57
+ return total_cost(measurements) > max_cost
tokenmeter/pricing.py ADDED
@@ -0,0 +1,67 @@
1
+ """Per-model token prices and the cost arithmetic on top of them.
2
+
3
+ Prices are expressed in US dollars per million tokens and carry an "as of"
4
+ date so a stale table is obvious. The numbers are easy to override or extend;
5
+ the cost functions are pure and do not care where the rates came from.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ from dataclasses import dataclass
11
+
12
+ PRICES_AS_OF = "2025-08-01"
13
+
14
+
15
+ @dataclass(frozen=True, slots=True)
16
+ class ModelPrice:
17
+ """Input and output price in USD per million tokens."""
18
+
19
+ model: str
20
+ encoding: str
21
+ input_per_mtok: float
22
+ output_per_mtok: float
23
+
24
+
25
+ # A small, explicit table. Values are USD per 1,000,000 tokens.
26
+ _PRICES: dict[str, ModelPrice] = {
27
+ "gpt-4o": ModelPrice("gpt-4o", "o200k_base", 2.50, 10.00),
28
+ "gpt-4o-mini": ModelPrice("gpt-4o-mini", "o200k_base", 0.15, 0.60),
29
+ "gpt-4-turbo": ModelPrice("gpt-4-turbo", "cl100k_base", 10.00, 30.00),
30
+ "gpt-3.5-turbo": ModelPrice("gpt-3.5-turbo", "cl100k_base", 0.50, 1.50),
31
+ "text-embedding-3-small": ModelPrice("text-embedding-3-small", "cl100k_base", 0.02, 0.0),
32
+ "text-embedding-3-large": ModelPrice("text-embedding-3-large", "cl100k_base", 0.13, 0.0),
33
+ }
34
+
35
+
36
+ class UnknownModel(KeyError):
37
+ """Raised when a model has no entry in the price table."""
38
+
39
+ def __init__(self, model: str) -> None:
40
+ self.model = model
41
+ super().__init__(model)
42
+
43
+ def __str__(self) -> str:
44
+ return f"unknown model: {self.model}"
45
+
46
+
47
+ def known_models() -> list[str]:
48
+ return sorted(_PRICES)
49
+
50
+
51
+ def price_for(model: str) -> ModelPrice:
52
+ try:
53
+ return _PRICES[model]
54
+ except KeyError as exc:
55
+ raise UnknownModel(model) from exc
56
+
57
+
58
+ def input_cost(model: str, tokens: int) -> float:
59
+ return price_for(model).input_per_mtok * tokens / 1_000_000
60
+
61
+
62
+ def output_cost(model: str, tokens: int) -> float:
63
+ return price_for(model).output_per_mtok * tokens / 1_000_000
64
+
65
+
66
+ def total_cost(model: str, input_tokens: int, output_tokens: int = 0) -> float:
67
+ return input_cost(model, input_tokens) + output_cost(model, output_tokens)
tokenmeter/render.py ADDED
@@ -0,0 +1,51 @@
1
+ """Render measurements for the terminal and as JSON."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from rich.console import Group
6
+ from rich.table import Table
7
+
8
+ from tokenmeter.meter import Measurement, total_cost, total_tokens
9
+
10
+
11
+ def measurements_to_json(measurements: list[Measurement]) -> dict:
12
+ return {
13
+ "inputs": [
14
+ {
15
+ "name": m.name,
16
+ "model": m.model,
17
+ "input_tokens": m.input_tokens,
18
+ "output_tokens": m.output_tokens,
19
+ "input_cost": round(m.input_cost, 6),
20
+ "output_cost": round(m.output_cost, 6),
21
+ "total_cost": round(m.total_cost, 6),
22
+ }
23
+ for m in measurements
24
+ ],
25
+ "total_tokens": total_tokens(measurements),
26
+ "total_cost": round(total_cost(measurements), 6),
27
+ }
28
+
29
+
30
+ def render_table(measurements: list[Measurement]) -> Group:
31
+ table = Table(box=None, pad_edge=False)
32
+ table.add_column("input")
33
+ table.add_column("in tok", justify="right")
34
+ table.add_column("out tok", justify="right")
35
+ table.add_column("cost (USD)", justify="right")
36
+ for m in measurements:
37
+ table.add_row(
38
+ m.name,
39
+ f"{m.input_tokens}",
40
+ f"{m.output_tokens}",
41
+ f"${m.total_cost:.6f}",
42
+ )
43
+ if len(measurements) != 1:
44
+ table.add_section()
45
+ table.add_row(
46
+ "total",
47
+ f"{sum(m.input_tokens for m in measurements)}",
48
+ f"{sum(m.output_tokens for m in measurements)}",
49
+ f"${total_cost(measurements):.6f}",
50
+ )
51
+ return Group(table)
@@ -0,0 +1,118 @@
1
+ Metadata-Version: 2.4
2
+ Name: tokenmeter-cli
3
+ Version: 0.2.0
4
+ Summary: Count tokens and estimate cost for prompts before you send them.
5
+ Project-URL: Homepage, https://github.com/jmweb-org/tokenmeter
6
+ Project-URL: Repository, https://github.com/jmweb-org/tokenmeter
7
+ Project-URL: Issues, https://github.com/jmweb-org/tokenmeter/issues
8
+ Author: José del Río
9
+ License: MIT License
10
+
11
+ Copyright (c) 2026 José del Río
12
+
13
+ Permission is hereby granted, free of charge, to any person obtaining a copy
14
+ of this software and associated documentation files (the "Software"), to deal
15
+ in the Software without restriction, including without limitation the rights
16
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
17
+ copies of the Software, and to permit persons to whom the Software is
18
+ furnished to do so, subject to the following conditions:
19
+
20
+ The above copyright notice and this permission notice shall be included in all
21
+ copies or substantial portions of the Software.
22
+
23
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
24
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
25
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
26
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
27
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
28
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
29
+ SOFTWARE.
30
+ License-File: LICENSE
31
+ Keywords: budget,cli,cost,llm,openai,tiktoken,tokens
32
+ Classifier: Development Status :: 4 - Beta
33
+ Classifier: Intended Audience :: Developers
34
+ Classifier: License :: OSI Approved :: MIT License
35
+ Classifier: Operating System :: OS Independent
36
+ Classifier: Programming Language :: Python :: 3.10
37
+ Classifier: Programming Language :: Python :: 3.11
38
+ Classifier: Programming Language :: Python :: 3.12
39
+ Classifier: Topic :: Utilities
40
+ Requires-Python: >=3.10
41
+ Requires-Dist: rich>=13.0
42
+ Requires-Dist: tiktoken>=0.7
43
+ Requires-Dist: typer>=0.12
44
+ Description-Content-Type: text/markdown
45
+
46
+ # tokenmeter
47
+
48
+ [![CI](https://github.com/jmweb-org/tokenmeter/actions/workflows/ci.yml/badge.svg)](https://github.com/jmweb-org/tokenmeter/actions/workflows/ci.yml)
49
+ [![PyPI](https://img.shields.io/pypi/v/tokenmeter-cli.svg)](https://pypi.org/project/tokenmeter-cli/)
50
+ [![Python](https://img.shields.io/badge/python-3.10%2B-blue.svg)](https://www.python.org)
51
+ [![License: MIT](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE)
52
+
53
+ Count tokens and estimate cost for prompts before you send them, from the
54
+ command line or as a CI budget gate.
55
+
56
+ Prompt templates grow, a few-shot example gets added, a retrieved context
57
+ balloons, and suddenly every call costs more than you thought. `tokenmeter`
58
+ gives you the exact token count and a dollar estimate up front, for a single
59
+ prompt or a whole directory of templates.
60
+
61
+ ```console
62
+ $ tokenmeter count prompts/system.txt --model gpt-4o
63
+ input in tok out tok cost (USD)
64
+ prompts/system.txt 812 0 $0.002030
65
+
66
+ $ tokenmeter count prompts/ --model gpt-4o-mini --json
67
+ ```
68
+
69
+ ## Install
70
+
71
+ ```console
72
+ $ pip install tokenmeter-cli # from PyPI, once released
73
+ $ pip install git+https://github.com/jmweb-org/tokenmeter # latest, available now
74
+ ```
75
+
76
+ Token counting is exact for the supported OpenAI encodings via `tiktoken`.
77
+
78
+ ## Usage
79
+
80
+ ```console
81
+ $ tokenmeter count system.txt -m gpt-4o # one file
82
+ $ tokenmeter count prompts/ -m gpt-4o-mini # every text file in a directory
83
+ $ cat prompt.txt | tokenmeter count - -m gpt-4o # standard input
84
+ $ tokenmeter count p.txt --output-tokens 500 # include an assumed completion
85
+ $ tokenmeter models # list models and prices
86
+ ```
87
+
88
+ ### As a budget gate
89
+
90
+ Fail a build when a prompt set would cost more than you allow:
91
+
92
+ ```console
93
+ $ tokenmeter budget prompts/ --model gpt-4o --max-cost 0.05
94
+ ```
95
+
96
+ ```yaml
97
+ - run: tokenmeter budget prompts/ --model gpt-4o --max-cost 0.05
98
+ ```
99
+
100
+ ## Cost model
101
+
102
+ Counts are real tokens. Cost multiplies tokens by a per-model rate from a small,
103
+ dated price table (`tokenmeter models` prints it with its "as of" date). By
104
+ default only input tokens are counted; pass `--output-tokens N` to add an
105
+ assumed completion length to the estimate. Prices change, so treat the dollar
106
+ figures as estimates and update the table when they move.
107
+
108
+ ## Exit codes
109
+
110
+ | Code | Meaning |
111
+ | --- | --- |
112
+ | 0 | Counted; under budget (or `count` was used) |
113
+ | 1 | `budget` estimate exceeded `--max-cost` |
114
+ | 2 | An input was missing, or the model is unknown |
115
+
116
+ ## License
117
+
118
+ MIT. See [LICENSE](LICENSE).
@@ -0,0 +1,13 @@
1
+ tokenmeter/__init__.py,sha256=ldifpdHQhVMiQ8V9qi91UJkjDto7XYyhhR7L9I7XxM0,470
2
+ tokenmeter/__main__.py,sha256=5uZgM542ygj2c6D6uKvQmqmQzo7uvG3qu5cLMb_zc68,83
3
+ tokenmeter/cli.py,sha256=4PDR9KLvZprG2P4aSSC48iZiZKZcoNf822_SJlj3F8Q,4234
4
+ tokenmeter/encoder.py,sha256=1CPu9ZHLQAaL8-3-wAOsSAOvteD8TzEl0_oeq3zCGkU,1628
5
+ tokenmeter/inputs.py,sha256=mTH9LgUqqxP5Wvl87-P6lGyG35F09C3ygg-12NEJ4Bg,1247
6
+ tokenmeter/meter.py,sha256=F7t7Ed64hMmTPckiEASYOGB5J5OHBVs2B3k8VLoxx7Y,1394
7
+ tokenmeter/pricing.py,sha256=H4ovbPi1gzlDMTbBkWxOSCtnDAgGpP1Q3ftchOo625E,2062
8
+ tokenmeter/render.py,sha256=a3Xx9HGeD_8-lP-HeRcE0iwUiRa8d-t15lTMWLjC-fk,1614
9
+ tokenmeter_cli-0.2.0.dist-info/METADATA,sha256=v3NRDEch9Cl66SATXhhLXIYoqp8sIVXpJRkcM0bHG0I,4733
10
+ tokenmeter_cli-0.2.0.dist-info/WHEEL,sha256=mffPy8wBnZQn2VnJUU5jE99KsxaSfiyMHV9Yt0aLVxs,87
11
+ tokenmeter_cli-0.2.0.dist-info/entry_points.txt,sha256=6NQkcMw35tZQW5WpX-MSx-PYG2WIyLwJJkIPFqqpPq8,57
12
+ tokenmeter_cli-0.2.0.dist-info/licenses/LICENSE,sha256=N4nJy_wSxYwULjDvuE2GupQWZSSwgOOU_HJSzuxHBsI,1071
13
+ tokenmeter_cli-0.2.0.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.30.1
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ tokenmeter = tokenmeter.cli:entrypoint
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 José del Río
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.