clevis 0.1.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.
clevis/__init__.py ADDED
@@ -0,0 +1,359 @@
1
+ """
2
+ Clevis - Configuration management for Python projects.
3
+
4
+ Provides dataclass-based configuration with TOML file support,
5
+ environment variable interpolation, and CLI argument generation.
6
+
7
+ TOML Parser Selection (priority order):
8
+ 1. envtoml - Env var interpolation (${VAR}) - install: pip install clevis[envtoml]
9
+ 2. tomlev - Tomlev parser - install: pip install clevis[tomlev]
10
+ 3. tomli - Pure Python TOML - install: pip install clevis[tomli]
11
+ 4. tomllib - Stdlib (Python 3.11+) - no extras needed
12
+ """
13
+
14
+ import argparse
15
+ import functools
16
+ from collections.abc import Callable
17
+ from dataclasses import Field, fields, is_dataclass
18
+ from pathlib import Path
19
+ from typing import Any, get_args
20
+
21
+ from dacite import from_dict
22
+ from dacite.exceptions import DaciteError, MissingValueError, WrongTypeError
23
+
24
+ __version__ = "0.1.0"
25
+
26
+
27
+ # TOML Parser Selection
28
+ # ---------------------
29
+ # Tries parsers in this order: envtoml > tomlev > tomli > tomllib
30
+
31
+
32
+ def _get_toml_parser() -> Callable[[Any], dict[str, Any]]:
33
+ """
34
+ Get the appropriate TOML parser based on installed packages.
35
+
36
+ Priority: envtoml > tomlev > tomli > tomllib (stdlib)
37
+
38
+ Returns:
39
+ A function that loads TOML from a file object
40
+
41
+ Raises:
42
+ ImportError: If no TOML parser is available
43
+ """
44
+ # envtoml: supports ${VAR} interpolation
45
+ try:
46
+ import envtoml
47
+
48
+ return envtoml.load
49
+ except ImportError:
50
+ pass
51
+
52
+ # tomlev: supports ${VAR|default} interpolation
53
+ try:
54
+ import tomllib # type: ignore[import-not-found]
55
+ from tomlev.env_loader import expandvars # type: ignore[attr-defined]
56
+
57
+ def load_with_tomlev(file: Any) -> dict[str, Any]:
58
+ content = file.read()
59
+ if isinstance(content, bytes):
60
+ content = content.decode("utf-8")
61
+ expanded = expandvars(content)
62
+ return tomllib.loads(expanded) # type: ignore[no-any-return]
63
+
64
+ return load_with_tomlev
65
+ except ImportError:
66
+ pass
67
+
68
+ # tomli: pure Python TOML (Python 3.10)
69
+ try:
70
+ import tomli
71
+
72
+ return tomli.load
73
+ except ImportError:
74
+ pass
75
+
76
+ # tomllib: stdlib (Python 3.11+)
77
+ try:
78
+ import tomllib
79
+
80
+ return tomllib.load # type: ignore[no-any-return]
81
+ except ImportError:
82
+ pass
83
+
84
+ raise ImportError(
85
+ "No TOML parser available.\n\n"
86
+ "Install one of:\n"
87
+ " pip install clevis[tomli] # Python 3.10\n"
88
+ " pip install clevis[envtoml] # Env var interpolation\n"
89
+ " pip install clevis[tomlev] # Env var with defaults\n\n"
90
+ "Note: Python 3.11+ has built-in tomllib (no extras needed)"
91
+ )
92
+
93
+
94
+ # Module-level parser (loaded once)
95
+ _toml_load: Callable[[Any], dict[str, Any]] | None = None
96
+
97
+
98
+ def _load_toml(file: Any) -> dict[str, Any]:
99
+ """
100
+ Load TOML from a file object using the selected parser.
101
+
102
+ Args:
103
+ file: File object opened in binary mode
104
+
105
+ Returns:
106
+ Dictionary of parsed TOML data
107
+ """
108
+ global _toml_load
109
+ if _toml_load is None:
110
+ _toml_load = _get_toml_parser()
111
+ return _toml_load(file)
112
+
113
+
114
+ class ConfigError(Exception):
115
+ """Raised when configuration is missing or invalid."""
116
+
117
+ def __init__(self, message: str, field_path: str, config_name: str):
118
+ self.message = message
119
+ self.field_path = field_path
120
+ self.config_name = config_name
121
+ super().__init__(self._format_message())
122
+
123
+ def _format_message(self) -> str:
124
+ """Format a helpful error message with actionable suggestions."""
125
+ lines = [f"\n{'=' * 70}"]
126
+ lines.append("Configuration Error")
127
+ lines.append(f"{'=' * 70}\n")
128
+
129
+ lines.append(f"Field: {self.field_path}")
130
+ lines.append(f"Issue: {self.message}\n")
131
+
132
+ lines.append("Provide this value in one of these ways:\n")
133
+
134
+ # Project config
135
+ lines.append(f" 1. Project config: ./{self.config_name}.toml")
136
+ parts = self.field_path.split(".")
137
+ if len(parts) == 1:
138
+ lines.append(f' {parts[0]} = "your_value"')
139
+ else:
140
+ lines.append(f" [{parts[0]}]")
141
+ lines.append(f' {".".join(parts[1:])} = "your_value"')
142
+ lines.append("")
143
+
144
+ # User config
145
+ lines.append(f" 2. User config: ~/.{self.config_name}.toml")
146
+ lines.append(" (same format as above)\n")
147
+
148
+ # CLI argument (use dashes for dots and underscores)
149
+ cli_arg = "--" + self.field_path.replace(".", "-").replace("_", "-")
150
+ lines.append(f" 3. CLI argument: {cli_arg} <value>\n")
151
+
152
+ lines.append(f"{'=' * 70}")
153
+ return "\n".join(lines)
154
+
155
+
156
+ def unpack_type(type_def: type) -> type:
157
+ """
158
+ Given a type, if a union type, return the not-None type (dataclass).
159
+
160
+ For Optional[T] or T | None, returns T.
161
+ For non-union types, returns the type as-is.
162
+
163
+ Args:
164
+ type_def: The type to unpack
165
+
166
+ Returns:
167
+ The non-None type from a union, or the type itself
168
+
169
+ Raises:
170
+ ValueError: If union has more than 2 types (not supported yet)
171
+ """
172
+ types = get_args(type_def)
173
+ # not a union type
174
+ if len(types) == 0:
175
+ return type_def
176
+ # <type> | None is only supported combination
177
+ if len(types) > 2:
178
+ raise ValueError("Complex unions not supported")
179
+ return types[0] if types[1] is type(None) else types[1] # type: ignore[no-any-return]
180
+
181
+
182
+ def list_fields(clz: type, path: list[str] | None = None) -> list[tuple[Field[Any], list[str]]]:
183
+ """
184
+ Recursively flatten and list all properties in nested dataclasses.
185
+
186
+ Args:
187
+ clz: The dataclass to inspect
188
+ path: Current path in the hierarchy
189
+
190
+ Yields:
191
+ Tuples of (field, path) for each leaf field
192
+ """
193
+ path = [] if not path else path
194
+ result = []
195
+ for f in fields(clz):
196
+ concrete_type = unpack_type(f.type) # type: ignore[arg-type]
197
+ if is_dataclass(concrete_type):
198
+ result.extend(list_fields(concrete_type, path=path + [f.name]))
199
+ else:
200
+ result.append((f, path))
201
+ return result
202
+
203
+
204
+ def get_args_config(clz: type, args: list[str] | None = None) -> dict[str, Any]:
205
+ """
206
+ Construct an argparse parser from a dataclass hierarchy.
207
+
208
+ Creates CLI arguments for each leaf field in the dataclass,
209
+ using dashed notation for nested fields (e.g., --database-host).
210
+
211
+ Args:
212
+ clz: The dataclass type to generate parser for
213
+ args: Optional list of CLI arguments (defaults to sys.argv[1:])
214
+
215
+ Returns:
216
+ Dictionary of parsed arguments with dotted keys
217
+ Unprovided arguments have None values
218
+ """
219
+ parser = argparse.ArgumentParser()
220
+ for f, path in list_fields(clz):
221
+ name = ".".join(path + [f.name]) # concatenate intermediate classes with "."
222
+ # Convert both dots and underscores to dashes for CLI args
223
+ cli_name = name.replace(".", "-").replace("_", "-")
224
+ arg = functools.partial(
225
+ parser.add_argument,
226
+ f"--{cli_name}",
227
+ dest=name, # name with dots
228
+ default=None, # Use None so TOML values aren't overridden
229
+ help=f"provide {name}",
230
+ )
231
+ # complete partial: boolean switch of store value
232
+ concrete_type = unpack_type(f.type) # type: ignore[arg-type]
233
+ if concrete_type is bool:
234
+ _ = arg(action="store_true")
235
+ else:
236
+ _ = arg(type=concrete_type)
237
+
238
+ return vars(parser.parse_args(args))
239
+
240
+
241
+ def apply_to_dict(args: dict[str, Any], dct: dict[str, Any]) -> None:
242
+ """
243
+ Apply dotted command line arguments to a nested dictionary.
244
+
245
+ Modifies the dictionary in-place, creating nested structure as needed.
246
+
247
+ Args:
248
+ args: Dictionary with dotted keys (e.g., "database.host")
249
+ dct: Target dictionary to modify
250
+ """
251
+ for key, value in args.items():
252
+ if value is not None: # default optional value, can't be set through command line
253
+ parts = key.split(".")
254
+ final_key = parts.pop()
255
+ # follow path into hierarchy
256
+ scope = dct
257
+ for step in parts:
258
+ try:
259
+ scope = scope[step] # follow
260
+ except KeyError:
261
+ scope[step] = {} # create missing
262
+ scope = scope[step]
263
+ # set value
264
+ scope[final_key] = value # upsert key=value
265
+
266
+
267
+ def get_config(
268
+ data_class: type,
269
+ name: str = "project",
270
+ user: bool = True,
271
+ project: bool = True,
272
+ args: list[str] | None = None,
273
+ ) -> Any:
274
+ """
275
+ Load configuration from TOML files and CLI arguments.
276
+
277
+ Merges configuration from (in order of precedence):
278
+ 1. CLI arguments (highest priority)
279
+ 2. Project-level TOML: ./{name}.toml
280
+ 3. User-level TOML: ~/.{name}.toml
281
+ 4. Dataclass defaults (lowest priority)
282
+
283
+ TOML Parser Selection:
284
+ Automatically selects parser based on installed extras:
285
+ - envtoml: Supports ${VAR} interpolation - pip install clevis[envtoml]
286
+ - tomlev: Alternative parser - pip install clevis[tomlev]
287
+ - tomli: Pure Python - pip install clevis[tomli]
288
+ - tomllib: Python 3.11+ stdlib (no extras needed)
289
+
290
+ Args:
291
+ data_class: The dataclass type to populate
292
+ name: Configuration file name (without .toml extension)
293
+ user: Whether to load user-level config(~/.{name}.toml)
294
+ project: Whether to load project-level config (./{name}.toml)
295
+ args: Optional list of CLI arguments (defaults to sys.argv[1:])
296
+
297
+ Returns:
298
+ An instance of the dataclass with merged configuration
299
+
300
+ Raises:
301
+ ConfigError: If required fields are missing or values have wrong type
302
+ ImportError: If no TOML parser is available
303
+ """
304
+ cfg: dict[str, Any] = {}
305
+
306
+ # Load user-level config
307
+ if user:
308
+ user_path = Path.home() / f".{name}.toml"
309
+ if user_path.exists():
310
+ cfg.update(_load_toml(user_path.open("rb")))
311
+
312
+ # Load project-level config
313
+ if project:
314
+ project_path = Path.cwd() / f"{name}.toml"
315
+ if project_path.exists():
316
+ cfg.update(_load_toml(project_path.open("rb")))
317
+
318
+ # Get CLI args based on dataclass hierarchy
319
+ cli_args = get_args_config(data_class, args)
320
+
321
+ # Merge CLI args into config
322
+ apply_to_dict(cli_args, cfg)
323
+
324
+ # Convert dict to dataclass
325
+ try:
326
+ return from_dict(data_class=data_class, data=cfg)
327
+ except MissingValueError as e:
328
+ # Extract field path from dacite error message
329
+ # Format: 'missing value for field "database.host"'
330
+ error_msg = str(e)
331
+ if '"' in error_msg:
332
+ field_path = error_msg.split('"')[1]
333
+ else:
334
+ field_path = error_msg
335
+ raise ConfigError(
336
+ message="Required field has no value",
337
+ field_path=field_path,
338
+ config_name=name,
339
+ ) from None
340
+ except WrongTypeError as e:
341
+ # Extract field path and type info from dacite error
342
+ error_msg = str(e)
343
+ if '"' in error_msg:
344
+ field_path = error_msg.split('"')[1]
345
+ else:
346
+ field_path = error_msg
347
+ raise ConfigError(
348
+ message="Wrong type for field",
349
+ field_path=field_path,
350
+ config_name=name,
351
+ ) from None
352
+ except DaciteError as e:
353
+ # Catch any other dacite errors
354
+ raise ConfigError(
355
+ message=str(e),
356
+ field_path="unknown",
357
+ config_name=name,
358
+ ) from None
359
+
clevis/py.typed ADDED
File without changes
@@ -0,0 +1,263 @@
1
+ Metadata-Version: 2.4
2
+ Name: clevis
3
+ Version: 0.1.0
4
+ Summary: Configuration management for Python projects with dataclass-based schemas
5
+ Project-URL: Homepage, https://github.com/christophevg/clevis
6
+ Project-URL: Documentation, https://clevis.readthedocs.io
7
+ Project-URL: Repository, https://github.com/christophevg/clevis
8
+ Author-email: Christophe Van Ginneken <christophe@christophe.vg>
9
+ License-Expression: MIT
10
+ License-File: LICENSE
11
+ Keywords: argparse,cli,configuration,dataclass,toml
12
+ Classifier: Development Status :: 4 - Beta
13
+ Classifier: Intended Audience :: Developers
14
+ Classifier: License :: OSI Approved :: MIT License
15
+ Classifier: Programming Language :: Python :: 3
16
+ Classifier: Programming Language :: Python :: 3.10
17
+ Classifier: Programming Language :: Python :: 3.11
18
+ Classifier: Programming Language :: Python :: 3.12
19
+ Classifier: Typing :: Typed
20
+ Requires-Python: >=3.10
21
+ Requires-Dist: dacite
22
+ Provides-Extra: dev
23
+ Requires-Dist: mypy; extra == 'dev'
24
+ Requires-Dist: pytest; extra == 'dev'
25
+ Requires-Dist: pytest-cov; extra == 'dev'
26
+ Requires-Dist: ruff; extra == 'dev'
27
+ Requires-Dist: tox; extra == 'dev'
28
+ Requires-Dist: twine; extra == 'dev'
29
+ Provides-Extra: docs
30
+ Requires-Dist: myst-parser>=2.0.0; extra == 'docs'
31
+ Requires-Dist: sphinx-rtd-theme>=2.0.0; extra == 'docs'
32
+ Requires-Dist: sphinx>=7.0.0; extra == 'docs'
33
+ Provides-Extra: envtoml
34
+ Requires-Dist: envtoml; extra == 'envtoml'
35
+ Provides-Extra: tomlev
36
+ Requires-Dist: tomlev; extra == 'tomlev'
37
+ Provides-Extra: tomli
38
+ Requires-Dist: tomli; extra == 'tomli'
39
+ Description-Content-Type: text/markdown
40
+
41
+ # Clevis
42
+
43
+ [![PyPI][pypi-badge]][pypi]
44
+ [![Python][python-badge]][python]
45
+ [![CI][ci-badge]][ci]
46
+ [![Coverage][coverage-badge]][coverage]
47
+ [![License][license-badge]][license]
48
+ [![Agentic][agentic-badge]][agentic]
49
+
50
+ > Configuration management for Python projects with dataclass-based schemas
51
+
52
+ Clevis provides type-safe configuration management for Python applications:
53
+
54
+ - **Dataclass schemas** — Define config structure with Python dataclasses
55
+ - **TOML support** — Load from `.toml` files
56
+ - **Env vars** — `${VAR}` interpolation (with envtoml/tomlev)
57
+ - **CLI generation** — Auto-generate argparse from dataclass
58
+ - **Layered config** — User config < project config < CLI args
59
+
60
+ ## About the Name
61
+
62
+ A **clevis** is a U-shaped mechanical fastener that connects components while allowing pivoting. It's used in everything from agricultural equipment to aerospace control systems — a simple, robust connector that provides flexibility without compromising strength.
63
+
64
+ This library follows the same principle: it **connects** multiple configuration sources (TOML files, environment variables, CLI arguments) into a single, cohesive interface. Just as a mechanical clevis allows articulation, Clevis allows your configuration to flex and adapt — user-level defaults, project-level settings, and runtime overrides all pivot around a single dataclass schema.
65
+
66
+ ## Quick Start
67
+
68
+ ```bash
69
+ # Install (Python 3.11+)
70
+ pip install clevis
71
+
72
+ # Or with env var support
73
+ pip install clevis[envtoml]
74
+ ```
75
+
76
+ ```python
77
+ from dataclasses import dataclass
78
+ from clevis import get_config
79
+
80
+ @dataclass
81
+ class Config:
82
+ name: str = "MyApp"
83
+ debug: bool = False
84
+
85
+ config = get_config(Config, name="app")
86
+ ```
87
+
88
+ ## Installation
89
+
90
+ Choose your TOML parser based on needs:
91
+
92
+ | Extra | Features | Use When |
93
+ |-------|----------|----------|
94
+ | *(none)* | Stdlib `tomllib` | Python 3.11+, minimal deps |
95
+ | [`tomli`][tomli] | Pure Python TOML | Python 3.10 compatibility |
96
+ | [`envtoml`][envtoml] | `${VAR}` interpolation | Environment-based config |
97
+ | [`tomlev`][tomlev] | `${VAR\|default}` syntax | Env vars with defaults |
98
+
99
+ ```bash
100
+ # Python 3.11+ - no extras needed
101
+ pip install clevis
102
+
103
+ # Python 3.10
104
+ pip install clevis[tomli]
105
+
106
+ # Environment variable support
107
+ pip install clevis[envtoml]
108
+ ```
109
+
110
+ ## Usage
111
+
112
+ ### Define Your Config
113
+
114
+ ```python
115
+ from dataclasses import dataclass, field
116
+
117
+ @dataclass
118
+ class DatabaseConfig:
119
+ host: str = "localhost"
120
+ port: int = 5432
121
+ user: str | None = None
122
+ password: str | None = None
123
+
124
+ @dataclass
125
+ class AppConfig:
126
+ name: str = "MyApp"
127
+ debug: bool = False
128
+ database: DatabaseConfig = field(default_factory=DatabaseConfig)
129
+ ```
130
+
131
+ ### Load Configuration
132
+
133
+ ```python
134
+ from clevis import get_config
135
+
136
+ # Load from ~/.myapp.toml and ./myapp.toml
137
+ config = get_config(AppConfig, name="myapp")
138
+ ```
139
+
140
+ Configuration layers (lowest to highest priority):
141
+
142
+ 1. **Dataclass defaults**
143
+ 2. **User-level TOML** — `~/.{name}.toml`
144
+ 3. **Project-level TOML** — `./{name}.toml`
145
+ 4. **CLI arguments** — `--database-host`, `--debug`
146
+
147
+ ### TOML Files
148
+
149
+ Create `myapp.toml`:
150
+
151
+ ```toml
152
+ name = "Production App"
153
+ debug = true
154
+
155
+ [database]
156
+ host = "db.example.com"
157
+ port = 5432
158
+ ```
159
+
160
+ With env var support (`clevis[envtoml]` or `clevis[tomlev]`):
161
+
162
+ ```toml
163
+ [database]
164
+ password = "${DB_PASSWORD}" # envtoml
165
+ host = "${DB_HOST|localhost}" # tomlev (with default)
166
+ ```
167
+
168
+ ### CLI Arguments
169
+
170
+ Clevis auto-generates CLI arguments:
171
+
172
+ ```bash
173
+ python app.py --database-host localhost
174
+ python app.py --database-port 5432
175
+ python app.py --debug
176
+ ```
177
+
178
+ Nested dataclasses become dashed arguments: `database.host` → `--database-host`
179
+
180
+ ## Testing
181
+
182
+ ```bash
183
+ # Run tests
184
+ make test
185
+
186
+ # Run with coverage
187
+ make test-cov
188
+ ```
189
+
190
+ ## API Reference
191
+
192
+ ### `get_config(data_class, name="project", user=True, project=True, args=None)`
193
+
194
+ Load configuration from TOML files and CLI arguments.
195
+
196
+ **Parameters:**
197
+ - `data_class` — The dataclass type to populate
198
+ - `name` — Config file name (without `.toml`)
199
+ - `user` — Load user-level config (`~/.{name}.toml`)
200
+ - `project` — Load project-level config (`./{name}.toml`)
201
+ - `args` — CLI arguments (defaults to `sys.argv[1:]`)
202
+
203
+ **Returns:** Instance of the dataclass with merged configuration
204
+
205
+ **Raises:**
206
+ - `ConfigError` — Missing required fields or wrong types
207
+ - `ImportError` — No TOML parser available
208
+
209
+ ## Error Messages
210
+
211
+ Clevis provides helpful, actionable errors:
212
+
213
+ ```
214
+ ======================================================================
215
+ Configuration Error
216
+ ======================================================================
217
+
218
+ Field: database.host
219
+ Issue: Required field has no value
220
+
221
+ Provide this value in one of these ways:
222
+
223
+ 1. Project config: ./project.toml
224
+ [database]
225
+ host = "your_value"
226
+
227
+ 2. User config: ~/.project.toml
228
+ (same format as above)
229
+
230
+ 3. CLI argument: --database-host <value>
231
+
232
+ ======================================================================
233
+ ```
234
+
235
+ ## Acknowledgments
236
+
237
+ Clevis builds on excellent work from the Python community:
238
+
239
+ - **[tomllib](https://docs.python.org/3/library/tomllib.html)** — Python 3.11+ stdlib
240
+ - **[tomli](https://github.com/hukkin/tomli)** — Pure Python TOML 1.0
241
+ - **[envtoml](https://github.com/sank8m/envtoml)** — Env var interpolation
242
+ - **[tomlev](https://github.com/thesimj/tomlev)** — Env vars with defaults
243
+ - **[dacite](https://github.com/konradhalas/dacite)** — Dict-to-dataclass conversion
244
+
245
+ ## License
246
+
247
+ MIT
248
+
249
+ [pypi]: https://pypi.org/project/clevis/
250
+ [pypi-badge]: https://img.shields.io/pypi/v/clevis.svg
251
+ [python]: https://www.python.org/
252
+ [python-badge]: https://img.shields.io/badge/Python-3.10+-blue.svg
253
+ [ci]: https://github.com/christophevg/clevis/actions/workflows/test.yml
254
+ [ci-badge]: https://img.shields.io/github/actions/workflow/status/christophevg/clevis/test.yml.svg
255
+ [coverage]: https://coveralls.io/github/christophevg/clevis
256
+ [coverage-badge]: https://img.shields.io/coveralls/github/christophevg/clevis.svg
257
+ [license]: LICENSE
258
+ [license-badge]: https://img.shields.io/github/license/christophevg/clevis.svg
259
+ [agentic]: https://christophe.vg/about/Agentic-Workflow
260
+ [agentic-badge]: https://img.shields.io/badge/workflow-agentic-blueviolet?style=flat-square
261
+ [tomli]: https://github.com/hukkin/tomli
262
+ [envtoml]: https://github.com/sank8m/envtoml
263
+ [tomlev]: https://github.com/thesimj/tomlev
@@ -0,0 +1,6 @@
1
+ clevis/__init__.py,sha256=_XKFN1QDr6ldIBEtR4cF5M4Ysgj4V3Wcn_Pr641vSp4,10611
2
+ clevis/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
3
+ clevis-0.1.0.dist-info/METADATA,sha256=6Y0d1Dlvj9PueqU6WtO-XGbIQScMeu-irsAlkLCEzUQ,7811
4
+ clevis-0.1.0.dist-info/WHEEL,sha256=QccIxa26bgl1E6uMy58deGWi-0aeIkkangHcxk2kWfw,87
5
+ clevis-0.1.0.dist-info/licenses/LICENSE,sha256=HZGWgZq6Gti1o1soHE3jmA5UNMVv3gRkPSmOuJz5fdY,1080
6
+ clevis-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.29.0
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Christophe Van Ginneken
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.