typerconf 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.
typerconf/.gitignore ADDED
@@ -0,0 +1,4 @@
1
+ __init__.py
2
+ init.tex
3
+ test.py
4
+
typerconf/Makefile ADDED
@@ -0,0 +1,18 @@
1
+ MODULES+= __init__.py
2
+
3
+ .PHONY: all
4
+ all: ${MODULES}
5
+
6
+ .INTERMEDIATE: init.py
7
+ __init__.py: init.py
8
+ ${MV} $< $@
9
+
10
+
11
+ .PHONY: clean
12
+ clean:
13
+ ${RM} -R ${MODULES} __pycache__
14
+ ${RM} *.tex
15
+
16
+
17
+ INCLUDE_MAKEFILES=../../../../makefiles
18
+ include ${INCLUDE_MAKEFILES}/noweb.mk
typerconf/__init__.py ADDED
@@ -0,0 +1,217 @@
1
+ """The typerconf module and config subcommand"""
2
+
3
+ import appdirs
4
+ import json
5
+ import logging
6
+ import os
7
+ import sys
8
+ import typer
9
+ import typing
10
+
11
+ dirs = appdirs.AppDirs(sys.argv[0])
12
+
13
+ class Config:
14
+ """Navigates nested JSON structures by dot-separated addressing."""
15
+
16
+ def __init__(self, json_data={}):
17
+ """
18
+ Constructs a config object to navigate from JSON data `json_data`.
19
+ """
20
+ self.__data = json_data
21
+
22
+ def get(self, path: str = "") -> typing.Any:
23
+ """
24
+ Returns object at `path`.
25
+ Example:
26
+ - `path = "courses.datintro22.url"` and
27
+ - Config contains `{"courses": {"datintro22": {"url": "https://..."}}}`
28
+ will return "https://...".
29
+
30
+ Any part of the path that can be converted to an integer, will be converted
31
+ to an integer. This way we can access elements of lists too.
32
+ """
33
+ structure = self.__data
34
+
35
+ if not path:
36
+ return structure
37
+
38
+ for part in path.split("."):
39
+ try:
40
+ part = int(part)
41
+ except ValueError:
42
+ pass
43
+
44
+ try:
45
+ structure = structure[part]
46
+ except KeyError:
47
+ raise KeyError(f"{part} along {path} doesn't exist")
48
+
49
+ return structure
50
+
51
+ def set(self, path: str, value: typing.Any):
52
+ """
53
+ Sets `value` at `path`. Any parts along the path that don't exist will be
54
+ created.
55
+
56
+ Example:
57
+ - `value = "https://..."`,
58
+ - `path = "courses.datintro22.url"`
59
+ will create `{"courses": {"datintro22": {"url": "https://..."}}}`.
60
+
61
+ Any part of the path that can be converted to an integer, will be converted
62
+ to an integer. This way we can access elements of lists too. However, we
63
+ cannot create index 3 in a list if it doesn't exist.
64
+ """
65
+ structure = self.__data
66
+
67
+ parts = path.split(".")
68
+ for part in parts[:-1]:
69
+ try:
70
+ part = int(part)
71
+ except ValueError:
72
+ pass
73
+
74
+ try:
75
+ structure = structure[part]
76
+ except KeyError:
77
+ structure[part] = {}
78
+ structure = structure[part]
79
+
80
+ part = parts[-1]
81
+ try:
82
+ part = int(part)
83
+ except ValueError:
84
+ pass
85
+ structure[part] = value
86
+
87
+ def paths(self, from_root=""):
88
+ """
89
+ Returns all existing paths.
90
+
91
+ The optional argument `from_root` is a path and the method return all
92
+ subpaths rooted at that point.
93
+ """
94
+ paths = []
95
+ root = self.get(from_root)
96
+
97
+ if isinstance(root, dict):
98
+ for part in root:
99
+ if from_root:
100
+ path = f"{from_root}.{part}"
101
+ else:
102
+ path = part
103
+
104
+ paths.append(path)
105
+ paths += self.paths(from_root=path)
106
+ elif isinstance(root, list):
107
+ paths += [f"{from_root}.{x}" for x in range(len(root))]
108
+
109
+ return paths
110
+ def add_config_cmd(cli: typer.Typer):
111
+ """
112
+ Add config command to Typer cli
113
+ """
114
+ path_arg = typer.Argument(...,
115
+ help="Path in config, e.g. 'courses.datintro22'. "
116
+ "Empty string is root of config.",
117
+ autocompletion=complete_path)
118
+ value_arg = typer.Option([], "-s", "--set",
119
+ help="Values to store. "
120
+ "More than one value makes a list. "
121
+ "Values are treated as JSON if possible.")
122
+
123
+ @cli.command(name="config")
124
+ def config_cmd(path: str = path_arg,
125
+ values: typing.List[str] = value_arg):
126
+ """
127
+ Reads values from or writes values to the config.
128
+ """
129
+ if values:
130
+ set(path, values)
131
+ else:
132
+ print_config(get(path), path)
133
+ def get(path: str = "") -> typing.Any:
134
+ """
135
+ Returns the value stored at `path` in the config.
136
+
137
+ By default, `path = ""`, which returns the entire configuration as a Config
138
+ object.
139
+ """
140
+ conf = read_config()
141
+ return conf.get(path)
142
+
143
+ def set(path: str, values: typing.List[typing.Any]):
144
+ """
145
+ Sets `value` at `path` in the config. `value` will be interpreted as JSON, if
146
+ conversion to JSON fails, it will be used as is.
147
+ """
148
+ conf = read_config()
149
+ for i in range(len(values)):
150
+ try:
151
+ values[i] = json.loads(values[i])
152
+ except json.decoder.JSONDecodeError:
153
+ pass
154
+
155
+ if len(values) == 1:
156
+ values = values[0]
157
+
158
+ conf.set(path, values)
159
+ write_config(conf)
160
+ def read_config(conf_path=f"{dirs.user_config_dir}/config.json"):
161
+ """
162
+ Returns the config data structure (JSON).
163
+ `conf_path` is an optional argument providing the path to the config file.
164
+ """
165
+ try:
166
+ with open(conf_path) as conf_file:
167
+ return Config(json.load(conf_file))
168
+ except FileNotFoundError:
169
+ logging.warning(f"Config file {conf_path} could not be found.")
170
+ except json.decoder.JSONDecodeError as err:
171
+ logging.warning(f"Config file {conf_path} could not be decoded: {err}")
172
+
173
+ return Config()
174
+
175
+ def write_config(conf,
176
+ conf_path=f"{dirs.user_config_dir}/config.json"):
177
+ """
178
+ Stores the config data `conf` (extracts JSON) in the config file.
179
+ `conf_path` is an optional argument providing the path to the config file.
180
+ """
181
+ conf_dir = os.path.dirname(conf_path)
182
+ if not os.path.isdir(conf_dir):
183
+ os.mkdir(conf_dir)
184
+
185
+ with open(conf_path, "w") as conf_file:
186
+ json.dump(conf.get(), conf_file)
187
+ def complete_path(initial_path: str, conf: Config = None):
188
+ """
189
+ Returns all valid paths in the config starting with `initial_path`.
190
+ If `conf` is not None, use that instead of the actual config.
191
+ """
192
+ if not conf:
193
+ conf = Config(get())
194
+
195
+ return list(filter(lambda x: x.startswith(initial_path),
196
+ conf.paths()))
197
+ def print_config(conf, path=""):
198
+ """
199
+ Prints the config tree contained in `conf` to stdout.
200
+ Optional `path` is prepended.
201
+ """
202
+ try:
203
+ for key in conf.keys():
204
+ if path:
205
+ print_config(conf[key], f"{path}.{key}")
206
+ else:
207
+ print_config(conf[key], key)
208
+ except AttributeError:
209
+ print(f"{path} = {conf}")
210
+
211
+ def main():
212
+ cli = typer.Typer()
213
+ add_config_cmd(cli)
214
+ cli()
215
+
216
+ if __name__ == "__main__":
217
+ main()
typerconf/init.nw ADDED
@@ -0,0 +1,442 @@
1
+ \section{Code outline}\label{configmodule}
2
+
3
+ The base structure is standard.
4
+ <<init.py>>=
5
+ """The typerconf module and config subcommand"""
6
+
7
+ import appdirs
8
+ import json
9
+ import logging
10
+ import os
11
+ import sys
12
+ import typer
13
+ import typing
14
+
15
+ <<set up appdirs dirs>>
16
+
17
+ <<classes>>
18
+ <<helper functions>>
19
+
20
+ def main():
21
+ <<main body>>
22
+
23
+ if __name__ == "__main__":
24
+ main()
25
+ @
26
+
27
+ \subsection{Not using [[add_typer]]}
28
+
29
+ With the design outlined in \cref{Overview}, we can't use the default way of
30
+ Typer ([[.add_typer]]), as that would require another level of subcommands:
31
+ \begin{minted}{bash}
32
+ nytid config get courses.datintro22.schedule.url
33
+ \end{minted}
34
+ to read out the value, but we don't want that [[get]] part in there.
35
+
36
+ To work around that we need the [[cli]] object from the parent module here.
37
+ Thus, instead of that module doing something like
38
+ \begin{minted}{python}
39
+ import typerconf as config
40
+ # ...
41
+ cli.add_typer(config.cli, name="config")
42
+ \end{minted}
43
+ as is normally done with Typer, that module will actually have to do
44
+ \begin{minted}{python}
45
+ import typerconf as config
46
+ # ...
47
+ config.add_config_cmd(cli)
48
+ \end{minted}
49
+ So we need to provide such a function.
50
+ <<helper functions>>=
51
+ def add_config_cmd(cli: typer.Typer):
52
+ """
53
+ Add config command to Typer cli
54
+ """
55
+ <<config subcommands>>
56
+ @
57
+
58
+ To make this module runnable on its own (using [[main]]), we will create a
59
+ [[cli]] object in the [[main]] function, then add the config command and
60
+ finally run it.
61
+ (This is exactly how a program would add the [[config]] subcommand.)
62
+ <<main body>>=
63
+ cli = typer.Typer()
64
+ add_config_cmd(cli)
65
+ cli()
66
+ @
67
+
68
+ \subsection{Testing}
69
+
70
+ We also want to test all functions we provide in this module.
71
+ These test functions are all prepended [[test_]] to the function name.
72
+ We will run them using [[pytest]].
73
+ <<test init.py>>=
74
+ from typerconf import *
75
+
76
+ <<test functions>>
77
+ @
78
+
79
+
80
+ \section{Application directories}
81
+
82
+ We use the application directory locations as provided by the [[appdirs]]
83
+ package.
84
+ We automated the step with the application name by using the value of the
85
+ command run, \ie~[[sys.argv[0]]].
86
+ (However, this default can be updated by just replacing the value of [[dirs]].)
87
+ <<set up appdirs dirs>>=
88
+ dirs = appdirs.AppDirs(sys.argv[0])
89
+ @
90
+
91
+
92
+ \section{Accessing the configuration: the [[get]] and [[set]] functions}
93
+
94
+ We will provide two functions, [[get]] and [[set]], that modifies the config,
95
+ immediately syncing to the file system.
96
+ This is the API to be used by any part of a program using this module to manage
97
+ the configuration.
98
+ <<helper functions>>=
99
+ def get(path: str = "") -> typing.Any:
100
+ """
101
+ Returns the value stored at `path` in the config.
102
+
103
+ By default, `path = ""`, which returns the entire configuration as a Config
104
+ object.
105
+ """
106
+ <<read config from file>>
107
+ <<return the value at path in config>>
108
+
109
+ def set(path: str, values: typing.List[typing.Any]):
110
+ """
111
+ Sets `value` at `path` in the config. `value` will be interpreted as JSON, if
112
+ conversion to JSON fails, it will be used as is.
113
+ """
114
+ <<read config from file>>
115
+ <<set the value at path in config>>
116
+ <<write config back to file>>
117
+ @
118
+
119
+ Let's start with reading and writing the config file.
120
+ <<read config from file>>=
121
+ conf = read_config()
122
+ <<write config back to file>>=
123
+ write_config(conf)
124
+ @ We'll see these functions in \cref{ConfigFile}.
125
+
126
+ Now that we have the config structure in [[conf]] we can use it.
127
+ Let's start with getting a value.
128
+ <<return the value at path in config>>=
129
+ return conf.get(path)
130
+ @
131
+
132
+ Now, to set a value, we have several cases.
133
+ If the user supplied more than one value, we want a list.
134
+ If only one value, we don't want to store a list with only one value in it.
135
+ Also, we want to try interpret any value as JSON.
136
+ If that fails, we'll use the value as-is.
137
+ <<set the value at path in config>>=
138
+ for i in range(len(values)):
139
+ try:
140
+ values[i] = json.loads(values[i])
141
+ except json.decoder.JSONDecodeError:
142
+ pass
143
+
144
+ if len(values) == 1:
145
+ values = values[0]
146
+
147
+ conf.set(path, values)
148
+ @
149
+
150
+ Now let's cover [[read_config]], [[write_config]], [[set_path]] and
151
+ [[get_path]] below.
152
+
153
+
154
+ \section{Reading and writing the config file}\label{ConfigFile}
155
+
156
+ The configuration file is stored in a suitable system location.
157
+ For this we use the AppDirs package, we have the [[dirs]] instance above.
158
+ We want to read the config and return a JSON structure as outlined above
159
+ (\cref{ConfigStructure}).
160
+ And conversely, write one to the config file as well.
161
+ <<helper functions>>=
162
+ def read_config(conf_path=f"{dirs.user_config_dir}/config.json"):
163
+ """
164
+ Returns the config data structure (JSON).
165
+ `conf_path` is an optional argument providing the path to the config file.
166
+ """
167
+ try:
168
+ with open(conf_path) as conf_file:
169
+ return Config(json.load(conf_file))
170
+ except FileNotFoundError:
171
+ logging.warning(f"Config file {conf_path} could not be found.")
172
+ except json.decoder.JSONDecodeError as err:
173
+ logging.warning(f"Config file {conf_path} could not be decoded: {err}")
174
+
175
+ return Config()
176
+
177
+ def write_config(conf,
178
+ conf_path=f"{dirs.user_config_dir}/config.json"):
179
+ """
180
+ Stores the config data `conf` (extracts JSON) in the config file.
181
+ `conf_path` is an optional argument providing the path to the config file.
182
+ """
183
+ conf_dir = os.path.dirname(conf_path)
184
+ if not os.path.isdir(conf_dir):
185
+ os.mkdir(conf_dir)
186
+
187
+ with open(conf_path, "w") as conf_file:
188
+ json.dump(conf.get(), conf_file)
189
+ @
190
+
191
+
192
+ \section{Navigating config structures}\label{ConfigClass}
193
+
194
+ We provide the class [[Config]] to navigate the config structure.
195
+ This class has two methods that are central:
196
+ The first gets a value out, the other sets a value in.
197
+ Both work with these dot-separated addresses.
198
+ <<classes>>=
199
+ class Config:
200
+ """Navigates nested JSON structures by dot-separated addressing."""
201
+
202
+ def __init__(self, json_data={}):
203
+ """
204
+ Constructs a config object to navigate from JSON data `json_data`.
205
+ """
206
+ self.__data = json_data
207
+
208
+ def get(self, path: str = "") -> typing.Any:
209
+ """
210
+ Returns object at `path`.
211
+ Example:
212
+ - `path = "courses.datintro22.url"` and
213
+ - Config contains `{"courses": {"datintro22": {"url": "https://..."}}}`
214
+ will return "https://...".
215
+
216
+ Any part of the path that can be converted to an integer, will be converted
217
+ to an integer. This way we can access elements of lists too.
218
+ """
219
+ <<get value at path>>
220
+
221
+ def set(self, path: str, value: typing.Any):
222
+ """
223
+ Sets `value` at `path`. Any parts along the path that don't exist will be
224
+ created.
225
+
226
+ Example:
227
+ - `value = "https://..."`,
228
+ - `path = "courses.datintro22.url"`
229
+ will create `{"courses": {"datintro22": {"url": "https://..."}}}`.
230
+
231
+ Any part of the path that can be converted to an integer, will be converted
232
+ to an integer. This way we can access elements of lists too. However, we
233
+ cannot create index 3 in a list if it doesn't exist.
234
+ """
235
+ <<set value at path>>
236
+
237
+ def paths(self, from_root=""):
238
+ """
239
+ Returns all existing paths.
240
+
241
+ The optional argument `from_root` is a path and the method return all
242
+ subpaths rooted at that point.
243
+ """
244
+ <<return list of all paths>>
245
+ @
246
+
247
+ We test these methods with the examples from the docstrings.
248
+ <<test functions>>=
249
+ conf = Config({
250
+ "courses": {
251
+ "datintro22": {
252
+ "url": "https://...",
253
+ "TAs": ["Asse", "Assa"]
254
+ }
255
+ }
256
+ })
257
+
258
+ def test_get_path():
259
+ assert conf.get("courses.datintro22.url") == "https://..."
260
+ assert conf.get("courses.datintro22.TAs.0") == "Asse"
261
+
262
+ def test_set_path():
263
+ value = "Asselina"
264
+ path = "courses.datintro22.TAs.0"
265
+ conf.set(path, value)
266
+ assert conf.get(path) == value
267
+
268
+ value = ["Asse", "Assa", "Asselina"]
269
+ path = "courses.prgx22.TAs"
270
+ conf.set(path, value)
271
+ assert len(conf.get(path)) == len(value)
272
+
273
+ def test_paths():
274
+ assert "courses.datintro22.TAs" in conf.paths()
275
+ for path in conf.paths():
276
+ assert conf.get(path)
277
+ @
278
+
279
+ \subsection{Getting values}
280
+
281
+ To get the value, we simply walk along the path and returns what remains.
282
+ <<get value at path>>=
283
+ structure = self.__data
284
+
285
+ if not path:
286
+ return structure
287
+
288
+ for part in path.split("."):
289
+ try:
290
+ part = int(part)
291
+ except ValueError:
292
+ pass
293
+
294
+ try:
295
+ structure = structure[part]
296
+ except KeyError:
297
+ raise KeyError(f"{part} along {path} doesn't exist")
298
+
299
+ return structure
300
+ @
301
+
302
+ \subsection{Setting values}
303
+
304
+ To set a value is a bit more complex.
305
+ We want to be able to specify a path and create all parents along the path if
306
+ they don't exist.
307
+ <<set value at path>>=
308
+ structure = self.__data
309
+
310
+ parts = path.split(".")
311
+ for part in parts[:-1]:
312
+ try:
313
+ part = int(part)
314
+ except ValueError:
315
+ pass
316
+
317
+ try:
318
+ structure = structure[part]
319
+ except KeyError:
320
+ structure[part] = {}
321
+ structure = structure[part]
322
+
323
+ part = parts[-1]
324
+ try:
325
+ part = int(part)
326
+ except ValueError:
327
+ pass
328
+ structure[part] = value
329
+ @
330
+
331
+ \subsection{All existing paths}
332
+
333
+ Lastly, what we want to do is to create a list containing all paths in the
334
+ config.
335
+ We will simply traverse the config tree and add paths as we go.
336
+ We need to treat dictionaries and lists differently.
337
+ And anything else is a leaf.
338
+ <<return list of all paths>>=
339
+ paths = []
340
+ root = self.get(from_root)
341
+
342
+ if isinstance(root, dict):
343
+ for part in root:
344
+ if from_root:
345
+ path = f"{from_root}.{part}"
346
+ else:
347
+ path = part
348
+
349
+ paths.append(path)
350
+ paths += self.paths(from_root=path)
351
+ elif isinstance(root, list):
352
+ paths += [f"{from_root}.{x}" for x in range(len(root))]
353
+
354
+ return paths
355
+ @
356
+
357
+
358
+ \section{The [[config]] command}
359
+
360
+ We will provide the [[config]] command as outlined above.
361
+ If it gets a value, it will set it as the value at path.
362
+ Otherwise, it will print the current value at path.
363
+ <<config subcommands>>=
364
+ path_arg = typer.Argument(...,
365
+ help="Path in config, e.g. 'courses.datintro22'. "
366
+ "Empty string is root of config.",
367
+ autocompletion=complete_path)
368
+ value_arg = typer.Option([], "-s", "--set",
369
+ help="Values to store. "
370
+ "More than one value makes a list. "
371
+ "Values are treated as JSON if possible.")
372
+
373
+ @cli.command(name="config")
374
+ def config_cmd(path: str = path_arg,
375
+ values: typing.List[str] = value_arg):
376
+ """
377
+ Reads values from or writes values to the config.
378
+ """
379
+ if values:
380
+ set(path, values)
381
+ else:
382
+ print_config(get(path), path)
383
+ @
384
+
385
+ \subsection{Autocompleting the path}
386
+
387
+ The [[complete_path]] functions returns the possible completions for an
388
+ incomplete path from the command line.
389
+ <<helper functions>>=
390
+ def complete_path(initial_path: str, conf: Config = None):
391
+ """
392
+ Returns all valid paths in the config starting with `initial_path`.
393
+ If `conf` is not None, use that instead of the actual config.
394
+ """
395
+ if not conf:
396
+ conf = Config(get())
397
+
398
+ return list(filter(lambda x: x.startswith(initial_path),
399
+ conf.paths()))
400
+ @
401
+
402
+ We test this function as follows.
403
+ <<test functions>>=
404
+ def test_complete_path():
405
+ conf = Config({
406
+ "courses": {
407
+ "datintro22": {
408
+ "url": "https://...",
409
+ "TAs": ["Asse", "Assa"]
410
+ }
411
+ }
412
+ })
413
+
414
+ incomplete = "courses.datintro22.T"
415
+ assert "courses.datintro22.TAs" in complete_path(incomplete, conf)
416
+ assert "courses.datintro22.url" not in complete_path(incomplete, conf)
417
+ assert len(complete_path(incomplete, conf)) >= 0
418
+ @
419
+
420
+ \subsection{Printing the config}
421
+
422
+ That [[print_config]] function should print the remaining levels of the config
423
+ tree.
424
+ And we want it to print on the format of
425
+ [[courses.datintro22.url = https://...]].
426
+ This function will do a depth-first traversal through the config to print all
427
+ values.
428
+ <<helper functions>>=
429
+ def print_config(conf, path=""):
430
+ """
431
+ Prints the config tree contained in `conf` to stdout.
432
+ Optional `path` is prepended.
433
+ """
434
+ try:
435
+ for key in conf.keys():
436
+ if path:
437
+ print_config(conf[key], f"{path}.{key}")
438
+ else:
439
+ print_config(conf[key], key)
440
+ except AttributeError:
441
+ print(f"{path} = {conf}")
442
+ @
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2022--2023 Daniel Bosk
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.
@@ -0,0 +1,90 @@
1
+ Metadata-Version: 2.1
2
+ Name: typerconf
3
+ Version: 1.0
4
+ Summary: Library to read and write configs using API and CLI with Typer
5
+ Home-page: https://github.com/dbosk/typerconf
6
+ License: MIT
7
+ Keywords: typer,conf,config,git-like,config lib,write conf
8
+ Author: Daniel Bosk
9
+ Author-email: daniel@bosk.se
10
+ Requires-Python: >=3.7,<4.0
11
+ Classifier: Environment :: Console
12
+ Classifier: License :: OSI Approved :: MIT License
13
+ Classifier: Operating System :: OS Independent
14
+ Classifier: Programming Language :: Python :: 3
15
+ Classifier: Programming Language :: Python :: 3.7
16
+ Classifier: Programming Language :: Python :: 3.8
17
+ Classifier: Programming Language :: Python :: 3.9
18
+ Classifier: Programming Language :: Python :: 3.10
19
+ Classifier: Programming Language :: Python :: 3.11
20
+ Classifier: Programming Language :: Python :: 3
21
+ Classifier: Topic :: Utilities
22
+ Requires-Dist: appdirs (>=1.4.4,<2.0.0)
23
+ Requires-Dist: typer (>=0.7.0,<0.8.0)
24
+ Project-URL: Bug Tracker, https://github.com/dbosk/typerconf/issues
25
+ Project-URL: Repository, https://github.com/dbosk/typerconf
26
+ Project-URL: Releases, https://github.com/dbosk/typerconf/releases
27
+ Description-Content-Type: text/markdown
28
+
29
+ The configuration is a JSON structure. We'll use the following for the
30
+ coming examples.
31
+
32
+ ```JSON
33
+ {
34
+ "courses": {
35
+ "datintro22": {
36
+ "timesheet": {
37
+ "url": "https://sheets.google..."
38
+ },
39
+ "schedule": {
40
+ "url": "https://timeedit.net/..."
41
+ }
42
+ }
43
+ }
44
+ }
45
+ ```
46
+
47
+ The format is actually irrelevant to anyone outside of this library,
48
+ since it will never be accessed directly anyway. But it will be used to
49
+ illustrate the examples.
50
+
51
+ We can access values by dot-separated addresses. For instance, we can
52
+ use `courses.datintro22.schedule.url` to access the TimeEdit URL of the
53
+ datintro22 course.
54
+
55
+ Let's have a look at some usage examples. Say we have the program
56
+ `nytid` that wants to use this config module and subcommand.
57
+
58
+ ```python
59
+ import typer
60
+ import typerconf as config
61
+
62
+ cli = typer.Typer()
63
+ # add some other subcommands
64
+ config.add_config_cmd(cli)
65
+ ```
66
+
67
+ We want the CLI command to have the following form when used with
68
+ `nytid`.
69
+
70
+ ```bash
71
+ nytid config courses.datintro22.schedule.url --set https://timeedit.net/...
72
+ ```
73
+
74
+ will set the configuration value at the path, whereas
75
+
76
+ ```bash
77
+ nytid config courses.datintro22.schedule.url
78
+ ```
79
+
80
+ will return it.
81
+
82
+ Internally, `nytid`'s different parts can access the config through the
83
+ following API.
84
+
85
+ ```python
86
+ import typerconf as config
87
+
88
+ url = config.get("courses.datintro22.schedule.url")
89
+ ```
90
+
@@ -0,0 +1,8 @@
1
+ typerconf/.gitignore,sha256=14GNAIXVZFnTv4YhegCyrOBnN79i2rSEKdcShzPmZZE,30
2
+ typerconf/Makefile,sha256=1l7-qXX_bMGudBSDP3T2S6TtxmFN1tKbf7RNPUEBci8,258
3
+ typerconf/__init__.py,sha256=T83A7OS70bzs1D075zV1JfYSzjeX1a2-spk_hajO5e4,5930
4
+ typerconf/init.nw,sha256=f0MtVu_8wna9mD9S033B5T-7FYW6AsvRcJO2M6RDqMY,11988
5
+ typerconf-1.0.dist-info/LICENSE,sha256=MAeY6b3f2eyrZasde3JOsc6TzcZF7tLdNPPnwVdsqz4,1074
6
+ typerconf-1.0.dist-info/METADATA,sha256=xzL8k8J2f12vey56R1jlNB5V7FJW92rsF7rusVC-Oqw,2508
7
+ typerconf-1.0.dist-info/WHEEL,sha256=kLuE8m1WYU0Ig0_YEGrXyTtiJvKPpLpDEiChiNyei5Y,88
8
+ typerconf-1.0.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: poetry-core 1.5.1
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any