21 |
404
22 |
23 |
Page not found :(
24 |
The requested page could not be found.
25 |
26 |
--------------------------------------------------------------------------------
/docs/CNAME:
--------------------------------------------------------------------------------
1 | devcli.io
--------------------------------------------------------------------------------
/docs/Gemfile:
--------------------------------------------------------------------------------
1 | source "https://rubygems.org"
2 |
3 | gem "jekyll"
4 | gem "jekyll-theme-minimal"
5 |
--------------------------------------------------------------------------------
/docs/Gemfile.lock:
--------------------------------------------------------------------------------
1 | GEM
2 | remote: https://rubygems.org/
3 | specs:
4 | addressable (2.8.7)
5 | public_suffix (>= 2.0.2, < 7.0)
6 | bigdecimal (3.1.8)
7 | colorator (1.1.0)
8 | concurrent-ruby (1.3.4)
9 | em-websocket (0.5.3)
10 | eventmachine (>= 0.12.9)
11 | http_parser.rb (~> 0)
12 | eventmachine (1.2.7)
13 | ffi (1.17.0)
14 | ffi (1.17.0-aarch64-linux-gnu)
15 | ffi (1.17.0-aarch64-linux-musl)
16 | ffi (1.17.0-arm-linux-gnu)
17 | ffi (1.17.0-arm-linux-musl)
18 | ffi (1.17.0-arm64-darwin)
19 | ffi (1.17.0-x86-linux-gnu)
20 | ffi (1.17.0-x86-linux-musl)
21 | ffi (1.17.0-x86_64-darwin)
22 | ffi (1.17.0-x86_64-linux-gnu)
23 | ffi (1.17.0-x86_64-linux-musl)
24 | forwardable-extended (2.6.0)
25 | google-protobuf (4.29.1)
26 | bigdecimal
27 | rake (>= 13)
28 | google-protobuf (4.29.1-aarch64-linux)
29 | bigdecimal
30 | rake (>= 13)
31 | google-protobuf (4.29.1-arm64-darwin)
32 | bigdecimal
33 | rake (>= 13)
34 | google-protobuf (4.29.1-x86-linux)
35 | bigdecimal
36 | rake (>= 13)
37 | google-protobuf (4.29.1-x86_64-darwin)
38 | bigdecimal
39 | rake (>= 13)
40 | google-protobuf (4.29.1-x86_64-linux)
41 | bigdecimal
42 | rake (>= 13)
43 | http_parser.rb (0.8.0)
44 | i18n (1.14.6)
45 | concurrent-ruby (~> 1.0)
46 | jekyll (4.3.4)
47 | addressable (~> 2.4)
48 | colorator (~> 1.0)
49 | em-websocket (~> 0.5)
50 | i18n (~> 1.0)
51 | jekyll-sass-converter (>= 2.0, < 4.0)
52 | jekyll-watch (~> 2.0)
53 | kramdown (~> 2.3, >= 2.3.1)
54 | kramdown-parser-gfm (~> 1.0)
55 | liquid (~> 4.0)
56 | mercenary (>= 0.3.6, < 0.5)
57 | pathutil (~> 0.9)
58 | rouge (>= 3.0, < 5.0)
59 | safe_yaml (~> 1.0)
60 | terminal-table (>= 1.8, < 4.0)
61 | webrick (~> 1.7)
62 | jekyll-sass-converter (3.0.0)
63 | sass-embedded (~> 1.54)
64 | jekyll-seo-tag (2.8.0)
65 | jekyll (>= 3.8, < 5.0)
66 | jekyll-theme-minimal (0.2.0)
67 | jekyll (> 3.5, < 5.0)
68 | jekyll-seo-tag (~> 2.0)
69 | jekyll-watch (2.2.1)
70 | listen (~> 3.0)
71 | kramdown (2.5.1)
72 | rexml (>= 3.3.9)
73 | kramdown-parser-gfm (1.1.0)
74 | kramdown (~> 2.0)
75 | liquid (4.0.4)
76 | listen (3.9.0)
77 | rb-fsevent (~> 0.10, >= 0.10.3)
78 | rb-inotify (~> 0.9, >= 0.9.10)
79 | mercenary (0.4.0)
80 | pathutil (0.16.2)
81 | forwardable-extended (~> 2.6)
82 | public_suffix (6.0.1)
83 | rake (13.2.1)
84 | rb-fsevent (0.11.2)
85 | rb-inotify (0.11.1)
86 | ffi (~> 1.0)
87 | rexml (3.4.0)
88 | rouge (4.5.1)
89 | safe_yaml (1.0.5)
90 | sass-embedded (1.83.0)
91 | google-protobuf (~> 4.28)
92 | rake (>= 13)
93 | sass-embedded (1.83.0-aarch64-linux-android)
94 | google-protobuf (~> 4.28)
95 | sass-embedded (1.83.0-aarch64-linux-gnu)
96 | google-protobuf (~> 4.28)
97 | sass-embedded (1.83.0-aarch64-linux-musl)
98 | google-protobuf (~> 4.28)
99 | sass-embedded (1.83.0-aarch64-mingw-ucrt)
100 | google-protobuf (~> 4.28)
101 | sass-embedded (1.83.0-arm-linux-androideabi)
102 | google-protobuf (~> 4.28)
103 | sass-embedded (1.83.0-arm-linux-gnueabihf)
104 | google-protobuf (~> 4.28)
105 | sass-embedded (1.83.0-arm-linux-musleabihf)
106 | google-protobuf (~> 4.28)
107 | sass-embedded (1.83.0-arm64-darwin)
108 | google-protobuf (~> 4.28)
109 | sass-embedded (1.83.0-riscv64-linux-android)
110 | google-protobuf (~> 4.28)
111 | sass-embedded (1.83.0-riscv64-linux-gnu)
112 | google-protobuf (~> 4.28)
113 | sass-embedded (1.83.0-riscv64-linux-musl)
114 | google-protobuf (~> 4.28)
115 | sass-embedded (1.83.0-x86-cygwin)
116 | google-protobuf (~> 4.28)
117 | sass-embedded (1.83.0-x86-linux-android)
118 | google-protobuf (~> 4.28)
119 | sass-embedded (1.83.0-x86-linux-gnu)
120 | google-protobuf (~> 4.28)
121 | sass-embedded (1.83.0-x86-linux-musl)
122 | google-protobuf (~> 4.28)
123 | sass-embedded (1.83.0-x86-mingw-ucrt)
124 | google-protobuf (~> 4.28)
125 | sass-embedded (1.83.0-x86_64-cygwin)
126 | google-protobuf (~> 4.28)
127 | sass-embedded (1.83.0-x86_64-darwin)
128 | google-protobuf (~> 4.28)
129 | sass-embedded (1.83.0-x86_64-linux-android)
130 | google-protobuf (~> 4.28)
131 | sass-embedded (1.83.0-x86_64-linux-gnu)
132 | google-protobuf (~> 4.28)
133 | sass-embedded (1.83.0-x86_64-linux-musl)
134 | google-protobuf (~> 4.28)
135 | terminal-table (3.0.2)
136 | unicode-display_width (>= 1.1.1, < 3)
137 | unicode-display_width (2.6.0)
138 | webrick (1.9.1)
139 |
140 | PLATFORMS
141 | aarch64-linux
142 | aarch64-linux-android
143 | aarch64-linux-gnu
144 | aarch64-linux-musl
145 | aarch64-mingw-ucrt
146 | arm-linux-androideabi
147 | arm-linux-gnu
148 | arm-linux-gnueabihf
149 | arm-linux-musl
150 | arm-linux-musleabihf
151 | arm64-darwin
152 | riscv64-linux-android
153 | riscv64-linux-gnu
154 | riscv64-linux-musl
155 | ruby
156 | x86-cygwin
157 | x86-linux
158 | x86-linux-android
159 | x86-linux-gnu
160 | x86-linux-musl
161 | x86-mingw-ucrt
162 | x86_64-cygwin
163 | x86_64-darwin
164 | x86_64-linux
165 | x86_64-linux-android
166 | x86_64-linux-gnu
167 | x86_64-linux-musl
168 |
169 | DEPENDENCIES
170 | jekyll
171 | jekyll-theme-minimal
172 |
173 | BUNDLED WITH
174 | 2.5.22
175 |
--------------------------------------------------------------------------------
/docs/Rakefile:
--------------------------------------------------------------------------------
1 |
2 | task default: %w[serve]
3 |
4 | task :serve do
5 | # starts the jekyll server and open the browser
6 | sh 'bundle exec jekyll serve -o'
7 | end
8 |
9 | task :doctor do
10 | # runs the jekyll doctor command
11 | sh 'bundle exec jekyll doctor'
12 | end
13 |
--------------------------------------------------------------------------------
/docs/_config.yml:
--------------------------------------------------------------------------------
1 | # Welcome to Jekyll!
2 | #
3 | # This config file is meant for settings that affect your whole blog, values
4 | # which you are expected to set up once and rarely edit after that. If you find
5 | # yourself editing this file very often, consider using Jekyll's data files
6 | # feature for the data you need to update frequently.
7 | #
8 | # For technical reasons, this file is *NOT* reloaded automatically when you use
9 | # 'bundle exec jekyll serve'. If you change this file, please restart the server process.
10 | #
11 | # If you need help with YAML syntax, here are some quick references for you:
12 | # https://learn-the-web.algonquindesign.ca/topics/markdown-yaml-cheat-sheet/#yaml
13 | # https://learnxinyminutes.com/docs/yaml/
14 | #
15 | # Site settings
16 | # These are used to personalize your new site. If you look in the HTML files,
17 | # you will see them accessed via {{ site.title }}, {{ site.email }}, and so on.
18 | # You can create any custom variable you would like, and they will be accessible
19 | # in the templates via {{ site.myvariable }}.
20 |
21 | title: devcli
22 | description: >-
23 | devcli is a command line tool to create command line
24 | tools.
25 |
26 | logo: /assets/img/logo.png
27 |
28 | # Build settings
29 | theme: jekyll-theme-minimal
30 |
31 | exclude:
32 | - CNAME
33 | - Rakefile
34 | - Gemfile
35 | - Gemfile.lock
36 | - node_modules
37 | - vendor/bundle/
38 | - vendor/cache/
39 | - vendor/gems/
40 | - vendor/ruby/
41 |
--------------------------------------------------------------------------------
/docs/assets/css/style.scss:
--------------------------------------------------------------------------------
1 | ---
2 | ---
3 |
4 | @import "{{ site.theme }}";
5 |
6 | a {
7 | color:#000;
8 | text-decoration:none;
9 | }
10 |
11 | a:hover, a:focus {
12 | color:#090;
13 | font-weight: bold;
14 | }
15 |
--------------------------------------------------------------------------------
/docs/assets/img/logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mvaltas/devcli/a5d501e4f993bee98c8d7a5de9b2da70a1482596/docs/assets/img/logo.png
--------------------------------------------------------------------------------
/docs/design/logo.graffle:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mvaltas/devcli/a5d501e4f993bee98c8d7a5de9b2da70a1482596/docs/design/logo.graffle
--------------------------------------------------------------------------------
/docs/index.md:
--------------------------------------------------------------------------------
1 | ---
2 | layout: default
3 | ---
4 |
5 | Welcome to the **devcli** documentation site! Since this tool aims to simplify
6 | the development process, we need to ensure it's easy to use and understand.
7 |
8 | ## Installation
9 |
10 | Installation can be done using Python tools [`poetry`](https://python-poetry.org/docs/#installation)
11 | and [`pipx`](https://pypi.org/project/pipx/). If you don't have them installed, you can use [`homebrew`](https://brew.sh/) to install them.
12 |
13 | ```bash
14 | $ brew install poetry
15 | $ brew install pipx
16 | ```
17 |
18 | Once you have them installed, you can install `devcli` by cloning the repository and running the following commands, like so:
19 |
20 | ```bash
21 | $ git clone https://github.com/mvaltas/devcli.git
22 | $ cd devcli
23 | $ poetry build --format=wheel
24 | $ pipx install --force dist/devcli*.whl
25 | ```
26 |
27 | ## Usage
28 |
29 | This tool is designed with the principle of *discoverability*, which means you
30 | can explore it by running it without any arguments. This way, you'll be able to
31 | see the available commands and learn more about them:
32 |
33 | ```
34 | $ devcli
35 |
36 | Usage: devcli [OPTIONS] COMMAND [ARGS]...
37 |
38 | ╭─ Options ────────────────────────────────────────────────────────────╮
39 | │ --debug Enable debug log │
40 | │ --verbose Enable info log │
41 | │ --help Show this message and exit. │
42 | ╰──────────────────────────────────────────────────────────────────────╯
43 | ╭─ Commands ───────────────────────────────────────────────────────────╮
44 | │ example Examples on how create cli commands with *devcli* │
45 | ╰──────────────────────────────────────────────────────────────────────╯
46 | ```
47 |
48 | ## Extending
49 |
50 | *devcli* will scan for directories named `.devcli`, where it will expect two
51 | things: other commands to load and a configuration file named `devcli.toml`
52 | (which can be empty). You can check the
53 | [`example`](https://github.com/mvaltas/devcli/blob/main/.devcli/example.py) for
54 | some ways to extend it. For the simplest case, you can create a file named
55 | `hello.py` in your `.devcli` directory, like this:
56 |
57 | ```python
58 | import devcli.framework as cmd
59 |
60 | cli = cmd.new("This is a hello world command")
61 |
62 | @cli.command()
63 | def say():
64 | """Simply replies with a Hello, World!"""
65 | print("Hello, World!")
66 |
67 | ```
68 |
69 | Once you did that, running *devcli* again you see a new command available called `hello`:
70 |
71 | ```
72 | $ devcli
73 |
74 | Usage: devcli [OPTIONS] COMMAND [ARGS]...
75 |
76 | ╭─ Options ────────────────────────────────────────────────────────────╮
77 | │ --debug Enable debug log │
78 | │ --verbose Enable info log │
79 | │ --help Show this message and exit. │
80 | ╰──────────────────────────────────────────────────────────────────────╯
81 | ╭─ Commands ───────────────────────────────────────────────────────────╮
82 | │ example Examples on how create cli commands with *devcli* │
83 | │ hello This is a hello world command │
84 | ╰──────────────────────────────────────────────────────────────────────╯
85 |
86 | ```
87 |
88 | That's it, now you can run the `--help` to inspect your new command documentation::
89 |
90 |
91 | ```
92 | $ devcli hello --help
93 |
94 | Usage: devcli hello [OPTIONS] COMMAND [ARGS]...
95 |
96 | This is a hello world command
97 |
98 | ╭─ Options ───────────────────────────────────────────╮
99 | │ --help Show this message and exit. │
100 | ╰─────────────────────────────────────────────────────╯
101 | ╭─ Commands ──────────────────────────────────────────╮
102 | │ say Simply replies with Hello, World! │
103 | ╰─────────────────────────────────────────────────────╯
104 | ```
105 |
106 | And if you call your new command::
107 |
108 | ```bash
109 | $ devcli hello say
110 | Hello, World!
111 | ```
112 |
113 | Now you have created your first *devcli* command. To learn more about how to create commands check
114 | the [`example`](https://github.com/mvaltas/devcli/blob/main/.devcli/example.py) command for more
115 | advanced options.
116 |
117 |
--------------------------------------------------------------------------------
/poetry.lock:
--------------------------------------------------------------------------------
1 | # This file is automatically @generated by Poetry 2.0.1 and should not be changed by hand.
2 |
3 | [[package]]
4 | name = "black"
5 | version = "25.1.0"
6 | description = "The uncompromising code formatter."
7 | optional = false
8 | python-versions = ">=3.9"
9 | groups = ["main"]
10 | files = [
11 | {file = "black-25.1.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:759e7ec1e050a15f89b770cefbf91ebee8917aac5c20483bc2d80a6c3a04df32"},
12 | {file = "black-25.1.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:0e519ecf93120f34243e6b0054db49c00a35f84f195d5bce7e9f5cfc578fc2da"},
13 | {file = "black-25.1.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:055e59b198df7ac0b7efca5ad7ff2516bca343276c466be72eb04a3bcc1f82d7"},
14 | {file = "black-25.1.0-cp310-cp310-win_amd64.whl", hash = "sha256:db8ea9917d6f8fc62abd90d944920d95e73c83a5ee3383493e35d271aca872e9"},
15 | {file = "black-25.1.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:a39337598244de4bae26475f77dda852ea00a93bd4c728e09eacd827ec929df0"},
16 | {file = "black-25.1.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:96c1c7cd856bba8e20094e36e0f948718dc688dba4a9d78c3adde52b9e6c2299"},
17 | {file = "black-25.1.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bce2e264d59c91e52d8000d507eb20a9aca4a778731a08cfff7e5ac4a4bb7096"},
18 | {file = "black-25.1.0-cp311-cp311-win_amd64.whl", hash = "sha256:172b1dbff09f86ce6f4eb8edf9dede08b1fce58ba194c87d7a4f1a5aa2f5b3c2"},
19 | {file = "black-25.1.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:4b60580e829091e6f9238c848ea6750efed72140b91b048770b64e74fe04908b"},
20 | {file = "black-25.1.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:1e2978f6df243b155ef5fa7e558a43037c3079093ed5d10fd84c43900f2d8ecc"},
21 | {file = "black-25.1.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:3b48735872ec535027d979e8dcb20bf4f70b5ac75a8ea99f127c106a7d7aba9f"},
22 | {file = "black-25.1.0-cp312-cp312-win_amd64.whl", hash = "sha256:ea0213189960bda9cf99be5b8c8ce66bb054af5e9e861249cd23471bd7b0b3ba"},
23 | {file = "black-25.1.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:8f0b18a02996a836cc9c9c78e5babec10930862827b1b724ddfe98ccf2f2fe4f"},
24 | {file = "black-25.1.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:afebb7098bfbc70037a053b91ae8437c3857482d3a690fefc03e9ff7aa9a5fd3"},
25 | {file = "black-25.1.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:030b9759066a4ee5e5aca28c3c77f9c64789cdd4de8ac1df642c40b708be6171"},
26 | {file = "black-25.1.0-cp313-cp313-win_amd64.whl", hash = "sha256:a22f402b410566e2d1c950708c77ebf5ebd5d0d88a6a2e87c86d9fb48afa0d18"},
27 | {file = "black-25.1.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:a1ee0a0c330f7b5130ce0caed9936a904793576ef4d2b98c40835d6a65afa6a0"},
28 | {file = "black-25.1.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:f3df5f1bf91d36002b0a75389ca8663510cf0531cca8aa5c1ef695b46d98655f"},
29 | {file = "black-25.1.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d9e6827d563a2c820772b32ce8a42828dc6790f095f441beef18f96aa6f8294e"},
30 | {file = "black-25.1.0-cp39-cp39-win_amd64.whl", hash = "sha256:bacabb307dca5ebaf9c118d2d2f6903da0d62c9faa82bd21a33eecc319559355"},
31 | {file = "black-25.1.0-py3-none-any.whl", hash = "sha256:95e8176dae143ba9097f351d174fdaf0ccd29efb414b362ae3fd72bf0f710717"},
32 | {file = "black-25.1.0.tar.gz", hash = "sha256:33496d5cd1222ad73391352b4ae8da15253c5de89b93a80b3e2c8d9a19ec2666"},
33 | ]
34 |
35 | [package.dependencies]
36 | click = ">=8.0.0"
37 | mypy-extensions = ">=0.4.3"
38 | packaging = ">=22.0"
39 | pathspec = ">=0.9.0"
40 | platformdirs = ">=2"
41 |
42 | [package.extras]
43 | colorama = ["colorama (>=0.4.3)"]
44 | d = ["aiohttp (>=3.10)"]
45 | jupyter = ["ipython (>=7.8.0)", "tokenize-rt (>=3.2.0)"]
46 | uvloop = ["uvloop (>=0.15.2)"]
47 |
48 | [[package]]
49 | name = "certifi"
50 | version = "2025.1.31"
51 | description = "Python package for providing Mozilla's CA Bundle."
52 | optional = false
53 | python-versions = ">=3.6"
54 | groups = ["main"]
55 | files = [
56 | {file = "certifi-2025.1.31-py3-none-any.whl", hash = "sha256:ca78db4565a652026a4db2bcdf68f2fb589ea80d0be70e03929ed730746b84fe"},
57 | {file = "certifi-2025.1.31.tar.gz", hash = "sha256:3d5da6925056f6f18f119200434a4780a94263f10d1c21d032a6f6b2baa20651"},
58 | ]
59 |
60 | [[package]]
61 | name = "charset-normalizer"
62 | version = "3.4.1"
63 | description = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet."
64 | optional = false
65 | python-versions = ">=3.7"
66 | groups = ["main"]
67 | files = [
68 | {file = "charset_normalizer-3.4.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:91b36a978b5ae0ee86c394f5a54d6ef44db1de0815eb43de826d41d21e4af3de"},
69 | {file = "charset_normalizer-3.4.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7461baadb4dc00fd9e0acbe254e3d7d2112e7f92ced2adc96e54ef6501c5f176"},
70 | {file = "charset_normalizer-3.4.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e218488cd232553829be0664c2292d3af2eeeb94b32bea483cf79ac6a694e037"},
71 | {file = "charset_normalizer-3.4.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:80ed5e856eb7f30115aaf94e4a08114ccc8813e6ed1b5efa74f9f82e8509858f"},
72 | {file = "charset_normalizer-3.4.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b010a7a4fd316c3c484d482922d13044979e78d1861f0e0650423144c616a46a"},
73 | {file = "charset_normalizer-3.4.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4532bff1b8421fd0a320463030c7520f56a79c9024a4e88f01c537316019005a"},
74 | {file = "charset_normalizer-3.4.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:d973f03c0cb71c5ed99037b870f2be986c3c05e63622c017ea9816881d2dd247"},
75 | {file = "charset_normalizer-3.4.1-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:3a3bd0dcd373514dcec91c411ddb9632c0d7d92aed7093b8c3bbb6d69ca74408"},
76 | {file = "charset_normalizer-3.4.1-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:d9c3cdf5390dcd29aa8056d13e8e99526cda0305acc038b96b30352aff5ff2bb"},
77 | {file = "charset_normalizer-3.4.1-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:2bdfe3ac2e1bbe5b59a1a63721eb3b95fc9b6817ae4a46debbb4e11f6232428d"},
78 | {file = "charset_normalizer-3.4.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:eab677309cdb30d047996b36d34caeda1dc91149e4fdca0b1a039b3f79d9a807"},
79 | {file = "charset_normalizer-3.4.1-cp310-cp310-win32.whl", hash = "sha256:c0429126cf75e16c4f0ad00ee0eae4242dc652290f940152ca8c75c3a4b6ee8f"},
80 | {file = "charset_normalizer-3.4.1-cp310-cp310-win_amd64.whl", hash = "sha256:9f0b8b1c6d84c8034a44893aba5e767bf9c7a211e313a9605d9c617d7083829f"},
81 | {file = "charset_normalizer-3.4.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:8bfa33f4f2672964266e940dd22a195989ba31669bd84629f05fab3ef4e2d125"},
82 | {file = "charset_normalizer-3.4.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:28bf57629c75e810b6ae989f03c0828d64d6b26a5e205535585f96093e405ed1"},
83 | {file = "charset_normalizer-3.4.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f08ff5e948271dc7e18a35641d2f11a4cd8dfd5634f55228b691e62b37125eb3"},
84 | {file = "charset_normalizer-3.4.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:234ac59ea147c59ee4da87a0c0f098e9c8d169f4dc2a159ef720f1a61bbe27cd"},
85 | {file = "charset_normalizer-3.4.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fd4ec41f914fa74ad1b8304bbc634b3de73d2a0889bd32076342a573e0779e00"},
86 | {file = "charset_normalizer-3.4.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:eea6ee1db730b3483adf394ea72f808b6e18cf3cb6454b4d86e04fa8c4327a12"},
87 | {file = "charset_normalizer-3.4.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:c96836c97b1238e9c9e3fe90844c947d5afbf4f4c92762679acfe19927d81d77"},
88 | {file = "charset_normalizer-3.4.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:4d86f7aff21ee58f26dcf5ae81a9addbd914115cdebcbb2217e4f0ed8982e146"},
89 | {file = "charset_normalizer-3.4.1-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:09b5e6733cbd160dcc09589227187e242a30a49ca5cefa5a7edd3f9d19ed53fd"},
90 | {file = "charset_normalizer-3.4.1-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:5777ee0881f9499ed0f71cc82cf873d9a0ca8af166dfa0af8ec4e675b7df48e6"},
91 | {file = "charset_normalizer-3.4.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:237bdbe6159cff53b4f24f397d43c6336c6b0b42affbe857970cefbb620911c8"},
92 | {file = "charset_normalizer-3.4.1-cp311-cp311-win32.whl", hash = "sha256:8417cb1f36cc0bc7eaba8ccb0e04d55f0ee52df06df3ad55259b9a323555fc8b"},
93 | {file = "charset_normalizer-3.4.1-cp311-cp311-win_amd64.whl", hash = "sha256:d7f50a1f8c450f3925cb367d011448c39239bb3eb4117c36a6d354794de4ce76"},
94 | {file = "charset_normalizer-3.4.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:73d94b58ec7fecbc7366247d3b0b10a21681004153238750bb67bd9012414545"},
95 | {file = "charset_normalizer-3.4.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dad3e487649f498dd991eeb901125411559b22e8d7ab25d3aeb1af367df5efd7"},
96 | {file = "charset_normalizer-3.4.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c30197aa96e8eed02200a83fba2657b4c3acd0f0aa4bdc9f6c1af8e8962e0757"},
97 | {file = "charset_normalizer-3.4.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2369eea1ee4a7610a860d88f268eb39b95cb588acd7235e02fd5a5601773d4fa"},
98 | {file = "charset_normalizer-3.4.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bc2722592d8998c870fa4e290c2eec2c1569b87fe58618e67d38b4665dfa680d"},
99 | {file = "charset_normalizer-3.4.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ffc9202a29ab3920fa812879e95a9e78b2465fd10be7fcbd042899695d75e616"},
100 | {file = "charset_normalizer-3.4.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:804a4d582ba6e5b747c625bf1255e6b1507465494a40a2130978bda7b932c90b"},
101 | {file = "charset_normalizer-3.4.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:0f55e69f030f7163dffe9fd0752b32f070566451afe180f99dbeeb81f511ad8d"},
102 | {file = "charset_normalizer-3.4.1-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:c4c3e6da02df6fa1410a7680bd3f63d4f710232d3139089536310d027950696a"},
103 | {file = "charset_normalizer-3.4.1-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:5df196eb874dae23dcfb968c83d4f8fdccb333330fe1fc278ac5ceeb101003a9"},
104 | {file = "charset_normalizer-3.4.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:e358e64305fe12299a08e08978f51fc21fac060dcfcddd95453eabe5b93ed0e1"},
105 | {file = "charset_normalizer-3.4.1-cp312-cp312-win32.whl", hash = "sha256:9b23ca7ef998bc739bf6ffc077c2116917eabcc901f88da1b9856b210ef63f35"},
106 | {file = "charset_normalizer-3.4.1-cp312-cp312-win_amd64.whl", hash = "sha256:6ff8a4a60c227ad87030d76e99cd1698345d4491638dfa6673027c48b3cd395f"},
107 | {file = "charset_normalizer-3.4.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:aabfa34badd18f1da5ec1bc2715cadc8dca465868a4e73a0173466b688f29dda"},
108 | {file = "charset_normalizer-3.4.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:22e14b5d70560b8dd51ec22863f370d1e595ac3d024cb8ad7d308b4cd95f8313"},
109 | {file = "charset_normalizer-3.4.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8436c508b408b82d87dc5f62496973a1805cd46727c34440b0d29d8a2f50a6c9"},
110 | {file = "charset_normalizer-3.4.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2d074908e1aecee37a7635990b2c6d504cd4766c7bc9fc86d63f9c09af3fa11b"},
111 | {file = "charset_normalizer-3.4.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:955f8851919303c92343d2f66165294848d57e9bba6cf6e3625485a70a038d11"},
112 | {file = "charset_normalizer-3.4.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:44ecbf16649486d4aebafeaa7ec4c9fed8b88101f4dd612dcaf65d5e815f837f"},
113 | {file = "charset_normalizer-3.4.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:0924e81d3d5e70f8126529951dac65c1010cdf117bb75eb02dd12339b57749dd"},
114 | {file = "charset_normalizer-3.4.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:2967f74ad52c3b98de4c3b32e1a44e32975e008a9cd2a8cc8966d6a5218c5cb2"},
115 | {file = "charset_normalizer-3.4.1-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:c75cb2a3e389853835e84a2d8fb2b81a10645b503eca9bcb98df6b5a43eb8886"},
116 | {file = "charset_normalizer-3.4.1-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:09b26ae6b1abf0d27570633b2b078a2a20419c99d66fb2823173d73f188ce601"},
117 | {file = "charset_normalizer-3.4.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:fa88b843d6e211393a37219e6a1c1df99d35e8fd90446f1118f4216e307e48cd"},
118 | {file = "charset_normalizer-3.4.1-cp313-cp313-win32.whl", hash = "sha256:eb8178fe3dba6450a3e024e95ac49ed3400e506fd4e9e5c32d30adda88cbd407"},
119 | {file = "charset_normalizer-3.4.1-cp313-cp313-win_amd64.whl", hash = "sha256:b1ac5992a838106edb89654e0aebfc24f5848ae2547d22c2c3f66454daa11971"},
120 | {file = "charset_normalizer-3.4.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f30bf9fd9be89ecb2360c7d94a711f00c09b976258846efe40db3d05828e8089"},
121 | {file = "charset_normalizer-3.4.1-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:97f68b8d6831127e4787ad15e6757232e14e12060bec17091b85eb1486b91d8d"},
122 | {file = "charset_normalizer-3.4.1-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7974a0b5ecd505609e3b19742b60cee7aa2aa2fb3151bc917e6e2646d7667dcf"},
123 | {file = "charset_normalizer-3.4.1-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fc54db6c8593ef7d4b2a331b58653356cf04f67c960f584edb7c3d8c97e8f39e"},
124 | {file = "charset_normalizer-3.4.1-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:311f30128d7d333eebd7896965bfcfbd0065f1716ec92bd5638d7748eb6f936a"},
125 | {file = "charset_normalizer-3.4.1-cp37-cp37m-musllinux_1_2_aarch64.whl", hash = "sha256:7d053096f67cd1241601111b698f5cad775f97ab25d81567d3f59219b5f1adbd"},
126 | {file = "charset_normalizer-3.4.1-cp37-cp37m-musllinux_1_2_i686.whl", hash = "sha256:807f52c1f798eef6cf26beb819eeb8819b1622ddfeef9d0977a8502d4db6d534"},
127 | {file = "charset_normalizer-3.4.1-cp37-cp37m-musllinux_1_2_ppc64le.whl", hash = "sha256:dccbe65bd2f7f7ec22c4ff99ed56faa1e9f785482b9bbd7c717e26fd723a1d1e"},
128 | {file = "charset_normalizer-3.4.1-cp37-cp37m-musllinux_1_2_s390x.whl", hash = "sha256:2fb9bd477fdea8684f78791a6de97a953c51831ee2981f8e4f583ff3b9d9687e"},
129 | {file = "charset_normalizer-3.4.1-cp37-cp37m-musllinux_1_2_x86_64.whl", hash = "sha256:01732659ba9b5b873fc117534143e4feefecf3b2078b0a6a2e925271bb6f4cfa"},
130 | {file = "charset_normalizer-3.4.1-cp37-cp37m-win32.whl", hash = "sha256:7a4f97a081603d2050bfaffdefa5b02a9ec823f8348a572e39032caa8404a487"},
131 | {file = "charset_normalizer-3.4.1-cp37-cp37m-win_amd64.whl", hash = "sha256:7b1bef6280950ee6c177b326508f86cad7ad4dff12454483b51d8b7d673a2c5d"},
132 | {file = "charset_normalizer-3.4.1-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:ecddf25bee22fe4fe3737a399d0d177d72bc22be6913acfab364b40bce1ba83c"},
133 | {file = "charset_normalizer-3.4.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8c60ca7339acd497a55b0ea5d506b2a2612afb2826560416f6894e8b5770d4a9"},
134 | {file = "charset_normalizer-3.4.1-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b7b2d86dd06bfc2ade3312a83a5c364c7ec2e3498f8734282c6c3d4b07b346b8"},
135 | {file = "charset_normalizer-3.4.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:dd78cfcda14a1ef52584dbb008f7ac81c1328c0f58184bf9a84c49c605002da6"},
136 | {file = "charset_normalizer-3.4.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6e27f48bcd0957c6d4cb9d6fa6b61d192d0b13d5ef563e5f2ae35feafc0d179c"},
137 | {file = "charset_normalizer-3.4.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:01ad647cdd609225c5350561d084b42ddf732f4eeefe6e678765636791e78b9a"},
138 | {file = "charset_normalizer-3.4.1-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:619a609aa74ae43d90ed2e89bdd784765de0a25ca761b93e196d938b8fd1dbbd"},
139 | {file = "charset_normalizer-3.4.1-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:89149166622f4db9b4b6a449256291dc87a99ee53151c74cbd82a53c8c2f6ccd"},
140 | {file = "charset_normalizer-3.4.1-cp38-cp38-musllinux_1_2_ppc64le.whl", hash = "sha256:7709f51f5f7c853f0fb938bcd3bc59cdfdc5203635ffd18bf354f6967ea0f824"},
141 | {file = "charset_normalizer-3.4.1-cp38-cp38-musllinux_1_2_s390x.whl", hash = "sha256:345b0426edd4e18138d6528aed636de7a9ed169b4aaf9d61a8c19e39d26838ca"},
142 | {file = "charset_normalizer-3.4.1-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:0907f11d019260cdc3f94fbdb23ff9125f6b5d1039b76003b5b0ac9d6a6c9d5b"},
143 | {file = "charset_normalizer-3.4.1-cp38-cp38-win32.whl", hash = "sha256:ea0d8d539afa5eb2728aa1932a988a9a7af94f18582ffae4bc10b3fbdad0626e"},
144 | {file = "charset_normalizer-3.4.1-cp38-cp38-win_amd64.whl", hash = "sha256:329ce159e82018d646c7ac45b01a430369d526569ec08516081727a20e9e4af4"},
145 | {file = "charset_normalizer-3.4.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:b97e690a2118911e39b4042088092771b4ae3fc3aa86518f84b8cf6888dbdb41"},
146 | {file = "charset_normalizer-3.4.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:78baa6d91634dfb69ec52a463534bc0df05dbd546209b79a3880a34487f4b84f"},
147 | {file = "charset_normalizer-3.4.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1a2bc9f351a75ef49d664206d51f8e5ede9da246602dc2d2726837620ea034b2"},
148 | {file = "charset_normalizer-3.4.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:75832c08354f595c760a804588b9357d34ec00ba1c940c15e31e96d902093770"},
149 | {file = "charset_normalizer-3.4.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0af291f4fe114be0280cdd29d533696a77b5b49cfde5467176ecab32353395c4"},
150 | {file = "charset_normalizer-3.4.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0167ddc8ab6508fe81860a57dd472b2ef4060e8d378f0cc555707126830f2537"},
151 | {file = "charset_normalizer-3.4.1-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:2a75d49014d118e4198bcee5ee0a6f25856b29b12dbf7cd012791f8a6cc5c496"},
152 | {file = "charset_normalizer-3.4.1-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:363e2f92b0f0174b2f8238240a1a30142e3db7b957a5dd5689b0e75fb717cc78"},
153 | {file = "charset_normalizer-3.4.1-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:ab36c8eb7e454e34e60eb55ca5d241a5d18b2c6244f6827a30e451c42410b5f7"},
154 | {file = "charset_normalizer-3.4.1-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:4c0907b1928a36d5a998d72d64d8eaa7244989f7aaaf947500d3a800c83a3fd6"},
155 | {file = "charset_normalizer-3.4.1-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:04432ad9479fa40ec0f387795ddad4437a2b50417c69fa275e212933519ff294"},
156 | {file = "charset_normalizer-3.4.1-cp39-cp39-win32.whl", hash = "sha256:3bed14e9c89dcb10e8f3a29f9ccac4955aebe93c71ae803af79265c9ca5644c5"},
157 | {file = "charset_normalizer-3.4.1-cp39-cp39-win_amd64.whl", hash = "sha256:49402233c892a461407c512a19435d1ce275543138294f7ef013f0b63d5d3765"},
158 | {file = "charset_normalizer-3.4.1-py3-none-any.whl", hash = "sha256:d98b1668f06378c6dbefec3b92299716b931cd4e6061f3c875a71ced1780ab85"},
159 | {file = "charset_normalizer-3.4.1.tar.gz", hash = "sha256:44251f18cd68a75b56585dd00dae26183e102cd5e0f9f1466e6df5da2ed64ea3"},
160 | ]
161 |
162 | [[package]]
163 | name = "click"
164 | version = "8.1.8"
165 | description = "Composable command line interface toolkit"
166 | optional = false
167 | python-versions = ">=3.7"
168 | groups = ["main"]
169 | files = [
170 | {file = "click-8.1.8-py3-none-any.whl", hash = "sha256:63c132bbbed01578a06712a2d1f497bb62d9c1c0d329b7903a866228027263b2"},
171 | {file = "click-8.1.8.tar.gz", hash = "sha256:ed53c9d8990d83c2a27deae68e4ee337473f6330c040a31d4225c9574d16096a"},
172 | ]
173 |
174 | [package.dependencies]
175 | colorama = {version = "*", markers = "platform_system == \"Windows\""}
176 |
177 | [[package]]
178 | name = "colorama"
179 | version = "0.4.6"
180 | description = "Cross-platform colored terminal text."
181 | optional = false
182 | python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7"
183 | groups = ["main", "dev"]
184 | files = [
185 | {file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"},
186 | {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"},
187 | ]
188 | markers = {main = "platform_system == \"Windows\" or sys_platform == \"win32\"", dev = "sys_platform == \"win32\""}
189 |
190 | [[package]]
191 | name = "idna"
192 | version = "3.10"
193 | description = "Internationalized Domain Names in Applications (IDNA)"
194 | optional = false
195 | python-versions = ">=3.6"
196 | groups = ["main"]
197 | files = [
198 | {file = "idna-3.10-py3-none-any.whl", hash = "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3"},
199 | {file = "idna-3.10.tar.gz", hash = "sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9"},
200 | ]
201 |
202 | [package.extras]
203 | all = ["flake8 (>=7.1.1)", "mypy (>=1.11.2)", "pytest (>=8.3.2)", "ruff (>=0.6.2)"]
204 |
205 | [[package]]
206 | name = "iniconfig"
207 | version = "2.0.0"
208 | description = "brain-dead simple config-ini parsing"
209 | optional = false
210 | python-versions = ">=3.7"
211 | groups = ["main", "dev"]
212 | files = [
213 | {file = "iniconfig-2.0.0-py3-none-any.whl", hash = "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374"},
214 | {file = "iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3"},
215 | ]
216 |
217 | [[package]]
218 | name = "markdown-it-py"
219 | version = "3.0.0"
220 | description = "Python port of markdown-it. Markdown parsing, done right!"
221 | optional = false
222 | python-versions = ">=3.8"
223 | groups = ["main"]
224 | files = [
225 | {file = "markdown-it-py-3.0.0.tar.gz", hash = "sha256:e3f60a94fa066dc52ec76661e37c851cb232d92f9886b15cb560aaada2df8feb"},
226 | {file = "markdown_it_py-3.0.0-py3-none-any.whl", hash = "sha256:355216845c60bd96232cd8d8c40e8f9765cc86f46880e43a8fd22dc1a1a8cab1"},
227 | ]
228 |
229 | [package.dependencies]
230 | mdurl = ">=0.1,<1.0"
231 |
232 | [package.extras]
233 | benchmarking = ["psutil", "pytest", "pytest-benchmark"]
234 | code-style = ["pre-commit (>=3.0,<4.0)"]
235 | compare = ["commonmark (>=0.9,<1.0)", "markdown (>=3.4,<4.0)", "mistletoe (>=1.0,<2.0)", "mistune (>=2.0,<3.0)", "panflute (>=2.3,<3.0)"]
236 | linkify = ["linkify-it-py (>=1,<3)"]
237 | plugins = ["mdit-py-plugins"]
238 | profiling = ["gprof2dot"]
239 | rtd = ["jupyter_sphinx", "mdit-py-plugins", "myst-parser", "pyyaml", "sphinx", "sphinx-copybutton", "sphinx-design", "sphinx_book_theme"]
240 | testing = ["coverage", "pytest", "pytest-cov", "pytest-regressions"]
241 |
242 | [[package]]
243 | name = "mdurl"
244 | version = "0.1.2"
245 | description = "Markdown URL utilities"
246 | optional = false
247 | python-versions = ">=3.7"
248 | groups = ["main"]
249 | files = [
250 | {file = "mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8"},
251 | {file = "mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba"},
252 | ]
253 |
254 | [[package]]
255 | name = "mock"
256 | version = "5.1.0"
257 | description = "Rolling backport of unittest.mock for all Pythons"
258 | optional = false
259 | python-versions = ">=3.6"
260 | groups = ["main"]
261 | files = [
262 | {file = "mock-5.1.0-py3-none-any.whl", hash = "sha256:18c694e5ae8a208cdb3d2c20a993ca1a7b0efa258c247a1e565150f477f83744"},
263 | {file = "mock-5.1.0.tar.gz", hash = "sha256:5e96aad5ccda4718e0a229ed94b2024df75cc2d55575ba5762d31f5767b8767d"},
264 | ]
265 |
266 | [package.extras]
267 | build = ["blurb", "twine", "wheel"]
268 | docs = ["sphinx"]
269 | test = ["pytest", "pytest-cov"]
270 |
271 | [[package]]
272 | name = "mypy-extensions"
273 | version = "1.0.0"
274 | description = "Type system extensions for programs checked with the mypy type checker."
275 | optional = false
276 | python-versions = ">=3.5"
277 | groups = ["main"]
278 | files = [
279 | {file = "mypy_extensions-1.0.0-py3-none-any.whl", hash = "sha256:4392f6c0eb8a5668a69e23d168ffa70f0be9ccfd32b5cc2d26a34ae5b844552d"},
280 | {file = "mypy_extensions-1.0.0.tar.gz", hash = "sha256:75dbf8955dc00442a438fc4d0666508a9a97b6bd41aa2f0ffe9d2f2725af0782"},
281 | ]
282 |
283 | [[package]]
284 | name = "packaging"
285 | version = "24.2"
286 | description = "Core utilities for Python packages"
287 | optional = false
288 | python-versions = ">=3.8"
289 | groups = ["main", "dev"]
290 | files = [
291 | {file = "packaging-24.2-py3-none-any.whl", hash = "sha256:09abb1bccd265c01f4a3aa3f7a7db064b36514d2cba19a2f694fe6150451a759"},
292 | {file = "packaging-24.2.tar.gz", hash = "sha256:c228a6dc5e932d346bc5739379109d49e8853dd8223571c7c5b55260edc0b97f"},
293 | ]
294 |
295 | [[package]]
296 | name = "pathspec"
297 | version = "0.12.1"
298 | description = "Utility library for gitignore style pattern matching of file paths."
299 | optional = false
300 | python-versions = ">=3.8"
301 | groups = ["main"]
302 | files = [
303 | {file = "pathspec-0.12.1-py3-none-any.whl", hash = "sha256:a0d503e138a4c123b27490a4f7beda6a01c6f288df0e4a8b79c7eb0dc7b4cc08"},
304 | {file = "pathspec-0.12.1.tar.gz", hash = "sha256:a482d51503a1ab33b1c67a6c3813a26953dbdc71c31dacaef9a838c4e29f5712"},
305 | ]
306 |
307 | [[package]]
308 | name = "platformdirs"
309 | version = "4.3.6"
310 | description = "A small Python package for determining appropriate platform-specific dirs, e.g. a `user data dir`."
311 | optional = false
312 | python-versions = ">=3.8"
313 | groups = ["main"]
314 | files = [
315 | {file = "platformdirs-4.3.6-py3-none-any.whl", hash = "sha256:73e575e1408ab8103900836b97580d5307456908a03e92031bab39e4554cc3fb"},
316 | {file = "platformdirs-4.3.6.tar.gz", hash = "sha256:357fb2acbc885b0419afd3ce3ed34564c13c9b95c89360cd9563f73aa5e2b907"},
317 | ]
318 |
319 | [package.extras]
320 | docs = ["furo (>=2024.8.6)", "proselint (>=0.14)", "sphinx (>=8.0.2)", "sphinx-autodoc-typehints (>=2.4)"]
321 | test = ["appdirs (==1.4.4)", "covdefaults (>=2.3)", "pytest (>=8.3.2)", "pytest-cov (>=5)", "pytest-mock (>=3.14)"]
322 | type = ["mypy (>=1.11.2)"]
323 |
324 | [[package]]
325 | name = "pluggy"
326 | version = "1.5.0"
327 | description = "plugin and hook calling mechanisms for python"
328 | optional = false
329 | python-versions = ">=3.8"
330 | groups = ["main", "dev"]
331 | files = [
332 | {file = "pluggy-1.5.0-py3-none-any.whl", hash = "sha256:44e1ad92c8ca002de6377e165f3e0f1be63266ab4d554740532335b9d75ea669"},
333 | {file = "pluggy-1.5.0.tar.gz", hash = "sha256:2cffa88e94fdc978c4c574f15f9e59b7f4201d439195c3715ca9e2486f1d0cf1"},
334 | ]
335 |
336 | [package.extras]
337 | dev = ["pre-commit", "tox"]
338 | testing = ["pytest", "pytest-benchmark"]
339 |
340 | [[package]]
341 | name = "pyfakefs"
342 | version = "5.7.4"
343 | description = "pyfakefs implements a fake file system that mocks the Python file system modules."
344 | optional = false
345 | python-versions = ">=3.7"
346 | groups = ["main"]
347 | files = [
348 | {file = "pyfakefs-5.7.4-py3-none-any.whl", hash = "sha256:3e763d700b91c54ade6388be2cfa4e521abc00e34f7defb84ee511c73031f45f"},
349 | {file = "pyfakefs-5.7.4.tar.gz", hash = "sha256:4971e65cc80a93a1e6f1e3a4654909c0c493186539084dc9301da3d68c8878fe"},
350 | ]
351 |
352 | [[package]]
353 | name = "pygments"
354 | version = "2.19.1"
355 | description = "Pygments is a syntax highlighting package written in Python."
356 | optional = false
357 | python-versions = ">=3.8"
358 | groups = ["main"]
359 | files = [
360 | {file = "pygments-2.19.1-py3-none-any.whl", hash = "sha256:9ea1544ad55cecf4b8242fab6dd35a93bbce657034b0611ee383099054ab6d8c"},
361 | {file = "pygments-2.19.1.tar.gz", hash = "sha256:61c16d2a8576dc0649d9f39e089b5f02bcd27fba10d8fb4dcc28173f7a45151f"},
362 | ]
363 |
364 | [package.extras]
365 | windows-terminal = ["colorama (>=0.4.6)"]
366 |
367 | [[package]]
368 | name = "pytest"
369 | version = "8.3.4"
370 | description = "pytest: simple powerful testing with Python"
371 | optional = false
372 | python-versions = ">=3.8"
373 | groups = ["main", "dev"]
374 | files = [
375 | {file = "pytest-8.3.4-py3-none-any.whl", hash = "sha256:50e16d954148559c9a74109af1eaf0c945ba2d8f30f0a3d3335edde19788b6f6"},
376 | {file = "pytest-8.3.4.tar.gz", hash = "sha256:965370d062bce11e73868e0335abac31b4d3de0e82f4007408d242b4f8610761"},
377 | ]
378 |
379 | [package.dependencies]
380 | colorama = {version = "*", markers = "sys_platform == \"win32\""}
381 | iniconfig = "*"
382 | packaging = "*"
383 | pluggy = ">=1.5,<2"
384 |
385 | [package.extras]
386 | dev = ["argcomplete", "attrs (>=19.2)", "hypothesis (>=3.56)", "mock", "pygments (>=2.7.2)", "requests", "setuptools", "xmlschema"]
387 |
388 | [[package]]
389 | name = "requests"
390 | version = "2.32.3"
391 | description = "Python HTTP for Humans."
392 | optional = false
393 | python-versions = ">=3.8"
394 | groups = ["main"]
395 | files = [
396 | {file = "requests-2.32.3-py3-none-any.whl", hash = "sha256:70761cfe03c773ceb22aa2f671b4757976145175cdfca038c02654d061d6dcc6"},
397 | {file = "requests-2.32.3.tar.gz", hash = "sha256:55365417734eb18255590a9ff9eb97e9e1da868d4ccd6402399eaf68af20a760"},
398 | ]
399 |
400 | [package.dependencies]
401 | certifi = ">=2017.4.17"
402 | charset-normalizer = ">=2,<4"
403 | idna = ">=2.5,<4"
404 | urllib3 = ">=1.21.1,<3"
405 |
406 | [package.extras]
407 | socks = ["PySocks (>=1.5.6,!=1.5.7)"]
408 | use-chardet-on-py3 = ["chardet (>=3.0.2,<6)"]
409 |
410 | [[package]]
411 | name = "rich"
412 | version = "13.9.4"
413 | description = "Render rich text, tables, progress bars, syntax highlighting, markdown and more to the terminal"
414 | optional = false
415 | python-versions = ">=3.8.0"
416 | groups = ["main"]
417 | files = [
418 | {file = "rich-13.9.4-py3-none-any.whl", hash = "sha256:6049d5e6ec054bf2779ab3358186963bac2ea89175919d699e378b99738c2a90"},
419 | {file = "rich-13.9.4.tar.gz", hash = "sha256:439594978a49a09530cff7ebc4b5c7103ef57baf48d5ea3184f21d9a2befa098"},
420 | ]
421 |
422 | [package.dependencies]
423 | markdown-it-py = ">=2.2.0"
424 | pygments = ">=2.13.0,<3.0.0"
425 |
426 | [package.extras]
427 | jupyter = ["ipywidgets (>=7.5.1,<9)"]
428 |
429 | [[package]]
430 | name = "shellingham"
431 | version = "1.5.4"
432 | description = "Tool to Detect Surrounding Shell"
433 | optional = false
434 | python-versions = ">=3.7"
435 | groups = ["main"]
436 | files = [
437 | {file = "shellingham-1.5.4-py2.py3-none-any.whl", hash = "sha256:7ecfff8f2fd72616f7481040475a65b2bf8af90a56c89140852d1120324e8686"},
438 | {file = "shellingham-1.5.4.tar.gz", hash = "sha256:8dbca0739d487e5bd35ab3ca4b36e11c4078f3a234bfce294b0a0291363404de"},
439 | ]
440 |
441 | [[package]]
442 | name = "toml"
443 | version = "0.10.2"
444 | description = "Python Library for Tom's Obvious, Minimal Language"
445 | optional = false
446 | python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*"
447 | groups = ["main"]
448 | files = [
449 | {file = "toml-0.10.2-py2.py3-none-any.whl", hash = "sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b"},
450 | {file = "toml-0.10.2.tar.gz", hash = "sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f"},
451 | ]
452 |
453 | [[package]]
454 | name = "typer"
455 | version = "0.15.1"
456 | description = "Typer, build great CLIs. Easy to code. Based on Python type hints."
457 | optional = false
458 | python-versions = ">=3.7"
459 | groups = ["main"]
460 | files = [
461 | {file = "typer-0.15.1-py3-none-any.whl", hash = "sha256:7994fb7b8155b64d3402518560648446072864beefd44aa2dc36972a5972e847"},
462 | {file = "typer-0.15.1.tar.gz", hash = "sha256:a0588c0a7fa68a1978a069818657778f86abe6ff5ea6abf472f940a08bfe4f0a"},
463 | ]
464 |
465 | [package.dependencies]
466 | click = ">=8.0.0"
467 | rich = ">=10.11.0"
468 | shellingham = ">=1.3.0"
469 | typing-extensions = ">=3.7.4.3"
470 |
471 | [[package]]
472 | name = "typing-extensions"
473 | version = "4.12.2"
474 | description = "Backported and Experimental Type Hints for Python 3.8+"
475 | optional = false
476 | python-versions = ">=3.8"
477 | groups = ["main"]
478 | files = [
479 | {file = "typing_extensions-4.12.2-py3-none-any.whl", hash = "sha256:04e5ca0351e0f3f85c6853954072df659d0d13fac324d0072316b67d7794700d"},
480 | {file = "typing_extensions-4.12.2.tar.gz", hash = "sha256:1a7ead55c7e559dd4dee8856e3a88b41225abfe1ce8df57b7c13915fe121ffb8"},
481 | ]
482 |
483 | [[package]]
484 | name = "urllib3"
485 | version = "2.3.0"
486 | description = "HTTP library with thread-safe connection pooling, file post, and more."
487 | optional = false
488 | python-versions = ">=3.9"
489 | groups = ["main"]
490 | files = [
491 | {file = "urllib3-2.3.0-py3-none-any.whl", hash = "sha256:1cee9ad369867bfdbbb48b7dd50374c0967a0bb7710050facf0dd6911440e3df"},
492 | {file = "urllib3-2.3.0.tar.gz", hash = "sha256:f8c5449b3cf0861679ce7e0503c7b44b5ec981bec0d1d3795a07f1ba96f0204d"},
493 | ]
494 |
495 | [package.extras]
496 | brotli = ["brotli (>=1.0.9)", "brotlicffi (>=0.8.0)"]
497 | h2 = ["h2 (>=4,<5)"]
498 | socks = ["pysocks (>=1.5.6,!=1.5.7,<2.0)"]
499 | zstd = ["zstandard (>=0.18.0)"]
500 |
501 | [metadata]
502 | lock-version = "2.1"
503 | python-versions = "^3.12"
504 | content-hash = "8a0e1d32d9db52d44502a044b687baf13ef09f7be2c7282107d04835a9bb697a"
505 |
--------------------------------------------------------------------------------
/pyproject.toml:
--------------------------------------------------------------------------------
1 | [tool.poetry]
2 | name = "devcli"
3 | version = "3.0.0"
4 | description = "A command line tool to create command line tools"
5 | authors = ["Marco Valtas"]
6 | license = "MIT"
7 | readme = "README.rst"
8 | include = ["pyproject.toml"]
9 | packages = [
10 | { include = "devcli/**/*" },
11 | ]
12 |
13 |
14 | [tool.poetry.dependencies]
15 | python = "^3.12"
16 | toml = "^0.10.2"
17 | mock = "^5.1.0"
18 | typer = "^0.15.1"
19 | rich = "^13.9.4"
20 | requests = "^2.31.0"
21 | pyfakefs = "^5.7.2"
22 | black = "==25.1.0"
23 | pytest = "==8.3.4"
24 |
25 | [tool.poetry.dev-dependencies]
26 | pytest = ">=7.0.0"
27 |
28 | [tool.poetry.scripts]
29 | devcli = "devcli.core.cli:cli"
30 |
31 |
32 | [build-system]
33 | requires = ["poetry-core"]
34 | build-backend = "poetry.core.masonry.api"
35 |
--------------------------------------------------------------------------------
/tests/commands/test_config.py:
--------------------------------------------------------------------------------
1 | from unittest.mock import patch
2 |
3 | import pytest
4 |
5 |
6 | @pytest.fixture(autouse=True)
7 | def setup(devcli_cmd):
8 | from devcli.config import Config
9 | from devcli.core import project_root
10 |
11 | # loads testing configuration
12 | Config().add_config(project_root("tests/fixtures/general.toml"))
13 |
14 | global config
15 | config = devcli_cmd("config")
16 |
17 |
18 | def test_get():
19 | result = config("get", "devcli.key")
20 | assert "value" in result.output
21 |
--------------------------------------------------------------------------------
/tests/commands/test_edit.py:
--------------------------------------------------------------------------------
1 | import pytest
2 |
3 |
4 | @pytest.fixture(autouse=True)
5 | def setup(devcli_cmd):
6 | global edit
7 | edit = devcli_cmd("edit")
8 |
9 |
10 | def test_simple():
11 | result = edit("config")
12 |
--------------------------------------------------------------------------------
/tests/commands/test_op.py:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/tests/commands/test_url.py:
--------------------------------------------------------------------------------
1 | from unittest.mock import patch
2 |
3 | import pytest
4 |
5 | from devcli.framework.errors import MissConfError
6 |
7 |
8 | @pytest.fixture(autouse=True)
9 | def setup(devcli_cmd):
10 | from devcli.config import Config
11 | from devcli.core import project_root
12 |
13 | # loads testing configuration
14 | Config().add_config(project_root("tests/fixtures/general.toml"))
15 |
16 | global url
17 | url = devcli_cmd("url")
18 |
19 |
20 | def test_list():
21 | result = url("list")
22 | assert "https://github.com/mvaltas/devcli" in result.output
23 | assert "https://duckduckgo.com" in result.output
24 |
25 |
26 | def test_basic_search():
27 | result = url("search", "duck")
28 | assert "duck: https://duckduckgo.com" in result.output
29 |
30 |
31 | def test_partial_match_search():
32 | result = url("search", "ck")
33 | assert "duck: https://duckduckgo.com" in result.output
34 |
35 |
36 | def test_exact_match_open():
37 | with patch("devcli.utils.shell.run") as mock_run:
38 | url("open", "duck")
39 | mock_run.assert_called_once_with("open 'https://duckduckgo.com'")
40 |
41 |
42 | def test_partial_match_multiple_first_alphabetical_wins():
43 | with patch("devcli.utils.shell.run") as mock_run:
44 | url("open", "g")
45 | mock_run.assert_called_once_with("open 'https://bing.com'")
46 |
47 |
48 | def test_raises_error_if_no_key_is_found():
49 | result = url("open", "undefined-key-in-configuration")
50 | assert result.exit_code != 0
51 | assert isinstance(result.exception, MissConfError)
52 |
--------------------------------------------------------------------------------
/tests/conftest.py:
--------------------------------------------------------------------------------
1 | import pytest
2 | from typer.testing import CliRunner
3 |
4 | from devcli.core.cli import cli
5 |
6 | runner = CliRunner()
7 |
8 |
9 | def invoke_command(cmd: str = None):
10 | # pre-prepare a command that will be invoked several times
11 | # with different parameters
12 | return lambda *params: runner.invoke(cli, ([cmd] if cmd else []) + list(params))
13 |
14 |
15 | @pytest.fixture
16 | def devcli_cmd():
17 | """
18 | This fixture allows for reusing of invoke_command on Typer tests, in the tests
19 | you can set up without having to worry about the invoke or CliRunner()
20 |
21 | Example::
22 |
23 | @pytest.fixture(autouse=True)
24 | def setup(setup_cmd):
25 | global cmd_under_test
26 | cmd_under_test = setup_cmd("cmd-under-test")
27 |
28 | def test_something():
29 | result = cmd_under_test("options", "parameters")
30 | assert "expected output" == result.output
31 | """
32 | return invoke_command
33 |
--------------------------------------------------------------------------------
/tests/fixtures/general.toml:
--------------------------------------------------------------------------------
1 | [devcli]
2 | key = "value"
3 |
4 | [overridable_configuration]
5 | key = "general_value"
6 |
7 | [devcli.commands.url]
8 |
9 | devcli = "https://github.com/mvaltas/devcli"
10 | google = "https://google.com"
11 | duck = "https://duckduckgo.com"
12 | bing = "https://bing.com"
13 |
14 | [decli.commands.edit]
15 |
16 |
--------------------------------------------------------------------------------
/tests/fixtures/specific.toml:
--------------------------------------------------------------------------------
1 | [a_specific_configuration]
2 | key = "value"
3 |
4 | [overridable_configuration]
5 | key = "specific_value"
6 |
--------------------------------------------------------------------------------
/tests/test_base.py:
--------------------------------------------------------------------------------
1 | from typer import Typer
2 |
3 | from unittest.mock import patch
4 | import devcli.framework.base as base
5 |
6 |
7 | def test_new_creates_typer_instance():
8 | typer_instance = base.new("Test description")
9 | assert isinstance(typer_instance, Typer)
10 |
11 |
12 | def test_stop_prints_error_and_exits():
13 | with patch("devcli.framework.console.error") as mock_error, patch(
14 | "sys.exit"
15 | ) as mock_exit:
16 | base.stop("Custom error message", 2)
17 | mock_error.assert_called_once_with("Custom error message")
18 | mock_exit.assert_called_once_with(2)
19 |
20 |
21 | def test_stop_has_default_error_message():
22 | with patch("devcli.framework.console.error") as mock_error, patch(
23 | "sys.exit"
24 | ) as mock_exit:
25 | base.stop(exit_code=2)
26 | mock_error.assert_called_once_with("Error")
27 | mock_exit.assert_called_once_with(2)
28 |
29 |
30 | def test_returns_logger_with_caller_file_name():
31 | with patch("inspect.stack") as mock_stack:
32 | mock_stack.return_value = [
33 | None,
34 | (
35 | "frame",
36 | ".devcli/filename",
37 | "lineno",
38 | "function",
39 | "code_context",
40 | "index",
41 | ),
42 | ]
43 | log = base.logger()
44 | assert log.name == "command:filename"
45 |
46 |
47 | def test_return_logger_with_name_given():
48 | log = base.logger("test-logger")
49 | assert log.name == "test-logger"
50 |
--------------------------------------------------------------------------------
/tests/test_cli.py:
--------------------------------------------------------------------------------
1 | import re
2 |
3 | import pytest
4 |
5 |
6 | @pytest.fixture(autouse=True)
7 | def setup(devcli_cmd):
8 | global devcli
9 | devcli = devcli_cmd()
10 |
11 |
12 | def test_version():
13 | result = devcli("show-version")
14 | assert re.match(r".*version\s\d+\.\d+\.\d+$", result.output) is not None
15 |
16 |
17 | def test_default_to_help_if_command_not_found():
18 | result = devcli()
19 | assert "Usage: devcli [OPTIONS] COMMAND [ARGS]" in result.output
20 |
21 |
22 | def test_passing_debug_changes_loglevel_to_debug(caplog):
23 | devcli("--debug", "show-version")
24 | assert any(record.levelname == "DEBUG" for record in caplog.records)
25 |
26 |
27 | def test_passing_debug_short_flag_changes_loglevel_to_debug(caplog):
28 | devcli("-d", "show-version")
29 | assert any(record.levelname == "DEBUG" for record in caplog.records)
30 |
31 |
32 | def test_passing_verbose_changes_loglevel_to_info(caplog):
33 | devcli("--verbose", "show-version")
34 | assert any(record.levelname == "INFO" for record in caplog.records)
35 |
36 |
37 | def test_passing_verbose_short_flag_changes_loglevel_to_info(caplog):
38 | devcli("-v", "show-version")
39 | assert any(record.levelname == "INFO" for record in caplog.records)
40 |
41 |
42 | def test_should_load_dynamic_commands():
43 | # example subcommand works because we have a .devcli in the root
44 | # of the project, tests of the subcommand 'example' itself
45 | # are in test_example.py
46 | result = devcli("example")
47 | assert "ping" in result.output
48 |
49 |
50 | def test_show_config():
51 | result = devcli("show-config")
52 | assert "[devcli]" in result.output
53 |
54 |
55 | def test_show_config_audit_log():
56 | result = devcli("show-config", "--explain")
57 | assert re.search(r"conf/defaults\.toml", result.output)
58 | assert re.search(r"\.config/devcli/devcli\.toml", result.output)
59 | assert re.search(r"\.devcli/devcli\.toml", result.output)
60 |
--------------------------------------------------------------------------------
/tests/test_conf.py:
--------------------------------------------------------------------------------
1 | import pytest
2 |
3 | from devcli.core import project_root
4 |
5 |
6 | @pytest.fixture(autouse=True)
7 | def setup():
8 | from devcli.config import Config
9 |
10 | # force reset of singleton for testing
11 | Config._instance = None
12 | global conf
13 | conf = Config().add_config(project_root("tests/fixtures/general.toml"))
14 |
15 |
16 | def test_it_parses_one_conf_file():
17 | assert conf["devcli"]["key"] == "value"
18 |
19 |
20 | def test_it_adds_configurations_of_other_files():
21 | assert conf["devcli"]["key"] == "value"
22 | assert conf["a_specific_configuration"] is None
23 |
24 | conf.add_config(project_root("tests/fixtures/specific.toml"))
25 |
26 | assert conf["devcli"]["key"] == "value"
27 | assert conf["a_specific_configuration"]["key"] == "value"
28 |
29 |
30 | def test_last_added_configuration_overrides_values():
31 | # from the initialization of general.toml
32 | assert conf["overridable_configuration"]["key"] == "general_value"
33 |
34 | # load specific configuration
35 | conf.add_config(project_root("tests/fixtures/specific.toml"))
36 | assert conf["overridable_configuration"]["key"] == "specific_value"
37 |
38 | # load general configuration again
39 | conf.add_config(project_root("tests/fixtures/general.toml"))
40 | assert conf["overridable_configuration"]["key"] == "general_value"
41 |
42 |
43 | def test_it_ignores_if_asked_to_load_non_existent_file():
44 | # non-existent file
45 | conf.add_config("this_file_does_not_exists")
46 | # does not affect the configuration
47 | assert conf["devcli"]["key"] == "value"
48 |
49 |
50 | def test_it_accepts_fetch_through_path_str():
51 | assert conf["devcli.key"] == "value"
52 | assert conf["overridable_configuration.key"] == "general_value"
53 |
54 |
55 | def test_audit_should_list_files_loaded():
56 | assert conf.audit()[project_root("tests/fixtures/general.toml")] is not None
57 |
58 |
59 | def test_audit_should_be_immutable():
60 | config_key = project_root("tests/fixtures/general.toml")
61 | audit = conf.audit()
62 | # config is listed in audit
63 | assert audit[config_key] is not None
64 | # dict has value altered
65 | audit[config_key] = None
66 | # fetching audit again does not alter the original dict
67 | assert conf.audit()[config_key] is not None
68 |
69 |
70 | def test_files_returns_files_loaded():
71 | gen_config = project_root("tests/fixtures/general.toml")
72 | spec_config = project_root("tests/fixtures/specific.toml")
73 | # general was loaded in setup, but specific not
74 | assert gen_config in conf.files()
75 | assert spec_config not in conf.files()
76 | # load specific
77 | conf.add_config(spec_config)
78 | # and now it is listed in files()
79 | assert spec_config in conf.files()
80 |
--------------------------------------------------------------------------------
/tests/test_console.py:
--------------------------------------------------------------------------------
1 | import pytest
2 |
3 | from unittest.mock import patch
4 | import devcli.framework.console as console
5 |
6 | MESSAGE = "Hello, World!"
7 |
8 |
9 | @pytest.fixture
10 | def mock_print():
11 | with patch("devcli.framework.console.print") as mock:
12 | yield mock
13 |
14 |
15 | def assert_print(mock, color=None):
16 | if color:
17 | mock.assert_called_once_with(f"[{color}]{MESSAGE}[/{color}]")
18 | else:
19 | mock.assert_called_once_with(f"{MESSAGE}")
20 |
21 |
22 | def test_echo(mock_print):
23 | console.echo(MESSAGE)
24 | assert_print(mock_print)
25 |
26 |
27 | def test_info(mock_print):
28 | console.info(MESSAGE)
29 | assert_print(mock_print, color="cyan")
30 |
31 |
32 | def test_warn(mock_print):
33 | console.warn(MESSAGE)
34 | assert_print(mock_print, color="yellow")
35 |
36 |
37 | def test_error(mock_print):
38 | console.error(MESSAGE)
39 | assert_print(mock_print, color="red")
40 |
--------------------------------------------------------------------------------
/tests/test_core.py:
--------------------------------------------------------------------------------
1 | import os
2 | from pathlib import Path
3 |
4 | import pytest
5 | from pyfakefs.fake_filesystem import PatchMode
6 | from pyfakefs.fake_filesystem_unittest import Patcher
7 | from typer import Typer
8 | from typer.testing import CliRunner
9 |
10 | from devcli.core import (
11 | project_root,
12 | traverse_search,
13 | traverse_load_dynamic_commands,
14 | load_dynamic_commands,
15 | )
16 |
17 | DYN_COMMAND = """
18 | import devcli.framework as cmd
19 | cli = cmd.new("test")
20 | @cli.command()
21 | def hello():
22 | pass
23 | """
24 |
25 | runner = CliRunner()
26 |
27 |
28 | @pytest.fixture
29 | def fs_patch_open():
30 | with Patcher(patch_open_code=PatchMode.AUTO) as p:
31 | yield p.fs
32 |
33 |
34 | def test_project_root_resolves_to_right_directory():
35 | assert str(project_root()) == os.path.abspath(
36 | os.path.join(os.path.dirname(__file__), "..")
37 | )
38 |
39 |
40 | def test_project_root_resolves_to_right_directory_with_filename():
41 | assert str(project_root(__file__)) == os.path.abspath(__file__)
42 |
43 |
44 | def test_traverse_search_for_file_name(fs):
45 | files = [Path("/org/dept/project/module/file.txt"), Path("/org/file.txt")]
46 | for f in files:
47 | fs.create_file(f)
48 |
49 | assert traverse_search("file.txt", "/org/dept/project/module") == files
50 |
51 |
52 | def test_traverse_search_for_directory(fs):
53 | expected = [
54 | Path("/org/dept/project/module/.devcli"),
55 | Path("/org/dept/project/.devcli"),
56 | Path("/org/dept/.devcli"),
57 | Path("/org/.devcli"),
58 | Path("/.devcli"),
59 | ]
60 | for d in expected:
61 | fs.create_dir(d)
62 |
63 | assert traverse_search(".devcli", "/org/dept/project/module") == expected
64 |
65 |
66 | def test_load_commands_dynamically():
67 | app = Typer()
68 | # This should load the example.py subcommand on .devcli
69 | load_dynamic_commands(app, project_root(".devcli"))
70 | # Executed the example command just loaded
71 | result = runner.invoke(app, ["example", "ping"])
72 |
73 | assert "PONG!" in result.output
74 |
75 |
76 | def test_traverse_load_commands_dynamically(fs_patch_open):
77 | """
78 | This test uses PatchMode.AUTO from pyfakefs as we load commands
79 | dynamic using module loading which user ``open_code()`` function.
80 | This function is not by default faked by pyfakefs.
81 |
82 | see: