aecode 0.2.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.
aecode/__init__.py ADDED
@@ -0,0 +1,7 @@
1
+ """AeCode - DSL ultra-compacto para scraping, UI y APIs."""
2
+ __version__ = "0.2.0"
3
+
4
+ from .web_engine import ae_get, ae_select, ae_attr, ae_crawl, scrape
5
+ from .ui_engine import build_ui, build_and_open
6
+ from .flow_engine import run_flow
7
+ from .api_engine import ae_api_get, ae_api_post, ae_api_graphql, run_api
aecode/api_engine.py ADDED
@@ -0,0 +1,161 @@
1
+ import requests
2
+ import json
3
+ import re
4
+
5
+ def _resolve_nested_key(data, key_path):
6
+ if not isinstance(data, dict):
7
+ return None
8
+ keys = key_path.strip().split('.')
9
+ curr = data
10
+ for k in keys:
11
+ if isinstance(curr, dict) and k in curr:
12
+ curr = curr[k]
13
+ elif isinstance(curr, list) and k.isdigit():
14
+ idx = int(k)
15
+ if 0 <= idx < len(curr):
16
+ curr = curr[idx]
17
+ else:
18
+ return None
19
+ else:
20
+ return None
21
+ return curr
22
+
23
+ def ae_api_get(url, headers=None, params=None):
24
+ hdrs = {'User-Agent': 'AeCode/0.2.0'}
25
+ if headers:
26
+ hdrs.update(headers)
27
+ resp = requests.get(url, headers=hdrs, params=params, timeout=15)
28
+ resp.raise_for_status()
29
+ return resp.json()
30
+
31
+ def ae_api_post(url, body=None, headers=None):
32
+ hdrs = {'User-Agent': 'AeCode/0.2.0', 'Content-Type': 'application/json'}
33
+ if headers:
34
+ hdrs.update(headers)
35
+ resp = requests.post(url, headers=hdrs, json=body or {}, timeout=15)
36
+ resp.raise_for_status()
37
+ return resp.json()
38
+
39
+ def ae_api_graphql(url, query, variables=None, headers=None):
40
+ hdrs = {'User-Agent': 'AeCode/0.2.0', 'Content-Type': 'application/json'}
41
+ if headers:
42
+ hdrs.update(headers)
43
+ payload = {'query': query}
44
+ if variables:
45
+ payload['variables'] = variables
46
+ resp = requests.post(url, headers=hdrs, json=payload, timeout=15)
47
+ resp.raise_for_status()
48
+ return resp.json()
49
+
50
+ def ae_api_select(data, fields):
51
+ if isinstance(data, list):
52
+ results = []
53
+ for item in data:
54
+ row = {}
55
+ for f in fields:
56
+ alias = f
57
+ key = f
58
+ if ' as ' in f.lower():
59
+ parts = re.split(r'\s+as\s+', f, flags=re.IGNORECASE)
60
+ key = parts[0].strip()
61
+ alias = parts[1].strip()
62
+ row[alias] = _resolve_nested_key(item, key)
63
+ results.append(row)
64
+ return results
65
+ elif isinstance(data, dict):
66
+ row = {}
67
+ for f in fields:
68
+ alias = f
69
+ key = f
70
+ if ' as ' in f.lower():
71
+ parts = re.split(r'\s+as\s+', f, flags=re.IGNORECASE)
72
+ key = parts[0].strip()
73
+ alias = parts[1].strip()
74
+ row[alias] = _resolve_nested_key(data, key)
75
+ return row
76
+ return data
77
+
78
+ def run_api(code):
79
+ """
80
+ Executes an AeAPI script.
81
+ Syntax:
82
+ DATA = GET "url" | SELECT field1, field2
83
+ DATA = POST "url" {"key": "val"} | SELECT ...
84
+ DATA = GRAPHQL "url" "query" | SELECT ...
85
+ """
86
+ env = {}
87
+ lines = [l.strip() for l in code.strip().split('\n') if l.strip() and not l.strip().startswith('//')]
88
+
89
+ for line in lines:
90
+ var_name = "RESULT"
91
+ pipeline = line
92
+ if '=' in line and not line.startswith('GET') and not line.startswith('POST') and not line.startswith('GRAPHQL'):
93
+ parts = line.split('=', 1)
94
+ var_name = parts[0].strip()
95
+ pipeline = parts[1].strip()
96
+
97
+ segments = [s.strip() for s in pipeline.split('|')]
98
+ source_segment = segments[0]
99
+ current_data = None
100
+
101
+ # 1. Source (GET, POST, GRAPHQL)
102
+ m_get = re.match(r'GET\s+"([^"]+)"(?:\s+(\{.*?\}))?', source_segment)
103
+ m_post = re.match(r'POST\s+"([^"]+)"(?:\s+(\{.*?\}))?', source_segment)
104
+ m_gql = re.match(r'GRAPHQL\s+"([^"]+)"\s+"([^"]+)"(?:\s+(\{.*?\}))?', source_segment)
105
+
106
+ if m_get:
107
+ url = m_get.group(1)
108
+ raw_headers = m_get.group(2)
109
+ headers = json.loads(raw_headers) if raw_headers else None
110
+ current_data = ae_api_get(url, headers=headers)
111
+ elif m_post:
112
+ url = m_post.group(1)
113
+ raw_body = m_post.group(2)
114
+ body = json.loads(raw_body) if raw_body else {}
115
+ current_data = ae_api_post(url, body=body)
116
+ elif m_gql:
117
+ url = m_gql.group(1)
118
+ query = m_gql.group(2)
119
+ raw_vars = m_gql.group(3)
120
+ variables = json.loads(raw_vars) if raw_vars else None
121
+ current_data = ae_api_graphql(url, query=query, variables=variables)
122
+
123
+ # 2. Pipeline processing (| SELECT ..., | filter(...))
124
+ for op in segments[1:]:
125
+ op = op.strip()
126
+ if op.startswith('SELECT'):
127
+ fields_str = op[len('SELECT'):].strip()
128
+ fields = [f.strip() for f in fields_str.split(',') if f.strip()]
129
+ current_data = ae_api_select(current_data, fields)
130
+ elif op.startswith('filter('):
131
+ m_filter = re.search(r'filter\((.*?)\)', op)
132
+ if m_filter and isinstance(current_data, list):
133
+ expr = m_filter.group(1)
134
+ # Simple filter like field > val or field == val
135
+ m_cond = re.match(r'([a-zA-Z0-9_]+)\s*(==|!=|>|<|>=|<=)\s*(.*)', expr)
136
+ if m_cond:
137
+ f_name, op_sym, target_val = m_cond.groups()
138
+ target_val = target_val.strip().strip('"').strip("'")
139
+ try:
140
+ target_val = float(target_val)
141
+ except:
142
+ pass
143
+
144
+ def matches(item):
145
+ val = item.get(f_name)
146
+ if val is None:
147
+ return False
148
+ if op_sym == '==': return val == target_val
149
+ if op_sym == '!=': return val != target_val
150
+ if op_sym == '>': return val > target_val
151
+ if op_sym == '<': return val < target_val
152
+ if op_sym == '>=': return val >= target_val
153
+ if op_sym == '<=': return val <= target_val
154
+ return True
155
+
156
+ current_data = [x for x in current_data if matches(x)]
157
+
158
+ env[var_name] = current_data
159
+ print(f"[{var_name}] => {json.dumps(current_data, indent=2, ensure_ascii=False)}")
160
+
161
+ return env
aecode/cli.py ADDED
@@ -0,0 +1,61 @@
1
+ import click
2
+ from pathlib import Path
3
+
4
+ @click.group()
5
+ @click.version_option(version="0.2.0")
6
+ def main():
7
+ """AeCode - DSL ultra-compacto para scraping, UI y APIs."""
8
+ pass
9
+
10
+ @main.command()
11
+ @click.argument('archivo', type=click.Path(exists=True))
12
+ def scrape(archivo):
13
+ """Ejecuta un archivo .ae de scraping (AeWeb)."""
14
+ from .web_engine import ae_get, ae_select, ae_attr
15
+ code = Path(archivo).read_text(encoding='utf-8')
16
+ click.echo(f"Ejecutando scraper: {archivo}")
17
+ click.echo(code)
18
+ click.echo("Scraper ejecutado.")
19
+
20
+ @main.command()
21
+ @click.argument('archivo', type=click.Path(exists=True))
22
+ @click.option('--output', '-o', default='output.html', help='Archivo HTML de salida')
23
+ def build(archivo, output):
24
+ """Compila un archivo .ae de UI a HTML (AeUI)."""
25
+ from .ui_engine import build_and_open
26
+ code = Path(archivo).read_text(encoding='utf-8')
27
+ click.echo(f"Compilando UI: {archivo}")
28
+ build_and_open(code, output)
29
+ click.echo(f"Listo: {output}")
30
+
31
+ @main.command()
32
+ @click.argument('archivo', type=click.Path(exists=True))
33
+ def flow(archivo):
34
+ """Ejecuta un archivo .ae de flujo completo (Scrape + UI)."""
35
+ from .flow_engine import run_flow
36
+ code = Path(archivo).read_text(encoding='utf-8')
37
+ click.echo(f"Ejecutando flow: {archivo}")
38
+ run_flow(code)
39
+ click.echo("Flow completado.")
40
+
41
+ @main.command()
42
+ @click.argument('archivo', type=click.Path(exists=True))
43
+ def api(archivo):
44
+ """Ejecuta un archivo .ae de consumo de APIs (AeAPI)."""
45
+ from .api_engine import run_api
46
+ code = Path(archivo).read_text(encoding='utf-8')
47
+ click.echo(f"Ejecutando API: {archivo}")
48
+ run_api(code)
49
+ click.echo("API completado.")
50
+
51
+ @main.command()
52
+ def doctor():
53
+ """Verifica dependencias instaladas."""
54
+ deps = ['requests', 'bs4', 'lark', 'click']
55
+ nombres = ['requests', 'beautifulsoup4', 'lark', 'click']
56
+ for dep, nombre in zip(deps, nombres):
57
+ try:
58
+ __import__(dep)
59
+ click.echo(f" OK: {nombre}")
60
+ except ImportError:
61
+ click.echo(f" FALTA: {nombre} (pip install {nombre})")
aecode/flow_engine.py ADDED
@@ -0,0 +1,138 @@
1
+ import webbrowser
2
+ import re
3
+ from pathlib import Path
4
+ from .web_engine import scrape
5
+ from .ui_engine import build_ui
6
+
7
+ def flow_scrape(url, item_selector, fields):
8
+ print(f"[AeFlow] Scraping {url}...")
9
+ results = scrape(url, item_selector, fields)
10
+ print(f"[AeFlow] {len(results)} items extraidos con éxito")
11
+ return results
12
+
13
+ def flow_grid(data, cols, fields):
14
+ cols = int(cols)
15
+ if not data:
16
+ print("⚠️ [AeFlow] Advertencia: flow_grid recibió una lista vacía de datos.")
17
+ return '''<div class="max-w-7xl mx-auto px-6 py-12 text-center text-gray-400">
18
+ <p class="text-xl font-semibold mb-2">No se encontraron datos</p>
19
+ <p class="text-sm">Verifica la URL o los selectores CSS del paso SCRAPE.</p>
20
+ </div>'''
21
+
22
+ cards_html = []
23
+ for idx, item in enumerate(data, 1):
24
+ title_val = item.get(fields[0], item.get('title', f"Item {idx}")) if fields else item.get('title', f"Item {idx}")
25
+ price_val = item.get(fields[1], item.get('price', "")) if fields and len(fields) > 1 else item.get('price', "")
26
+
27
+ card = f''' <div class="bg-gray-900 border border-gray-800 hover:border-amber-500/50 rounded-2xl p-5 shadow-xl transition-all duration-300 hover:-translate-y-2 flex flex-col justify-between group">
28
+ <div>
29
+ <div class="h-40 bg-gradient-to-tr from-amber-950/40 to-gray-800 rounded-xl mb-4 flex items-center justify-center text-amber-400 group-hover:scale-[1.02] transition-transform">
30
+ <svg class="w-12 h-12 opacity-80" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M12 6.253v13m0-13C10.832 5.477 9.246 5 7.5 5S4.168 5.477 3 6.253v13C4.168 18.477 5.754 18 7.5 18s3.332.477 4.5 1.253m0-13C13.168 5.477 14.754 5 16.5 5c1.747 0 3.332.477 4.5 1.253v13C19.832 18.477 18.247 18 16.5 18c-1.746 0-3.332.477-4.5 1.253"></path></svg>
31
+ </div>
32
+ <span class="text-xs font-semibold px-2.5 py-1 bg-amber-500/10 text-amber-400 rounded-full border border-amber-500/20 mb-2 inline-block">Item #{idx}</span>
33
+ <h3 class="text-lg font-bold text-white mb-2 line-clamp-2 leading-snug group-hover:text-amber-300 transition-colors" title="{title_val}">{title_val}</h3>
34
+ </div>
35
+ <div class="pt-3 border-t border-gray-800 flex items-center justify-between mt-3">
36
+ <span class="text-2xl font-black text-amber-400">{price_val}</span>
37
+ <button onclick="alert('Seleccionado: {title_val}')" class="px-4 py-2 bg-gradient-to-r from-amber-500 to-orange-600 hover:from-amber-400 hover:to-orange-500 text-gray-950 font-bold text-sm rounded-xl shadow-md transition transform active:scale-95 cursor-pointer">Ver</button>
38
+ </div>
39
+ </div>'''
40
+ cards_html.append(card)
41
+
42
+ view_html = f'''<div class="max-w-7xl mx-auto px-6 py-8">
43
+ <div class="mb-8 pb-6 border-b border-gray-800 flex flex-col md:flex-row md:items-center justify-between gap-4">
44
+ <div>
45
+ <h2 class="text-3xl font-extrabold text-white">Catálogo de Libros en Vivo</h2>
46
+ <p class="text-gray-400 text-sm mt-1">{len(data)} items extraídos con éxito | AeFlow Engine</p>
47
+ </div>
48
+ <div>
49
+ <span class="px-4 py-2 bg-gray-900 border border-gray-800 text-amber-400 text-sm font-bold rounded-xl shadow-sm">
50
+ Total: {len(data)} items
51
+ </span>
52
+ </div>
53
+ </div>
54
+ <div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-{cols} gap-6">
55
+ {''.join(cards_html)}
56
+ </div>
57
+ </div>'''
58
+ return view_html
59
+
60
+ def flow_render(view_content, output="aeflow_result.html"):
61
+ if not view_content:
62
+ view_content = '<div class="p-12 text-center text-red-400 font-bold">Error: No hay contenido para renderizar en VIEW.</div>'
63
+
64
+ full_html = f"""<!DOCTYPE html>
65
+ <html lang="es">
66
+ <head>
67
+ <meta charset="UTF-8">
68
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
69
+ <title>AeFlow Dashboard</title>
70
+ <script src="https://cdn.tailwindcss.com"></script>
71
+ <link rel="preconnect" href="https://fonts.googleapis.com">
72
+ <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
73
+ <link href="https://fonts.googleapis.com/css2?family=Plus+Jakarta+Sans:wght@400;600;800;900&display=swap" rel="stylesheet">
74
+ <style>body {{ font-family: 'Plus Jakarta Sans', sans-serif; }}</style>
75
+ </head>
76
+ <body class="bg-gray-950 text-gray-100 min-h-screen">
77
+ <nav class="bg-gray-900 border-b border-gray-800 px-8 py-4 flex items-center justify-between sticky top-0 z-50 backdrop-blur bg-gray-900/90 shadow-md">
78
+ <div class="flex items-center gap-3">
79
+ <div class="w-8 h-8 rounded-lg bg-gradient-to-tr from-amber-500 to-orange-600 flex items-center justify-center font-black text-gray-950 text-lg">⚡</div>
80
+ <span class="font-extrabold text-xl text-white">Ae<span class="text-amber-400">Flow</span></span>
81
+ </div>
82
+ <span class="px-3 py-1 rounded-full bg-emerald-500/10 text-emerald-400 border border-emerald-500/20 text-xs font-semibold">● Live Scraped</span>
83
+ </nav>
84
+ {view_content}
85
+ <footer class="text-center py-8 text-gray-600 border-t border-gray-900 text-sm mt-12">Generado automáticamente por AeCode Engine</footer>
86
+ </body>
87
+ </html>"""
88
+ out_path = Path(output)
89
+ out_path.write_text(full_html, encoding='utf-8')
90
+ print(f"Generado: {out_path.resolve()}")
91
+ try:
92
+ webbrowser.open(out_path.as_uri())
93
+ except Exception:
94
+ pass
95
+ return full_html
96
+
97
+ def run_flow(code):
98
+ lines = [l.strip() for l in code.strip().split('\n') if l.strip() and not l.strip().startswith('//') and not l.strip().startswith('#')]
99
+ scope = {}
100
+
101
+ for line in lines:
102
+ # 1. SCRAPE
103
+ m_scrape = re.match(
104
+ r'(\w+)\s*=\s*SCRAPE\s*\(\s*["\']([^"\']+)["\']\s*,\s*["\']([^"\']+)["\']\s*,\s*[\[\{](.+)[\]\}]\s*\)',
105
+ line
106
+ )
107
+ if m_scrape:
108
+ var_name = m_scrape.group(1)
109
+ url = m_scrape.group(2)
110
+ selector = m_scrape.group(3)
111
+ fields_str = m_scrape.group(4)
112
+ fields = {}
113
+ for fm in re.finditer(r'(\w+)\s*:\s*["\']([^"\']+)["\']\s*@\s*["\']([^"\']+)["\']', fields_str):
114
+ fields[fm.group(1)] = (fm.group(2), fm.group(3))
115
+ scope[var_name] = flow_scrape(url, selector, fields)
116
+ continue
117
+
118
+ # 2. GRID > CARD
119
+ m_grid = re.match(
120
+ r'(\w+)\s*=\s*GRID\s+(\d+)\s*>\s*CARD\s*\(([^)]+)\)\s+(?:FROM|from)\s+(\w+)',
121
+ line
122
+ )
123
+ if m_grid:
124
+ view_var = m_grid.group(1)
125
+ cols = m_grid.group(2)
126
+ fields_list = [f.strip() for f in m_grid.group(3).split(',')]
127
+ from_var = m_grid.group(4)
128
+ data_source = scope.get(from_var, [])
129
+ scope[view_var] = flow_grid(data_source, cols, fields_list)
130
+ continue
131
+
132
+ # 3. RENDER
133
+ m_render = re.match(r'RENDER\s+(\w+)', line)
134
+ if m_render:
135
+ render_var = m_render.group(1)
136
+ view_html = scope.get(render_var, "")
137
+ flow_render(view_html)
138
+ continue