duqlang 0.1.0__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.
- duqlang-0.1.0/PKG-INFO +8 -0
- duqlang-0.1.0/pyproject.toml +23 -0
- duqlang-0.1.0/src/duq/__init__.py +3 -0
- duqlang-0.1.0/src/duq/__main__.py +30 -0
- duqlang-0.1.0/src/duq/_evaluation.py +115 -0
- duqlang-0.1.0/src/duq/_preview.py +165 -0
- duqlang-0.1.0/src/duq/_std/__init__.py +37 -0
- duqlang-0.1.0/src/duq/_std/_basic.py +38 -0
- duqlang-0.1.0/src/duq/_std/_bool.py +12 -0
- duqlang-0.1.0/src/duq/_std/_csv.py +20 -0
- duqlang-0.1.0/src/duq/_std/_file.py +8 -0
- duqlang-0.1.0/src/duq/_std/_future.py +33 -0
- duqlang-0.1.0/src/duq/_std/_html.py +20 -0
- duqlang-0.1.0/src/duq/_std/_http.py +25 -0
- duqlang-0.1.0/src/duq/_std/_json.py +16 -0
- duqlang-0.1.0/src/duq/_std/_list.py +146 -0
- duqlang-0.1.0/src/duq/_std/_null.py +18 -0
- duqlang-0.1.0/src/duq/_std/_number.py +57 -0
- duqlang-0.1.0/src/duq/_std/_record.py +50 -0
- duqlang-0.1.0/src/duq/_std/_str.py +42 -0
- duqlang-0.1.0/src/duq/_std/_stream.py +69 -0
- duqlang-0.1.0/src/duq/_syntax.py +312 -0
- duqlang-0.1.0/src/duq/_tui.py +165 -0
- duqlang-0.1.0/src/duq/_type_check.py +228 -0
- duqlang-0.1.0/src/duq/_util.py +198 -0
duqlang-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "duqlang"
|
|
3
|
+
version = "0.1.0"
|
|
4
|
+
description = "Data-processing language and CLI"
|
|
5
|
+
requires-python = ">=3.12"
|
|
6
|
+
dependencies = [
|
|
7
|
+
"bs4>=0.0.2",
|
|
8
|
+
"httpx>=0.28.1",
|
|
9
|
+
"prompt-toolkit>=3.0.53",
|
|
10
|
+
]
|
|
11
|
+
|
|
12
|
+
[dependency-groups]
|
|
13
|
+
dev = [
|
|
14
|
+
"ipykernel>=7.3.0",
|
|
15
|
+
"ipywidgets>=8.1.9",
|
|
16
|
+
]
|
|
17
|
+
|
|
18
|
+
[build-system]
|
|
19
|
+
requires = ["uv_build>=0.8.19,<0.9.0"]
|
|
20
|
+
build-backend = "uv_build"
|
|
21
|
+
|
|
22
|
+
[tool.uv.build-backend]
|
|
23
|
+
module-name = "duq"
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
import sys
|
|
3
|
+
import warnings
|
|
4
|
+
|
|
5
|
+
import duq
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def catch_warning(message, category, filename, lineno, file=None, line=None):
|
|
9
|
+
pass
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
warnings.showwarning = catch_warning
|
|
13
|
+
# warnings.filterwarnings(
|
|
14
|
+
# "ignore", message=".*was never awaited.*", category=RuntimeWarning
|
|
15
|
+
# )
|
|
16
|
+
# warnings.filterwarnings(
|
|
17
|
+
# "ignore", message=".*invalid escape sequence.*", category=SyntaxWarning
|
|
18
|
+
# )
|
|
19
|
+
|
|
20
|
+
if len(sys.argv) == 1:
|
|
21
|
+
asyncio.run(duq.run_tui())
|
|
22
|
+
sys.exit(0)
|
|
23
|
+
|
|
24
|
+
if len(sys.argv) != 2:
|
|
25
|
+
print("Usage: duq '<source>'", file=sys.stderr)
|
|
26
|
+
sys.exit(1)
|
|
27
|
+
|
|
28
|
+
source = sys.argv[1]
|
|
29
|
+
result = duq.evaluate(source)
|
|
30
|
+
asyncio.run(duq.print_value_async(result))
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
from typing import Any, Callable, assert_never
|
|
5
|
+
|
|
6
|
+
from duq._syntax import ChainExpr, Expr, OpExpr, parse
|
|
7
|
+
from duq._type_check import DuqTypeError, OpSignature, type_check_value
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
@dataclass
|
|
11
|
+
class OpInfo:
|
|
12
|
+
name: str
|
|
13
|
+
signature: OpSignature
|
|
14
|
+
op_func: Callable
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class DuqContext:
|
|
18
|
+
def __init__(self) -> None:
|
|
19
|
+
self.ops: dict[str, OpInfo] = {}
|
|
20
|
+
self.cache: dict[str, Any] = {}
|
|
21
|
+
self.cursor: int | None = None
|
|
22
|
+
|
|
23
|
+
@staticmethod
|
|
24
|
+
def create() -> DuqContext:
|
|
25
|
+
import duq._std
|
|
26
|
+
|
|
27
|
+
context = DuqContext()
|
|
28
|
+
context.load_ops(duq._std.op_definitions)
|
|
29
|
+
return context
|
|
30
|
+
|
|
31
|
+
def load_op(self, name: str, op_func: Callable) -> None:
|
|
32
|
+
if name in self.ops:
|
|
33
|
+
raise Exception(f"redefined op: `{name}`")
|
|
34
|
+
signature = OpSignature.of(name, op_func)
|
|
35
|
+
self.ops[name] = OpInfo(name, signature, op_func)
|
|
36
|
+
|
|
37
|
+
def load_ops(self, ops: dict[str, Callable]) -> None:
|
|
38
|
+
for op_name, op_func in ops.items():
|
|
39
|
+
self.load_op(op_name, op_func)
|
|
40
|
+
|
|
41
|
+
def resolve_op(self, name: str, input: Any) -> OpInfo:
|
|
42
|
+
overloads = [op for op in self.ops.values() if op.signature.matches_name(name)]
|
|
43
|
+
if len(overloads) == 1:
|
|
44
|
+
return overloads[0]
|
|
45
|
+
if len(overloads) == 0:
|
|
46
|
+
raise Exception(f"unknown operation `{name}`")
|
|
47
|
+
|
|
48
|
+
matches = [op for op in overloads if op.signature.matches_input(input)]
|
|
49
|
+
if len(matches) == 1:
|
|
50
|
+
return matches[0]
|
|
51
|
+
if len(matches) == 0:
|
|
52
|
+
example_op = overloads[0]
|
|
53
|
+
assert (input_param := example_op.signature.input_param) is not None
|
|
54
|
+
try:
|
|
55
|
+
type_check_value(input, input_param.annotation)
|
|
56
|
+
except DuqTypeError as e:
|
|
57
|
+
raise DuqTypeError(
|
|
58
|
+
f"{example_op.name} (+{len(overloads) - 1} overloads): {e.args[0]}"
|
|
59
|
+
)
|
|
60
|
+
|
|
61
|
+
op_names = ", ".join(f"`{op.name}`" for op in matches)
|
|
62
|
+
raise Exception(f"ambiguous operation `{name}`: {op_names}")
|
|
63
|
+
|
|
64
|
+
def evaluate_op_expr(self, expr: OpExpr, input: Any) -> Any:
|
|
65
|
+
args = []
|
|
66
|
+
if expr.name.text.startswith("."):
|
|
67
|
+
op = self.resolve_op("std.record.field", input)
|
|
68
|
+
args.append(expr.name.text.removeprefix("."))
|
|
69
|
+
elif expr.name.text == "{":
|
|
70
|
+
op = self.resolve_op("map", input)
|
|
71
|
+
else:
|
|
72
|
+
op = self.resolve_op(expr.name.text, input)
|
|
73
|
+
|
|
74
|
+
arg_exprs = () if expr.arg_list is None else expr.arg_list.args
|
|
75
|
+
if op.signature.is_macro:
|
|
76
|
+
args += arg_exprs
|
|
77
|
+
else:
|
|
78
|
+
args += [self.evaluate(arg_expr, input) for arg_expr in arg_exprs]
|
|
79
|
+
|
|
80
|
+
args = tuple(args)
|
|
81
|
+
call_args = op.signature.type_check_inputs(self, input, args)
|
|
82
|
+
|
|
83
|
+
return op.op_func(*call_args)
|
|
84
|
+
|
|
85
|
+
def evaluate_chain_expr(self, expr: ChainExpr, input: Any) -> Any:
|
|
86
|
+
contains_cursor = (
|
|
87
|
+
self.cursor is not None
|
|
88
|
+
and self.cursor >= expr.span[0]
|
|
89
|
+
and self.cursor <= expr.span[1]
|
|
90
|
+
)
|
|
91
|
+
for subexpr in expr.exprs:
|
|
92
|
+
if (
|
|
93
|
+
self.cursor is not None
|
|
94
|
+
and contains_cursor
|
|
95
|
+
and subexpr.span[0] >= self.cursor
|
|
96
|
+
):
|
|
97
|
+
break
|
|
98
|
+
input = self.evaluate(subexpr, input)
|
|
99
|
+
return input
|
|
100
|
+
|
|
101
|
+
def evaluate(self, expr: Expr, input: Any) -> Any:
|
|
102
|
+
if expr.type == "literal":
|
|
103
|
+
return expr.value
|
|
104
|
+
elif expr.type == "op":
|
|
105
|
+
return self.evaluate_op_expr(expr, input)
|
|
106
|
+
elif expr.type == "chain":
|
|
107
|
+
return self.evaluate_chain_expr(expr, input)
|
|
108
|
+
else:
|
|
109
|
+
assert_never(expr)
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def evaluate(source: str, input: Any = None) -> Any:
|
|
113
|
+
context = DuqContext.create()
|
|
114
|
+
expr = parse(source)
|
|
115
|
+
return context.evaluate(expr, input)
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
import html
|
|
3
|
+
import json
|
|
4
|
+
import re
|
|
5
|
+
from typing import Any, AsyncIterator, Awaitable, Callable, Iterable
|
|
6
|
+
|
|
7
|
+
import bs4
|
|
8
|
+
|
|
9
|
+
from duq._evaluation import DuqContext
|
|
10
|
+
from duq._syntax import parse
|
|
11
|
+
from duq._util import DuqFuture, DuqStream, Hinted, Reactive
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def render_items(
|
|
15
|
+
start: str, end: str, items: Iterable[str], indent: int, sep=","
|
|
16
|
+
) -> str:
|
|
17
|
+
start = html.escape(start)
|
|
18
|
+
end = html.escape(end)
|
|
19
|
+
sep = html.escape(sep)
|
|
20
|
+
indent_str = " " * indent
|
|
21
|
+
if not items:
|
|
22
|
+
return start + end
|
|
23
|
+
elif len(result := start + (sep + " ").join(items) + end) < 40:
|
|
24
|
+
return result
|
|
25
|
+
else:
|
|
26
|
+
result = start
|
|
27
|
+
for item in items:
|
|
28
|
+
result += html.escape("\n") + indent_str + " " + item + sep
|
|
29
|
+
result += html.escape("\n") + indent_str + end
|
|
30
|
+
return result
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def render_value(value: Any, indent=0) -> Reactive[str]:
|
|
34
|
+
if value is None or isinstance(value, (int, float, str, bool)):
|
|
35
|
+
return Reactive.of(html.escape(json.dumps(value)))
|
|
36
|
+
elif isinstance(value, list):
|
|
37
|
+
child_strs_rx = Reactive.from_list(
|
|
38
|
+
[render_value(child, indent + 2) for child in value]
|
|
39
|
+
)
|
|
40
|
+
return child_strs_rx.map(
|
|
41
|
+
lambda child_strs: render_items("[", "]", child_strs, indent)
|
|
42
|
+
)
|
|
43
|
+
elif isinstance(value, dict):
|
|
44
|
+
child_strs_rx = Reactive.from_list(
|
|
45
|
+
[
|
|
46
|
+
render_value(child, indent + 2).map(
|
|
47
|
+
lambda s: f"{html.escape(json.dumps(key))}: {s}"
|
|
48
|
+
)
|
|
49
|
+
for key, child in value.items()
|
|
50
|
+
]
|
|
51
|
+
)
|
|
52
|
+
return child_strs_rx.map(
|
|
53
|
+
lambda child_strs: render_items("{", "}", child_strs, indent)
|
|
54
|
+
)
|
|
55
|
+
elif isinstance(value, Awaitable):
|
|
56
|
+
return Reactive.from_awaitable(value).flat_map(
|
|
57
|
+
lambda child_maybe: (
|
|
58
|
+
render_value(child_maybe.value, indent + 2)
|
|
59
|
+
if child_maybe.is_some
|
|
60
|
+
else Reactive.of("...")
|
|
61
|
+
).map(
|
|
62
|
+
lambda child_str: render_items(
|
|
63
|
+
"future(", ")", [child_str], indent, sep=""
|
|
64
|
+
)
|
|
65
|
+
)
|
|
66
|
+
)
|
|
67
|
+
elif isinstance(value, DuqFuture):
|
|
68
|
+
return render_value(value.create(), indent)
|
|
69
|
+
elif isinstance(value, AsyncIterator):
|
|
70
|
+
|
|
71
|
+
async def child_strs_iter() -> AsyncIterator[Reactive[str]]:
|
|
72
|
+
async for child in value:
|
|
73
|
+
yield render_value(child, indent + 2)
|
|
74
|
+
|
|
75
|
+
return Reactive.collect_from_iter(child_strs_iter()).map(
|
|
76
|
+
lambda child_strs: render_items(
|
|
77
|
+
"stream[",
|
|
78
|
+
"]",
|
|
79
|
+
child_strs.items + ([] if child_strs.is_done else ["..."]),
|
|
80
|
+
indent,
|
|
81
|
+
)
|
|
82
|
+
)
|
|
83
|
+
elif isinstance(value, DuqStream):
|
|
84
|
+
return render_value(value.create(), indent)
|
|
85
|
+
elif isinstance(value, Hinted):
|
|
86
|
+
child_rx = render_value(value.value, indent)
|
|
87
|
+
if value.hint == "cursor":
|
|
88
|
+
|
|
89
|
+
def bold(s: str) -> str:
|
|
90
|
+
lines = s.split("\n")
|
|
91
|
+
lines = [f"<green>{line}</green>" for line in lines]
|
|
92
|
+
return "\n".join(lines)
|
|
93
|
+
|
|
94
|
+
return child_rx.map(bold)
|
|
95
|
+
else:
|
|
96
|
+
return child_rx
|
|
97
|
+
elif isinstance(value, bs4.Tag):
|
|
98
|
+
indented = re.sub(
|
|
99
|
+
r"^(\s*)",
|
|
100
|
+
(indent * " ") + r"\1\1",
|
|
101
|
+
value.prettify(),
|
|
102
|
+
flags=re.MULTILINE,
|
|
103
|
+
).removeprefix(indent * " ")
|
|
104
|
+
output = re.sub(r"\s*$", "", indented)
|
|
105
|
+
return Reactive.of(html.escape(output))
|
|
106
|
+
else:
|
|
107
|
+
raise Exception(f"unimplemented: {type(value)}")
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
class Preview:
|
|
111
|
+
def __init__(
|
|
112
|
+
self, set_output: Callable[[str], None], set_error: Callable[[str], None]
|
|
113
|
+
) -> None:
|
|
114
|
+
self.ctx = DuqContext.create()
|
|
115
|
+
self.source = ""
|
|
116
|
+
self.cursor_position = 0
|
|
117
|
+
self.current_task: asyncio.Task | None = None
|
|
118
|
+
self.set_output = set_output
|
|
119
|
+
self.set_error = set_error
|
|
120
|
+
self.refresh()
|
|
121
|
+
|
|
122
|
+
def set_source(self, source: str) -> None:
|
|
123
|
+
self.source = source
|
|
124
|
+
self.refresh()
|
|
125
|
+
|
|
126
|
+
def set_cursor_position(self, cursor_position: int) -> None:
|
|
127
|
+
self.cursor_position = cursor_position
|
|
128
|
+
self.refresh()
|
|
129
|
+
|
|
130
|
+
def refresh(self) -> None:
|
|
131
|
+
try:
|
|
132
|
+
expr = parse(self.source, self.cursor_position)
|
|
133
|
+
self.ctx.cursor = self.cursor_position
|
|
134
|
+
result = self.ctx.evaluate(expr, None)
|
|
135
|
+
except Exception as e:
|
|
136
|
+
self.set_error(f"Error: {e}")
|
|
137
|
+
else:
|
|
138
|
+
|
|
139
|
+
async def set_output_task():
|
|
140
|
+
try:
|
|
141
|
+
output_rx = render_value(result)
|
|
142
|
+
self.set_output(output_rx.initial)
|
|
143
|
+
async for output in output_rx.updates:
|
|
144
|
+
self.set_output(output)
|
|
145
|
+
except Exception as e:
|
|
146
|
+
while isinstance(e, ExceptionGroup):
|
|
147
|
+
e = e.exceptions[0]
|
|
148
|
+
self.set_error(f"Error: {e}")
|
|
149
|
+
|
|
150
|
+
if self.current_task != None:
|
|
151
|
+
self.current_task.cancel()
|
|
152
|
+
self.current_task = asyncio.create_task(set_output_task())
|
|
153
|
+
self.set_error("")
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
async def print_value_async(value: Any) -> None:
|
|
157
|
+
value_str_rx = render_value(value)
|
|
158
|
+
print(html.unescape(value_str_rx.initial))
|
|
159
|
+
async for value_str in value_str_rx.updates:
|
|
160
|
+
print(html.unescape(value_str))
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
def print_value(value: Any) -> None:
|
|
164
|
+
value_str_rx = render_value(value)
|
|
165
|
+
print(html.unescape(value_str_rx.initial))
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
from duq._std import (
|
|
2
|
+
_basic,
|
|
3
|
+
_csv,
|
|
4
|
+
_file,
|
|
5
|
+
_future,
|
|
6
|
+
_html,
|
|
7
|
+
_http,
|
|
8
|
+
_json,
|
|
9
|
+
_bool,
|
|
10
|
+
_list,
|
|
11
|
+
_null,
|
|
12
|
+
_number,
|
|
13
|
+
_record,
|
|
14
|
+
_str,
|
|
15
|
+
_stream,
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
op_definitions = {
|
|
19
|
+
op_name: op_func
|
|
20
|
+
for op_definitions in [
|
|
21
|
+
_basic.op_definitions,
|
|
22
|
+
_bool.op_definitions,
|
|
23
|
+
_csv.op_definitions,
|
|
24
|
+
_file.op_definitions,
|
|
25
|
+
_future.op_definitions,
|
|
26
|
+
_html.op_definitions,
|
|
27
|
+
_http.op_definitions,
|
|
28
|
+
_json.op_definitions,
|
|
29
|
+
_list.op_definitions,
|
|
30
|
+
_null.op_definitions,
|
|
31
|
+
_number.op_definitions,
|
|
32
|
+
_record.op_definitions,
|
|
33
|
+
_str.op_definitions,
|
|
34
|
+
_stream.op_definitions,
|
|
35
|
+
]
|
|
36
|
+
for op_name, op_func in op_definitions.items()
|
|
37
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
from datetime import datetime
|
|
2
|
+
from typing import Any
|
|
3
|
+
|
|
4
|
+
from duq._evaluation import DuqContext
|
|
5
|
+
from duq._syntax import Expr
|
|
6
|
+
from duq._util import Hinted
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def op_id(input: Any) -> Any:
|
|
10
|
+
return input
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def op_eq(input: Any, arg: Any) -> bool:
|
|
14
|
+
return input == arg
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def op_hint(input: Any, hint: str) -> Hinted[Any]:
|
|
18
|
+
return Hinted(hint, input)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def op_now() -> str:
|
|
22
|
+
return datetime.now().isoformat(timespec="milliseconds") + "Z"
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def op_try(ctx: DuqContext, input: Any, body: Expr) -> Any:
|
|
26
|
+
try:
|
|
27
|
+
return ctx.evaluate(body, input)
|
|
28
|
+
except:
|
|
29
|
+
return None
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
op_definitions = {
|
|
33
|
+
"std.basic.id": op_id,
|
|
34
|
+
"std.basic.eq": op_eq,
|
|
35
|
+
"std.basic.hint": op_hint,
|
|
36
|
+
"std.time.now": op_now,
|
|
37
|
+
"std.basic.try": op_try,
|
|
38
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
from typing import Any
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
def op_tableToRecords(input: list[list[str]]) -> list[dict[str, str]]:
|
|
5
|
+
if len(input) == 0:
|
|
6
|
+
raise Exception("missing header row")
|
|
7
|
+
headers = input[0]
|
|
8
|
+
rows = input[1:]
|
|
9
|
+
result = []
|
|
10
|
+
for row in rows:
|
|
11
|
+
record = {}
|
|
12
|
+
for i, header in enumerate(headers):
|
|
13
|
+
record[header] = None if i >= len(row) else row[i]
|
|
14
|
+
result.append(record)
|
|
15
|
+
return result
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
op_definitions = {
|
|
19
|
+
"std.csv.tableToRecords": op_tableToRecords,
|
|
20
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
from typing import Any
|
|
3
|
+
|
|
4
|
+
from duq._evaluation import DuqContext
|
|
5
|
+
from duq._syntax import Expr
|
|
6
|
+
from duq._util import DuqFuture
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def op_sleep(seconds: int | float) -> DuqFuture[None]:
|
|
10
|
+
return DuqFuture(lambda: asyncio.sleep(seconds))
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def op_map(ctx: DuqContext, input: DuqFuture[Any], body: Expr) -> DuqFuture[Any]:
|
|
14
|
+
async def task():
|
|
15
|
+
value = await input.create()
|
|
16
|
+
return ctx.evaluate(body, value)
|
|
17
|
+
|
|
18
|
+
return DuqFuture(task)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def op_all(input: list[DuqFuture[Any]]) -> DuqFuture[list[Any]]:
|
|
22
|
+
async def task():
|
|
23
|
+
children = [child.create() for child in input]
|
|
24
|
+
return await asyncio.gather(*children)
|
|
25
|
+
|
|
26
|
+
return DuqFuture(task)
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
op_definitions = {
|
|
30
|
+
"std.future.sleep": op_sleep,
|
|
31
|
+
"std.future.map": op_map,
|
|
32
|
+
"std.future.all": op_all,
|
|
33
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import bs4
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
def op_html(input: str) -> bs4.BeautifulSoup:
|
|
5
|
+
return bs4.BeautifulSoup(input, "html.parser")
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def op_select(input: bs4.Tag, query: str) -> list[bs4.Tag]:
|
|
9
|
+
return list(input.select(query))
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def op_text(input: bs4.Tag) -> str:
|
|
13
|
+
return input.text
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
op_definitions = {
|
|
17
|
+
"std.html.html": op_html,
|
|
18
|
+
"std.html.select": op_select,
|
|
19
|
+
"std.html.text": op_text,
|
|
20
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import httpx
|
|
2
|
+
|
|
3
|
+
from duq._evaluation import DuqContext
|
|
4
|
+
from duq._util import DuqFuture
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def op_fetch(ctx: DuqContext, input: str) -> DuqFuture[str]:
|
|
8
|
+
async def task() -> str:
|
|
9
|
+
async with httpx.AsyncClient() as client:
|
|
10
|
+
response = await client.get(
|
|
11
|
+
input,
|
|
12
|
+
headers={
|
|
13
|
+
"User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/152.0.0.0 Safari/537.36"
|
|
14
|
+
},
|
|
15
|
+
)
|
|
16
|
+
if response.status_code != 200:
|
|
17
|
+
raise Exception(f"request error: {input} -> {response.status_code}")
|
|
18
|
+
return response.text
|
|
19
|
+
|
|
20
|
+
return DuqFuture(task).cached(ctx, ["std.http.fetch", input])
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
op_definitions = {
|
|
24
|
+
"std.http.fetch": op_fetch,
|
|
25
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import json
|
|
2
|
+
from typing import Any
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
def op_json(input: str) -> Any:
|
|
6
|
+
return json.loads(input)
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def op_pretty(input: Any) -> str:
|
|
10
|
+
return json.dumps(input, indent=2)
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
op_definitions = {
|
|
14
|
+
"std.json.json": op_json,
|
|
15
|
+
"std.json.pretty": op_pretty,
|
|
16
|
+
}
|