📝 Add docs for CLI parameter types

This commit is contained in:
Sebastián Ramírez 2019-12-31 22:14:18 +01:00
parent 355c223bcb
commit 073a72469c
30 changed files with 929 additions and 0 deletions

View file

@ -0,0 +1,12 @@
from datetime import datetime
import typer
def main(birth: datetime):
typer.echo(f"Interesting day to be born: {birth}")
typer.echo(f"Birth hour: {birth.hour}")
if __name__ == "__main__":
typer.run(main)

View file

@ -0,0 +1,15 @@
from datetime import datetime
import typer
def main(
launch_date: datetime = typer.Argument(
..., formats=["%Y-%m-%d", "%Y-%m-%dT%H:%M:%S", "%Y-%m-%d %H:%M:%S", "%m/%d/%Y"]
)
):
typer.echo(f"Launch will be at: {launch_date}")
if __name__ == "__main__":
typer.run(main)

View file

@ -0,0 +1,17 @@
from enum import Enum
import typer
class NeuralNetwork(str, Enum):
simple = "simple"
conv = "conv"
lstm = "lstm"
def main(network: NeuralNetwork = NeuralNetwork.simple):
typer.echo(f"Training neural network of type: {network.value}")
if __name__ == "__main__":
typer.run(main)

View file

@ -0,0 +1,19 @@
from enum import Enum
import typer
class NeuralNetwork(str, Enum):
simple = "simple"
conv = "conv"
lstm = "lstm"
def main(
network: NeuralNetwork = typer.Option(NeuralNetwork.simple, case_sensitive=False)
):
typer.echo(f"Training neural network of type: {network.value}")
if __name__ == "__main__":
typer.run(main)

View file

@ -0,0 +1,10 @@
import typer
def main(config: typer.FileText = typer.Option(...)):
for line in config:
typer.echo(f"Config line: {line}")
if __name__ == "__main__":
typer.run(main)

View file

@ -0,0 +1,10 @@
import typer
def main(config: typer.FileTextWrite = typer.Option(...)):
config.write("Some config written by the app")
typer.echo("Config written")
if __name__ == "__main__":
typer.run(main)

View file

@ -0,0 +1,13 @@
import typer
def main(file: typer.FileBinaryRead = typer.Option(...)):
processed_total = 0
for bytes_chunk in file:
# Process the bytes in bytes_chunk
processed_total += len(bytes_chunk)
typer.echo(f"Processed bytes total: {processed_total}")
if __name__ == "__main__":
typer.run(main)

View file

@ -0,0 +1,17 @@
import typer
def main(file: typer.FileBinaryWrite = typer.Option(...)):
first_line_str = "some settings\n"
# You cannot write str directly to a binary file, you have to encode it to get bytes
first_line_bytes = first_line_str.encode("utf-8")
# Then you can write the bytes
file.write(first_line_bytes)
# This is already bytes, it starts with b"
second_line = b"la cig\xc3\xbce\xc3\xb1a trae al ni\xc3\xb1o"
file.write(second_line)
typer.echo("Binary file written")
if __name__ == "__main__":
typer.run(main)

View file

@ -0,0 +1,10 @@
import typer
def main(config: typer.FileText = typer.Option(..., mode="a")):
config.write("This is a single line\n")
typer.echo("Config line written")
if __name__ == "__main__":
typer.run(main)

View file

@ -0,0 +1,14 @@
import typer
def main(
name: str, age: int = 20, height_meters: float = 1.89, female: bool = True,
):
typer.echo(f"NAME is {name}, of type: {type(name)}")
typer.echo(f"--age is {age}, of type: {type(age)}")
typer.echo(f"--height-meters is {height_meters}, of type: {type(height_meters)}")
typer.echo(f"--female is {female}, of type: {type(female)}")
if __name__ == "__main__":
typer.run(main)

View file

@ -0,0 +1,15 @@
import typer
def main(
id: int = typer.Argument(..., min=0, max=1000),
age: int = typer.Option(20, min=18),
score: float = typer.Option(0, max=100),
):
typer.echo(f"ID is {id}")
typer.echo(f"--age is {age}")
typer.echo(f"--score is {score}")
if __name__ == "__main__":
typer.run(main)

View file

@ -0,0 +1,15 @@
import typer
def main(
id: int = typer.Argument(..., min=0, max=1000),
rank: int = typer.Option(0, max=10, clamp=True),
score: float = typer.Option(0, min=0, max=100, clamp=True),
):
typer.echo(f"ID is {id}")
typer.echo(f"--rank is {rank}")
typer.echo(f"--score is {score}")
if __name__ == "__main__":
typer.run(main)

View file

@ -0,0 +1,17 @@
from pathlib import Path
import typer
def main(config: Path = typer.Option(...)):
if config.is_file():
text = config.read_text()
typer.echo(f"Config file contents: {text}")
elif config.is_dir():
typer.echo("Config is a directory, will use all its config files")
elif not config.exists():
typer.echo("The config doesn't exist")
if __name__ == "__main__":
typer.run(main)

View file

@ -0,0 +1,22 @@
from pathlib import Path
import typer
def main(
config: Path = typer.Option(
...,
exists=True,
file_okay=True,
dir_okay=False,
writable=False,
readable=True,
resolve_path=True,
)
):
text = config.read_text()
typer.echo(f"Config file contents: {text}")
if __name__ == "__main__":
typer.run(main)

View file

@ -0,0 +1,12 @@
from uuid import UUID
import typer
def main(user_id: UUID):
typer.echo(f"USER_ID is {user_id}")
typer.echo(f"UUID version is: {user_id.version}")
if __name__ == "__main__":
typer.run(main)

View file

@ -0,0 +1,79 @@
You can specify a *CLI parameter* as a Python <a href="https://docs.python.org/3/library/datetime.html" target="_blank">`datetime`</a>.
Your function will receive a standard Python `datetime` object, and again, your editor will give you completion, etc.
```Python hl_lines="1 6 7 8"
{!./src/parameter_types/datetime/tutorial001.py!}
```
By default, it will expect a datetime in ISO format in the input.
Check it:
<div class="termy">
```console
$ python main.py --help
Usage: main.py [OPTIONS] [%Y-%m-%d|%Y-%m-%dT%H:%M:%S|%Y-%m-%d %H:%M:%S]
Options:
--install-completion Install completion for the current shell.
--show-completion Show completion for the current shell, to copy it or customize the installation.
--help Show this message and exit.
// Pass a datetime
$ python main.py 1956-01-31T10:00:00
Interesting day to be born: 1956-01-31 10:00:00
Birth hour: 10
// An invalid date
$ python main.py july-19-1989
Usage: main.py [OPTIONS] [%Y-%m-%d|%Y-%m-%dT%H:%M:%S|%Y-%m-%d%H:%M:%S]
Error: Invalid value for "[%Y-%m-%d|%Y-%m-%dT%H:%M:%S|%Y-%m-%d %H:%M:%S]": invalid datetime format: july-19-1989. (choose from %Y-%m-%d, %Y-%m-%dT%H:%M:%S, %Y-%m-%d %H:%M:%S)
```
</div>
## Custom date format
You can also customize the formats received for the `datetime` with the `formats` parameter.
`formats` receives a list of strings with the date formats that would be passed to <a href="https://docs.python.org/3/library/datetime.html#datetime.date.strftime" target="_blank">datetime.strptime()</a>.
For example, let's imagine that you want to accept an ISO formatted datetime, but for some strange reason, you also want to accept a format with:
* first the month
* then the day
* then the year
* separated with "`/`"
...It's a crazy example, but let's say you also needed that strange format:
```Python hl_lines="8"
{!./src/parameter_types/datetime/tutorial002.py!}
```
!!! tip
Notice the last string in `formats`: `"%m/%d/%Y"`.
Check it:
<div class="termy">
```console
// ISO dates work
$ python main.py 1969-10-29
Launch will be at: 1969-10-29 00:00:00
// But the strange custom format also works
$ python main.py 10/29/1969
Launch will be at: 1969-10-29 00:00:00
```
</div>

View file

@ -0,0 +1,68 @@
To define a *CLI parameter* that can take a value from a predefined set of values you can use a standard Python <a href="https://docs.python.org/3/library/enum.html" target="_blank">`enum.Enum`</a>:
```Python hl_lines="1 6 7 8 9 12 13"
{!./src/parameter_types/enum/tutorial001.py!}
```
!!! tip
Notice that the function parameter `network` will be an `Enum`, not a `str`.
To get the `str` value in your function's code use `network.value`.
Check it:
<div class="termy">
```console
$ python main.py --help
// Notice the predefined values [simple|conv|lstm]
Usage: main.py [OPTIONS]
Options:
--network [simple|conv|lstm]
--install-completion Install completion for the current shell.
--show-completion Show completion for the current shell, to copy it or customize the installation.
--help Show this message and exit.
// Try it
$ python main.py --network conv
Training neural network of type: conv
// Invalid value
$ python main.py --network capsule
Usage: main.py [OPTIONS]
Try "main.py --help" for help.
Error: Invalid value for "--network": invalid choice: capsule. (choose from simple, conv, lstm)
```
</div>
### Case insensitive Enum choices
You can make an `Enum` (choice) *CLI parameter* be case-insensitive with the `case_sensitive` parameter:
```Python hl_lines="13"
{!./src/parameter_types/enum/tutorial002.py!}
```
And then the values of the `Enum` will be checked no matter if lower case, upper case, or a mix:
<div class="termy">
```console
// Notice the upper case CONV
$ python main.py --network CONV
Training neural network of type: conv
// A mix also works
$ python main.py --network LsTm
Training neural network of type: lstm
```
</div>

View file

@ -0,0 +1,248 @@
Apart from `Path` *CLI parameters* you can also declare some types of "files".
!!! tip
In most of the cases you are probably fine just using `Path`.
You can read and write data with `Path` the same way.
The difference is that these types will give you a Python <a href="https://docs.python.org/3/glossary.html#term-file-object" target="_blank">file-like object</a> instead of a Python <a href="https://docs.python.org/3/library/pathlib.html#basic-use" target="_blank">Path</a>.
A "file-like object" is the same type of object returned by `open()` as in:
```Python
with open('file.txt') as f:
# Here f is the file-like object
read_data = f.read()
print(read_data)
```
But in some special use cases you might want to use these special types. For example if you are migrating an existing application.
## `FileText` reading
`typer.FileText` gives you a file-like object for reading text, you will get `str` data from it.
This means that even if your file has text written in a non-english language, e.g. a `text.txt` file with:
```
la cigüeña trae al niño
```
You will have a `str` with the text inside, e.g.:
```Python
content = "la cigüeña trae al niño"
```
instead of having `bytes`, e.g.:
```Python
content = b"la cig\xc3\xbce\xc3\xb1a trae al ni\xc3\xb1o"
```
You will get all the correct editor support, attributes, methods, etc for the file-like object:
```Python hl_lines="4"
{!./src/parameter_types/file/tutorial001.py!}
```
Check it:
<div class="termy">
```console
// Create a quick text config
$ echo "some settings" > config.txt
// Add another line to the config to test it
$ echo "some more settings" >> config.txt
// Now run your program
$ python main.py --config config.txt
Config line: some settings
Config line: some more settings
```
</div>
## `FileTextWrite`
For writing text, you can use `typer.FileTextWrite`:
```Python hl_lines="4 5"
{!./src/parameter_types/file/tutorial002.py!}
```
This would be for writing human text, like:
```
some settings
la cigüeña trae al niño
```
...not to write binary `bytes`.
Check it:
<div class="termy">
```console
$ python main.py --config text.txt
Config written
// Check the contents of the file
$ cat text.txt
Some config written by the app
```
</div>
!!! info "Technical Details"
`typer.FileTextWrite` is a just a convenience class.
It's the same as using `typer.FileText` and setting `mode="w"`. You will learn about `mode` later below.
## `FileBinaryRead`
To read binary data you can use `typer.FileBinaryRead`.
You will receive `bytes` from it.
It's useful for reading binary files like images:
```Python hl_lines="4"
{!./src/parameter_types/file/tutorial003.py!}
```
Check it:
<div class="termy">
```console
$ python main.py --file lena.jpg
Processed bytes total: 512
Processed bytes total: 1024
Processed bytes total: 1536
Processed bytes total: 2048
```
</div>
## `FileBinaryWrite`
To write binary data you can use `typer.FileBinaryWrite`.
You would write `bytes` to it.
It's useful for writing binary files like images.
Have in mind that you have to pass `bytes` to its `.write()` method, not `str`.
If you have a `str`, you have to encode it first to get `bytes`.
```Python hl_lines="4"
{!./src/parameter_types/file/tutorial004.py!}
```
<div class="termy">
```console
$ python main.py --file binary.dat
Binary file written
// Check the binary file was created
$ ls ./binary.dat
./binary.dat
```
</div>
## File *CLI parameter* configurations
You can use several configuration parameters for these types (classes) in `typer.Option()` and `typer.Argument()`:
* `mode`: controls the "<a href="https://docs.python.org/3/library/functions.html#open" target="_blank">mode</a>" to open the file with.
* It's automatically set for you by using the classes above.
* Read more about it below.
* `encoding`: to force a specific encoding, e.g. `"utf-8"`.
* `lazy`: delay <abbr title="input and output, reading and writing files">I/O</abbr> operations. Automatic by default.
* By default, when writing files, Click will generate a file-like object that is not yet the actual file. Once you start writing, it will go, open the file and start writing to it, but not before. This is mainly useful to avoid creating the file until you start writing to it. It's normally safe to leave this automatic. But you can overwrite it setting `lazy=False`. By default, it's `lazy=True` for writing and `lazy=False` for reading.
* `atomic`: if true, all writes will actually go to a temporal file and then moved to the final destination after completing. This is useful with files modified frequently by several users/programs.
## Advanced `mode`
By default, **Typer** will configure the <a href="https://docs.python.org/3/library/functions.html#open" target="_blank">`mode`</a> for you:
* `typer.FileText`: `mode="r"`, to read text.
* `typer.FileTextWrite`: `mode="w"`, to write text.
* `typer.FileBinaryRead`: `mode="rb"`, to read binary data.
* `typer.FileBinaryWrite`: `mode="wb"`, to write binary data.
### Note about `FileTextWrite`
`typer.FileTextWrite` is actually just a convenience class. It's the same as using `typer.FileText` with `mode="w"`.
But it's probably shorter and more intuitive as you can get it with autocompletion in your editor by just starting to type `typer.File`... just like the other classes.
### Customize `mode`
You can override the `mode` from the defaults above.
For example, you could use `mode="a"` to write "appending" to the same file:
```Python hl_lines="4"
{!./src/parameter_types/file/tutorial005.py!}
```
!!! tip
As you are manually setting `mode="a"`, you can use `typer.FileText` or `typer.FileTextWrite`, both will work.
Check it:
<div class="termy">
```console
$ python main.py --config config.txt
Config line written
// Run your program a couple more times to see how it appends instead of overwriting
$ python main.py --config config.txt
Config line written
$ python main.py --config config.txt
Config line written
// Check the contents of the file, it should have each of the 3 lines appended
$ cat config.txt
This is a single line
This is a single line
This is a single line
```
</div>
## About the different types
!!! info
These are technical details about why the different types/classes provided by **Typer**.
But you don't need this information to be able to use them. You can skip it.
**Typer** provides you these different types (classes) because they inherit directly from the actual Python implementation that will be provided underneath for each case.
This way your editor will give you the right type checks and completion for each type.
Even if you use `lazy`. When you use `lazy` Click creates a especial object to delay writes, and serves as a "proxy" to the actual file that will be written. But this especial proxy object doesn't expose the attributes and methods needed for type checks and completion in the editor. If you access those attributes or call the methods, the "proxy" lazy object will call them in the final object and it will all work. But you wouldn't get autocompletion for them.
But because these **Typer** classes inherit from the actual implementation that will be provided underneath (not the lazy object), you will get all the autocompletion and type checks in the editor.

View file

@ -0,0 +1,65 @@
You can use several data types for the *CLI options* and *CLI arguments*, and you can add data validation requirements too.
## Data conversion
When you declare a *CLI parameter* with some type **Typer** will convert the data received in the command line to that data type.
For example:
```Python hl_lines="5"
{!./src/parameter_types/index/tutorial001.py!}
```
In this example, the value received for the *CLI argument* `NAME` will be treated as `str`.
The value for the *CLI option* `--age` will be converted to an `int` and `--height-meters` will be converted to a `float`.
And as `female` is a `bool` *CLI option*, **Typer** will convert it to a "flag" `--female` and the counterpart `--no-female`.
And here's how it looks like:
<div class="termy">
```console
$ python main.py --help
// Notice how --age is an INTEGER and --height-meters is a FLOAT
Usage: main.py [OPTIONS] NAME
Options:
--age INTEGER
--height-meters FLOAT
--female / --no-female
--install-completion Install completion for the current shell.
--show-completion Show completion for the current shell, to copy it or customize the installation.
--help Show this message and exit.
// Call it with CLI parameters
$ python main.py Camila --age 15 --height-meters 1.70 --female
// All the data has the correct Python type
NAME is Camila, of type: class 'str'
--age is 15, of type: class 'int'
--height-meters is 1.7, of type: class 'float'
--female is True, of type: class 'bool'
// And if you pass an incorrect type
$ python main.py Camila --age 15.3
Usage: main.py [OPTIONS] NAME
Try "main.py --help" for help.
Error: Invalid value for "--age": 15.3 is not a valid integer
// Because 15.3 is not an INTEGER (it's a float)
```
</div>
## Watch next
See more about specific types and validations in the next sections...
!!! info "Technical Details"
All the types you will see in the next sections are handled underneath by <a href="https://click.palletsprojects.com/en/7.x/parameters/#parameter-types" target="_blank">Click's Parameter Types</a>.

View file

@ -0,0 +1,100 @@
You can define numeric validations with `max` and `min` values for `int` and `float` *CLI parameters*:
```Python hl_lines="5 6 7"
{!./src/parameter_types/number/tutorial001.py!}
```
*CLI arguments* and *CLI options* can both use these validations.
You can specify `min`, `max` or both.
Check it:
<div class="termy">
```console
$ python main.py --help
// Notice the extra RANGE in the help text for --age and --score
Usage: main.py [OPTIONS] ID
Options:
--age INTEGER RANGE
--score FLOAT RANGE
--install-completion Install completion for the current shell.
--show-completion Show completion for the current shell, to copy it or customize the installation.
--help Show this message and exit.
// Pass all the CLI parameters
$ python main.py 5 --age 20 --score 90
ID is 5
--age is 20
--score is 90.0
// Pass an invalid ID
$ python main.py 1002
Usage: main.py [OPTIONS] ID
Try "main.py --help" for help.
Error: Invalid value for "ID": 1002 is not in the valid range of 0 to 1000.
// Pass an invalid age
$ python main.py 5 --age 15
Usage: main.py [OPTIONS] ID
Try "main.py --help" for help.
Error: Invalid value for "--age": 15 is smaller than the minimum valid value 18.
// Pass an invalid score
$ python main.py 5 --age 20 --score 100.5
Usage: main.py [OPTIONS] ID
Try "main.py --help" for help.
Error: Invalid value for "--score": 100.5 is bigger than the maximum valid value 100.
// But as we didn't specify a minimum score, this is accepted
$ python main.py 5 --age 20 --score -5
ID is 5
--age is 20
--score is -5.0
```
</div>
## Clamping numbers
You might want to, instead of showing an error, use the closest minimum or maximum valid values.
You can do it with the `clamp` parameter:
```Python hl_lines="5 6 7"
{!./src/parameter_types/number/tutorial002.py!}
```
And then, when you pass data that is out of the valid range, it will be "clamped", the closest valid value will be used:
<div class="termy">
```console
// ID doesn't have clamp, so it shows an error
$ python main.py 1002
Usage: main.py [OPTIONS] ID
Try "main.py --help" for help.
Error: Invalid value for "ID": 1002 is not in the valid range of 0 to 1000.
// But --rank and --score use clamp
$ python main.py 5 --rank 11 --score -5
ID is 5
--rank is 10
--score is 0
```
</div>

View file

@ -0,0 +1,95 @@
You can declare a *CLI parameter* to be a standard Python <a href="https://docs.python.org/3/library/pathlib.html#basic-use" target="_blank">`pathlib.Path`</a>.
This is what you would do for directory paths, file paths, etc:
```Python hl_lines="1 6"
{!./src/parameter_types/path/tutorial001.py!}
```
And again, as you receive a standard Python `Path` object the same as the type annotation, your editor will give you autocompletion for all its attributes and methods.
Check it:
<div class="termy">
```console
$ python main.py --config config.txt
The config doesn't exist
// Now create a quick config
$ echo "some settings" > config.txt
// And try again
$ python main.py --config config.txt
Config file contents: some settings
// And with a directory
$ python main.py --config ./
Config is a directory, will use all its config files
```
</div>
## Path validations
You can perform several validations for `Path` *CLI parameters*:
* `exists`: if set to true, the file or directory needs to exist for this value to be valid. If this is not required and a file does indeed not exist, then all further checks are silently skipped.
* `file_okay`: controls if a file is a possible value.
* `dir_okay`: controls if a directory is a possible value.
* `writable`: if true, a writable check is performed.
* `readable`: if true, a readable check is performed.
* `resolve_path`: if this is true, then the path is fully resolved before the value is passed onwards. This means that its absolute and symlinks are resolved. It will not expand a tilde-prefix, as this is supposed to be done by the shell only.
!!! tip
All these parameters come directly from <a href="https://click.palletsprojects.com/en/7.x/parameters/#parameter-types" target="_blank">Click</a>.
For example:
```Python hl_lines="9 10 11 12 13 14"
{!./src/parameter_types/path/tutorial002.py!}
```
Check it:
<div class="termy">
```console
$ python main.py --config config.txt
Usage: main.py [OPTIONS]
Try "main.py --help" for help.
Error: Invalid value for "--config": File "config.txt" does not exist.
// Now create a quick config
$ echo "some settings" > config.txt
// And try again
$ python main.py --config config.txt
Config file contents: some settings
// And with a directory
$ python main.py --config ./
Usage: main.py [OPTIONS]
Try "main.py --help" for help.
Error: Invalid value for "--config": File "./" is a directory.
```
</div>
### Advanced `Path` configurations
!!! warning "Advanced Details"
You probably won't need these configurations at first, you may want to skip it.
They are used for more advanced use cases.
* `allow_dash`: If this is set to True, a single dash to indicate standard streams is permitted.
* `path_type`: optionally a string type that should be used to represent the path. The default is None which means the return value will be either bytes or unicode depending on what makes most sense given the input data Click deals with.

View file

@ -0,0 +1,48 @@
!!! info
A UUID is a <a href="https://en.wikipedia.org/wiki/Universally_unique_identifier" target="_blank">"Universally Unique Identifier"</a>.
It's a standard format for identifiers, like passport numbers, but for anything, not just people in countries.
They look like this:
```
d48edaa6-871a-4082-a196-4daab372d4a1
```
The way they are generated makes them sufficiently long and random that you could assume that every UUID generated is unique. Even if it was generated by a different application, database, or system.
So, if your system uses UUIDs to identify your data, you could mix it with the data from some other system that also uses UUIDs with some confidence that their IDs (UUIDs) won't clash with yours.
This wouldn't be true if you just used `int`s as identifiers, as most databases do.
You can declare a *CLI parameter* as a UUID:
```Python hl_lines="1 6 7 8"
{!./src/parameter_types/uuid/tutorial001.py!}
```
Your Python code will receive a standard Python <a href="https://docs.python.org/3.8/library/uuid.html" target="_blank">`UUID`</a> object with all its attributes and methods, and as you are annotating your function parameter with that type, you will have type checks, autocompletion in your editor, etc.
Check it:
<div class="termy">
```console
// Pass a valid UUID v4
$ python main.py d48edaa6-871a-4082-a196-4daab372d4a1
USER_ID is d48edaa6-871a-4082-a196-4daab372d4a1
UUID version is: 4
// An invalid value
$ python main.py 7479706572-72756c6573
Usage: main.py [OPTIONS] USER_ID
Try "main.py --help" for help.
Error: Invalid value for "USER_ID": 7479706572-72756c6573 is not a valid UUID value
```
</div>

View file

@ -26,6 +26,14 @@ nav:
- CLI Options: 'tutorial/options.md'
- CLI Arguments: 'tutorial/arguments.md'
- Commands: 'tutorial/commands.md'
- CLI Parameter Types:
- CLI Parameter Types Intro: 'tutorial/parameter-types/index.md'
- Number: 'tutorial/parameter-types/number.md'
- UUID: 'tutorial/parameter-types/uuid.md'
- DateTime: 'tutorial/parameter-types/datetime.md'
- Enum - Choices: 'tutorial/parameter-types/enum.md'
- Path: 'tutorial/parameter-types/path.md'
- File: 'tutorial/parameter-types/file.md'
- Alternatives, Inspiration and Comparisons: 'alternatives.md'
- Help Typer - Get Help: 'help-typer.md'
- Development - Contributing: 'contributing.md'