Skip to content

Commit 845fd1c

Browse files
committed
chore: reposition python-uv as a general Actor and add dev tooling
1 parent 8ca9d4c commit 845fd1c

9 files changed

Lines changed: 207 additions & 230 deletions

File tree

.github/workflows/test_python_templates.yaml

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -37,11 +37,9 @@ jobs:
3737
with:
3838
python-version: ${{ matrix.python-version }}
3939

40-
# Used by the uv-based templates (e.g. python-uv). uv installs the Python version
41-
# pinned in the template's .python-version file on its own, so it intentionally
42-
# does not get the matrix Python version passed in.
40+
# For uv-based templates (e.g. python-uv); uv provides its own Python from .python-version.
4341
- name: Setup uv
44-
uses: astral-sh/setup-uv@v8
42+
uses: astral-sh/setup-uv@v8.3.2
4543

4644
- name: Install Chrome and Chromedriver
4745
# Use pnpm dlx instead of npx — npx is the npm CLI and trips

templates/manifest.json

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -68,10 +68,10 @@
6868
{
6969
"id": "python-uv",
7070
"name": "python-uv",
71-
"label": "One‑Page HTML Scraper with uv",
71+
"label": "Python project managed by uv",
7272
"category": "python",
73-
"technologies": ["uv", "beautifulsoup", "httpx"],
74-
"description": "Scrape single page with provided URL with HTTPX and extract data from page's HTML with Beautiful Soup. The project and its dependencies are managed by the uv package manager.",
73+
"technologies": ["uv"],
74+
"description": "A general-purpose Python Actor with its project and dependencies managed by the uv package manager. A minimal starting point for any use case.",
7575
"messages": {
7676
"postCreate": "To install the dependencies, run \"uv sync\" in the Actor directory. It creates a virtual environment in the \".venv\" folder and installs the locked dependencies from \"uv.lock\". To install additional Python packages, use \"uv add <package>\"."
7777
},
@@ -82,7 +82,7 @@
8282
"timeoutSecs": 3600
8383
},
8484
"showcaseFiles": ["my_actor/main.py", "pyproject.toml", "my_actor/__main__.py"],
85-
"useCases": ["STARTER", "WEB_SCRAPING"]
85+
"useCases": ["STARTER"]
8686
},
8787
{
8888
"id": "python-beautifulsoup",

templates/python-uv/.actor/actor.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,8 @@
22
"$schema": "https://apify.com/schemas/v1/actor.ide.json",
33
"actorSpecification": 1,
44
"name": "python-uv",
5-
"title": "Scrape single page in Python with uv",
6-
"description": "Scrape data from single page with provided URL, with project and dependencies managed by uv.",
5+
"title": "Python Actor managed by uv",
6+
"description": "A Python Actor with its project and dependencies managed by uv.",
77
"version": "0.0",
88
"buildTag": "latest",
99
"meta": {

templates/python-uv/.actor/dataset_schema.json

Lines changed: 13 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -6,17 +6,25 @@
66
"overview": {
77
"title": "Overview",
88
"transformation": {
9-
"fields": ["level", "text"]
9+
"fields": ["greeting", "repeated", "python_version", "managed_by"]
1010
},
1111
"display": {
1212
"component": "table",
1313
"properties": {
14-
"level": {
15-
"label": "Level",
14+
"greeting": {
15+
"label": "Greeting",
1616
"format": "text"
1717
},
18-
"text": {
19-
"label": "Text",
18+
"repeated": {
19+
"label": "Repeated",
20+
"format": "number"
21+
},
22+
"python_version": {
23+
"label": "Python version",
24+
"format": "text"
25+
},
26+
"managed_by": {
27+
"label": "Managed by",
2028
"format": "text"
2129
}
2230
}
Lines changed: 15 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,25 @@
11
{
22
"$schema": "https://apify.com/schemas/v1/input.ide.json",
3-
"title": "Scrape data from a web page",
3+
"title": "Input schema for a uv-managed Apify Actor",
44
"type": "object",
55
"schemaVersion": 1,
66
"properties": {
7-
"url": {
8-
"title": "URL of the page",
7+
"name": {
8+
"title": "Name",
99
"type": "string",
10-
"description": "The URL of website you want to get the data from.",
10+
"description": "The name the Actor greets in its output. Replace it with whatever input your Actor needs.",
1111
"editor": "textfield",
12-
"prefill": "https://www.apify.com/"
12+
"default": "world",
13+
"prefill": "world"
14+
},
15+
"repeat": {
16+
"title": "Repeat count",
17+
"type": "integer",
18+
"description": "How many times to log the greeting.",
19+
"default": 3,
20+
"minimum": 1,
21+
"maximum": 10
1322
}
1423
},
15-
"required": ["url"]
24+
"required": []
1625
}

templates/python-uv/README.md

Lines changed: 13 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -2,30 +2,25 @@
22

33
<!-- This is an Apify template readme -->
44

5-
A template for [web scraping](https://apify.com/web-scraping) data from a single web page in Python, with the project and its dependencies managed by [uv](https://docs.astral.sh/uv/) - an extremely fast Python package and project manager. The URL of the web page is passed in via input, which is defined by the [input schema](https://docs.apify.com/platform/actors/development/input-schema). The template uses [HTTPX](https://www.python-httpx.org) to get the HTML of the page and [Beautiful Soup](https://www.crummy.com/software/BeautifulSoup/bs4/doc/) to parse the data from it. The data are then stored in a [dataset](https://docs.apify.com/sdk/python/docs/concepts/storages#working-with-datasets) where you can easily access them.
5+
A general-purpose [Actor](https://apify.com/actors) template for Python, with the project and its dependencies managed by [uv](https://docs.astral.sh/uv/) - a fast Python package and project manager. It's a minimal starting point for any kind of Actor, for example a scraper, a browser automation, an AI agent, an MCP server, a RAG pipeline, or a standby web server.
66

7-
The scraped data in this template are page headings, but you can easily edit the code to scrape whatever you want from the page.
7+
The example code reads an input, does a little work with it, logs its progress, and stores a result in a [dataset](https://docs.apify.com/sdk/python/docs/concepts/storages#working-with-datasets). Replace the body of `main()` with whatever your Actor should do. The only thing this template locks in is the tooling, uv, not the use case.
88

99
## Included features
1010

1111
- **[uv](https://docs.astral.sh/uv/)** - a single fast tool that manages the project's Python version (`.python-version`), virtual environment (`.venv`), and dependencies (`pyproject.toml` + `uv.lock`)
1212
- **Reproducible builds** - the `uv.lock` lockfile guarantees that the Actor's Docker image is built with exactly the dependency versions you developed against
13-
- **[Apify SDK](https://docs.apify.com/sdk/python/)** for Python - a toolkit for building Apify [Actors](https://apify.com/actors) and scrapers in Python
13+
- **[Apify SDK](https://docs.apify.com/sdk/python/)** for Python - a toolkit for building Apify [Actors](https://apify.com/actors) in Python
1414
- **[Input schema](https://docs.apify.com/platform/actors/development/input-schema)** - define and easily validate a schema for your Actor's input
1515
- **[Dataset](https://docs.apify.com/sdk/python/docs/concepts/storages#working-with-datasets)** - store structured data where each object stored has the same attributes
16-
- **[HTTPX](https://www.python-httpx.org)** - library for making asynchronous HTTP requests in Python
17-
- **[Beautiful Soup](https://www.crummy.com/software/BeautifulSoup/bs4/doc/)** - library for pulling data out of HTML and XML files
1816

1917
## How it works
2018

21-
1. `Actor.get_input()` gets the input where the page URL is defined
22-
2. `httpx.AsyncClient().get(url)` fetches the page
23-
3. `BeautifulSoup(response.content, 'lxml')` loads the page data and enables parsing the headings
24-
4. This parses the headings from the page and here you can edit the code to parse whatever you need from the page
25-
```python
26-
for heading in soup.find_all(["h1", "h2", "h3", "h4", "h5", "h6"]):
27-
```
28-
5. `Actor.push_data(headings)` stores the headings in the dataset
19+
1. `Actor.get_input()` reads the input defined in the [input schema](https://docs.apify.com/platform/actors/development/input-schema)
20+
2. The Actor logs the Python version it runs on (managed by uv) and greets the given name a few times
21+
3. `Actor.push_data(...)` stores a structured result in the dataset
22+
23+
This is only a placeholder so the template runs out of the box. Swap it for your own logic and add the dependencies you need with `uv add`.
2924

3025
## Working with uv
3126

@@ -47,17 +42,16 @@ uv remove <package>
4742
uv lock --upgrade && uv sync
4843
```
4944

50-
The Actor's `Dockerfile` installs the dependencies with `uv sync --locked --no-dev`, so the image is built with exactly the versions recorded in `uv.lock`, skipping the development-only dependencies. Commit `uv.lock` and `.python-version` whenever they change.
45+
The Actor's `Dockerfile` installs the dependencies with `uv sync --locked --no-dev`, so the image is built with exactly the versions recorded in `uv.lock` (skipping any development-only dependencies you add under `[dependency-groups]`). Commit `uv.lock` and `.python-version` whenever they change.
5146

5247
## Resources
5348

5449
- [Apify SDK for Python: uv guide](https://docs.apify.com/sdk/python/docs/guides/uv)
5550
- [uv: Official documentation](https://docs.astral.sh/uv/)
56-
- [BeautifulSoup Scraper](https://apify.com/apify/beautifulsoup-scraper)
51+
- [Apify SDK for Python documentation](https://docs.apify.com/sdk/python/)
5752
- [Python tutorials in Academy](https://docs.apify.com/academy/python)
58-
- [Web scraping with Beautiful Soup and Requests](https://blog.apify.com/web-scraping-with-beautiful-soup/)
5953
- [Integration with Make, GitHub, Zapier, Google Drive, and other apps](https://apify.com/integrations)
60-
- [Video guide on getting scraped data using Apify API](https://www.youtube.com/watch?v=ViYYDHSBAKM)
61-
- A short guide on how to build web scrapers using code templates:
54+
- [Video guide on getting data using the Apify API](https://www.youtube.com/watch?v=ViYYDHSBAKM)
55+
- A short guide on how to build Actors using code templates:
6256

63-
[web scraper template](https://www.youtube.com/watch?v=u-i-Korzf8w)
57+
[Actor code templates](https://www.youtube.com/watch?v=u-i-Korzf8w)

templates/python-uv/my_actor/main.py

Lines changed: 30 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -8,47 +8,44 @@
88

99
from __future__ import annotations
1010

11-
# Beautiful Soup - A library for pulling data out of HTML and XML files. Read more at:
12-
# https://www.crummy.com/software/BeautifulSoup/bs4/doc
11+
import sys
12+
1313
# Apify SDK - A toolkit for building Apify Actors. Read more at:
1414
# https://docs.apify.com/sdk/python
1515
from apify import Actor
16-
from bs4 import BeautifulSoup
17-
18-
# HTTPX - A library for making asynchronous HTTP requests in Python. Read more at:
19-
# https://www.python-httpx.org/
20-
from httpx import AsyncClient
2116

2217

2318
async def main() -> None:
2419
"""Define a main entry point for the Apify Actor.
2520
2621
This coroutine is executed using `asyncio.run()`, so it must remain an asynchronous function for proper execution.
27-
Asynchronous execution is required for communication with Apify platform, and it also enhances performance in
28-
the field of web scraping significantly.
22+
Asynchronous execution is required for communication with the Apify platform.
23+
24+
This is a minimal, general-purpose Actor: it reads an input, does a little work with it, logs its progress, and
25+
stores a result in the dataset. Replace the body with whatever your Actor should do, for example a scraper, a
26+
browser automation, an AI agent, an MCP server, or a web server.
2927
"""
3028
async with Actor:
31-
# Retrieve the input object for the Actor. The structure of input is defined in input_schema.json.
32-
actor_input = await Actor.get_input() or {'url': 'https://apify.com/'}
33-
url = actor_input.get('url')
34-
if not url:
35-
raise ValueError('Missing "url" attribute in input!')
36-
37-
# Create an asynchronous HTTPX client for making HTTP requests.
38-
async with AsyncClient() as client:
39-
# Fetch the HTML content of the page, following redirects if necessary.
40-
Actor.log.info(f'Sending a request to {url}')
41-
response = await client.get(url, follow_redirects=True)
42-
43-
# Parse the HTML content using Beautiful Soup and lxml parser.
44-
soup = BeautifulSoup(response.content, 'lxml')
45-
46-
# Extract all headings from the page (tag name and text).
47-
headings = []
48-
for heading in soup.find_all(['h1', 'h2', 'h3', 'h4', 'h5', 'h6']):
49-
heading_object = {'level': heading.name, 'text': heading.text}
50-
Actor.log.info(f'Extracted heading: {heading_object}')
51-
headings.append(heading_object)
52-
53-
# Save the extracted headings to the dataset, which is a table-like storage.
54-
await Actor.push_data(headings)
29+
# uv manages both the dependencies and the Python version, so the interpreter running here is exactly the
30+
# one pinned in `.python-version`.
31+
python_version = '.'.join(map(str, sys.version_info[:3]))
32+
Actor.log.info(f'Hello from a uv-managed Apify Actor, running on Python {python_version}!')
33+
34+
# Retrieve the Actor input. The structure of the input is defined in `input_schema.json`.
35+
actor_input = await Actor.get_input() or {}
36+
name = actor_input.get('name', 'world')
37+
repeat = actor_input.get('repeat', 3)
38+
39+
# Do something with the input. Here we simply greet the given name a few times.
40+
for i in range(1, repeat + 1):
41+
Actor.log.info(f'[{i}/{repeat}] Hello, {name}!')
42+
43+
# Save a structured result to the dataset, which is a table-like storage.
44+
await Actor.push_data(
45+
{
46+
'greeting': f'Hello, {name}!',
47+
'repeated': repeat,
48+
'python_version': python_version,
49+
'managed_by': 'uv',
50+
}
51+
)

templates/python-uv/pyproject.toml

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,19 +5,27 @@ description = "An Apify Actor with dependencies managed by uv."
55
requires-python = ">=3.14"
66
dependencies = [
77
"apify>=3.0.0,<4.0.0",
8-
"beautifulsoup4[lxml]>=4.0.0,<5.0.0",
9-
"httpx>=0.28.0,<1.0.0",
108
]
119

1210
[dependency-groups]
1311
# Development-only dependencies. They are installed by `uv sync` locally,
1412
# but skipped in the Actor's Docker image thanks to `uv sync --no-dev`.
1513
dev = [
16-
"types-beautifulsoup4>=4.0.0,<5.0.0",
14+
"pytest>=9.1.1",
15+
"pytest-asyncio>=1.4.0",
16+
"ruff>=0.15.21",
17+
"ty>=0.0.58",
1718
]
1819

1920
[tool.uv]
2021
# The Actor runs straight from the source tree as a module (`python -m my_actor`).
2122
# uv only manages its dependencies, the project itself is not built and installed
2223
# as a package.
2324
package = false
25+
26+
[tool.ruff]
27+
line-length = 120
28+
29+
[tool.ruff.format]
30+
# Match the single-quote style used across the Apify Python codebase.
31+
quote-style = "single"

0 commit comments

Comments
 (0)