blux-framework 0.1.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.
blux/__init__.py ADDED
@@ -0,0 +1,10 @@
1
+
2
+ from .core.elements import *
3
+ from .core.render import (
4
+ Page,
5
+ action,
6
+ args
7
+ )
8
+ from .core.api import *
9
+ from fastapi.requests import Request
10
+ from starlette.datastructures import FormData
@@ -0,0 +1,24 @@
1
+ import blux
2
+ from pydantic import BaseModel
3
+
4
+ class Payment(BaseModel):
5
+ amount:float
6
+ client_name:str
7
+ client_email: str
8
+
9
+ @blux.api()
10
+ def get_payment(nome:str):
11
+ """ Criar um novo produto"""
12
+
13
+ return {"ok":"ok"}
14
+
15
+
16
+ @blux.api(method="POST")
17
+ def create_produtos(nome:str):
18
+ """ Criar um novo produto"""
19
+
20
+ return {"ok":"ok"}
21
+
22
+
23
+
24
+
@@ -0,0 +1,18 @@
1
+ import blux
2
+
3
+ @blux.api()
4
+ def get_produtos(nome:str):
5
+ """ Criar um novo produto"""
6
+
7
+ return {"ok":"ok"}
8
+
9
+
10
+ @blux.api(method="POST")
11
+ def create_produtos(nome:str):
12
+ """ Criar um novo produto"""
13
+
14
+ return {"ok":"ok"}
15
+
16
+
17
+
18
+
@@ -0,0 +1,47 @@
1
+ .content_app {
2
+ padding: 10px;
3
+ height: 100%;
4
+
5
+ display: flex;
6
+ justify-content: center;
7
+ align-items: center;
8
+ padding: 2rem;
9
+ text-align: center;
10
+
11
+ font-family: Verdana, Geneva, Tahoma, sans-serif;
12
+
13
+ }
14
+
15
+ .title {
16
+ font-weight: bold;
17
+ font-size: 2rem;
18
+ }
19
+
20
+ .text {
21
+ font-size: .8rem;
22
+ opacity: .7;
23
+ max-width: 70%;
24
+ margin: auto;
25
+ }
26
+
27
+ .content {
28
+ display: flex;
29
+ flex-direction: column;
30
+ gap: 10px;
31
+ }
32
+
33
+ .button_count {
34
+ background-color: #141414;
35
+ padding: 10px;
36
+ border: 0;
37
+ border-radius: 10px;
38
+ color: #d2d2d2;
39
+ cursor: pointer;
40
+ }
41
+
42
+ .row2 {
43
+ display: flex;
44
+ align-items: center;
45
+ gap: 10px;
46
+ justify-content: center;
47
+ }
@@ -0,0 +1,68 @@
1
+ import blux
2
+
3
+ num = 0
4
+
5
+ class Index(blux.Page):
6
+
7
+ title = "index"
8
+ styles = ["globals.css"]
9
+
10
+ @blux.action
11
+ def contador(self, request:blux.Request):
12
+ global num
13
+ num += 1
14
+
15
+ def render(self, request:blux.Request):
16
+
17
+ return (
18
+ blux.View(
19
+ state=self.state,
20
+ class_name="content_app",
21
+ elements=[
22
+ blux.View(
23
+ class_name="content",
24
+ elements=[
25
+
26
+ blux.Text(content="Bem vindo ao Box", class_name="title"),
27
+ blux.Text(
28
+ content="O framework pensado na produtividade de criar aplicações full-stack com apenas uma base de codigo",
29
+ class_name="text"
30
+ ),
31
+ blux.View(
32
+ elements=[
33
+ blux.Button(
34
+ content=f"contador {num}",
35
+ on_click=self.contador,
36
+ class_name="button_count"
37
+ ),
38
+ ]
39
+ ),
40
+ blux.View(
41
+ elements=[
42
+ blux.Link(
43
+ content="Develop",
44
+ href="http://ricardocayoca.onrender.com/",
45
+ target="_blank"
46
+ ),
47
+ blux.Link(
48
+ content="Doc box",
49
+ href="/"
50
+ ),
51
+ blux.Link(
52
+ content="Swager",
53
+ href="/docs",
54
+ target="_blank"
55
+ ),
56
+ blux.Link(
57
+ content="API doc",
58
+ href="/redoc",
59
+ target="_blank"
60
+ )
61
+ ],
62
+ class_name="row2"
63
+ )
64
+ ]
65
+ )
66
+ ]
67
+ )
68
+ )
@@ -0,0 +1,16 @@
1
+ import blux
2
+
3
+
4
+ class Sobre(blux.Page):
5
+
6
+ title = "sobre"
7
+
8
+ def render(self, request):
9
+ return (
10
+ blux.View(
11
+ elements=[
12
+ blux.Text(content="Pagina sobre"),
13
+ blux.Link(content="voltar", href="/")
14
+ ]
15
+ )
16
+ )
File without changes
blux/app.py ADDED
@@ -0,0 +1,48 @@
1
+ import os
2
+ from pathlib import Path
3
+
4
+ from fastapi import FastAPI
5
+ from fastapi.staticfiles import StaticFiles
6
+ from fastapi.templating import Jinja2Templates
7
+
8
+ from .core.routes import build_router
9
+ from .utils import install_form_middleware
10
+
11
+ APP_DIR = None
12
+ WEB_DIR = os.path.join(os.path.dirname(__file__), "web")
13
+
14
+
15
+ template = Jinja2Templates(
16
+ directory=os.path.join(WEB_DIR, "templates")
17
+ )
18
+
19
+ def create_app():
20
+ app = FastAPI()
21
+
22
+ install_form_middleware(app)
23
+
24
+ app.mount("/static", StaticFiles(
25
+ directory=os.path.join(WEB_DIR, "static")
26
+ )
27
+ )
28
+
29
+ for page in Path(os.getcwd()).rglob("**/app"):
30
+ if page.exists():
31
+ APP_DIR = page
32
+ break
33
+
34
+ if not APP_DIR: raise FileNotFoundError(
35
+ f"Diretório de páginas não encontrado: app\n"
36
+ f"Rode o comando a partir da raiz do projeto, ou passe --app-dir."
37
+ )
38
+
39
+
40
+
41
+ app.mount("/assets", StaticFiles(
42
+ directory=APP_DIR.joinpath("assets")
43
+ ))
44
+
45
+ for route in build_router(APP_DIR, template):
46
+ app.include_router(route, include_in_schema=True)
47
+
48
+ return app
blux/cli.py ADDED
@@ -0,0 +1,103 @@
1
+
2
+ from __future__ import annotations
3
+
4
+ import os
5
+ import shutil
6
+ from pathlib import Path
7
+
8
+ import typer
9
+ import uvicorn
10
+
11
+
12
+ app = typer.Typer(help="CLI do blux — sobe o servidor e escaneia as rotas de app/")
13
+
14
+ FACTORY_TARGET = "blux.app:create_app"
15
+
16
+
17
+ @app.command()
18
+ def dev(
19
+ host: str = typer.Option("127.0.0.1", help="Endereço para escutar"),
20
+ port: int = typer.Option(8000, help="Porta para escutar"),
21
+ ):
22
+ """Sobe o servidor de desenvolvimento com reload automático."""
23
+ typer.echo(f"blux dev -> http://{host}:{port}")
24
+ uvicorn.run(FACTORY_TARGET, factory=True, host=host, port=port, reload=True)
25
+
26
+
27
+ @app.command()
28
+ def init(name:str=typer.Argument(".", help="Diretório")):
29
+ """ Cria um estrutura do projeto """
30
+
31
+ ROOT = Path(name)
32
+
33
+ if ROOT.exists():
34
+ typer.echo(f"O projeto '{ROOT}' Já existe")
35
+ return
36
+
37
+ origem = Path(os.path.dirname(__file__)).joinpath("__projeto_test")
38
+ print(origem)
39
+
40
+ for arquivo in origem.rglob("*"):
41
+ if arquivo.is_file():
42
+ relativo = arquivo.relative_to(origem)
43
+ destino_arquivo = ROOT / relativo
44
+
45
+ destino_arquivo.parent.mkdir(parents=True, exist_ok=True)
46
+ shutil.copy2(arquivo, destino_arquivo)
47
+
48
+
49
+ typer.echo(f"Projeto {ROOT} inicializado")
50
+ typer.echo(f"use: cd {ROOT}")
51
+ typer.echo("start develop server: blux dev")
52
+ typer.echo("cli help use: blux --help")
53
+
54
+
55
+ return True
56
+
57
+
58
+
59
+
60
+
61
+
62
+
63
+
64
+
65
+ @app.command()
66
+ def start(
67
+ host: str = typer.Option("0.0.0.0", help="Endereço para escutar"),
68
+ port: int = typer.Option(8000, help="Porta para escutar"),
69
+ app_dir: str = typer.Option("app", "--app-dir", help="Diretório com as páginas"),
70
+ workers: int = typer.Option(1, help="Número de processos worker"),
71
+ ):
72
+ """Sobe o servidor em modo produção (sem reload)."""
73
+ os.environ["blux_APP_DIR"] = app_dir
74
+ typer.echo(f"backpay start -> http://{host}:{port} (páginas em ./{app_dir}, workers={workers})")
75
+ uvicorn.run(FACTORY_TARGET, factory=True, host=host, port=port, workers=workers)
76
+
77
+
78
+ @app.command()
79
+ def routes(
80
+ app_dir: str = typer.Option("app", "--app-dir", help="Diretório com as páginas"),
81
+ ):
82
+ """Lista as rotas descobertas em app/, sem subir o servidor."""
83
+ from pathlib import Path
84
+
85
+ from .core.routes import discover_pages
86
+
87
+ pages = discover_pages(Path(app_dir))
88
+ if not pages:
89
+ typer.echo(f"Nenhuma página encontrada em ./{app_dir}")
90
+ raise typer.Exit(code=1)
91
+
92
+ for page in pages:
93
+ typer.echo(f"GET {page.route_path}")
94
+ base = page.route_path.rstrip("/")
95
+ typer.echo(f"POST {base}/_action/{{component_id}}/{{action_name}}")
96
+
97
+
98
+ def main():
99
+ app()
100
+
101
+
102
+ if __name__ == "__main__":
103
+ main()
blux/core/api.py ADDED
@@ -0,0 +1,33 @@
1
+ from functools import wraps
2
+ from fastapi import (
3
+ APIRouter,
4
+ HTTPException,
5
+ Depends
6
+ )
7
+
8
+ from typing import Literal
9
+
10
+
11
+ def api(
12
+ method:Literal["GET", "POST", "PUT"]="GET",
13
+ middeware:list=[],
14
+ title="",
15
+ ):
16
+
17
+ def decorador(f:callable):
18
+ @wraps(f)
19
+ def wrapper(*args, **kwargs):
20
+
21
+ return f(*args, **kwargs)
22
+ wrapper._api = True
23
+ wrapper._method = method
24
+ wrapper._title = title
25
+ return wrapper
26
+
27
+ return decorador
28
+
29
+
30
+
31
+
32
+
33
+
blux/core/elements.py ADDED
@@ -0,0 +1,264 @@
1
+
2
+ from __future__ import annotations
3
+
4
+ import json
5
+ from html import escape
6
+ from typing import ClassVar, Optional, Callable, Any
7
+
8
+ from pydantic import BaseModel, Field
9
+
10
+ # Tags que não têm fechamento (não podem ter filhos nem content)
11
+ SELF_CLOSING_TAGS = {"input", "img", "br", "hr"}
12
+ ROOT = "root_blux_app_ricardo_cayoca"
13
+
14
+ class Element(BaseModel):
15
+ """Classe base de todo componente. Cada subclasse define seu próprio tag_name."""
16
+
17
+ # ClassVar = atributo fixo da classe, não é campo do Pydantic (não entra
18
+ # no __init__, não pode ser sobrescrito por instância)
19
+ tag_name: ClassVar[str] = "div"
20
+
21
+ id: Optional[str] = None
22
+ class_name: Optional[str] = None
23
+ content: Optional[str] = None
24
+ elements: list["Element"] = Field(default_factory=list)
25
+
26
+ # atributos HTML livres (ex: {"data-foo": "1", "hx-post": "/x"})
27
+ attrs: dict[str, str] = Field(default_factory=dict)
28
+ on_click:Callable=None
29
+ action_args:dict = None
30
+ state:Any=None
31
+
32
+ def html_attrs(self) -> dict[str, str]:
33
+ result: dict[str, str] = {}
34
+ if self.id:
35
+ result["id"] = self.id
36
+ if self.class_name:
37
+ result["class"] = self.class_name
38
+
39
+ if self.on_click:
40
+ result["hx-target"] = f"#{ROOT}"
41
+ result["hx-post"] = f"/_action/{self.on_click.uuid}"
42
+ result["hx-swap"] = "morph:innerHTML"
43
+ result["hx-push-url"] = "false"
44
+
45
+ if self.action_args:
46
+ result["hx-vals"] = json.dumps(self.action_args)
47
+
48
+ if self.state:
49
+ result["data-state"] = self.state
50
+ result.update(self.attrs)
51
+ return result
52
+
53
+
54
+ class View(Element):
55
+ """<div>"""
56
+ tag_name: ClassVar[str] = "div"
57
+
58
+
59
+ class Text(Element):
60
+ """<span>"""
61
+ tag_name: ClassVar[str] = "span"
62
+
63
+
64
+ class Button(Element):
65
+ """<button>"""
66
+ tag_name: ClassVar[str] = "button"
67
+ type: str = "button" # evita submit acidental dentro de um <form>
68
+
69
+ def html_attrs(self) -> dict[str, str]:
70
+ attrs = super().html_attrs()
71
+ attrs["type"] = self.type
72
+ return attrs
73
+
74
+
75
+ class Input(Element):
76
+ """<input> — self-closing, não aceita content nem elements."""
77
+ tag_name: ClassVar[str] = "input"
78
+ type: str = "text" # text, email, password, number, checkblux, radio...
79
+ name: Optional[str] = None
80
+ value: Optional[str] = None
81
+ placeholder: Optional[str] = None
82
+ required: bool = False
83
+ preserve:bool=False
84
+
85
+ def html_attrs(self) -> dict[str, str]:
86
+ attrs = super().html_attrs()
87
+ attrs["type"] = self.type
88
+ if self.name:
89
+ attrs["name"] = self.name
90
+ if self.value is not None:
91
+ attrs["value"] = self.value
92
+ if self.placeholder:
93
+ attrs["placeholder"] = self.placeholder
94
+ if self.required:
95
+ attrs["required"] = "required"
96
+ if self.preserve:
97
+ if not self.id:
98
+ raise ValueError("Input com preserve=True precisa de um id fixo")
99
+ attrs["hx-preserve"] = "true"
100
+ return attrs
101
+
102
+
103
+ class Textarea(Element):
104
+ """<textarea>"""
105
+ tag_name: ClassVar[str] = "textarea"
106
+ name: Optional[str] = None
107
+ placeholder: Optional[str] = None
108
+ rows: Optional[int] = None
109
+
110
+ def html_attrs(self) -> dict[str, str]:
111
+ attrs = super().html_attrs()
112
+ if self.name:
113
+ attrs["name"] = self.name
114
+ if self.placeholder:
115
+ attrs["placeholder"] = self.placeholder
116
+ if self.rows is not None:
117
+ attrs["rows"] = str(self.rows)
118
+ return attrs
119
+
120
+
121
+ class Image(Element):
122
+ """<img> — self-closing."""
123
+ tag_name: ClassVar[str] = "img"
124
+ src: str = ""
125
+ alt: str = ""
126
+
127
+ def html_attrs(self) -> dict[str, str]:
128
+ attrs = super().html_attrs()
129
+ attrs["src"] = self.src
130
+ attrs["alt"] = self.alt
131
+ return attrs
132
+
133
+
134
+ class Link(Element):
135
+ """<a>"""
136
+ tag_name: ClassVar[str] = "a"
137
+ href: str = "#"
138
+ target: Optional[str] = None # ex: "_blank"
139
+ onClick:Callable=None
140
+
141
+ def html_attrs(self) -> dict[str, str]:
142
+ attrs = super().html_attrs()
143
+ attrs["href"] = self.href
144
+
145
+
146
+ if self.target:
147
+ attrs["target"] = self.target
148
+
149
+ if self.target not in ["_blank"] and self.href != "#":
150
+ attrs["hx-get"] = self.href
151
+ attrs["hx-push-url"] = "true"
152
+ attrs["hx-target"] = f"#{ROOT}"
153
+
154
+ #attrs["hx-swap"]="morph"
155
+
156
+ return attrs
157
+
158
+
159
+ class TableCell(Element):
160
+ """<td>"""
161
+ tag_name: ClassVar[str] = "td"
162
+
163
+
164
+ class TableHeaderCell(Element):
165
+ """<th>"""
166
+ tag_name: ClassVar[str] = "th"
167
+
168
+
169
+ class TableRow(Element):
170
+ """<tr> — elements deve ser uma lista de TableCell ou TableHeaderCell."""
171
+ tag_name: ClassVar[str] = "tr"
172
+
173
+
174
+ class Table(Element):
175
+ """<table> — elements deve ser uma lista de TableRow.
176
+
177
+ Helper opcional: Table.from_rows(header, rows) monta a tabela inteira
178
+ a partir de listas simples, sem você ter que montar TableRow/TableCell
179
+ manualmente.
180
+ """
181
+ tag_name: ClassVar[str] = "table"
182
+
183
+ @classmethod
184
+ def from_rows(cls, header: list[str], rows: list[list[str]], **kwargs) -> "Table":
185
+ header_row = TableRow(elements=[TableHeaderCell(content=h) for h in header])
186
+ body_rows = [
187
+ TableRow(elements=[TableCell(content=str(cell)) for cell in row])
188
+ for row in rows
189
+ ]
190
+ return cls(elements=[header_row, *body_rows], **kwargs)
191
+
192
+
193
+ class Modal(Element):
194
+ """<dialog> — modal nativo do HTML, sem depender de JS extra pro caso
195
+ básico. Alterne `open` entre renders pra abrir/fechar.
196
+ """
197
+ tag_name: ClassVar[str] = "dialog"
198
+ open: bool = False
199
+
200
+ def html_attrs(self) -> dict[str, str]:
201
+ attrs = super().html_attrs()
202
+ if self.open:
203
+ attrs["open"] = "open"
204
+ return attrs
205
+
206
+
207
+ class Form(Element):
208
+ """<form> — elements deve conter os campos (Input, Textarea, Button...)."""
209
+ tag_name: ClassVar[str] = "form"
210
+ action:Callable = None
211
+ method: str = "post" # get | post
212
+
213
+ def html_attrs(self) -> dict[str, str]:
214
+ attrs = super().html_attrs()
215
+ if self.action:
216
+ #attrs["action"] = self.action
217
+ attrs["hx-target"] = f"#{ROOT}"
218
+ attrs["hx-post"] = f"/_action/{self.action.uuid}"
219
+ attrs["hx-swap"] = "morph:innerHTML"
220
+ attrs["hx-push-url"] = "false"
221
+
222
+
223
+ attrs["method"] = self.method
224
+ return attrs
225
+
226
+
227
+ class Raw(Element):
228
+ """Escape hatch: qualquer tag HTML sem componente próprio ainda.
229
+ Ex: Raw(tag="video", attrs={"controls": "controls", "src": "/v.mp4"})
230
+ """
231
+ tag: str = "div"
232
+
233
+ @property
234
+ def tag_name(self) -> str: # type: ignore[override]
235
+ return self.tag
236
+
237
+
238
+ # Pydantic v2 precisa disso pra resolver `list["Element"]` (referência
239
+ # recursiva declarada antes da classe existir por completo).
240
+ Element.model_rebuild()
241
+
242
+
243
+ def render(element: Element) -> str:
244
+ """Serializa recursivamente uma árvore de Element em uma string HTML."""
245
+ tag = element.tag_name
246
+ attrs = element.html_attrs()
247
+
248
+ attrs_str = "".join(
249
+ f' {key}="{escape(str(value), quote=True)}"' for key, value in attrs.items()
250
+ )
251
+
252
+ if tag in SELF_CLOSING_TAGS:
253
+ return f"<{tag}{attrs_str} />"
254
+
255
+ inner = ""
256
+ if element.content:
257
+ inner += escape(element.content)
258
+
259
+ for child in element.elements:
260
+ inner += render(child)
261
+
262
+ return f"<{tag}{attrs_str}>{inner}</{tag}>"
263
+
264
+