rusticai-playwright 0.0.1__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.
@@ -0,0 +1,40 @@
1
+ Metadata-Version: 2.1
2
+ Name: rusticai-playwright
3
+ Version: 0.0.1
4
+ Summary: Rustic AI module for scraping using playwright
5
+ Home-page: https://www.rustic.ai/
6
+ License: Apache-2.0
7
+ Author: Dragonscale Industries Inc.
8
+ Author-email: dev@dragonscale.ai
9
+ Requires-Python: >=3.12,<4.0
10
+ Classifier: License :: OSI Approved :: Apache Software License
11
+ Classifier: Programming Language :: Python :: 3
12
+ Classifier: Programming Language :: Python :: 3.12
13
+ Classifier: Programming Language :: Python :: 3.13
14
+ Provides-Extra: test
15
+ Requires-Dist: install-playwright (>=0.1.1,<0.2.0)
16
+ Requires-Dist: markdownify (>=1.1.0,<2.0.0)
17
+ Requires-Dist: playwright (>=1.51.0,<2.0.0)
18
+ Requires-Dist: rusticai-core (>=0.0.5,<0.1.0)
19
+ Project-URL: Repository, https://github.com/rustic-ai/python-framework
20
+ Project-URL: Rustic AI Core, https://pypi.org/project/rusticai-core/
21
+ Description-Content-Type: text/markdown
22
+
23
+ # Rustic AI Playwright
24
+
25
+ This module employs [Playwright](https://playwright.dev/python/docs/intro) to facilitate web scraping tasks.
26
+
27
+ ## Installing
28
+
29
+ ```shell
30
+ pip install rusticai-playwright
31
+ ```
32
+ **Note:** It depends on [rusticai-core](https://pypi.org/project/rusticai-core/)
33
+
34
+ ## Building from Source
35
+
36
+ ```shell
37
+ poetry install --with dev
38
+ poetry build
39
+ ```
40
+
@@ -0,0 +1,17 @@
1
+ # Rustic AI Playwright
2
+
3
+ This module employs [Playwright](https://playwright.dev/python/docs/intro) to facilitate web scraping tasks.
4
+
5
+ ## Installing
6
+
7
+ ```shell
8
+ pip install rusticai-playwright
9
+ ```
10
+ **Note:** It depends on [rusticai-core](https://pypi.org/project/rusticai-core/)
11
+
12
+ ## Building from Source
13
+
14
+ ```shell
15
+ poetry install --with dev
16
+ poetry build
17
+ ```
@@ -0,0 +1,79 @@
1
+ [build-system]
2
+ requires = ["poetry-core"]
3
+ build-backend = "poetry.core.masonry.api"
4
+
5
+ [tool.poetry]
6
+ name = "rusticai-playwright"
7
+ version = "0.0.1"
8
+ description = "Rustic AI module for scraping using playwright"
9
+ authors = ["Dragonscale Industries Inc. <dev@dragonscale.ai>"]
10
+ license = "Apache-2.0"
11
+ readme = "README.md"
12
+ homepage = "https://www.rustic.ai/"
13
+ repository = "https://github.com/rustic-ai/python-framework"
14
+ packages = [{ include = "rustic_ai", from = "src" }]
15
+
16
+ [tool.poetry.urls]
17
+ "Rustic AI Core" = "https://pypi.org/project/rusticai-core/"
18
+
19
+ [tool.poetry.dependencies]
20
+ python = "^3.12"
21
+ rusticai-core = { version = "0.0.5"}
22
+ playwright = "^1.51.0"
23
+ install-playwright = "^0.1.1"
24
+ markdownify = "^1.1.0"
25
+
26
+ [tool.poetry-monorepo.deps]
27
+
28
+
29
+ [tool.poetry.group.dev]
30
+ optional = true
31
+
32
+ [tool.poetry.group.dev.dependencies]
33
+ pytest = "^8.3.4"
34
+ pytest-asyncio = "^0.26.0"
35
+ black = { version = "^24.2.0", extras = ["jupyter"] }
36
+ flake8 = "^7.1.2"
37
+ tox = "^4.24.1"
38
+ isort = "^6.0.0"
39
+ mypy = "^1.15.0"
40
+ flaky = "^3.8.1"
41
+ rusticai-testing = { version = "0.0.1"}
42
+
43
+ [tool.poetry.extras]
44
+ test = ["pytest", "pytest-asyncio", "flaky", "rusticai-testing"]
45
+
46
+ [tool.black]
47
+ line-length = 120
48
+ target-version = ['py312']
49
+ include = '\.pyi?$'
50
+ exclude = '''
51
+ /(
52
+ \.git
53
+ | \.mypy_cache
54
+ | \.tox
55
+ | \.venv
56
+ | _build
57
+ | buck-out
58
+ | build
59
+ | dist
60
+ )/
61
+ '''
62
+
63
+ [tool.mypy]
64
+ python_version = "3.12"
65
+ ignore_missing_imports = true
66
+ check_untyped_defs = true
67
+ plugins = "pydantic.mypy"
68
+ explicit_package_bases = true
69
+
70
+
71
+ [tool.isort]
72
+ profile = "black"
73
+ known_third_party = ["pydantic"]
74
+ known_first_party = ["rustic_ai.core","rustic_ai.playwright"]
75
+ known_local_folder = ["rustic_ai.testing"]
76
+
77
+ [tool.pytest.ini_options]
78
+ asyncio_mode = "auto"
79
+ asyncio_default_fixture_loop_scope="function"
@@ -0,0 +1,135 @@
1
+ import hashlib
2
+ import logging
3
+ import mimetypes
4
+ import os
5
+ from enum import StrEnum
6
+ from typing import List
7
+ from urllib.parse import urlsplit
8
+
9
+ import shortuuid
10
+ from install_playwright import install
11
+ from markdownify import markdownify as md
12
+ from pydantic import BaseModel, Field
13
+
14
+ from playwright.async_api import async_playwright
15
+ from rustic_ai.core.agents.commons.media import MediaLink
16
+ from rustic_ai.core.agents.commons.message_formats import ErrorMessage
17
+ from rustic_ai.core.guild import Agent, AgentSpec, agent
18
+ from rustic_ai.core.guild.agent_ext.depends.filesystem.filesystem import FileSystem
19
+ from rustic_ai.core.utils.json_utils import JsonDict
20
+
21
+
22
+ class ScrapingOutputFormat(StrEnum):
23
+ TEXT_HTML = "text/html"
24
+ MARKDOWN = "text/markdown"
25
+
26
+
27
+ class WebScrapingRequest(BaseModel):
28
+ id: str = Field(default_factory=shortuuid.uuid, title="ID of the request")
29
+ links: List[MediaLink] = Field(..., title="URL to scrape")
30
+ output_format: ScrapingOutputFormat = Field(
31
+ default=ScrapingOutputFormat.TEXT_HTML,
32
+ title="Output format of the scraped content",
33
+ description="The format in which the scraped content will be saved. Default is text/html.",
34
+ )
35
+
36
+ transformer_options: JsonDict = Field(
37
+ default={},
38
+ title="Options for the transformer",
39
+ description="Options for the transformer to be applied to the scraped content. Default is an empty dictionary.",
40
+ )
41
+
42
+
43
+ class WebScrapingCompleted(BaseModel):
44
+ id: str = Field(..., title="ID of the request")
45
+ links: List[MediaLink] = Field(..., title="URL to scrape")
46
+
47
+
48
+ class PlaywrightScraperAgent(Agent):
49
+ def __init__(self, agent_spec: AgentSpec):
50
+ super().__init__(agent_spec=agent_spec)
51
+
52
+ @agent.processor(
53
+ WebScrapingRequest, depends_on=[agent.AgentDependency(dependency_key="filesystem", guild_level=True)]
54
+ )
55
+ async def scrape(self, ctx: agent.ProcessContext[WebScrapingRequest], filesystem: FileSystem) -> None:
56
+ async with async_playwright() as p:
57
+ install(p.chromium)
58
+ browser = await p.chromium.launch()
59
+ page = await browser.new_page()
60
+
61
+ scraping_request = ctx.payload
62
+
63
+ scraped_docs: List[MediaLink] = []
64
+
65
+ for link in scraping_request.links:
66
+ url = link.url
67
+
68
+ response = await page.goto(url)
69
+
70
+ if response and response.status != 200:
71
+ ctx.send_error(
72
+ ErrorMessage(
73
+ agent_type=self.get_qualified_class_name(),
74
+ error_type="HTTP_ERROR_{response.status}",
75
+ error_message=f"HTTP error: {response.status} for URL: {url}. Error message: {response.status_text}",
76
+ )
77
+ )
78
+ continue
79
+
80
+ title = await page.title()
81
+ content = await page.content()
82
+ urls = urlsplit(url)
83
+ upath = urls.path
84
+ basename = os.path.basename(upath)
85
+ _, filetype = os.path.splitext(basename)
86
+
87
+ if not filetype and scraping_request.output_format == ScrapingOutputFormat.TEXT_HTML:
88
+ filetype = ".html"
89
+ elif scraping_request.output_format == ScrapingOutputFormat.MARKDOWN:
90
+ filetype = ".md"
91
+ elif not filetype:
92
+ filetype = ".txt"
93
+
94
+ filename = hashlib.md5(content.encode("utf-8")).hexdigest() + filetype
95
+
96
+ mimetype = mimetypes.guess_type(filename)[0]
97
+
98
+ try:
99
+ if scraping_request.output_format == ScrapingOutputFormat.MARKDOWN:
100
+ content = md(
101
+ content,
102
+ **scraping_request.transformer_options,
103
+ )
104
+ with filesystem.open(f"scraped_data/{filename}", "w") as f:
105
+ f.write(content)
106
+ except Exception as e:
107
+ logging.error(f"Error writing file: {e}")
108
+ ctx.send_error(
109
+ ErrorMessage(
110
+ agent_type=self.get_qualified_class_name(),
111
+ error_type="FileWriteError",
112
+ error_message=str(e),
113
+ )
114
+ )
115
+
116
+ meta = link.metadata or {}
117
+ metadata = meta | {"scraped_url": url, "title": title, "request_id": scraping_request.id}
118
+
119
+ output = MediaLink(
120
+ url=f"scraped_data/{filename}",
121
+ name=filename,
122
+ metadata=metadata,
123
+ on_filesystem=True,
124
+ mimetype=mimetype,
125
+ encoding="utf-8",
126
+ )
127
+
128
+ scraped_docs.append(output)
129
+ ctx.send(output)
130
+
131
+ await browser.close()
132
+
133
+ unique_scraped_docs = list({doc.name: doc for doc in scraped_docs}.values())
134
+
135
+ ctx.send(WebScrapingCompleted(id=scraping_request.id, links=unique_scraped_docs))