plain.dev 0.0.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,134 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>Requestlog</title>
7
+ {% tailwind_css %}
8
+ </head>
9
+ <body class="bg-zinc-900 text-zinc-200">
10
+
11
+ <div class="flex">
12
+ <div class="overflow-auto border-r sm:w-1/2 md:2/5 border-zinc-700">
13
+ <table>
14
+ <tbody>
15
+ {% for log in requestlogs %}
16
+ <tr class="hover:bg-zinc-800 cursor-pointer text-zinc-400 {% if log.name == requestlog.name %}bg-zinc-700{% endif %}">
17
+ <td class="whitespace-nowrap px-2 py-2 border-b border-zinc-700 border-l-4 {% if log.name == requestlog.name %}border-l-orange-600{% else %}border-l-transparent{% endif %}">
18
+ {% if log.response.status_code >= 400 %}
19
+ <span class="px-1.5 py-0.5 text-xs text-white bg-red-600 rounded-sm">{{ log.response.status_code }}</span>
20
+ {% elif log.response.status_code >= 300 %}
21
+ <span class="px-1.5 py-0.5 text-xs text-white bg-zinc-600 rounded-sm">{{ log.response.status_code }}</span>
22
+ {% else %}
23
+ <span class="px-1.5 py-0.5 text-xs text-zinc-700 bg-zinc-300 rounded-sm">{{ log.response.status_code }}</span>
24
+ {% endif %}
25
+ </td>
26
+ <td class="px-1 py-2 border-b whitespace-nowrap border-zinc-700">
27
+ <span class="font-mono text-sm text-zinc-400">{{ log.request.method }}</span>
28
+ </td>
29
+ <td class="w-full px-2 py-2 overflow-hidden border-b whitespace-nowrap border-zinc-700 text-ellipsis" style="max-width: 1px;">
30
+ <a href="?log={{ log.name }}">
31
+ <span class="font-mono text-sm text-zinc-300">{{ log.request.full_path }}</span>
32
+ </a>
33
+ </td>
34
+ <td class="px-2 py-2 text-right border-b whitespace-nowrap border-zinc-700">
35
+ <span title="{{ log.timestamp }}" class="text-xs tabular-nums whitespace-nowrap">{{ log.timestamp|strftime("%H:%M:%S") }}</span>
36
+ </td>
37
+ </tr>
38
+ {% else %}
39
+ <tr>
40
+ <td class="px-4 py-2 text-center">
41
+ <span class="text-zinc-500">No logs yet!</span>
42
+ </td>
43
+ </tr>
44
+ {% endfor %}
45
+ {% if requestlogs %}
46
+ <tr>
47
+ <td colspan="4" class="px-1 py-3">
48
+ <form method="post">
49
+ {{ csrf_input }}
50
+ <input type="hidden" name="action" value="clear">
51
+ <button type="submit" class="flex items-center mx-auto text-xs text-red-500/75 hover:text-red-500">
52
+ <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="w-4 h-4 mr-2 bi bi-trash3" viewBox="0 0 16 16">
53
+ <path d="M6.5 1h3a.5.5 0 0 1 .5.5v1H6v-1a.5.5 0 0 1 .5-.5ZM11 2.5v-1A1.5 1.5 0 0 0 9.5 0h-3A1.5 1.5 0 0 0 5 1.5v1H2.506a.58.58 0 0 0-.01 0H1.5a.5.5 0 0 0 0 1h.538l.853 10.66A2 2 0 0 0 4.885 16h6.23a2 2 0 0 0 1.994-1.84l.853-10.66h.538a.5.5 0 0 0 0-1h-.995a.59.59 0 0 0-.01 0H11Zm1.958 1-.846 10.58a1 1 0 0 1-.997.92h-6.23a1 1 0 0 1-.997-.92L3.042 3.5h9.916Zm-7.487 1a.5.5 0 0 1 .528.47l.5 8.5a.5.5 0 0 1-.998.06L5 5.03a.5.5 0 0 1 .47-.53Zm5.058 0a.5.5 0 0 1 .47.53l-.5 8.5a.5.5 0 1 1-.998-.06l.5-8.5a.5.5 0 0 1 .528-.47ZM8 4.5a.5.5 0 0 1 .5.5v8.5a.5.5 0 0 1-1 0V5a.5.5 0 0 1 .5-.5Z"/>
54
+ </svg>
55
+ Clear all
56
+ </button>
57
+ </form>
58
+ </td>
59
+ </tr>
60
+ {% endif %}
61
+ </tbody>
62
+ </table>
63
+ </div>
64
+
65
+ {% if requestlog %}
66
+ <div class="px-8 py-6 overflow-auto sm:w-1/2 md:w-3/5">
67
+ {% with request=requestlog.request %}
68
+ <div class="flex items-center justify-between">
69
+ <h3 class="text-lg font-medium">
70
+ {{ request.method }} <a href="{{ request.absolute_uri }}" class="font-mono hover:underline">{{ request.full_path }}</a>
71
+ </h3>
72
+ <form method="POST">
73
+ {{ csrf_input }}
74
+ <input type="hidden" name="log" value="{{ requestlog.name }}">
75
+ <button type="submit" class="flex items-center px-3 py-2 ml-4 text-sm rounded-full bg-zinc-600 hover:bg-zinc-500">
76
+ <svg xmlns="http://www.w3.org/2000/svg" class="w-4 h-4 mr-2" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-refresh-cw"><polyline points="23 4 23 10 17 10"></polyline><polyline points="1 20 1 14 7 14"></polyline><path d="M3.51 9a9 9 0 0 1 14.85-3.36L23 10M1 14l4.64 4.36A9 9 0 0 0 20.49 15"></path></svg>
77
+ replay
78
+ </button>
79
+ </form>
80
+ </div>
81
+
82
+ {% if requestlog.exception %}
83
+ <div class="p-2 mt-4 bg-red-600 rounded text-zinc-100">
84
+ <h3 class="text-sm font-medium">{{ requestlog.exception.type }} Exception</h3>
85
+ <pre class="mt-2 overflow-auto text-xs rounded bg-zinc-800 text-zinc-200 max-h-96"><code>
86
+ {{- requestlog.exception.str -}}
87
+ </code></pre>
88
+ <pre class="mt-2 overflow-auto text-xs rounded bg-zinc-800 text-zinc-200 max-h-96"><code>
89
+ {{- requestlog.exception.args -}}
90
+ </code></pre>
91
+ <pre class="mt-2 overflow-auto text-xs rounded bg-zinc-800 text-zinc-200"><code>
92
+ {{- requestlog.exception.traceback -}}
93
+ </code></pre>
94
+ </div>
95
+ {% endif %}
96
+
97
+ {% if request.querydict %}
98
+ <dl class="px-2 pt-1 pb-2 mt-4 rounded bg-zinc-800">
99
+ {% for key, value in request.querydict.items() %}
100
+ <dt class="mt-2 text-xs text-zinc-400">{{ key }}</dt>
101
+ <dd class="text-xs break-all text-zinc-300"><code>{{ value or "(Empty)" }}</code></dd>
102
+ {% endfor %}
103
+ </dl>
104
+ {% endif %}
105
+
106
+ <dl class="mt-4">
107
+ {% for key, value in request.headers.items() %}
108
+ <dt class="mt-2 text-xs text-zinc-400">{{ key }}</dt>
109
+ <dd class="text-xs break-all text-zinc-300"><code>{{ value or "(Empty)" }}</code></dd>
110
+ {% endfor %}
111
+ </dl>
112
+ {% if "body_json" in request %}
113
+ <pre class="p-2 mt-4 overflow-y-auto text-xs rounded bg-zinc-800 text-zinc-200"><code>{{ request.body_json }}</code></pre>
114
+ {% else %}
115
+ <pre class="p-2 mt-4 overflow-y-auto text-xs rounded bg-zinc-800 text-zinc-200 max-h-96"><code>{{ request.body or "(Empty)" }}</code></pre>
116
+ {% endif %}
117
+ {% endwith %}
118
+
119
+ {% with response=requestlog.response %}
120
+ <h3 class="mt-10 text-lg font-medium">HTTP <span class="font-mono">{{ response.status_code }}</span></h3>
121
+ <dl class="mt-4">
122
+ {% for key, value in response.headers.items() %}
123
+ <dt class="mt-2 text-xs text-zinc-400">{{ key }}</dt>
124
+ <dd class="text-xs break-all text-zinc-300"><code>{{ value or "(Empty)" }}</code></dd>
125
+ {% endfor %}
126
+ </dl>
127
+ <pre class="p-2 mt-4 overflow-y-auto text-xs rounded bg-zinc-800 text-zinc-200 max-h-96"><code>{{ response.content or "(Empty)" }}</code></pre>
128
+ {% endwith %}
129
+ </div>
130
+ {% endif %}
131
+ </div>
132
+
133
+ </body>
134
+ </html>
plain/dev/urls.py ADDED
@@ -0,0 +1,9 @@
1
+ from plain.urls import path
2
+
3
+ from . import views
4
+
5
+ default_namespace = "dev"
6
+
7
+ urlpatterns = [
8
+ path("", views.RequestsView, name="requests"),
9
+ ]
plain/dev/utils.py ADDED
@@ -0,0 +1,14 @@
1
+ import importlib
2
+ from pathlib import Path
3
+
4
+
5
+ def plainpackage_installed(name: str) -> bool:
6
+ try:
7
+ importlib.import_module(f"plain.{name}")
8
+ return True
9
+ except ImportError:
10
+ return False
11
+
12
+
13
+ def has_pyproject_toml(target_path):
14
+ return (Path(target_path) / "pyproject.toml").exists()
plain/dev/views.py ADDED
@@ -0,0 +1,37 @@
1
+ from plain.http import ResponseRedirect
2
+ from plain.views import TemplateView
3
+
4
+ from .requests import RequestLog
5
+
6
+
7
+ class RequestsView(TemplateView):
8
+ template_name = "dev/requests.html"
9
+
10
+ def get_template_context(self):
11
+ ctx = super().get_template_context()
12
+ requestlogs = RequestLog.load_json_logs()
13
+
14
+ if self.request.GET.get("log"):
15
+ try:
16
+ requestlog = [
17
+ x for x in requestlogs if x.get("name") == self.request.GET["log"]
18
+ ][0]
19
+ except IndexError:
20
+ requestlog = None
21
+ elif requestlogs:
22
+ requestlog = requestlogs[0]
23
+ else:
24
+ requestlog = None
25
+
26
+ ctx["requestlogs"] = requestlogs
27
+ ctx["requestlog"] = requestlog
28
+
29
+ return ctx
30
+
31
+ def post(self):
32
+ if self.request.POST.get("action") == "clear":
33
+ RequestLog.clear()
34
+ return ResponseRedirect(self.request.path)
35
+ else:
36
+ RequestLog.replay_request(self.request.POST["log"])
37
+ return ResponseRedirect(".")
@@ -0,0 +1,28 @@
1
+ BSD 3-Clause License
2
+
3
+ Copyright (c) 2023, Dropseed, LLC
4
+
5
+ Redistribution and use in source and binary forms, with or without
6
+ modification, are permitted provided that the following conditions are met:
7
+
8
+ 1. Redistributions of source code must retain the above copyright notice, this
9
+ list of conditions and the following disclaimer.
10
+
11
+ 2. Redistributions in binary form must reproduce the above copyright notice,
12
+ this list of conditions and the following disclaimer in the documentation
13
+ and/or other materials provided with the distribution.
14
+
15
+ 3. Neither the name of the copyright holder nor the names of its
16
+ contributors may be used to endorse or promote products derived from
17
+ this software without specific prior written permission.
18
+
19
+ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
20
+ AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
21
+ IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
22
+ DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
23
+ FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
24
+ DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
25
+ SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
26
+ CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
27
+ OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
28
+ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
@@ -0,0 +1,178 @@
1
+ Metadata-Version: 2.1
2
+ Name: plain.dev
3
+ Version: 0.0.0
4
+ Summary: Work library for Plain
5
+ Home-page: https://www.plainpackages.com/
6
+ License: MIT
7
+ Author: Dave Gaeddert
8
+ Author-email: dave.gaeddert@dropseed.dev
9
+ Requires-Python: >=3.8,<4.0
10
+ Classifier: License :: OSI Approved :: MIT License
11
+ Classifier: Programming Language :: Python :: 3
12
+ Classifier: Programming Language :: Python :: 3.8
13
+ Classifier: Programming Language :: Python :: 3.9
14
+ Classifier: Programming Language :: Python :: 3.10
15
+ Classifier: Programming Language :: Python :: 3.11
16
+ Classifier: Programming Language :: Python :: 3.12
17
+ Requires-Dist: click (>=8.0.0)
18
+ Requires-Dist: debugpy (>=1.6.3,<2.0.0)
19
+ Requires-Dist: gunicorn (>20)
20
+ Requires-Dist: honcho (>=1.1.0,<2.0.0)
21
+ Requires-Dist: psycopg2-binary (>=2.9.3,<3.0.0)
22
+ Requires-Dist: requests (>=2.0.0)
23
+ Requires-Dist: tomli (>=2.0.1,<3.0.0) ; python_version < "3.11"
24
+ Project-URL: Documentation, https://www.plainpackages.com/docs/
25
+ Project-URL: Repository, https://github.com/plainpackages/plain-dev
26
+ Description-Content-Type: text/markdown
27
+
28
+ <!-- This file is compiled from plain-dev/plain/dev/README.md. Do not edit this file directly. -->
29
+
30
+ # plain-dev
31
+
32
+ A single command to run everything you need for local Plain development.
33
+
34
+ ![Plain work command example](https://user-images.githubusercontent.com/649496/176533533-cfd44dc5-afe5-42af-8b5d-33a9fa23f8d9.gif)
35
+
36
+ The `plain dev` command runs a combination of local commands + a Docker container for your database.
37
+
38
+ The following processes will run simultaneously (some will only run if they are detected as available):
39
+
40
+ <!-- - [`manage.py runserver` (and migrations)](#runserver)
41
+ - [`plain-models start --logs`](#plain-models)
42
+ - [`plain-tailwind compile --watch`](#plain-tailwind)
43
+ - [`npm run watch`](#package-json)
44
+ - [`stripe listen --forward-to`](#stripe)
45
+ - [`ngrok http --subdomain`](#ngrok)
46
+
47
+ It also comes with [debugging](#debugging) tools to make local debugging easier with VS Code. -->
48
+
49
+ ## Installation
50
+
51
+ ```sh
52
+ pip install plain-dev
53
+ ```
54
+
55
+ If you have `plain-models` installed (i.e. you're using a database),
56
+ then add `DATABASE_URL` to your `.env` file.
57
+
58
+ ```sh
59
+ DATABASE_URL=postgres://postgres:postgres@localhost:54321/postgres
60
+ ```
61
+
62
+ ```toml
63
+ # pyproject.toml
64
+ [tool.plain.dev.services]
65
+ postgres = {cmd = "docker run --name app-postgres --rm -p 54321:5432 -v $(pwd)/.plain/dev/pgdata:/var/lib/postgresql/data -e POSTGRES_PASSWORD=postgres postgres:15 postgres"}
66
+ ```
67
+
68
+ ```sh
69
+ plain dev
70
+ ```
71
+
72
+ ## `plain dev`
73
+
74
+ ### Default processes
75
+
76
+ - plain preflight
77
+ - gunicorn
78
+ - migrations
79
+ - tailwind
80
+
81
+ ### Custom processes
82
+
83
+ - package.json "dev" script
84
+ - pyproject.toml `tool.plain.dev.run = {command = "..."}`
85
+
86
+ ### GitHub Codespaces
87
+
88
+ The `BASE_URL` setting is automatically set to the Codespace URL.
89
+
90
+ TODO
91
+
92
+ ## `plain dev db`
93
+
94
+ Only supports Postgres currently.
95
+
96
+ - snapshot
97
+ - import
98
+ - export
99
+
100
+
101
+ ## Development processes
102
+
103
+ ### Gunicorn
104
+
105
+ The key process here is still `manage.py runserver`.
106
+ But, before that runs, it will also wait for the database to be available and run `manage.py migrate`.
107
+
108
+ ### plain-models
109
+
110
+ If [`plain-models`](https://github.com/plainpackages/plain-models) is installed, it will automatically start and show the logs of the running database container.
111
+
112
+ ### plain-tailwind
113
+
114
+ If [`plain-tailwind`](https://github.com/plainpackages/plain-tailwind) is installed, it will automatically run the Tailwind `compile --watch` process.
115
+
116
+ ## Debugging
117
+
118
+ [View on YouTube →](https://www.youtube.com/watch?v=pG0KaJSVyBw)
119
+
120
+ Since `plain work` runs multiple processes at once, the regular [pdb](https://docs.python.org/3/library/pdb.html) debuggers can be hard to use.
121
+ Instead, we include [microsoft/debugpy](https://github.com/microsoft/debugpy) and an `attach` function to make it even easier to use VS Code's debugger.
122
+
123
+ First, import and run the `debug.attach()` function:
124
+
125
+ ```python
126
+ class HomeView(TemplateView):
127
+ template_name = "home.html"
128
+
129
+ def get_context(self, **kwargs):
130
+ context = super().get_context(**kwargs)
131
+
132
+ # Make sure the debugger is attached (will need to be if runserver reloads)
133
+ from plain.work import debug; debug.attach()
134
+
135
+ # Add a breakpoint (or use the gutter in VSCode to add one)
136
+ breakpoint()
137
+
138
+ return context
139
+ ```
140
+
141
+ When you load the page, you'll see "Waiting for debugger to attach...".
142
+
143
+ Add a new VS Code debug configuration (using localhost and port 5768) by saving this to `.vscode/launch.json` or using the GUI:
144
+
145
+ ```json
146
+ // .vscode/launch.json
147
+ {
148
+ // Use IntelliSense to learn about possible attributes.
149
+ // Hover to view descriptions of existing attributes.
150
+ // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
151
+ "version": "0.2.0",
152
+ "configurations": [
153
+ {
154
+ "name": "Plain: Attach to Django",
155
+ "type": "python",
156
+ "request": "attach",
157
+ "connect": {
158
+ "host": "localhost",
159
+ "port": 5678
160
+ },
161
+ "pathMappings": [
162
+ {
163
+ "localRoot": "${workspaceFolder}",
164
+ "remoteRoot": "."
165
+ }
166
+ ],
167
+ "justMyCode": true,
168
+ "django": true
169
+ }
170
+ ]
171
+ }
172
+ ```
173
+
174
+ Then in the "Run and Debug" tab, you can click the green arrow next to "Plain: Attach to Django" to start the debugger.
175
+
176
+ In your terminal is should tell you it was attached, and when you hit a breakpoint you'll see the debugger information in VS Code.
177
+ If Django's runserver reloads, you'll be prompted to reattach by clicking the green arrow again.
178
+
@@ -0,0 +1,25 @@
1
+ plain/dev/README.md,sha256=WUuS441Nn0hiolb105wXPiip4l-WN_K_FU7bvwrPg58,4393
2
+ plain/dev/__init__.py,sha256=C1JrkNE5XX2DLgBXXLAV_UyhofwVd0ZPL59fPUMbOKo,139
3
+ plain/dev/cli.py,sha256=hOQuAAGsMZ2tkFlDNfYl393Kn-QLhN4ruF1N5BZBzDQ,4859
4
+ plain/dev/config.py,sha256=h6o5YZtJhg-cFIWoqIDWuMCC5T09cxEsBaa3BP4Nii0,632
5
+ plain/dev/contribute/__init__.py,sha256=9ByBOIdM8DebChjNz-RH2atdz4vWe8somlwNEsbhwh4,40
6
+ plain/dev/contribute/cli.py,sha256=SKy2UG5cD4M8E2gN48MJxH-PNTcxt53P3N0Xfb5nZWY,3529
7
+ plain/dev/db/__init__.py,sha256=9ByBOIdM8DebChjNz-RH2atdz4vWe8somlwNEsbhwh4,40
8
+ plain/dev/db/cli.py,sha256=058HjRKLGz-FxauQEpwsPoh_LCiy-_NEIpRZl9W1ZKM,2855
9
+ plain/dev/db/container.py,sha256=RlPJU_CCMKA-zN8Kp0sYAu3jabOizxYAj8fSCsjCf60,5147
10
+ plain/dev/debug.py,sha256=fIrecLfAK_lXSDyn3WmYikzZSse3KY47xcVVbZqJGhk,294
11
+ plain/dev/default_settings.py,sha256=uXWYORWP_aRDwXIFXdu5kHyiBFUZzARIJdhPeFaX35c,75
12
+ plain/dev/pid.py,sha256=gRMBf7aGndrra1TnmKtPghTijnd0i0Xeo63mzfPWp7M,436
13
+ plain/dev/precommit/__init__.py,sha256=9ByBOIdM8DebChjNz-RH2atdz4vWe8somlwNEsbhwh4,40
14
+ plain/dev/precommit/cli.py,sha256=mvP5pxuVZdmOKXgDpJkoShx-28Py3XdS8jKKzsWLmrc,3554
15
+ plain/dev/requests.py,sha256=0HyCH7iZ32ne94ypMdE96z5iYb_Qbd705WItVik1SyA,6839
16
+ plain/dev/services.py,sha256=jviyiNawYVmZGeZ2wjeX8bR7n7-43tcY2bpMD20acbA,2120
17
+ plain/dev/templates/dev/requests.html,sha256=kQKJZq5L77juuL_t8UjcAehEU61U4RXNnKaAET-wAm8,7627
18
+ plain/dev/urls.py,sha256=b4NL2I6Ok-t7nTPjRnKoz_LQRttE3_mp8l2NlmeYQ9I,146
19
+ plain/dev/utils.py,sha256=mL3C3l3GsKmtI6eF4sRjv7w9n7Y9lLVqJulj81JrqWw,312
20
+ plain/dev/views.py,sha256=r2Ivk7OXytpRhXq4DZpsb7FXNP9vzmEE3D5kLajYG4w,1073
21
+ plain_dev-0.0.0.dist-info/LICENSE,sha256=cvKM3OlqHx3ijD6e34zsSUkPvzl-ya3Dd63A6EHL94U,1500
22
+ plain_dev-0.0.0.dist-info/METADATA,sha256=7AIMJ0TwE7T26QgyLTL1Cn_WXuOuA2ECOyOO0V7HiVU,5536
23
+ plain_dev-0.0.0.dist-info/WHEEL,sha256=FMvqSimYX_P7y0a7UY-_Mc83r5zkBZsCYPm7Lr0Bsq4,88
24
+ plain_dev-0.0.0.dist-info/entry_points.txt,sha256=rBo-S4THn07f55UwHBuUhIbDhlUq3EzTOD8mIb5fGQg,99
25
+ plain_dev-0.0.0.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: poetry-core 1.8.1
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,5 @@
1
+ [plain.cli]
2
+ contrib=plain.dev.contribute:cli
3
+ dev=plain.dev:cli
4
+ pre-commit=plain.dev.precommit:cli
5
+