hyperclass 0.0.1__tar.gz → 0.0.2__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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: hyperclass
3
- Version: 0.0.1
3
+ Version: 0.0.2
4
4
  Summary: Build interactive web applications as Python class hierarchies.
5
5
  Author: Grant Jenks
6
6
  License-Expression: Apache-2.0
@@ -85,6 +85,10 @@ Hyperclass treats classes and instances differently:
85
85
  - Multiple inheritance composes multiple CSS classes.
86
86
  - Classes are also usable as selectors and htmx targets.
87
87
  - Instances contain attributes, state, and child content.
88
+ - Typed route parameters turn URL segments into handler arguments.
89
+ - htmx 4 partials can update several object-selected regions from one response.
90
+ - `id.some_name` creates an interned Python reference for `some-name`.
91
+ - Decorated handlers are reversible route references; URLs need not be repeated.
88
92
 
89
93
  ~~~python
90
94
  class compact:
@@ -149,24 +153,23 @@ The class `counter` simultaneously represents:
149
153
 
150
154
  ## WSGI and htmx
151
155
 
152
- Routes return elements directly:
156
+ Application subclasses collect decorated method routes:
153
157
 
154
158
  ~~~python
155
- from hyperclass import App
159
+ from hyperclass import App, get, post
156
160
 
157
- app = App()
158
161
 
162
+ class counter_app(App):
163
+ @get("/")
164
+ def index(self, request):
165
+ return counter(0)
159
166
 
160
- @app.get("/")
161
- def index(request):
162
- return counter(0)
163
-
164
-
165
- @app.post("/counter")
166
- def increment(request):
167
- return counter(request.form.int("value") + 1)
167
+ @post("/counter")
168
+ def increment(self, request):
169
+ return counter(request.form.int("value") + 1)
168
170
 
169
171
 
172
+ app = counter_app()
170
173
  if __name__ == "__main__":
171
174
  app.run()
172
175
  ~~~
@@ -174,10 +177,88 @@ if __name__ == "__main__":
174
177
  The application is a normal WSGI callable. The development server can use
175
178
  Python's standard library; production deployment can use any WSGI server.
176
179
 
180
+ Decorated handlers retain their routing metadata, so application code can refer
181
+ to Python rather than repeat URL strings:
182
+
183
+ ~~~python
184
+ hx.patch(
185
+ increment,
186
+ target=closest(counter),
187
+ swap=outer_morph,
188
+ )
189
+ ~~~
190
+
191
+ Typed path parameters are supplied alongside htmx options. Class attributes
192
+ make the handler available to components without a route registry:
193
+
194
+ ~~~python
195
+ hx.patch(
196
+ bookmarks.toggle,
197
+ bookmark_id=bookmark.id,
198
+ target=closest(bookmark_card),
199
+ )
200
+ ~~~
201
+
202
+ The same endpoint exposes `.url(...)` for ordinary links and form actions. IDs
203
+ work similarly: `id.unread_count` renders as `unread-count` in an HTML
204
+ attribute and as `#unread-count` when used as a selector.
205
+
177
206
  htmx supplies browser-to-server interaction without introducing a client-side
178
207
  component runtime. Hyperclass should favor native HTML and CSS for local
179
208
  behavior and use htmx when the server needs to participate.
180
209
 
210
+ ## Pages
211
+
212
+ For ordinary browser requests, returning an element wraps it in a complete page.
213
+ For htmx requests, the same route returns only the fragment to swap. Use
214
+ `Page` when you want to control the document explicitly:
215
+
216
+ ~~~python
217
+ from hyperclass import Page
218
+
219
+ return Page(counter(0), title="Counter")
220
+ ~~~
221
+
222
+ Pages collect the styles used by their element tree and include pinned htmx
223
+ 4.0.0 from its CDN.
224
+
225
+ ## Responsive CSS and states
226
+
227
+ Pseudo-states and media rules are ordinary class attributes:
228
+
229
+ ~~~python
230
+ from hyperclass import css, media, rem
231
+
232
+
233
+ class primary_button(button):
234
+ style = css(background="#6d28d9", color="white")
235
+ hover = css(background="#5b21b6")
236
+ focus_visible = css(outline="3px solid #c4b5fd")
237
+ narrow = media(max_width=40 * rem, width="100%")
238
+ ~~~
239
+
240
+ State names translate underscores to CSS hyphens, and named media rules can use
241
+ Python values for width, orientation, and color-scheme conditions. They follow
242
+ the same inheritance and collection rules as base styles.
243
+
244
+ ## Try the examples
245
+
246
+ The repository includes the counter and a complete SQLite bookmark inbox:
247
+
248
+ ~~~console
249
+ git clone https://github.com/grantjenks/python-hyperclass
250
+ cd python-hyperclass
251
+ python -m examples.bookmarks
252
+ ~~~
253
+
254
+ Then open <http://127.0.0.1:8000>. The bookmark app supports adding, filtering,
255
+ marking read or unread, and deleting bookmarks. Its implementation is still only
256
+ Python and the standard library: semantic subclasses style read and unread
257
+ cards, routes such as `/bookmarks/<int:bookmark_id>` receive typed arguments,
258
+ and htmx 4 `<hx-partial>` responses update a card and the unread count together.
259
+
260
+ Use `python -m examples.counter` for the smaller introduction.
261
+
181
262
  ## Principles
182
263
 
183
264
  - **Python is the authoring language.** Control flow, composition, inheritance,
@@ -195,5 +276,8 @@ behavior and use htmx when the server needs to participate.
195
276
 
196
277
  ## Status
197
278
 
198
- Hyperclass is currently a design exploration. The examples above describe the
199
- intended direction, not a released API.
279
+ Version 0.0.2 is the first working vertical slice: HTML elements, semantic
280
+ subclasses, inherited and responsive CSS, object and ID selectors, htmx
281
+ attributes and partials, pages, request parsing, reversible typed WSGI routing,
282
+ class-based applications, and a persistent example application. The API remains
283
+ deliberately pre-alpha.
@@ -55,6 +55,10 @@ Hyperclass treats classes and instances differently:
55
55
  - Multiple inheritance composes multiple CSS classes.
56
56
  - Classes are also usable as selectors and htmx targets.
57
57
  - Instances contain attributes, state, and child content.
58
+ - Typed route parameters turn URL segments into handler arguments.
59
+ - htmx 4 partials can update several object-selected regions from one response.
60
+ - `id.some_name` creates an interned Python reference for `some-name`.
61
+ - Decorated handlers are reversible route references; URLs need not be repeated.
58
62
 
59
63
  ~~~python
60
64
  class compact:
@@ -119,24 +123,23 @@ The class `counter` simultaneously represents:
119
123
 
120
124
  ## WSGI and htmx
121
125
 
122
- Routes return elements directly:
126
+ Application subclasses collect decorated method routes:
123
127
 
124
128
  ~~~python
125
- from hyperclass import App
129
+ from hyperclass import App, get, post
126
130
 
127
- app = App()
128
131
 
132
+ class counter_app(App):
133
+ @get("/")
134
+ def index(self, request):
135
+ return counter(0)
129
136
 
130
- @app.get("/")
131
- def index(request):
132
- return counter(0)
133
-
134
-
135
- @app.post("/counter")
136
- def increment(request):
137
- return counter(request.form.int("value") + 1)
137
+ @post("/counter")
138
+ def increment(self, request):
139
+ return counter(request.form.int("value") + 1)
138
140
 
139
141
 
142
+ app = counter_app()
140
143
  if __name__ == "__main__":
141
144
  app.run()
142
145
  ~~~
@@ -144,10 +147,88 @@ if __name__ == "__main__":
144
147
  The application is a normal WSGI callable. The development server can use
145
148
  Python's standard library; production deployment can use any WSGI server.
146
149
 
150
+ Decorated handlers retain their routing metadata, so application code can refer
151
+ to Python rather than repeat URL strings:
152
+
153
+ ~~~python
154
+ hx.patch(
155
+ increment,
156
+ target=closest(counter),
157
+ swap=outer_morph,
158
+ )
159
+ ~~~
160
+
161
+ Typed path parameters are supplied alongside htmx options. Class attributes
162
+ make the handler available to components without a route registry:
163
+
164
+ ~~~python
165
+ hx.patch(
166
+ bookmarks.toggle,
167
+ bookmark_id=bookmark.id,
168
+ target=closest(bookmark_card),
169
+ )
170
+ ~~~
171
+
172
+ The same endpoint exposes `.url(...)` for ordinary links and form actions. IDs
173
+ work similarly: `id.unread_count` renders as `unread-count` in an HTML
174
+ attribute and as `#unread-count` when used as a selector.
175
+
147
176
  htmx supplies browser-to-server interaction without introducing a client-side
148
177
  component runtime. Hyperclass should favor native HTML and CSS for local
149
178
  behavior and use htmx when the server needs to participate.
150
179
 
180
+ ## Pages
181
+
182
+ For ordinary browser requests, returning an element wraps it in a complete page.
183
+ For htmx requests, the same route returns only the fragment to swap. Use
184
+ `Page` when you want to control the document explicitly:
185
+
186
+ ~~~python
187
+ from hyperclass import Page
188
+
189
+ return Page(counter(0), title="Counter")
190
+ ~~~
191
+
192
+ Pages collect the styles used by their element tree and include pinned htmx
193
+ 4.0.0 from its CDN.
194
+
195
+ ## Responsive CSS and states
196
+
197
+ Pseudo-states and media rules are ordinary class attributes:
198
+
199
+ ~~~python
200
+ from hyperclass import css, media, rem
201
+
202
+
203
+ class primary_button(button):
204
+ style = css(background="#6d28d9", color="white")
205
+ hover = css(background="#5b21b6")
206
+ focus_visible = css(outline="3px solid #c4b5fd")
207
+ narrow = media(max_width=40 * rem, width="100%")
208
+ ~~~
209
+
210
+ State names translate underscores to CSS hyphens, and named media rules can use
211
+ Python values for width, orientation, and color-scheme conditions. They follow
212
+ the same inheritance and collection rules as base styles.
213
+
214
+ ## Try the examples
215
+
216
+ The repository includes the counter and a complete SQLite bookmark inbox:
217
+
218
+ ~~~console
219
+ git clone https://github.com/grantjenks/python-hyperclass
220
+ cd python-hyperclass
221
+ python -m examples.bookmarks
222
+ ~~~
223
+
224
+ Then open <http://127.0.0.1:8000>. The bookmark app supports adding, filtering,
225
+ marking read or unread, and deleting bookmarks. Its implementation is still only
226
+ Python and the standard library: semantic subclasses style read and unread
227
+ cards, routes such as `/bookmarks/<int:bookmark_id>` receive typed arguments,
228
+ and htmx 4 `<hx-partial>` responses update a card and the unread count together.
229
+
230
+ Use `python -m examples.counter` for the smaller introduction.
231
+
151
232
  ## Principles
152
233
 
153
234
  - **Python is the authoring language.** Control flow, composition, inheritance,
@@ -165,5 +246,8 @@ behavior and use htmx when the server needs to participate.
165
246
 
166
247
  ## Status
167
248
 
168
- Hyperclass is currently a design exploration. The examples above describe the
169
- intended direction, not a released API.
249
+ Version 0.0.2 is the first working vertical slice: HTML elements, semantic
250
+ subclasses, inherited and responsive CSS, object and ID selectors, htmx
251
+ attributes and partials, pages, request parsing, reversible typed WSGI routing,
252
+ class-based applications, and a persistent example application. The API remains
253
+ deliberately pre-alpha.
@@ -0,0 +1,132 @@
1
+ """Build interactive web applications as Python class hierarchies."""
2
+
3
+ from . import html as _html
4
+ from .css import (
5
+ AlphaColor,
6
+ Color,
7
+ Length,
8
+ Media,
9
+ Style,
10
+ Unit,
11
+ block,
12
+ css,
13
+ em,
14
+ flex,
15
+ grid,
16
+ inline,
17
+ media,
18
+ none,
19
+ orange,
20
+ percent,
21
+ pointer,
22
+ px,
23
+ rem,
24
+ vh,
25
+ vw,
26
+ )
27
+ from .htmx import (
28
+ Attributes,
29
+ Htmx,
30
+ Target,
31
+ after,
32
+ append,
33
+ before,
34
+ closest,
35
+ find,
36
+ hx,
37
+ inner_html,
38
+ inner_morph,
39
+ next,
40
+ outer_html,
41
+ outer_morph,
42
+ outer_sync,
43
+ prepend,
44
+ previous,
45
+ )
46
+ from .htmx import (
47
+ delete as delete_swap,
48
+ )
49
+ from .wsgi import (
50
+ App,
51
+ BoundEndpoint,
52
+ Endpoint,
53
+ Request,
54
+ Response,
55
+ Route,
56
+ Values,
57
+ get,
58
+ patch,
59
+ post,
60
+ put,
61
+ route,
62
+ )
63
+ from .wsgi import delete as delete_route
64
+
65
+ __version__ = "0.0.2"
66
+
67
+ hidden = "hidden"
68
+ submit = "submit"
69
+
70
+ for _name in _html.__all__:
71
+ globals()[_name] = getattr(_html, _name)
72
+
73
+ __all__ = [
74
+ "__version__",
75
+ "AlphaColor",
76
+ "App",
77
+ "Attributes",
78
+ "BoundEndpoint",
79
+ "Color",
80
+ "Endpoint",
81
+ "Htmx",
82
+ "Length",
83
+ "Media",
84
+ "Request",
85
+ "Response",
86
+ "Route",
87
+ "Style",
88
+ "Target",
89
+ "Unit",
90
+ "Values",
91
+ "after",
92
+ "append",
93
+ "before",
94
+ "block",
95
+ "closest",
96
+ "css",
97
+ "delete_swap",
98
+ "delete_route",
99
+ "em",
100
+ "find",
101
+ "flex",
102
+ "grid",
103
+ "get",
104
+ "hidden",
105
+ "hx",
106
+ "inline",
107
+ "inner_html",
108
+ "inner_morph",
109
+ "media",
110
+ "next",
111
+ "none",
112
+ "orange",
113
+ "outer_html",
114
+ "outer_morph",
115
+ "outer_sync",
116
+ "percent",
117
+ "pointer",
118
+ "patch",
119
+ "prepend",
120
+ "previous",
121
+ "post",
122
+ "put",
123
+ "px",
124
+ "rem",
125
+ "route",
126
+ "submit",
127
+ "vh",
128
+ "vw",
129
+ *_html.__all__,
130
+ ]
131
+
132
+ del _html, _name
@@ -0,0 +1,158 @@
1
+ """Small Python values for authoring CSS."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from dataclasses import dataclass
6
+ from typing import Any
7
+
8
+
9
+ def _number(value: int | float) -> str:
10
+ if isinstance(value, float) and value.is_integer():
11
+ return str(int(value))
12
+ return str(value)
13
+
14
+
15
+ @dataclass(frozen=True)
16
+ class Unit:
17
+ """A CSS unit which can be multiplied by a number."""
18
+
19
+ suffix: str
20
+
21
+ def __rmul__(self, value: int | float) -> Length:
22
+ return Length(value, self)
23
+
24
+
25
+ @dataclass(frozen=True)
26
+ class Length:
27
+ value: int | float
28
+ unit: Unit
29
+
30
+ def __str__(self) -> str:
31
+ return f"{_number(self.value)}{self.unit.suffix}"
32
+
33
+
34
+ @dataclass(frozen=True)
35
+ class Color:
36
+ name: str
37
+ rgb: tuple[int, int, int] | None = None
38
+
39
+ def __str__(self) -> str:
40
+ return self.name
41
+
42
+ def fade(self, alpha: float) -> AlphaColor:
43
+ if not 0 <= alpha <= 1:
44
+ raise ValueError("alpha must be between 0 and 1")
45
+ if self.rgb is None:
46
+ return AlphaColor(self.name, alpha)
47
+ red, green, blue = self.rgb
48
+ return AlphaColor(f"{red} {green} {blue}", alpha, rgb=True)
49
+
50
+
51
+ @dataclass(frozen=True)
52
+ class AlphaColor:
53
+ color: str
54
+ alpha: float
55
+ rgb: bool = False
56
+
57
+ def __str__(self) -> str:
58
+ if self.rgb:
59
+ return f"rgb({self.color} / {_number(self.alpha)})"
60
+ return f"color-mix(in srgb, {self.color} {self.alpha * 100:g}%, transparent)"
61
+
62
+
63
+ def css_value(value: Any) -> str:
64
+ if isinstance(value, bool):
65
+ return "true" if value else "false"
66
+ return str(value)
67
+
68
+
69
+ class Style:
70
+ """An ordered collection of CSS declarations."""
71
+
72
+ def __init__(self, **declarations: Any):
73
+ self.declarations = tuple(
74
+ (name.replace("_", "-"), value)
75
+ for name, value in declarations.items()
76
+ if value is not None
77
+ )
78
+
79
+ def render(self) -> str:
80
+ return ";".join(
81
+ f"{name}:{css_value(value)}" for name, value in self.declarations
82
+ )
83
+
84
+ def __str__(self) -> str:
85
+ return self.render()
86
+
87
+
88
+ def css(**declarations: Any) -> Style:
89
+ return Style(**declarations)
90
+
91
+
92
+ @dataclass(frozen=True)
93
+ class Media:
94
+ """A stylesheet rule guarded by a CSS media query."""
95
+
96
+ conditions: tuple[tuple[str, Any], ...]
97
+ style: Style
98
+
99
+ def query(self) -> str:
100
+ return " and ".join(
101
+ f"({name.replace('_', '-')}:{css_value(value)})"
102
+ for name, value in self.conditions
103
+ )
104
+
105
+
106
+ def media(
107
+ *,
108
+ min_width: Any = None,
109
+ max_width: Any = None,
110
+ orientation: str | None = None,
111
+ prefers_color_scheme: str | None = None,
112
+ **declarations: Any,
113
+ ) -> Media:
114
+ """Create a media rule with Python-named conditions and declarations."""
115
+
116
+ conditions = tuple(
117
+ (name, value)
118
+ for name, value in (
119
+ ("min_width", min_width),
120
+ ("max_width", max_width),
121
+ ("orientation", orientation),
122
+ ("prefers_color_scheme", prefers_color_scheme),
123
+ )
124
+ if value is not None
125
+ )
126
+ if not conditions:
127
+ raise ValueError("media requires at least one condition")
128
+ return Media(conditions, css(**declarations))
129
+
130
+
131
+ PSEUDO_STATES = {
132
+ "active",
133
+ "checked",
134
+ "disabled",
135
+ "focus",
136
+ "focus_visible",
137
+ "focus_within",
138
+ "hover",
139
+ "invalid",
140
+ "visited",
141
+ }
142
+
143
+
144
+ px = Unit("px")
145
+ rem = Unit("rem")
146
+ em = Unit("em")
147
+ percent = Unit("%")
148
+ vh = Unit("vh")
149
+ vw = Unit("vw")
150
+
151
+ orange = Color("orange", (255, 165, 0))
152
+
153
+ grid = "grid"
154
+ flex = "flex"
155
+ block = "block"
156
+ inline = "inline"
157
+ none = "none"
158
+ pointer = "pointer"