conclude 1.0.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.
conclude/merge.py ADDED
@@ -0,0 +1,58 @@
1
+ """The merge itself: layers, lowest priority first.
2
+
3
+ Design: every layer (defaults, a config file, environment variables,
4
+ CLI flags) produces a dict of the *same* keys, using ``None`` (or
5
+ "not present" -- simply omitting the key) to mean "this layer
6
+ expresses no opinion". :func:`resolve` walks the layers from lowest
7
+ to highest priority and lets later, non-``None`` values overwrite
8
+ earlier ones. This avoids nested if/elif/else chains entirely --
9
+ adding a new setting to an application built on conclude means adding
10
+ one entry to its own ``DEFAULTS`` dict, one caster, and (if it should
11
+ be settable that way) one CLI flag and one env var mapping, nothing
12
+ else.
13
+ """
14
+
15
+ from collections.abc import Mapping
16
+ from typing import Any
17
+
18
+ from conclude.infer import Caster
19
+
20
+
21
+ def resolve(
22
+ cli: Mapping[str, Any],
23
+ env: Mapping[str, Any],
24
+ config_file: Mapping[str, Any],
25
+ defaults: Mapping[str, Any],
26
+ casters: Mapping[str, Caster],
27
+ *,
28
+ developer: Mapping[str, Any] | None = None,
29
+ ) -> dict[str, Any]:
30
+ """Merge ``defaults < config_file < env < developer < cli``,
31
+ casting each value as it lands. ``developer`` (the private,
32
+ project-local developer config layer -- see
33
+ :mod:`conclude.developer`) is optional and, when given, sits
34
+ between the environment and the CLI.
35
+
36
+ Returns a plain dict of fully-resolved, correctly-typed settings --
37
+ every key in ``defaults`` is present, plus any key from a layer
38
+ that ``casters`` knows how to cast (a layer key with no matching
39
+ caster is silently ignored, so a config file or environment can
40
+ carry unrelated keys without upsetting the merge).
41
+
42
+ A required-ness check (e.g. "this particular key must not still be
43
+ None once everything is merged") is a caller-level concern, not a
44
+ merging one -- ``resolve`` never raises for a still-``None`` result,
45
+ only for a caster itself raising on a value it was actually given
46
+ (e.g. an unparseable number).
47
+ """
48
+ merged: dict[str, Any] = {}
49
+ for layer in (defaults, config_file, env, developer or {}, cli):
50
+ for key, value in layer.items():
51
+ if key not in casters:
52
+ continue
53
+ if value is None:
54
+ continue
55
+ merged[key] = casters[key](value)
56
+ for key, value in defaults.items():
57
+ merged.setdefault(key, value)
58
+ return merged
conclude/naming.py ADDED
@@ -0,0 +1,58 @@
1
+ """Deriving the conventional name for a setting in each layer -- its
2
+ CLI flag, its environment variable, its config-file key -- from its
3
+ Python name alone, so an application writes that name exactly once
4
+ (as a key in its defaults dict) instead of once per layer.
5
+ """
6
+
7
+ import re
8
+
9
+ _NON_IDENTIFIER_CHARS = re.compile(r"[^0-9a-zA-Z_]")
10
+
11
+
12
+ def cli_flag_name(key: str) -> str:
13
+ """``"filter_col"`` -> ``"--filter-col"``."""
14
+ return "--" + key.replace("_", "-")
15
+
16
+
17
+ def env_var_name(app_name: str, key: str) -> str:
18
+ """``("remind", "retries")`` -> ``"REMIND_RETRIES"``.
19
+
20
+ A hyphen (or any other character that can't appear in a shell
21
+ environment variable name) in either part is replaced with an
22
+ underscore first -- e.g. ``("my-app", "foo_bar")`` ->
23
+ ``"MY_APP_FOO_BAR"``, not the shell-illegal ``"MY-APP_FOO_BAR"``
24
+ a bare ``.upper()`` would otherwise produce. Common for the app
25
+ name in particular, since "my-app"-style hyphenated names are a
26
+ normal CLI-tool naming convention that an app's own ``name`` is
27
+ often taken straight from (e.g. its package name).
28
+
29
+ A leading underscore is added if the result would otherwise start
30
+ with a digit (``("123app", "foo")`` -> ``"_123APP_FOO"``, not the
31
+ equally shell-illegal ``"123APP_FOO"`` -- a shell identifier can't
32
+ start with a digit even though it can contain one).
33
+
34
+ One thing this can't fix: two app names that only differ by which
35
+ non-identifier character they use in the same position --
36
+ ``"my-app"`` and ``"my_app"``, say -- sanitize to the *same* env
37
+ var prefix. Not a concern for a single app calling this on its own
38
+ name, but worth knowing if you're generating names for several
39
+ apps at once.
40
+ """
41
+ name = f"{_sanitize(app_name)}_{_sanitize(key)}".upper()
42
+ if name[:1].isdigit():
43
+ name = f"_{name}"
44
+ return name
45
+
46
+
47
+ def config_key_name(key: str) -> str:
48
+ """The config-file key for ``key`` -- always just ``key`` itself,
49
+ since a config file's keys already *are* your defaults dict's
50
+ keys. Provided for symmetry with :func:`cli_flag_name`/
51
+ :func:`env_var_name`, and so calling code never has to special-case
52
+ "no translation needed" for this one layer.
53
+ """
54
+ return key
55
+
56
+
57
+ def _sanitize(text: str) -> str:
58
+ return _NON_IDENTIFIER_CHARS.sub("_", text)
conclude/paths.py ADDED
@@ -0,0 +1,26 @@
1
+ """The conventional per-app config file locations."""
2
+
3
+ from pathlib import Path
4
+
5
+
6
+ def default_config_home_path(app_name: str) -> Path:
7
+ """``~/.config/<app_name>/config.toml`` -- the usual user-global
8
+ config file location (XDG-style, minus actually honoring
9
+ ``$XDG_CONFIG_HOME``; pass your own path to the loading functions
10
+ instead if your app needs that).
11
+ """
12
+ return Path.home() / ".config" / app_name / "config.toml"
13
+
14
+
15
+ def default_config_system_path(app_name: str) -> Path:
16
+ """``/etc/<app_name>/config.toml`` -- the conventional system-wide
17
+ config file location, shared by every user on the machine (the
18
+ lowest-priority config file: any user-global, project-local, env,
19
+ or CLI value overrides it).
20
+
21
+ Unlike the user-global path this is never read unless the app opts
22
+ in (``config_system_path=AUTO`` on :class:`conclude.App`), since
23
+ silently picking up machine-wide configuration is a decision about
24
+ the program, not something that should happen by default.
25
+ """
26
+ return Path("/etc") / app_name / "config.toml"
conclude/py.typed ADDED
File without changes
conclude/templates.py ADDED
@@ -0,0 +1,83 @@
1
+ """Rendering a setting's default value as plain text, an ``.env``
2
+ value, or a TOML value -- the building blocks behind
3
+ :meth:`conclude.App.format_env`, :meth:`conclude.App.format_toml`, and
4
+ :meth:`conclude.App.format_cli`, which generate a ready-to-fill-in
5
+ configuration template (or a bit of reference documentation) straight
6
+ from an app's ``defaults``, so there's no hand-written example config
7
+ to keep in sync with them.
8
+
9
+ Unlike :mod:`conclude.formatters`, whose output is a shell-quoted CLI
10
+ token, everything here renders for a *reader* or a *file*: unquoted
11
+ ``localhost``, not ``'localhost'``.
12
+ """
13
+
14
+ import re
15
+ import shlex
16
+ from collections.abc import Callable
17
+ from typing import Any
18
+
19
+ from conclude.tomlwrite import toml_string
20
+
21
+ # Characters that never need quoting in an .env value: word characters
22
+ # (letters, digits, "_" -- Unicode-aware, so "café" stays bare) plus a
23
+ # few punctuation marks that are unremarkable in every .env dialect.
24
+ _ENV_BARE_RE = re.compile(r"[\w./:@%+,-]*")
25
+
26
+ _ENV_DOUBLE_QUOTE_ESCAPES = {"\\": "\\\\", '"': '\\"', "\n": "\\n", "\t": "\\t", "\r": "\\r"}
27
+
28
+
29
+ def plain_text(value: Any, formatter: Callable[[Any], str | None] | None = None) -> str | None:
30
+ """``value`` as unquoted, human-readable text, or ``None`` for
31
+ "no value" (an unset ``None`` setting).
32
+
33
+ A bool is ``true``/``false`` (never the bare-flag-or-nothing shape a
34
+ CLI formatter gives it); a list is comma-joined, matching what
35
+ :func:`conclude.cast_comma_list` casts back out of. ``formatter``,
36
+ if given, is an explicit per-setting :data:`conclude.Formatter`
37
+ override -- its (shell-quoted) output is unquoted again, so a
38
+ setting with a custom caster/formatter pair renders as text that
39
+ casts back to the same value.
40
+ """
41
+ if value is None:
42
+ return None
43
+ if isinstance(value, bool):
44
+ return "true" if value else "false"
45
+ if formatter is not None:
46
+ rendered = formatter(value)
47
+ if rendered is None:
48
+ return None
49
+ return " ".join(shlex.split(rendered))
50
+ if isinstance(value, (list, tuple)):
51
+ return ",".join(str(item) for item in value)
52
+ return str(value)
53
+
54
+
55
+ def env_value(text: str) -> str:
56
+ """``text`` as the right-hand side of a ``NAME=value`` line that
57
+ :func:`conclude.load_dotenv` reads back as exactly ``text``: bare
58
+ when it's made only of unremarkable characters, single-quoted
59
+ (taken literally) when it has anything else, and double-quoted with
60
+ backslash escapes only when it contains a single quote or a
61
+ newline/tab/carriage return, which single quotes can't carry.
62
+ """
63
+ if _ENV_BARE_RE.fullmatch(text):
64
+ return text
65
+ if "'" not in text and not any(ch in text for ch in "\n\t\r"):
66
+ return f"'{text}'"
67
+ return '"' + "".join(_ENV_DOUBLE_QUOTE_ESCAPES.get(ch, ch) for ch in text) + '"'
68
+
69
+
70
+ def toml_value(value: Any) -> str:
71
+ """``value`` as a native TOML value: ``true``/``false``, an integer
72
+ or float as-is, a list as an array, and anything else as a quoted
73
+ string.
74
+ """
75
+ if isinstance(value, bool):
76
+ return "true" if value else "false"
77
+ if isinstance(value, int):
78
+ return str(value)
79
+ if isinstance(value, float):
80
+ return repr(value)
81
+ if isinstance(value, (list, tuple)):
82
+ return "[" + ", ".join(toml_value(item) for item in value) + "]"
83
+ return toml_string(str(value))
conclude/tomlwrite.py ADDED
@@ -0,0 +1,29 @@
1
+ """Writing (not reading) small bits of TOML -- for an app feature like
2
+ "print me a ready-to-paste config entry for what I just set up".
3
+ """
4
+
5
+ import re
6
+
7
+ _TOML_BARE_KEY_RE = re.compile(r"^[A-Za-z0-9_-]+$")
8
+ _TOML_CONTROL_RE = re.compile(r"[\x00-\x1f\x7f]")
9
+
10
+
11
+ def toml_string(s: str) -> str:
12
+ """``s`` as a double-quoted TOML basic string, with the characters
13
+ that need escaping there escaped.
14
+ """
15
+ escaped = s.replace("\\", "\\\\").replace('"', '\\"')
16
+ escaped = escaped.replace("\n", "\\n").replace("\t", "\\t").replace("\r", "\\r")
17
+ # Every other control character (and DEL) is illegal unescaped in a
18
+ # TOML basic string.
19
+ escaped = _TOML_CONTROL_RE.sub(lambda m: f"\\u{ord(m.group()):04X}", escaped)
20
+ return f'"{escaped}"'
21
+
22
+
23
+ def toml_key(s: str) -> str:
24
+ """A TOML table/key name for ``s`` -- bare if that's valid syntax
25
+ (letters, digits, ``_``/``-``, non-empty), a quoted string key
26
+ otherwise (TOML allows arbitrary quoted keys, e.g. one containing a
27
+ space).
28
+ """
29
+ return s if _TOML_BARE_KEY_RE.match(s) else toml_string(s)
@@ -0,0 +1,260 @@
1
+ Metadata-Version: 2.5
2
+ Name: conclude
3
+ Version: 1.0.0
4
+ Summary: Set up your defaults; conclude infers the env vars, config-file keys, and CLI flags, and resolves them all into one settings object.
5
+ Project-URL: Homepage, https://github.com/tanakapayam/conclude
6
+ Project-URL: Documentation, https://github.com/tanakapayam/conclude/blob/main/docs/guide.md
7
+ Project-URL: Repository, https://github.com/tanakapayam/conclude
8
+ Project-URL: Issues, https://github.com/tanakapayam/conclude/issues
9
+ Project-URL: Changelog, https://github.com/tanakapayam/conclude/blob/main/CHANGELOG.md
10
+ Author: Payam Tanaka
11
+ License-Expression: MIT
12
+ License-File: LICENSE
13
+ Keywords: cli,config,configuration,dotenv,environment-variables,settings,toml
14
+ Classifier: Development Status :: 5 - Production/Stable
15
+ Classifier: Environment :: Console
16
+ Classifier: Intended Audience :: Developers
17
+ Classifier: Operating System :: OS Independent
18
+ Classifier: Programming Language :: Python :: 3
19
+ Classifier: Programming Language :: Python :: 3 :: Only
20
+ Classifier: Programming Language :: Python :: 3.11
21
+ Classifier: Programming Language :: Python :: 3.12
22
+ Classifier: Programming Language :: Python :: 3.13
23
+ Classifier: Programming Language :: Python :: 3.14
24
+ Classifier: Topic :: Software Development :: Libraries
25
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
26
+ Classifier: Typing :: Typed
27
+ Requires-Python: >=3.11
28
+ Provides-Extra: gitignore
29
+ Requires-Dist: pathspec>=0.12; extra == 'gitignore'
30
+ Description-Content-Type: text/markdown
31
+
32
+ # conclude
33
+
34
+ A setting that can come from a flag, an environment variable, or a
35
+ config file usually gets written down three times: an argparse flag, an
36
+ environment variable, and a config-file key -- each with its own name,
37
+ its own type conversion, and its own copy of the default. Add a setting
38
+ and you touch three places; rename one and the other two quietly drift.
39
+
40
+ conclude has you write it once. Set up your defaults; conclude works
41
+ out the rest. You write down a setting's name, its type, and its
42
+ starting value -- as an entry in a plain dict -- and conclude infers
43
+ the environment variable name, the config-file key, the CLI flag, and
44
+ how to cast a raw value from any of those into the right type.
45
+
46
+ ```python
47
+ import conclude
48
+
49
+ DEFAULTS = {
50
+ "host": "localhost",
51
+ "port": 8080,
52
+ "debug": False,
53
+ "tags": conclude.opt(list), # unset by default, but still a list when set
54
+ }
55
+ app = conclude.App("myapp", DEFAULTS)
56
+
57
+ parser = app.build_arg_parser(prog="myapp")
58
+ namespace = parser.parse_args()
59
+ cli = {k: v for k, v in vars(namespace).items() if v is not None}
60
+ print(app.resolve(cli))
61
+ ```
62
+
63
+ ```
64
+ $ MYAPP_PORT=9000 myapp --debug --tags a,b
65
+ {'host': 'localhost', 'port': 9000, 'debug': True, 'tags': ['a', 'b']}
66
+ ```
67
+
68
+ That one dict gave you the CLI flags (`--host`, `--port`, `--debug`,
69
+ `--tags`), the environment variables (`MYAPP_HOST`, `MYAPP_PORT`, ...),
70
+ the config-file keys (a `[myapp]` table in `~/.config/myapp/config.toml`
71
+ or `./.config.toml`), and a caster for each -- with no second list of
72
+ names to keep in sync.
73
+
74
+ ## Where it fits
75
+
76
+ Configuration libraries tend to start from different things. conclude
77
+ starts from a dict of defaults and derives the rest:
78
+
79
+ | Package | Primary abstraction | Reach for it when... |
80
+ | ----------------- | ------------------------------------------------------------------------ | --------------------------------------------------------------------------------- |
81
+ | `argparse` | CLI parser | you only need flags |
82
+ | ConfigArgParse | argparse plus config files and environment variables | you have an argparse CLI and want files and env vars added |
83
+ | jsonargparse | CLI and config built from type hints | your program is functions, classes or dataclasses with type hints |
84
+ | python-dotenv | `.env` loader | you just need a `.env` in `os.environ` |
85
+ | python-decouple | individual value reader (env, then file, then default) | you read a handful of values, Django-style |
86
+ | pydantic-settings | typed, validated settings model | you want validation, nested models or secret-manager sources |
87
+ | Dynaconf | general-purpose layered configuration system | you want named environments, many file formats, or Vault |
88
+ | Hydra / OmegaConf | hierarchical, composable configuration | you compose config groups, run sweeps or manage experiments |
89
+ | **conclude** | **defaults → inferred env / TOML / CLI interfaces → layered resolution** | **you want all three interfaces from one dict, and safe private local overrides** |
90
+
91
+ conclude deliberately does not do schema validation (values are cast,
92
+ not validated), secret-manager backends, YAML or JSON files, or named
93
+ environments; the table says where to look for those. For a dated,
94
+ feature-by-feature comparison, see
95
+ [How conclude compares](https://github.com/tanakapayam/conclude/blob/main/docs/comparison.md).
96
+
97
+ ## Design principles
98
+
99
+ - **One source of truth.** The defaults dict. Flags, environment
100
+ variable names, config keys, casters and the generated templates are
101
+ all derived from it, so they can't drift apart.
102
+ - **Opt in to anything unusual.** The system file, `.env` and the
103
+ developer file are off until you ask; the user and project files are
104
+ one `None` from off.
105
+ - **Private files have to prove they're private.** The developer file
106
+ (and `.env`, if you ask) is read only when git is ignoring it.
107
+ - **Quiet for ordinary situations, loud for broken setups.** A missing
108
+ local file is normal and silent, and `describe_sources()` explains
109
+ it; a malformed developer file or a missing `pathspec` raises.
110
+ - **Standard library only.** `pathspec`, for the gitignore check, is an
111
+ optional extra.
112
+
113
+ ## The layers
114
+
115
+ ```
116
+ 1. CLI flags
117
+ 2. Developer config file (opt-in; private, gitignored, project-local,
118
+ TOML or dotenv -- beats the environment)
119
+ 3. Environment variables (with an opt-in `.env` file as a fallback)
120
+ 4. Config file(s), in this order (each overrides the previous):
121
+ a. /etc/<app>/config.toml (system-wide; opt-in, off by default)
122
+ b. ~/.config/<app>/config.toml (user-global, overrides system)
123
+ c. ./.config.toml (project-local, overrides user)
124
+ d. ./.config.*.toml (sibling files, sorted by name)
125
+ 5. Hardcoded defaults
126
+ ```
127
+
128
+ Or, lowest priority to highest:
129
+
130
+ ```
131
+ defaults < system < user < project < env < developer < CLI
132
+ ```
133
+
134
+ Everything hangs off that one dict: the layers it feeds, and what
135
+ conclude infers from it so you never write any of it twice:
136
+
137
+ ```
138
+ ┌── CLI
139
+ defaults ───────┼── developer config (explicit opt-in, must be gitignored)
140
+ │ ├── environment
141
+ │ ├── .env (explicit opt-in)
142
+ │ └── config files
143
+ │
144
+ └── inference
145
+ ├── caster
146
+ ├── formatter
147
+ ├── CLI name
148
+ └── environment name
149
+ ```
150
+
151
+ (The layers are listed highest priority first; `defaults` sits beneath
152
+ all of them.) The user and project config files are on by default, and
153
+ each is one `None` away from off. The system file, `.env`, and the
154
+ developer file are off until you ask for them.
155
+
156
+ ## Local files: which one?
157
+
158
+ Two of the layers are about your own machine, and each comes in two
159
+ formats:
160
+
161
+ | Format | Below the environment | Above the environment |
162
+ | ------ | ----------------------------------------- | ------------------------------------------ |
163
+ | TOML | the system, user and project config files | the developer file, e.g. `.developer.toml` |
164
+ | dotenv | `.env` | the developer file, e.g. `.env.local` |
165
+
166
+ - **Non-secret local defaults you're happy to commit** -- use `.env`.
167
+ A real `export` in the shell still beats it, so it can't override a
168
+ deployment.
169
+ - **Private values that must win over stray shell exports** (a local
170
+ `DATABASE_URL`, say) -- use the developer file. Make it TOML if only
171
+ your app reads it (it gets `[table]`s, including per-recipient ones);
172
+ make it dotenv, like `.env.local`, if docker compose, direnv or your
173
+ IDE need the same values.
174
+ - **A private `.env` that stays below the environment** -- use `.env`
175
+ with `dotenv_require_gitignored=True`.
176
+
177
+ The developer file's location lives in your committed `pyproject.toml`
178
+ (any name works; the name decides the format, and only `.toml` means
179
+ TOML):
180
+
181
+ ```toml
182
+ [tool.conclude.developer]
183
+ config = ".developer.toml" # or ".env.local"
184
+ ```
185
+
186
+ ```python
187
+ app = conclude.App(
188
+ "myapp",
189
+ DEFAULTS,
190
+ pyproject_path=conclude.AUTO, # the developer file
191
+ dotenv_path=conclude.AUTO, # .env
192
+ )
193
+ ```
194
+
195
+ The developer file is always guarded, and `.env` is when you ask: the
196
+ file is used only if it exists, sits in a git working tree, and is
197
+ gitignored. Otherwise it is skipped quietly, and `describe_sources()`
198
+ says why. Details: [the `.env` file](https://github.com/tanakapayam/conclude/blob/main/docs/guide.md#6-local-development-a-env-file), [the developer file](https://github.com/tanakapayam/conclude/blob/main/docs/guide.md#8-a-private-developer-config-file).
199
+
200
+ ## What's in it
201
+
202
+ - **Inference** of casters, formatters, CLI flags, env var names and
203
+ config keys from the defaults alone; `opt(type)` for a setting that
204
+ starts out unset ([guide, section 1](https://github.com/tanakapayam/conclude/blob/main/docs/guide.md#1-the-bare-minimum), [section 3](https://github.com/tanakapayam/conclude/blob/main/docs/guide.md#3-when-inference-isnt-quite-enough-casters-and-formatters)).
205
+ - **Config-file tables**, including a positional shorthand that picks a
206
+ table per recipient/deck/profile ([section 4](https://github.com/tanakapayam/conclude/blob/main/docs/guide.md#4-a-positional-shorthand--per-recipient-config-tables)).
207
+ - **`--print-invocation`** and `App.format_invocation()`: print the
208
+ command line that reproduces a resolved configuration
209
+ ([section 5](https://github.com/tanakapayam/conclude/blob/main/docs/guide.md#5-debugging-and-documentation---print-invocation)).
210
+ - **A `.env` fallback**, optionally required to be gitignored, sitting
211
+ beneath real environment variables ([section 6](https://github.com/tanakapayam/conclude/blob/main/docs/guide.md#6-local-development-a-env-file)).
212
+ - **A system-wide config file**, the lowest-priority file ([section 7](https://github.com/tanakapayam/conclude/blob/main/docs/guide.md#7-machine-wide-defaults-a-system-config-file)).
213
+ - **A private developer file**, TOML or dotenv, that beats ambient
214
+ environment variables ([section 8](https://github.com/tanakapayam/conclude/blob/main/docs/guide.md#8-a-private-developer-config-file)).
215
+ - **`describe_sources()`** for `--help`: which sources are in play, and
216
+ why a given one isn't ([section 9](https://github.com/tanakapayam/conclude/blob/main/docs/guide.md#9-turning-off-a-source-and-telling-the-user)).
217
+ - **Templates and docs generated from your defaults**:
218
+ `format_env()`, `format_toml()`, `format_cli()`
219
+ ([section 10](https://github.com/tanakapayam/conclude/blob/main/docs/guide.md#10-generating-docs-and-templates)).
220
+ - Standard library only, fully typed (`py.typed`), Python 3.11+. The
221
+ gitignore check (developer file, strict `.env`) uses the optional
222
+ `pathspec` package.
223
+
224
+ ## Install
225
+
226
+ ```
227
+ pip install conclude
228
+ ```
229
+
230
+ The developer config layer (and a `.env` you ask to be held to the same
231
+ standard) needs one small pure-Python dependency, only to check that a
232
+ private file is gitignored:
233
+
234
+ ```
235
+ pip install 'conclude[gitignore]'
236
+ ```
237
+
238
+ ## Documentation
239
+
240
+ - [Guide](https://github.com/tanakapayam/conclude/blob/main/docs/guide.md) -- builds a small CLI, `remind`, one
241
+ idea at a time.
242
+ - [Reference](https://github.com/tanakapayam/conclude/blob/main/docs/reference.md) -- every class, method, and
243
+ module.
244
+ - [Changelog](https://github.com/tanakapayam/conclude/blob/main/CHANGELOG.md).
245
+
246
+ ## Development
247
+
248
+ ```
249
+ uv sync
250
+ uv run pytest
251
+ uv run ruff check . && uv run ruff format --check .
252
+ uv run mypy
253
+ uv build && uvx twine check --strict dist/*
254
+ ```
255
+
256
+ CI runs these same commands on Python 3.11 through 3.14.
257
+
258
+ ## License
259
+
260
+ MIT -- see [LICENSE](https://github.com/tanakapayam/conclude/blob/main/LICENSE).
@@ -0,0 +1,19 @@
1
+ conclude/__init__.py,sha256=MKDasCQZ2cQJX4Ts9-8hvflyKhjc8eenAhLgZpfoxWs,8155
2
+ conclude/app.py,sha256=hFGiacq_ID_nOwNf_mGlSMi0yZF2a7vBAiTIyZYwTE8,38572
3
+ conclude/casters.py,sha256=crgvKmugrgG9ElCtRxajbqEj4tUQymPFc3qwcCs0V_8,11044
4
+ conclude/developer.py,sha256=emRRrO0OCVNUoZc7Lr5ettah_bQXd1SdNyhiLvIusAM,9415
5
+ conclude/env.py,sha256=BFMjiHZ2btIphmHZ4qnRDRlaGadUb29-hBfSPTBS4IE,7018
6
+ conclude/files.py,sha256=if_XvwlGKvAob7GTa3LTlVYTaEEyDYcA0mfzCOGipwM,10383
7
+ conclude/formatters.py,sha256=qTJJqVG6XV4k3YQCPNS2bneTfxy1LIya4LHOTaldOas,3724
8
+ conclude/guard.py,sha256=2IhlxeivFaLcJ1mEYmADS2lObrxdZzlLLwAE6j1yn3c,7223
9
+ conclude/infer.py,sha256=zsZ8iMZG5o-7nNwaZGff_AKFKuQAQ04sup_hDL2fZbE,4493
10
+ conclude/merge.py,sha256=tlOLgz0APaVov65K2GamBIkQSz7oRFzMZXZ54F9Kqwk,2355
11
+ conclude/naming.py,sha256=BLZkW9qbq4ReFfjEOFUwwkcouHoI5muP6sNf7aep3P0,2315
12
+ conclude/paths.py,sha256=d4b0t-rYIk5p7mnzNqgaDR-sgA0RLX12HhHjMSDJeD8,1072
13
+ conclude/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
14
+ conclude/templates.py,sha256=kMhOgFmXS-PGmJF_jVR6qJ-d-6LB-XOtC4oiaFqSatU,3376
15
+ conclude/tomlwrite.py,sha256=oGTZbaOyyOmv42JJqG1GFLrs-vJNBmiF-jCFL_VxZXk,1076
16
+ conclude-1.0.0.dist-info/METADATA,sha256=IQqX_hoDVcejBJ0qvPfsRXWeyelrS2bF2y-YK66Hlzg,12802
17
+ conclude-1.0.0.dist-info/WHEEL,sha256=THafob7ofN-NsuMN7Mg4qZyHaQI7KkD-QlcQatYhXPo,87
18
+ conclude-1.0.0.dist-info/licenses/LICENSE,sha256=EywLvILKrIuRUTXOSGJgDxKm1YH0g7zpbRG8vvzwMHA,1060
19
+ conclude-1.0.0.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.32.3
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,7 @@
1
+ Copyright 2026 Payam Tanaka
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
4
+
5
+ The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
6
+
7
+ THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.