hyperclass 0.0.2__tar.gz → 0.0.3__tar.gz

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.
Files changed (26) hide show
  1. hyperclass-0.0.3/PKG-INFO +348 -0
  2. hyperclass-0.0.3/README.md +318 -0
  3. {hyperclass-0.0.2 → hyperclass-0.0.3}/hyperclass/__init__.py +1 -1
  4. hyperclass-0.0.3/hyperclass/__main__.py +37 -0
  5. {hyperclass-0.0.2 → hyperclass-0.0.3}/hyperclass/wsgi.py +90 -6
  6. hyperclass-0.0.3/hyperclass.egg-info/PKG-INFO +348 -0
  7. {hyperclass-0.0.2 → hyperclass-0.0.3}/hyperclass.egg-info/SOURCES.txt +2 -0
  8. {hyperclass-0.0.2 → hyperclass-0.0.3}/pyproject.toml +1 -1
  9. {hyperclass-0.0.2 → hyperclass-0.0.3}/tests/test_bookmarks.py +64 -3
  10. hyperclass-0.0.3/tests/test_cli.py +25 -0
  11. hyperclass-0.0.3/tests/test_package.py +5 -0
  12. {hyperclass-0.0.2 → hyperclass-0.0.3}/tests/test_wsgi.py +66 -0
  13. hyperclass-0.0.2/PKG-INFO +0 -283
  14. hyperclass-0.0.2/README.md +0 -253
  15. hyperclass-0.0.2/hyperclass.egg-info/PKG-INFO +0 -283
  16. hyperclass-0.0.2/tests/test_package.py +0 -5
  17. {hyperclass-0.0.2 → hyperclass-0.0.3}/LICENSE +0 -0
  18. {hyperclass-0.0.2 → hyperclass-0.0.3}/hyperclass/css.py +0 -0
  19. {hyperclass-0.0.2 → hyperclass-0.0.3}/hyperclass/html.py +0 -0
  20. {hyperclass-0.0.2 → hyperclass-0.0.3}/hyperclass/htmx.py +0 -0
  21. {hyperclass-0.0.2 → hyperclass-0.0.3}/hyperclass.egg-info/dependency_links.txt +0 -0
  22. {hyperclass-0.0.2 → hyperclass-0.0.3}/hyperclass.egg-info/requires.txt +0 -0
  23. {hyperclass-0.0.2 → hyperclass-0.0.3}/hyperclass.egg-info/top_level.txt +0 -0
  24. {hyperclass-0.0.2 → hyperclass-0.0.3}/setup.cfg +0 -0
  25. {hyperclass-0.0.2 → hyperclass-0.0.3}/tests/test_html.py +0 -0
  26. {hyperclass-0.0.2 → hyperclass-0.0.3}/tests/test_htmx.py +0 -0
@@ -0,0 +1,348 @@
1
+ Metadata-Version: 2.4
2
+ Name: hyperclass
3
+ Version: 0.0.3
4
+ Summary: Build interactive web applications as Python class hierarchies.
5
+ Author: Grant Jenks
6
+ License-Expression: Apache-2.0
7
+ Project-URL: Homepage, https://github.com/grantjenks/python-hyperclass
8
+ Project-URL: Issue Tracker, https://github.com/grantjenks/python-hyperclass/issues
9
+ Project-URL: Source Code, https://github.com/grantjenks/python-hyperclass
10
+ Keywords: html,htmx,css,wsgi,web
11
+ Classifier: Development Status :: 2 - Pre-Alpha
12
+ Classifier: Intended Audience :: Developers
13
+ Classifier: Programming Language :: Python
14
+ Classifier: Programming Language :: Python :: 3
15
+ Classifier: Programming Language :: Python :: 3 :: Only
16
+ Classifier: Programming Language :: Python :: 3.10
17
+ Classifier: Programming Language :: Python :: 3.11
18
+ Classifier: Programming Language :: Python :: 3.12
19
+ Classifier: Programming Language :: Python :: 3.13
20
+ Classifier: Programming Language :: Python :: 3.14
21
+ Classifier: Topic :: Internet :: WWW/HTTP
22
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
23
+ Requires-Python: >=3.10
24
+ Description-Content-Type: text/markdown
25
+ License-File: LICENSE
26
+ Provides-Extra: dev
27
+ Requires-Dist: pytest>=8.0; extra == "dev"
28
+ Requires-Dist: nox>=2024.4.15; extra == "dev"
29
+ Dynamic: license-file
30
+
31
+ # Hyperclass
32
+
33
+ > **Subclass the web.**
34
+
35
+ Hyperclass is a small experiment in building interactive web applications as
36
+ Python class hierarchies.
37
+
38
+ HTML elements are Python base classes. Python subclasses become CSS classes.
39
+ Styles follow inheritance. Routes are methods. Decorated handlers are URLs.
40
+ Interaction is ordinary HTTP over WSGI, with htmx 4 in the browser.
41
+
42
+ ~~~console
43
+ pip install hyperclass
44
+ ~~~
45
+
46
+ ## Sixty-second tour
47
+
48
+ ~~~python
49
+ from dataclasses import dataclass
50
+
51
+ from hyperclass import (
52
+ App, button, css, div, form, get, grid, hx, input, outer_morph,
53
+ post, rem,
54
+ )
55
+
56
+
57
+ class card(div):
58
+ style = css(
59
+ display=grid,
60
+ gap=1 * rem,
61
+ padding=1.25 * rem,
62
+ border="1px solid #ddd",
63
+ border_radius=.75 * rem,
64
+ )
65
+
66
+
67
+ class guest_form(form):
68
+ style = css(display=grid, gap=.75 * rem)
69
+
70
+ def content(self):
71
+ yield input(name="name", placeholder="Your name", required=True)
72
+ yield button("Say hello", type="submit")
73
+
74
+
75
+ @dataclass
76
+ class Guest:
77
+ name: str
78
+
79
+
80
+ class guestbook(App):
81
+ @get("/")
82
+ def index(self, request):
83
+ return card(
84
+ "Who are you?",
85
+ guest_form(
86
+ hx=hx.post(
87
+ guestbook.create,
88
+ target=card,
89
+ swap=outer_morph,
90
+ )
91
+ ),
92
+ )
93
+
94
+ @post("/guests")
95
+ def create(self, request, form: Guest):
96
+ return card(f"Hello, {form.name}!")
97
+
98
+
99
+ app = guestbook(title="Guestbook")
100
+ ~~~
101
+
102
+ Run it:
103
+
104
+ ~~~console
105
+ python -m hyperclass myapp:app
106
+ ~~~
107
+
108
+ Then open <http://127.0.0.1:8000>. There is no JavaScript build, template
109
+ language, ASGI dependency, or CSS file hidden elsewhere.
110
+
111
+ ## HTML classes are Python classes
112
+
113
+ Every built-in element can be subclassed:
114
+
115
+ ~~~python
116
+ from hyperclass import css, div, grid, orange, rem
117
+
118
+
119
+ class card(div):
120
+ style = css(display=grid, gap=1 * rem, padding=1.25 * rem)
121
+
122
+
123
+ class warning_card(card):
124
+ style = css(
125
+ border_color=orange,
126
+ background=orange.fade(0.08),
127
+ )
128
+ ~~~
129
+
130
+ Calling:
131
+
132
+ ~~~python
133
+ warning_card("Something happened")
134
+ ~~~
135
+
136
+ produces ordinary, inspectable HTML:
137
+
138
+ ~~~html
139
+ <div class="card warning-card">Something happened</div>
140
+ ~~~
141
+
142
+ The first built-in HTML ancestor determines the tag. Each semantic subclass
143
+ contributes a CSS class. `snake_case` becomes `kebab-case`.
144
+
145
+ Multiple inheritance composes behavior and styles:
146
+
147
+ ~~~python
148
+ class compact:
149
+ style = css(padding=.5 * rem)
150
+
151
+
152
+ class clickable:
153
+ style = css(cursor="pointer")
154
+
155
+
156
+ class result_card(card, compact, clickable):
157
+ pass
158
+ ~~~
159
+
160
+ ~~~html
161
+ <div class="card compact clickable result-card"></div>
162
+ ~~~
163
+
164
+ Components use normal Python state and methods:
165
+
166
+ ~~~python
167
+ from hyperclass import strong
168
+
169
+
170
+ class greeting(card):
171
+ def __init__(self, name):
172
+ self.name = name
173
+
174
+ def content(self):
175
+ yield "Hello, "
176
+ yield strong(self.name)
177
+ ~~~
178
+
179
+ Text and attribute values are escaped by default. `markup(...)` is the explicit
180
+ escape hatch for trusted HTML.
181
+
182
+ ## CSS is Python too
183
+
184
+ Base styles, pseudo-states, and media rules live on the component:
185
+
186
+ ~~~python
187
+ from hyperclass import button, css, media, rem
188
+
189
+
190
+ class primary_button(button):
191
+ style = css(
192
+ padding=".7rem 1rem",
193
+ background="#6d28d9",
194
+ color="white",
195
+ border=0,
196
+ border_radius=.5 * rem,
197
+ )
198
+ hover = css(background="#5b21b6")
199
+ focus_visible = css(outline="3px solid #c4b5fd")
200
+ narrow = media(max_width=40 * rem, width="100%")
201
+ ~~~
202
+
203
+ Pages collect only the rules used by their element tree. Python inheritance and
204
+ the CSS cascade cooperate instead of imitating one another.
205
+
206
+ ## Classes and IDs are selectors
207
+
208
+ Classes can be used directly anywhere a selector is expected:
209
+
210
+ ~~~python
211
+ hx.get(search, target=result_card)
212
+ closest(card)
213
+ ~~~
214
+
215
+ IDs are lazy, interned Python objects:
216
+
217
+ ~~~python
218
+ from hyperclass import id, span
219
+
220
+ span("3 unread", id=id.unread_count)
221
+ hx.get(count, target=id.unread_count)
222
+
223
+ assert id.unread_count is id.unread_count
224
+ ~~~
225
+
226
+ As an HTML attribute, `id.unread_count` renders as `unread-count`. As a
227
+ selector, it renders as `#unread-count`.
228
+
229
+ ## Routes are references, not strings
230
+
231
+ Application subclasses collect decorated method routes:
232
+
233
+ ~~~python
234
+ from hyperclass import App, get, patch
235
+
236
+
237
+ class bookmarks(App):
238
+ @get("/")
239
+ def index(self, request):
240
+ return bookmark_list(...)
241
+
242
+ @patch("/bookmarks/<int:bookmark_id>")
243
+ def toggle(self, request, bookmark_id):
244
+ return bookmark_card(...)
245
+ ~~~
246
+
247
+ Decorated handlers retain their route metadata:
248
+
249
+ ~~~python
250
+ bookmarks.toggle.url(bookmark_id=42)
251
+ # '/bookmarks/42'
252
+
253
+ hx.patch(
254
+ bookmarks.toggle,
255
+ bookmark_id=42,
256
+ target=bookmark_card,
257
+ )
258
+ ~~~
259
+
260
+ Typed path parameters are converted before the handler runs. Query strings can
261
+ be attached with `.url(query={...})` or the `query=` option on an htmx request.
262
+
263
+ ## Forms bind to dataclasses
264
+
265
+ Annotate a route parameter with a dataclass and Hyperclass builds it from the
266
+ submitted form:
267
+
268
+ ~~~python
269
+ @dataclass
270
+ class NewBookmark:
271
+ url: str
272
+ title: str = ""
273
+
274
+
275
+ class bookmarks(App):
276
+ @post("/bookmarks")
277
+ def create(self, request, form: NewBookmark):
278
+ self.store.add(form.url, form.title)
279
+ return bookmark_list(...)
280
+ ~~~
281
+
282
+ Binding supports strings, integers, floats, booleans, optional values, and
283
+ lists or tuples of those values. Dataclass defaults remain defaults. Invalid or
284
+ missing required values produce a `400 Bad Request`; application validation can
285
+ return a more specific `Response`.
286
+
287
+ The underlying values remain available as `request.form`, `request.query`,
288
+ `.get(...)`, `.getlist(...)`, and `.int(...)` when explicit parsing is clearer.
289
+
290
+ ## WSGI and htmx 4
291
+
292
+ A Hyperclass application is a normal WSGI callable. Use the standard-library
293
+ development server:
294
+
295
+ ~~~console
296
+ python -m hyperclass package.module:app
297
+ python -m hyperclass package.module:app --host 0.0.0.0 --port 9000
298
+ ~~~
299
+
300
+ Production can use any WSGI server. Returning an element from an ordinary
301
+ browser request wraps it in a complete page. Returning the same element to an
302
+ htmx request sends only the fragment to swap.
303
+
304
+ `Page(...)` controls the document explicitly. Pages include a pinned htmx 4
305
+ asset from jsDelivr. htmx 4 `<hx-partial>` responses can update several
306
+ object-selected regions from one request.
307
+
308
+ ## Try the examples
309
+
310
+ Clone the repository and run the persistent SQLite bookmark inbox:
311
+
312
+ ~~~console
313
+ git clone https://github.com/grantjenks/python-hyperclass
314
+ cd python-hyperclass
315
+ python -m hyperclass examples.bookmarks:app
316
+ ~~~
317
+
318
+ The bookmark app adds, searches, filters, edits, marks, and deletes bookmarks.
319
+ Its implementation is Python plus SQLite, WSGI, generated CSS, and htmx. It is
320
+ also a compact integration test for the framework's ideas.
321
+
322
+ For the smallest example:
323
+
324
+ ~~~console
325
+ python -m hyperclass examples.counter:app
326
+ ~~~
327
+
328
+ ## Principles
329
+
330
+ - **Python is the authoring language.** Control flow, composition, inheritance,
331
+ validation, and reuse are ordinary Python.
332
+ - **The browser remains the browser.** Hyperclass emits standard HTML and CSS
333
+ rather than recreating the DOM on the server.
334
+ - **Classes mean classes.** Python inheritance has a visible relationship to
335
+ HTML classes and the CSS cascade.
336
+ - **HTTP is the state boundary.** There is no hydration protocol or hidden
337
+ client component lifecycle.
338
+ - **Output should be boring.** Generated markup stays readable in View Source
339
+ and DevTools.
340
+ - **Small is a feature.** Prefer the standard library, WSGI, and a pinned htmx
341
+ asset over a framework stack.
342
+
343
+ ## Status
344
+
345
+ Hyperclass is deliberately pre-alpha: useful enough to build small applications
346
+ and young enough for its API to change. Python 3.10 through 3.14 are tested.
347
+
348
+ Apache-2.0 licensed.
@@ -0,0 +1,318 @@
1
+ # Hyperclass
2
+
3
+ > **Subclass the web.**
4
+
5
+ Hyperclass is a small experiment in building interactive web applications as
6
+ Python class hierarchies.
7
+
8
+ HTML elements are Python base classes. Python subclasses become CSS classes.
9
+ Styles follow inheritance. Routes are methods. Decorated handlers are URLs.
10
+ Interaction is ordinary HTTP over WSGI, with htmx 4 in the browser.
11
+
12
+ ~~~console
13
+ pip install hyperclass
14
+ ~~~
15
+
16
+ ## Sixty-second tour
17
+
18
+ ~~~python
19
+ from dataclasses import dataclass
20
+
21
+ from hyperclass import (
22
+ App, button, css, div, form, get, grid, hx, input, outer_morph,
23
+ post, rem,
24
+ )
25
+
26
+
27
+ class card(div):
28
+ style = css(
29
+ display=grid,
30
+ gap=1 * rem,
31
+ padding=1.25 * rem,
32
+ border="1px solid #ddd",
33
+ border_radius=.75 * rem,
34
+ )
35
+
36
+
37
+ class guest_form(form):
38
+ style = css(display=grid, gap=.75 * rem)
39
+
40
+ def content(self):
41
+ yield input(name="name", placeholder="Your name", required=True)
42
+ yield button("Say hello", type="submit")
43
+
44
+
45
+ @dataclass
46
+ class Guest:
47
+ name: str
48
+
49
+
50
+ class guestbook(App):
51
+ @get("/")
52
+ def index(self, request):
53
+ return card(
54
+ "Who are you?",
55
+ guest_form(
56
+ hx=hx.post(
57
+ guestbook.create,
58
+ target=card,
59
+ swap=outer_morph,
60
+ )
61
+ ),
62
+ )
63
+
64
+ @post("/guests")
65
+ def create(self, request, form: Guest):
66
+ return card(f"Hello, {form.name}!")
67
+
68
+
69
+ app = guestbook(title="Guestbook")
70
+ ~~~
71
+
72
+ Run it:
73
+
74
+ ~~~console
75
+ python -m hyperclass myapp:app
76
+ ~~~
77
+
78
+ Then open <http://127.0.0.1:8000>. There is no JavaScript build, template
79
+ language, ASGI dependency, or CSS file hidden elsewhere.
80
+
81
+ ## HTML classes are Python classes
82
+
83
+ Every built-in element can be subclassed:
84
+
85
+ ~~~python
86
+ from hyperclass import css, div, grid, orange, rem
87
+
88
+
89
+ class card(div):
90
+ style = css(display=grid, gap=1 * rem, padding=1.25 * rem)
91
+
92
+
93
+ class warning_card(card):
94
+ style = css(
95
+ border_color=orange,
96
+ background=orange.fade(0.08),
97
+ )
98
+ ~~~
99
+
100
+ Calling:
101
+
102
+ ~~~python
103
+ warning_card("Something happened")
104
+ ~~~
105
+
106
+ produces ordinary, inspectable HTML:
107
+
108
+ ~~~html
109
+ <div class="card warning-card">Something happened</div>
110
+ ~~~
111
+
112
+ The first built-in HTML ancestor determines the tag. Each semantic subclass
113
+ contributes a CSS class. `snake_case` becomes `kebab-case`.
114
+
115
+ Multiple inheritance composes behavior and styles:
116
+
117
+ ~~~python
118
+ class compact:
119
+ style = css(padding=.5 * rem)
120
+
121
+
122
+ class clickable:
123
+ style = css(cursor="pointer")
124
+
125
+
126
+ class result_card(card, compact, clickable):
127
+ pass
128
+ ~~~
129
+
130
+ ~~~html
131
+ <div class="card compact clickable result-card"></div>
132
+ ~~~
133
+
134
+ Components use normal Python state and methods:
135
+
136
+ ~~~python
137
+ from hyperclass import strong
138
+
139
+
140
+ class greeting(card):
141
+ def __init__(self, name):
142
+ self.name = name
143
+
144
+ def content(self):
145
+ yield "Hello, "
146
+ yield strong(self.name)
147
+ ~~~
148
+
149
+ Text and attribute values are escaped by default. `markup(...)` is the explicit
150
+ escape hatch for trusted HTML.
151
+
152
+ ## CSS is Python too
153
+
154
+ Base styles, pseudo-states, and media rules live on the component:
155
+
156
+ ~~~python
157
+ from hyperclass import button, css, media, rem
158
+
159
+
160
+ class primary_button(button):
161
+ style = css(
162
+ padding=".7rem 1rem",
163
+ background="#6d28d9",
164
+ color="white",
165
+ border=0,
166
+ border_radius=.5 * rem,
167
+ )
168
+ hover = css(background="#5b21b6")
169
+ focus_visible = css(outline="3px solid #c4b5fd")
170
+ narrow = media(max_width=40 * rem, width="100%")
171
+ ~~~
172
+
173
+ Pages collect only the rules used by their element tree. Python inheritance and
174
+ the CSS cascade cooperate instead of imitating one another.
175
+
176
+ ## Classes and IDs are selectors
177
+
178
+ Classes can be used directly anywhere a selector is expected:
179
+
180
+ ~~~python
181
+ hx.get(search, target=result_card)
182
+ closest(card)
183
+ ~~~
184
+
185
+ IDs are lazy, interned Python objects:
186
+
187
+ ~~~python
188
+ from hyperclass import id, span
189
+
190
+ span("3 unread", id=id.unread_count)
191
+ hx.get(count, target=id.unread_count)
192
+
193
+ assert id.unread_count is id.unread_count
194
+ ~~~
195
+
196
+ As an HTML attribute, `id.unread_count` renders as `unread-count`. As a
197
+ selector, it renders as `#unread-count`.
198
+
199
+ ## Routes are references, not strings
200
+
201
+ Application subclasses collect decorated method routes:
202
+
203
+ ~~~python
204
+ from hyperclass import App, get, patch
205
+
206
+
207
+ class bookmarks(App):
208
+ @get("/")
209
+ def index(self, request):
210
+ return bookmark_list(...)
211
+
212
+ @patch("/bookmarks/<int:bookmark_id>")
213
+ def toggle(self, request, bookmark_id):
214
+ return bookmark_card(...)
215
+ ~~~
216
+
217
+ Decorated handlers retain their route metadata:
218
+
219
+ ~~~python
220
+ bookmarks.toggle.url(bookmark_id=42)
221
+ # '/bookmarks/42'
222
+
223
+ hx.patch(
224
+ bookmarks.toggle,
225
+ bookmark_id=42,
226
+ target=bookmark_card,
227
+ )
228
+ ~~~
229
+
230
+ Typed path parameters are converted before the handler runs. Query strings can
231
+ be attached with `.url(query={...})` or the `query=` option on an htmx request.
232
+
233
+ ## Forms bind to dataclasses
234
+
235
+ Annotate a route parameter with a dataclass and Hyperclass builds it from the
236
+ submitted form:
237
+
238
+ ~~~python
239
+ @dataclass
240
+ class NewBookmark:
241
+ url: str
242
+ title: str = ""
243
+
244
+
245
+ class bookmarks(App):
246
+ @post("/bookmarks")
247
+ def create(self, request, form: NewBookmark):
248
+ self.store.add(form.url, form.title)
249
+ return bookmark_list(...)
250
+ ~~~
251
+
252
+ Binding supports strings, integers, floats, booleans, optional values, and
253
+ lists or tuples of those values. Dataclass defaults remain defaults. Invalid or
254
+ missing required values produce a `400 Bad Request`; application validation can
255
+ return a more specific `Response`.
256
+
257
+ The underlying values remain available as `request.form`, `request.query`,
258
+ `.get(...)`, `.getlist(...)`, and `.int(...)` when explicit parsing is clearer.
259
+
260
+ ## WSGI and htmx 4
261
+
262
+ A Hyperclass application is a normal WSGI callable. Use the standard-library
263
+ development server:
264
+
265
+ ~~~console
266
+ python -m hyperclass package.module:app
267
+ python -m hyperclass package.module:app --host 0.0.0.0 --port 9000
268
+ ~~~
269
+
270
+ Production can use any WSGI server. Returning an element from an ordinary
271
+ browser request wraps it in a complete page. Returning the same element to an
272
+ htmx request sends only the fragment to swap.
273
+
274
+ `Page(...)` controls the document explicitly. Pages include a pinned htmx 4
275
+ asset from jsDelivr. htmx 4 `<hx-partial>` responses can update several
276
+ object-selected regions from one request.
277
+
278
+ ## Try the examples
279
+
280
+ Clone the repository and run the persistent SQLite bookmark inbox:
281
+
282
+ ~~~console
283
+ git clone https://github.com/grantjenks/python-hyperclass
284
+ cd python-hyperclass
285
+ python -m hyperclass examples.bookmarks:app
286
+ ~~~
287
+
288
+ The bookmark app adds, searches, filters, edits, marks, and deletes bookmarks.
289
+ Its implementation is Python plus SQLite, WSGI, generated CSS, and htmx. It is
290
+ also a compact integration test for the framework's ideas.
291
+
292
+ For the smallest example:
293
+
294
+ ~~~console
295
+ python -m hyperclass examples.counter:app
296
+ ~~~
297
+
298
+ ## Principles
299
+
300
+ - **Python is the authoring language.** Control flow, composition, inheritance,
301
+ validation, and reuse are ordinary Python.
302
+ - **The browser remains the browser.** Hyperclass emits standard HTML and CSS
303
+ rather than recreating the DOM on the server.
304
+ - **Classes mean classes.** Python inheritance has a visible relationship to
305
+ HTML classes and the CSS cascade.
306
+ - **HTTP is the state boundary.** There is no hydration protocol or hidden
307
+ client component lifecycle.
308
+ - **Output should be boring.** Generated markup stays readable in View Source
309
+ and DevTools.
310
+ - **Small is a feature.** Prefer the standard library, WSGI, and a pinned htmx
311
+ asset over a framework stack.
312
+
313
+ ## Status
314
+
315
+ Hyperclass is deliberately pre-alpha: useful enough to build small applications
316
+ and young enough for its API to change. Python 3.10 through 3.14 are tested.
317
+
318
+ Apache-2.0 licensed.
@@ -62,7 +62,7 @@ from .wsgi import (
62
62
  )
63
63
  from .wsgi import delete as delete_route
64
64
 
65
- __version__ = "0.0.2"
65
+ __version__ = "0.0.3"
66
66
 
67
67
  hidden = "hidden"
68
68
  submit = "submit"
@@ -0,0 +1,37 @@
1
+ """Run a Hyperclass WSGI application."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import argparse
6
+ from importlib import import_module
7
+ from typing import Any
8
+
9
+
10
+ def load(target: str) -> Any:
11
+ try:
12
+ module_name, attribute_path = target.split(":", 1)
13
+ except ValueError as error:
14
+ raise ValueError("application must be written as module:attribute") from error
15
+ value: Any = import_module(module_name)
16
+ for name in attribute_path.split("."):
17
+ value = getattr(value, name)
18
+ return value
19
+
20
+
21
+ def main(argv: list[str] | None = None) -> None:
22
+ parser = argparse.ArgumentParser(prog="python -m hyperclass")
23
+ parser.add_argument("application", help="WSGI application as module:attribute")
24
+ parser.add_argument("--host", default="127.0.0.1")
25
+ parser.add_argument("--port", type=int, default=8000)
26
+ options = parser.parse_args(argv)
27
+ app = load(options.application)
28
+ try:
29
+ run = app.run
30
+ except AttributeError as error:
31
+ parser.error(f"{options.application} is not a Hyperclass application")
32
+ raise AssertionError from error
33
+ run(host=options.host, port=options.port)
34
+
35
+
36
+ if __name__ == "__main__":
37
+ main()