hf-mem 0.0.3__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.
hf_mem/__init__.py ADDED
File without changes
hf_mem/cli.py ADDED
@@ -0,0 +1,195 @@
1
+ import argparse
2
+ import asyncio
3
+ import json
4
+ import math
5
+ import os
6
+ import struct
7
+ import sys
8
+ from functools import reduce
9
+ from typing import Any, Dict, List, Optional
10
+
11
+ import httpx
12
+
13
+ from hf_mem.print import print_report_for_diffusers, print_report_for_transformers
14
+
15
+ # NOTE: Defines the bytes that will be fetched per safetensors file, but the metadata
16
+ # can indeed be larger than that
17
+ MAX_METADATA_SIZE = 100_000
18
+ REQUEST_TIMEOUT = float(os.getenv("REQUEST_TIMEOUT", 10.0))
19
+ MAX_CONCURRENCY = int(os.getenv("MAX_WORKERS", min(32, (os.cpu_count() or 1) + 4)))
20
+
21
+
22
+ # NOTE: Return type-hint set to `Any`, but it will only be a JSON-compatible object
23
+ async def get_json_file(
24
+ client: httpx.AsyncClient, url: str, headers: Optional[Dict[str, str]] = None
25
+ ) -> Any:
26
+ response = await client.get(url, headers=headers, timeout=REQUEST_TIMEOUT)
27
+ response.raise_for_status()
28
+ return response.json()
29
+
30
+
31
+ async def fetch_safetensors_metadata(
32
+ client: httpx.AsyncClient, url: str, headers: Optional[Dict[str, str]] = None
33
+ ) -> Dict[str, Any]:
34
+ headers = {"Range": f"bytes=0-{MAX_METADATA_SIZE}", **(headers or {})}
35
+ response = await client.get(url, headers=headers, timeout=REQUEST_TIMEOUT)
36
+ response.raise_for_status()
37
+
38
+ metadata = response.read()
39
+ # NOTE: Parse the first 8 bytes as a little-endian uint64 (size of the metadata)
40
+ metadata_size = struct.unpack("<Q", metadata[:8])[0]
41
+
42
+ if metadata_size < MAX_METADATA_SIZE:
43
+ metadata = metadata[8 : metadata_size + 8]
44
+ return json.loads(metadata)
45
+
46
+ # NOTE: Given that by default we just fetch the first 100_000 bytes, if the content is larger
47
+ # then we simply fetch the remainder again
48
+ metadata = metadata[8 : MAX_METADATA_SIZE + 8]
49
+ headers["Range"] = f"bytes={MAX_METADATA_SIZE + 1}-{metadata_size + 7}"
50
+
51
+ response = await client.get(url, headers=headers, timeout=REQUEST_TIMEOUT)
52
+ response.raise_for_status()
53
+
54
+ metadata += response.read()
55
+ return json.loads(metadata)
56
+
57
+
58
+ async def run(model_id: str, revision: str) -> None:
59
+ headers = {}
60
+ if token := os.getenv("HF_TOKEN", None):
61
+ headers["Authorization"] = f"Bearer {token}"
62
+
63
+ client = httpx.AsyncClient(
64
+ limits=httpx.Limits(
65
+ max_keepalive_connections=MAX_CONCURRENCY,
66
+ max_connections=MAX_CONCURRENCY,
67
+ ),
68
+ timeout=httpx.Timeout(REQUEST_TIMEOUT),
69
+ http2=True,
70
+ follow_redirects=True,
71
+ )
72
+
73
+ # TODO: `recursive=true` shouldn't really be required unless it's a Diffusers
74
+ # models... I don't think this adds extra latency anyway
75
+ url = f"https://huggingface.co/api/models/{model_id}/tree/{revision}?recursive=true"
76
+
77
+ files = await get_json_file(client=client, url=url, headers=headers)
78
+
79
+ file_paths = [
80
+ f["path"]
81
+ for f in files
82
+ if f.get("path", None) is not None and f.get("type", None) == "file"
83
+ ]
84
+
85
+ if "model.safetensors" in file_paths:
86
+ url = f"https://huggingface.co/{model_id}/resolve/{revision}/model.safetensors"
87
+ metadata = await fetch_safetensors_metadata(
88
+ client=client, url=url, headers=headers
89
+ )
90
+ print_report_for_transformers(model_id, revision, metadata)
91
+ elif "model.safetensors.index.json" in file_paths:
92
+ # TODO: We could eventually skip this request in favour of a greedy approach on trying to pull all the
93
+ # files following the formatting `model-00000-of-00000.safetensors`
94
+ url = f"https://huggingface.co/{model_id}/resolve/{revision}/model.safetensors.index.json"
95
+ files_index = await get_json_file(client=client, url=url, headers=headers)
96
+
97
+ urls = {
98
+ f"https://huggingface.co/{model_id}/resolve/{revision}/{f}"
99
+ for f in set(files_index["weight_map"].values())
100
+ }
101
+
102
+ semaphore = asyncio.Semaphore(MAX_CONCURRENCY)
103
+
104
+ async def fetch_semaphore(url: str) -> Dict[str, Any]:
105
+ async with semaphore:
106
+ return await fetch_safetensors_metadata(
107
+ client=client, url=url, headers=headers
108
+ )
109
+
110
+ tasks = [asyncio.create_task(fetch_semaphore(url)) for url in urls]
111
+ metadata_list: List[Dict[str, Any]] = await asyncio.gather(
112
+ *tasks, return_exceptions=False
113
+ )
114
+
115
+ metadata = reduce(lambda acc, metadata: acc | metadata, metadata_list, {})
116
+ print_report_for_transformers(model_id, revision, metadata)
117
+ elif "model_index.json" in file_paths:
118
+ url = f"https://huggingface.co/{model_id}/resolve/{revision}/model_index.json"
119
+ files_index = await get_json_file(client=client, url=url, headers=headers)
120
+ paths = {k for k, _ in files_index.items() if not k.startswith("_")}
121
+
122
+ path_urls: Dict[str, List[str]] = {}
123
+ for path in paths:
124
+ if f"{path}/diffusion_pytorch_model.safetensors" in file_paths:
125
+ path_urls[path] = [
126
+ f"https://huggingface.co/{model_id}/resolve/{revision}/{path}/diffusion_pytorch_model.safetensors"
127
+ ]
128
+ elif f"{path}/model.safetensors" in file_paths:
129
+ path_urls[path] = [
130
+ f"https://huggingface.co/{model_id}/resolve/{revision}/{path}/model.safetensors"
131
+ ]
132
+ elif f"{path}/diffusion_pytorch_model.safetensors.index.json" in file_paths:
133
+ url = f"https://huggingface.co/{model_id}/resolve/{revision}/{path}/diffusion_pytorch_model.safetensors.index.json"
134
+ files_index = await get_json_file(
135
+ client=client, url=url, headers=headers
136
+ )
137
+ path_urls[path] = list({
138
+ f"https://huggingface.co/{model_id}/resolve/{revision}/{path}/{f}"
139
+ for _, f in files_index["weight_map"].items() # type: ignore
140
+ })
141
+ elif f"{path}/model.safetensors.index.json" in file_paths:
142
+ url = f"https://huggingface.co/{model_id}/resolve/{revision}/{path}/model.safetensors.index.json"
143
+ files_index = await get_json_file(
144
+ client=client, url=url, headers=headers
145
+ )
146
+ path_urls[path] = list({
147
+ f"https://huggingface.co/{model_id}/resolve/{revision}/{path}/{f}"
148
+ for _, f in files_index["weight_map"].items() # type: ignore
149
+ })
150
+
151
+ semaphore = asyncio.Semaphore(MAX_CONCURRENCY)
152
+
153
+ async def fetch_semaphore(url: str) -> Dict[str, Any]:
154
+ async with semaphore:
155
+ return await fetch_safetensors_metadata(
156
+ client=client, url=url, headers=headers
157
+ )
158
+
159
+ metadata = {}
160
+ for path, urls in path_urls.items():
161
+ tasks = [asyncio.create_task(fetch_semaphore(url)) for url in urls]
162
+ metadata_list: List[Dict[str, Any]] = await asyncio.gather(
163
+ *tasks, return_exceptions=False
164
+ )
165
+ metadata[path] = reduce(
166
+ lambda acc, metadata: acc | metadata, metadata_list, {}
167
+ )
168
+
169
+ print_report_for_diffusers(
170
+ model_id=model_id, revision=revision, metadata=metadata
171
+ )
172
+ else:
173
+ raise RuntimeError(
174
+ "NONE OF `model.safetensors`, `model.safetensors.index.json`, `model_index.json` HAS BEEN FOUND"
175
+ )
176
+
177
+ sys.exit(0)
178
+
179
+
180
+ def main() -> None:
181
+ parser = argparse.ArgumentParser()
182
+ parser.add_argument(
183
+ "--model-id", required=True, help="Model ID on the Hugging Face Hub"
184
+ )
185
+ parser.add_argument(
186
+ "--revision",
187
+ default="main",
188
+ help="Model revision on the Hugging Face Hub",
189
+ )
190
+
191
+ args = parser.parse_args()
192
+ model_id = args.model_id
193
+ revision = args.revision
194
+
195
+ asyncio.run(run(model_id, revision))
hf_mem/print.py ADDED
@@ -0,0 +1,290 @@
1
+ import math
2
+ from typing import Any, Dict, Literal, Optional
3
+
4
+ MIN_NAME_LEN = 5
5
+ MAX_NAME_LEN = 13
6
+ MIN_DATA_LEN = 20
7
+ MAX_DATA_LEN = 32
8
+ BORDERS_AND_PADDING = 7
9
+
10
+ BOX = {
11
+ "tl": "┌",
12
+ "tr": "┐",
13
+ "bl": "└",
14
+ "br": "┘",
15
+ "ht": "─",
16
+ "vt": "│",
17
+ "tsep": "┬",
18
+ "bsep": "┴",
19
+ "lm": "├",
20
+ "rm": "┤",
21
+ "mm": "┼",
22
+ }
23
+
24
+
25
+ def _print_with_color(content: str) -> None:
26
+ print(f"\x1b[38;2;244;183;63m{content}\x1b[0m")
27
+
28
+
29
+ def _print_header(current_len: int) -> None:
30
+ length = current_len + MAX_NAME_LEN + BORDERS_AND_PADDING
31
+ top = BOX["tl"] + (BOX["tsep"] * (length - 2)) + BOX["tr"]
32
+ _print_with_color(top)
33
+
34
+ bottom = BOX["lm"] + (BOX["bsep"] * (length - 2)) + BOX["rm"]
35
+ _print_with_color(bottom)
36
+
37
+
38
+ def _print_centered(text: str, current_len: int) -> None:
39
+ max_len = current_len + MAX_NAME_LEN - BORDERS_AND_PADDING
40
+ total_width = max_len + 12
41
+ text_len = len(text)
42
+ pad_left = (total_width - text_len) // 2
43
+ pad_right = total_width - text_len - pad_left
44
+ _print_with_color(f"{BOX['vt']}{' ' * pad_left}{text}{' ' * pad_right}{BOX['vt']}")
45
+
46
+
47
+ def _print_divider(
48
+ current_len: int,
49
+ side: Optional[Literal["top", "top-continue", "bottom", "bottom-continue"]] = None,
50
+ ) -> None:
51
+ match side:
52
+ case "top":
53
+ left, mid, right = BOX["lm"], BOX["tsep"], BOX["rm"]
54
+ case "top-continue":
55
+ left, mid, right = BOX["lm"], BOX["bsep"], BOX["rm"]
56
+ case "bottom":
57
+ left, mid, right = BOX["bl"], BOX["bsep"], BOX["br"]
58
+ case "bottom-continue":
59
+ left, mid, right = BOX["lm"], BOX["bsep"], BOX["rm"]
60
+ case _:
61
+ left, mid, right = BOX["lm"], BOX["mm"], BOX["rm"]
62
+
63
+ name_col_inner = MAX_NAME_LEN + 2
64
+ data_col_inner = current_len + 1
65
+
66
+ line = left
67
+ line += BOX["ht"] * name_col_inner
68
+ line += mid
69
+ line += BOX["ht"] * data_col_inner
70
+ line += right
71
+ _print_with_color(line)
72
+
73
+
74
+ def _format_name(name: str) -> str:
75
+ if len(name) < MIN_NAME_LEN:
76
+ return f"{name:<{MIN_NAME_LEN}}"
77
+ if len(name) > MAX_NAME_LEN:
78
+ return name[: MAX_NAME_LEN - 3] + "..."
79
+ return f"{name:<{MAX_NAME_LEN}}"
80
+
81
+
82
+ def _print_row(name: str, text: str, current_len: int) -> None:
83
+ name_fmt = _format_name(name)
84
+ data_fmt = f"{str(text):<{current_len}}"
85
+ _print_with_color(f"{BOX['vt']} {name_fmt} {BOX['vt']} {data_fmt} {BOX['vt']}")
86
+
87
+
88
+ def _make_bar(used: float, total: float, width: int) -> str:
89
+ if total <= 0:
90
+ return "░" * width
91
+ frac = min(max(used / total, 0.0), 1.0)
92
+ filled = int(round(frac * width))
93
+ filled = max(0, min(width, filled))
94
+ return "█" * filled + "░" * (width - filled)
95
+
96
+
97
+ def _format_short_number(n: float) -> Optional[str]:
98
+ n = float(n)
99
+ for unit in ("", "K", "M", "B", "T"):
100
+ if abs(n) < 1000.0:
101
+ return f"{int(n)}" if unit == "" else f"{n:.1f}{unit}"
102
+ n /= 1000.0
103
+
104
+
105
+ def _bytes_to_gb(nbytes: int) -> float:
106
+ return nbytes / (1024**3)
107
+
108
+
109
+ def print_report_for_transformers(
110
+ model_id: str, revision: str, metadata: Dict[str, Any]
111
+ ) -> None:
112
+ ppdt = {}
113
+ for key, value in metadata.items():
114
+ if key in {"__metadata__"}:
115
+ continue
116
+ if value["dtype"] not in ppdt:
117
+ ppdt[value["dtype"]] = (0, 0)
118
+
119
+ match value["dtype"]:
120
+ case "F64" | "I64" | "U64":
121
+ dtype_b = 8
122
+ case "F32" | "I32" | "U32":
123
+ dtype_b = 4
124
+ case "F16" | "BF16" | "I16" | "U16":
125
+ dtype_b = 2
126
+ case "F8_E5M2" | "F8_E4M3" | "I8" | "U8":
127
+ dtype_b = 1
128
+ case _:
129
+ raise RuntimeError(f"DTYPE={value['dtype']} NOT HANDLED")
130
+
131
+ current_shape = math.prod(value["shape"])
132
+ current_shape_bytes = current_shape * dtype_b
133
+
134
+ ppdt[value["dtype"]] = (
135
+ ppdt[value["dtype"]][0] + current_shape,
136
+ ppdt[value["dtype"]][1] + current_shape_bytes,
137
+ )
138
+
139
+ rows = [
140
+ "INFERENCE MEMORY ESTIMATE FOR",
141
+ f"https://hf.co/{model_id} @ {revision}",
142
+ "TOTAL MEMORY",
143
+ "REQUIREMENTS",
144
+ ]
145
+ for dt, (params, nbytes) in ppdt.items():
146
+ rows.append(f"{dt} {params} {nbytes}")
147
+
148
+ max_len = 0
149
+ for r in rows:
150
+ max_len = max(max_len, len(str(r)))
151
+
152
+ current_len = min(max_len, MAX_DATA_LEN)
153
+
154
+ total_bytes = sum(nbytes for _, nbytes in ppdt.values())
155
+ total_params = sum(params for params, _ in ppdt.values())
156
+ total_gb = _bytes_to_gb(total_bytes)
157
+
158
+ _print_header(current_len)
159
+ _print_centered("INFERENCE MEMORY ESTIMATE FOR", current_len)
160
+ _print_centered(f"https://hf.co/{model_id} @ {revision}", current_len)
161
+ _print_divider(current_len + 1, "top")
162
+
163
+ total_text = f"{_bytes_to_gb(total_bytes):.2f} GB ({_format_short_number(total_params)} params)"
164
+ _print_row("TOTAL MEMORY", total_text, current_len)
165
+
166
+ total_bar = _make_bar(total_bytes, total_bytes, current_len)
167
+ _print_row("REQUIREMENTS", total_bar, current_len)
168
+ _print_divider(current_len + 1)
169
+
170
+ max_length = max([
171
+ len(f"{_format_short_number(params)} PARAMS") for params, _ in ppdt.values()
172
+ ])
173
+ for i, (dtype, (params, nbytes)) in enumerate(ppdt.items()):
174
+ dtype_name = dtype.upper()
175
+ dtype_gb = _bytes_to_gb(nbytes)
176
+
177
+ gb_text = f"{dtype_gb:.1f} / {total_gb:.1f} GB"
178
+ _print_row(
179
+ dtype_name + " " * (max_length - len(dtype_name)),
180
+ gb_text,
181
+ current_len,
182
+ )
183
+
184
+ bar = _make_bar(dtype_gb, total_gb, current_len)
185
+ _print_row(f"{_format_short_number(params)} PARAMS", bar, current_len)
186
+
187
+ if i < len(ppdt) - 1:
188
+ _print_divider(current_len + 1)
189
+
190
+ _print_divider(current_len + 1, "bottom")
191
+
192
+
193
+ def print_report_for_diffusers(
194
+ model_id: str, revision: str, metadata: Dict[str, Dict[str, Any]]
195
+ ) -> None:
196
+ components_ppdt: Dict[str, Dict[str, tuple[int, int]]] = {}
197
+ total_bytes = 0
198
+ total_params = 0
199
+
200
+ for path, path_metadata in metadata.items():
201
+ ppdt: Dict[str, tuple[int, int]] = {}
202
+ for key, value in path_metadata.items():
203
+ if key in {"__metadata__"}:
204
+ continue
205
+
206
+ dtype = value["dtype"]
207
+ match dtype:
208
+ case "F64" | "I64" | "U64":
209
+ dtype_b = 8
210
+ case "F32" | "I32" | "U32":
211
+ dtype_b = 4
212
+ case "F16" | "BF16" | "I16" | "U16":
213
+ dtype_b = 2
214
+ case "F8_E5M2" | "F8_E4M3" | "I8" | "U8":
215
+ dtype_b = 1
216
+ case _:
217
+ raise RuntimeError(f"DTYPE={dtype} NOT HANDLED")
218
+
219
+ current_shape = math.prod(value["shape"])
220
+ current_shape_bytes = current_shape * dtype_b
221
+
222
+ if dtype not in ppdt:
223
+ ppdt[dtype] = (0, 0)
224
+ ppdt[dtype] = (
225
+ ppdt[dtype][0] + current_shape,
226
+ ppdt[dtype][1] + current_shape_bytes,
227
+ )
228
+
229
+ components_ppdt[path] = ppdt
230
+ for params, nbytes in ppdt.values():
231
+ total_params += params
232
+ total_bytes += nbytes
233
+
234
+ rows = [
235
+ "INFERENCE MEMORY ESTIMATE FOR",
236
+ f"https://hf.co/{model_id} @ {revision}",
237
+ "TOTAL MEMORY",
238
+ "REQUIREMENTS",
239
+ ]
240
+ for path, ppdt in components_ppdt.items():
241
+ rows.append(path)
242
+ for dt, (params, nbytes) in ppdt.items():
243
+ rows.append(f"{dt} {params} {nbytes}")
244
+
245
+ max_len = 0
246
+ for r in rows:
247
+ max_len = max(max_len, len(str(r)))
248
+ current_len = min(max_len, MAX_DATA_LEN)
249
+
250
+ _print_header(current_len)
251
+ _print_centered("INFERENCE MEMORY ESTIMATE FOR", current_len)
252
+ _print_centered(f"https://hf.co/{model_id} @ {revision}", current_len)
253
+ _print_divider(current_len + 1, "top")
254
+
255
+ total_text = f"{_bytes_to_gb(total_bytes):.2f} GB ({_format_short_number(total_params)} params)"
256
+ _print_row("TOTAL MEMORY", total_text, current_len)
257
+
258
+ total_bar = _make_bar(total_bytes, total_bytes, current_len)
259
+ _print_row("REQUIREMENTS", total_bar, current_len)
260
+
261
+ for path, ppdt in components_ppdt.items():
262
+ _print_divider(current_len + 1, "top-continue")
263
+
264
+ path_bytes = sum(nbytes for _, nbytes in ppdt.values())
265
+ path_gb = _bytes_to_gb(path_bytes)
266
+
267
+ _print_centered(f"{path.upper()} ({path_gb:.2f} GB)", current_len)
268
+ _print_divider(current_len + 1, "top")
269
+
270
+ max_length = max([
271
+ len(f"{_format_short_number(params)} PARAMS") for params, _ in ppdt.values()
272
+ ])
273
+ for i, (dtype, (params, nbytes)) in enumerate(ppdt.items()):
274
+ dtype_name = dtype.upper()
275
+ dtype_gb = _bytes_to_gb(nbytes)
276
+
277
+ gb_text = f"{dtype_gb:.1f} / {_bytes_to_gb(total_bytes):.1f} GB"
278
+ _print_row(
279
+ dtype_name + " " * (max_length - len(dtype_name)),
280
+ gb_text,
281
+ current_len,
282
+ )
283
+
284
+ bar = _make_bar(dtype_gb, _bytes_to_gb(total_bytes), current_len)
285
+ _print_row(f"{_format_short_number(params)} PARAMS", bar, current_len)
286
+
287
+ if i < len(ppdt) - 1:
288
+ _print_divider(current_len + 1, "bottom-continue")
289
+
290
+ _print_divider(current_len + 1, "bottom")
hf_mem/py.typed ADDED
File without changes
@@ -0,0 +1,36 @@
1
+ Metadata-Version: 2.4
2
+ Name: hf-mem
3
+ Version: 0.0.3
4
+ Summary: A CLI to estimate inference memory requirements for Hugging Face models, written in Python
5
+ Author: Alvaro Bartolome
6
+ Author-email: Alvaro Bartolome <36760800+alvarobartt@users.noreply.github.com>
7
+ License-Expression: MIT
8
+ License-File: LICENSE
9
+ Requires-Dist: httpx[http2]>=0.28.1
10
+ Requires-Python: >=3.10
11
+ Description-Content-Type: text/markdown
12
+
13
+ # `hf-mem`
14
+
15
+ `hf-mem` is an experimental CLI to estimate inference memory requirements for Hugging Face models, written in Python. `hf-mem` is lightweight, only depends on `httpx`. It's recommended to run with [`uv`](https://github.com/astral-sh/uv) for a better experience.
16
+
17
+ `hf-mem` lets you estimate the inference requirements to run any model from the Hugging Face Hub, including Transformers, Diffusers and Sentence Transformers models, as well as any model that contains [Safetensors](https://github.com/huggingface/safetensors) compatible weights.
18
+
19
+ ## Usage
20
+
21
+ ```bash
22
+ uvx hf-mem --model-id MiniMaxAI/MiniMax-M2
23
+ ```
24
+
25
+ <img width="899" height="648" src="https://github.com/user-attachments/assets/530f8b14-a415-4fd6-9054-bcd81cafae09" />
26
+
27
+ ```bash
28
+ uvx hf-mem --model-id Qwen/Qwen-Image
29
+ ```
30
+
31
+ <img width="894" height="872" src="https://github.com/user-attachments/assets/cd4234ec-bdcc-4db4-8b01-0ac9b5cd390c" />
32
+
33
+ ## References
34
+
35
+ - [Safetensors Metadata parsing](https://huggingface.co/docs/safetensors/en/metadata_parsing)
36
+ - [usgraphics - TR-100 Machine Report](https://github.com/usgraphics/usgc-machine-report)
@@ -0,0 +1,9 @@
1
+ hf_mem/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
2
+ hf_mem/cli.py,sha256=m6W32NBWYvfqU_Odh8N0YAQHQ53P3HIhCdw90aICHAI,7781
3
+ hf_mem/print.py,sha256=G3bDy2ND1WY8LyGTYf07E4zFZE54TJZu1uhVWGFnFkg,9269
4
+ hf_mem/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
5
+ hf_mem-0.0.3.dist-info/licenses/LICENSE,sha256=TUO4cSlmwlYsHZfepsmo1zLoYhmT0RImXoRihVYNHhw,1060
6
+ hf_mem-0.0.3.dist-info/WHEEL,sha256=M6du7VZflc4UPsGphmOXHANdgk8zessdJG0DBUuoA-U,78
7
+ hf_mem-0.0.3.dist-info/entry_points.txt,sha256=p6o2afl5zELLNtkKwQxfLdOpal2nRlcshDZSfV02pOY,44
8
+ hf_mem-0.0.3.dist-info/METADATA,sha256=n9wvVCPHGzSWC86iVJIH2jSS8hvovUAvjC-mB9o43Ac,1512
9
+ hf_mem-0.0.3.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: uv 0.9.5
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,3 @@
1
+ [console_scripts]
2
+ hf-mem = hf_mem.cli:main
3
+
@@ -0,0 +1,25 @@
1
+ Copyright (c) 2025 Alvaro Bartolome
2
+
3
+ Permission is hereby granted, free of charge, to any
4
+ person obtaining a copy of this software and associated
5
+ documentation files (the "Software"), to deal in the
6
+ Software without restriction, including without
7
+ limitation the rights to use, copy, modify, merge,
8
+ publish, distribute, sublicense, and/or sell copies of
9
+ the Software, and to permit persons to whom the Software
10
+ is furnished to do so, subject to the following
11
+ conditions:
12
+
13
+ The above copyright notice and this permission notice
14
+ shall be included in all copies or substantial portions
15
+ of the Software.
16
+
17
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF
18
+ ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED
19
+ TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
20
+ PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT
21
+ SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
22
+ CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
23
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR
24
+ IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
25
+ DEALINGS IN THE SOFTWARE.