Skip to content

Commit 928f6bc

Browse files
authored
Merge pull request #35 from nodetool-ai/codex/add-nodes-für-urllib
2 parents 97b19be + eb492fb commit 928f6bc

File tree

3 files changed

+271
-0
lines changed

3 files changed

+271
-0
lines changed

src/nodetool/dsl/lib/urllib.py

Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
from pydantic import BaseModel, Field
2+
import typing
3+
from typing import Any
4+
import nodetool.metadata.types
5+
import nodetool.metadata.types as types
6+
from nodetool.dsl.graph import GraphNode
7+
8+
9+
class EncodeQueryParams(GraphNode):
10+
"""
11+
Encode a dictionary of parameters into a query string using
12+
``urllib.parse.urlencode``.
13+
urllib, query, encode, params
14+
15+
Use cases:
16+
- Build GET request URLs
17+
- Serialize data for APIs
18+
- Convert parameters to query strings
19+
"""
20+
21+
params: dict[str, str] | GraphNode | tuple[GraphNode, str] = Field(default=PydanticUndefined, description='Parameters to encode')
22+
23+
@classmethod
24+
def get_node_type(cls): return "lib.urllib.EncodeQueryParams"
25+
26+
27+
28+
class JoinURL(GraphNode):
29+
"""
30+
Join a base URL with a relative URL using ``urllib.parse.urljoin``.
31+
urllib, join, url
32+
33+
Use cases:
34+
- Build absolute links from relative paths
35+
- Combine API base with endpoints
36+
- Resolve resources from a base URL
37+
"""
38+
39+
base: str | GraphNode | tuple[GraphNode, str] = Field(default='', description='Base URL')
40+
url: str | GraphNode | tuple[GraphNode, str] = Field(default='', description='Relative or absolute URL')
41+
42+
@classmethod
43+
def get_node_type(cls): return "lib.urllib.JoinURL"
44+
45+
46+
47+
class ParseURL(GraphNode):
48+
"""
49+
Parse a URL into its components using ``urllib.parse.urlparse``.
50+
urllib, parse, url
51+
52+
Use cases:
53+
- Inspect links for validation
54+
- Extract host or path information
55+
- Analyze query parameters
56+
"""
57+
58+
url: str | GraphNode | tuple[GraphNode, str] = Field(default='', description='URL to parse')
59+
60+
@classmethod
61+
def get_node_type(cls): return "lib.urllib.ParseURL"
62+
63+
64+
65+
class QuoteURL(GraphNode):
66+
"""
67+
Percent-encode a string for safe use in URLs using ``urllib.parse.quote``.
68+
urllib, quote, encode
69+
70+
Use cases:
71+
- Escape spaces or special characters
72+
- Prepare text for query parameters
73+
- Encode file names in URLs
74+
"""
75+
76+
text: str | GraphNode | tuple[GraphNode, str] = Field(default='', description='Text to quote')
77+
78+
@classmethod
79+
def get_node_type(cls): return "lib.urllib.QuoteURL"
80+
81+
82+
83+
class UnquoteURL(GraphNode):
84+
"""
85+
Decode a percent-encoded URL string using ``urllib.parse.unquote``.
86+
urllib, unquote, decode
87+
88+
Use cases:
89+
- Convert encoded URLs to readable form
90+
- Parse user input from URLs
91+
- Display unescaped paths
92+
"""
93+
94+
text: str | GraphNode | tuple[GraphNode, str] = Field(default='', description='Encoded text')
95+
96+
@classmethod
97+
def get_node_type(cls): return "lib.urllib.UnquoteURL"
98+
99+

src/nodetool/nodes/lib/urllib.py

Lines changed: 125 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,125 @@
1+
from urllib.parse import urlparse, urljoin, urlencode, quote, unquote
2+
from pydantic import Field
3+
from nodetool.workflows.base_node import BaseNode
4+
from nodetool.workflows.processing_context import ProcessingContext
5+
6+
7+
class ParseURL(BaseNode):
8+
"""
9+
Parse a URL into its components using ``urllib.parse.urlparse``.
10+
urllib, parse, url
11+
12+
Use cases:
13+
- Inspect links for validation
14+
- Extract host or path information
15+
- Analyze query parameters
16+
"""
17+
18+
url: str = Field(default="", description="URL to parse")
19+
20+
@classmethod
21+
def get_title(cls):
22+
return "Parse URL"
23+
24+
async def process(self, context: ProcessingContext) -> dict:
25+
parsed = urlparse(self.url)
26+
return {
27+
"scheme": parsed.scheme,
28+
"netloc": parsed.netloc,
29+
"path": parsed.path,
30+
"params": parsed.params,
31+
"query": parsed.query,
32+
"fragment": parsed.fragment,
33+
"username": parsed.username,
34+
"password": parsed.password,
35+
"hostname": parsed.hostname,
36+
"port": parsed.port,
37+
}
38+
39+
40+
class JoinURL(BaseNode):
41+
"""
42+
Join a base URL with a relative URL using ``urllib.parse.urljoin``.
43+
urllib, join, url
44+
45+
Use cases:
46+
- Build absolute links from relative paths
47+
- Combine API base with endpoints
48+
- Resolve resources from a base URL
49+
"""
50+
51+
base: str = Field(default="", description="Base URL")
52+
url: str = Field(default="", description="Relative or absolute URL")
53+
54+
@classmethod
55+
def get_title(cls):
56+
return "Join URL"
57+
58+
async def process(self, context: ProcessingContext) -> str:
59+
return urljoin(self.base, self.url)
60+
61+
62+
class EncodeQueryParams(BaseNode):
63+
"""
64+
Encode a dictionary of parameters into a query string using
65+
``urllib.parse.urlencode``.
66+
urllib, query, encode, params
67+
68+
Use cases:
69+
- Build GET request URLs
70+
- Serialize data for APIs
71+
- Convert parameters to query strings
72+
"""
73+
74+
params: dict[str, str] = Field(
75+
default_factory=dict, description="Parameters to encode"
76+
)
77+
78+
@classmethod
79+
def get_title(cls):
80+
return "Encode Query Params"
81+
82+
async def process(self, context: ProcessingContext) -> str:
83+
return urlencode(self.params, doseq=True)
84+
85+
86+
class QuoteURL(BaseNode):
87+
"""
88+
Percent-encode a string for safe use in URLs using ``urllib.parse.quote``.
89+
urllib, quote, encode
90+
91+
Use cases:
92+
- Escape spaces or special characters
93+
- Prepare text for query parameters
94+
- Encode file names in URLs
95+
"""
96+
97+
text: str = Field(default="", description="Text to quote")
98+
99+
@classmethod
100+
def get_title(cls):
101+
return "Quote URL"
102+
103+
async def process(self, context: ProcessingContext) -> str:
104+
return quote(self.text)
105+
106+
107+
class UnquoteURL(BaseNode):
108+
"""
109+
Decode a percent-encoded URL string using ``urllib.parse.unquote``.
110+
urllib, unquote, decode
111+
112+
Use cases:
113+
- Convert encoded URLs to readable form
114+
- Parse user input from URLs
115+
- Display unescaped paths
116+
"""
117+
118+
text: str = Field(default="", description="Encoded text")
119+
120+
@classmethod
121+
def get_title(cls):
122+
return "Unquote URL"
123+
124+
async def process(self, context: ProcessingContext) -> str:
125+
return unquote(self.text)

tests/nodetool/test_urllib.py

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
import pytest
2+
from nodetool.workflows.processing_context import ProcessingContext
3+
from nodetool.nodes.lib.urllib import (
4+
ParseURL,
5+
JoinURL,
6+
EncodeQueryParams,
7+
QuoteURL,
8+
UnquoteURL,
9+
)
10+
11+
12+
@pytest.fixture
13+
def context():
14+
return ProcessingContext(user_id="test", auth_token="test")
15+
16+
17+
@pytest.mark.asyncio
18+
async def test_parse_url(context: ProcessingContext):
19+
node = ParseURL(url="https://example.com/path?x=1#frag")
20+
result = await node.process(context)
21+
assert result["scheme"] == "https"
22+
assert result["netloc"] == "example.com"
23+
assert result["path"] == "/path"
24+
assert result["query"] == "x=1"
25+
assert result["fragment"] == "frag"
26+
27+
28+
@pytest.mark.asyncio
29+
async def test_join_url(context: ProcessingContext):
30+
node = JoinURL(base="https://example.com/api/", url="v1")
31+
result = await node.process(context)
32+
assert result == "https://example.com/api/v1"
33+
34+
35+
@pytest.mark.asyncio
36+
async def test_encode_query_params(context: ProcessingContext):
37+
node = EncodeQueryParams(params={"a": "1", "b": "2"})
38+
result = await node.process(context)
39+
assert "a=1" in result and "b=2" in result
40+
41+
42+
@pytest.mark.asyncio
43+
async def test_quote_unquote(context: ProcessingContext):
44+
quoted = await QuoteURL(text="hello world").process(context)
45+
assert "%20" in quoted
46+
unquoted = await UnquoteURL(text=quoted).process(context)
47+
assert unquoted == "hello world"

0 commit comments

Comments
 (0)