Switch from poetry to uv (#203)

* Switch from poetry to uv
* Suppress an info message (close #204)
* Fix end of line for markdown files
This commit is contained in:
Bo
2025-07-25 23:40:19 -05:00
committed by GitHub
parent 8103fa4ab2
commit 6b6408b02b
17 changed files with 2706 additions and 3112 deletions

View File

@@ -20,16 +20,15 @@ jobs:
- name: Install system deps
shell: bash
run: |
pip install poetry
poetry config virtualenvs.in-project true
poetry install --no-root --only dev --only linters --sync
pip install uv
uv sync --extra dev --extra linters
- name: Run autoupdate
run: poetry run pre-commit autoupdate
run: uv run pre-commit autoupdate
continue-on-error: true
- name: Run pre-commit
run: poetry run pre-commit run --all-files
run: uv run pre-commit run --all-files
- uses: peter-evans/create-pull-request@v7.0.8
with:

View File

@@ -23,12 +23,11 @@ jobs:
- name: Install system deps
shell: bash
run: |
pip install poetry
poetry config virtualenvs.in-project true
pip install uv
- name: Build package
run: |
poetry build --ansi
uv build
- name: Publish package on PyPI
uses: pypa/gh-action-pypi-publish@v1.12.4

View File

@@ -21,13 +21,12 @@ jobs:
- name: Install system deps
shell: bash
run: |
pip install poetry
poetry config virtualenvs.in-project true
poetry install --no-root --only dev --only linters --sync
pip install uv
uv sync --extra dev --extra linters
- name: Linting
shell: bash
run: poetry run pre-commit run --all-files
run: uv run pre-commit run --all-files
tests:
needs: linting
@@ -49,18 +48,17 @@ jobs:
- name: Install system deps
shell: bash
run: |
pip install nox-poetry==1.1.0
pip install poetry==1.8.5
poetry config virtualenvs.in-project true
pip install nox
pip install uv
- name: Run mypy with nox
shell: bash
run: nox --force-color -s mypy-${{ matrix.python-version }}
- name: Install Playwright Browsers
- name: Install dependencies and Playwright Browsers
run: |
pip install playwright
playwright install --with-deps # Ensures browsers and dependencies are installed
uv sync --extra dev --extra test
uv run playwright install --with-deps
- name: Run tests with nox
shell: bash
@@ -93,9 +91,8 @@ jobs:
# - name: Install system deps
# shell: bash
# run: |
# pip install nox-poetry==1.1.0
# pip install poetry==1.8.5
# poetry config virtualenvs.in-project true
# pip install nox==1.1.0
# pip install uv==1.8.5
# - name: Download coverage data
# uses: actions/download-artifact@v4.1.8

View File

@@ -35,8 +35,8 @@ repos:
rev: 5295f87c0e261da61a7b919fc754e3a77edd98a7
hooks:
- id: validate-cff
- repo: https://github.com/python-poetry/poetry
rev: 1.8.3
- repo: https://github.com/astral-sh/uv-pre-commit
# uv version.
rev: 0.7.19
hooks:
- id: poetry-check
- id: poetry-install
- id: uv-lock

View File

@@ -7,6 +7,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## [Unreleased]
## [0.9.6]
- Fix searching across regions.
- Switch from `poetry` to `uv` for development.
## [0.9.5]
- [issue 155](https://github.com/BoPeng/ai-marketplace-monitor/issues/155) Fix output of pushbullet

View File

@@ -13,10 +13,10 @@ We take our open source community seriously and hold ourselves and other contrib
### Requirements
We use `poetry` to manage and install dependencies. [Poetry](https://python-poetry.org/) provides a custom installer that will install `poetry` isolated from the rest of your system.
We use `uv` to manage and install dependencies. [uv](https://docs.astral.sh/uv/) is a fast Python package manager that can be installed with:
```
curl -sSL https://raw.githubusercontent.com/python-poetry/poetry/master/install-poetry.py | python -
pip install uv
```
We'll also need `nox` for automated testing in multiple Python environments so [install that too](https://nox.thea.codes/en/stable/).
@@ -24,11 +24,11 @@ We'll also need `nox` for automated testing in multiple Python environments so [
To install the local development requirements inside a virtual environment run:
```
$ poetry install
$ poetry run inv install-hooks
$ uv sync --all-extras
$ uv run inv install-hooks
```
> For more information about `poetry` check the [docs](https://python-poetry.org/docs/).
> For more information about `uv` check the [docs](https://docs.astral.sh/uv/).
We use [invoke](http://www.pyinvoke.org/) to wrap up some useful tasks like formatting, linting, testing and more.
@@ -59,7 +59,7 @@ git checkout dev
Then install the tool from source code with command
```sh
poetry install
uv sync
```
## Contributing

79
MIGRATION_TO_UV.md Normal file
View File

@@ -0,0 +1,79 @@
# Migration from Poetry to uv
This project has been migrated from Poetry to uv for faster dependency management and better performance.
## For Contributors
If you were previously contributing to this project using Poetry, here's how to migrate:
### 1. Remove Poetry artifacts
```bash
# Remove the old virtual environment (if using poetry's default location)
rm -rf .venv
# Remove poetry.lock (now replaced by uv.lock)
rm poetry.lock
```
### 2. Install uv
```bash
pip install uv
```
### 3. Set up the development environment
```bash
# Install all dependencies including development extras
uv sync --all-extras
# Install pre-commit hooks
uv run inv install-hooks
```
### 4. Common command translations
| Poetry Command | uv Equivalent |
| -------------------------------- | ----------------------------------------------------------------------------- |
| `poetry install` | `uv sync` |
| `poetry add package` | `uv add package` |
| `poetry add --group dev package` | `uv add --dev package` |
| `poetry run command` | `uv run command` |
| `poetry shell` | `source .venv/bin/activate` (Linux/Mac) or `.venv\Scripts\activate` (Windows) |
| `poetry build` | `uv build` |
| `poetry publish` | `uv publish` |
### 5. Running tasks
All invoke tasks now use uv instead of poetry:
```bash
# Run tests
uv run inv tests
# Format code
uv run inv format
# Run linting
uv run inv lint
# Type checking
uv run inv mypy
```
## For End Users
If you were installing the package from source using Poetry, now use:
```bash
git clone https://github.com/BoPeng/ai-marketplace-monitor
cd ai-marketplace-monitor
uv sync
```
The published package on PyPI remains the same:
```bash
pip install ai-marketplace-monitor
```
## Benefits of uv
- **Faster**: uv is significantly faster than Poetry for dependency resolution and installation
- **Better caching**: More efficient caching mechanism
- **Simpler**: Fewer configuration files and simpler setup
- **Standard**: Uses standard Python packaging (pyproject.toml) without Poetry-specific extensions

View File

@@ -128,7 +128,7 @@ playwright install
### Set up a notification method (optional)
If you would like to receive notification from your phone via PushBullet
If you would like to receive notification from your phone
- Sign up for [PushBullet](https://www.pushbullet.com/), [PushOver](https://pushover.net/) or [Ntfy](https://ntfy.sh/)
- Install the app on your phone

View File

@@ -3,19 +3,17 @@
import platform
import nox
from nox_poetry import Session, session
from nox import Session
nox.options.sessions = ["tests", "mypy"]
python_versions = ["3.10", "3.11", "3.12"]
@session(python=python_versions)
@nox.session(python=python_versions)
def tests(session: Session) -> None:
"""Run the test suite."""
session.install(".")
session.install(
"invoke", "pytest", "xdoctest", "coverage[toml]", "pytest-cov", "pytest-playwright"
)
session.run("uv", "sync", "--extra", "test", external=True)
session.install("invoke")
try:
session.run(
"inv",
@@ -29,24 +27,26 @@ def tests(session: Session) -> None:
session.notify("coverage")
@session(python=python_versions)
@nox.session(python=python_versions)
def coverage(session: Session) -> None:
"""Produce the coverage report."""
args = session.posargs if session.posargs and len(session._runner.manifest) == 1 else []
session.install("invoke", "coverage[toml]")
session.run("uv", "sync", "--extra", "test", external=True)
session.install("invoke")
session.run("inv", "coverage", *args)
@session(python=python_versions)
@nox.session(python=python_versions)
def mypy(session: Session) -> None:
"""Type-check using mypy."""
session.install(".")
session.install("invoke", "mypy")
session.run("uv", "sync", "--extra", "typing", external=True)
session.install("invoke")
session.run("inv", "mypy")
@session(python="3.12")
@nox.session(python="3.12")
def security(session: Session) -> None:
"""Scan dependencies for insecure packages."""
session.install("invoke", "safety")
session.run("uv", "sync", "--extra", "security", external=True)
session.install("invoke")
session.run("inv", "security")

2963
poetry.lock generated
View File

File diff suppressed because it is too large Load Diff

View File

@@ -1,12 +1,10 @@
[tool.poetry]
[project]
name = "ai-marketplace-monitor"
version = "0.9.5"
description = "An AI-based tool for monitoring facebook marketplace"
authors = ["Bo Peng <ben.bob@gmail.com>"]
authors = [{name = "Bo Peng", email = "ben.bob@gmail.com"}]
readme = "README.md"
homepage = "https://github.com/BoPeng/ai-marketplace-monitor"
repository = "https://github.com/BoPeng/ai-marketplace-monitor"
documentation = "https://ai-marketplace-monitor.readthedocs.io"
requires-python = ">=3.10"
keywords = ["ai-marketplace-monitor"]
classifiers = [
"Development Status :: 2 - Pre-Alpha",
@@ -14,66 +12,72 @@ classifiers = [
"License :: OSI Approved :: GNU Affero General Public License v3",
"Natural Language :: English",
"Programming Language :: Python :: 3",
"Programming Language :: Python :: 3.9",
"Programming Language :: Python :: 3.10",
"Programming Language :: Python :: 3.11",
"Programming Language :: Python :: 3.12",
]
dependencies = [
"typer>=0.15.1,<0.17.0",
"playwright>=1.41.0",
"rich>=13.7.0",
"pushbullet.py>=0.12.0",
"diskcache>=5.6.3",
"watchdog>=4.0.0",
"openai>=1.24.0",
"parsedatetime>=2.5",
"humanize>=4.0.0",
"schedule>=1.2.2",
"inflect>=7.0.0",
"pynput>=1.7.0",
"pillow>=10.0.0",
"jinja2>=3.0.0",
"pyparsing>=3.1.0",
"requests>=2.30.0",
"CurrencyConverter>=0.18.0",
"tomli==2.2.1; python_version < '3.11'",
"safety>=3.5.2",
"pip-audit>=2.9.0",
]
[tool.poetry.urls]
[project.urls]
Homepage = "https://github.com/BoPeng/ai-marketplace-monitor"
Repository = "https://github.com/BoPeng/ai-marketplace-monitor"
Documentation = "https://ai-marketplace-monitor.readthedocs.io"
"Bug Tracker" = "https://github.com/BoPeng/ai-marketplace-monitor/issues"
[tool.poetry.scripts]
ai-marketplace-monitor = 'ai_marketplace_monitor.cli:app'
[project.scripts]
ai-marketplace-monitor = "ai_marketplace_monitor.cli:app"
[tool.poetry.dependencies]
python = ">=3.10"
typer = { extras = ["all"], version = ">=0.15.1,<0.17.0" }
playwright = ">=1.41.0"
rich = ">=13.7.0"
"pushbullet.py" = ">=0.12.0"
diskcache = ">=5.6.3"
watchdog = ">=4.0.0"
openai = ">=1.24.0"
parsedatetime = ">=2.5"
humanize = ">=4.0.0"
schedule = ">=1.2.2"
inflect = ">=7.0.0"
pynput = ">=1.7.0"
pillow = ">=10.0.0"
jinja2 = ">=3.0.0"
pyparsing = ">=3.1.0"
requests = ">=2.30.0"
CurrencyConverter = ">=0.18.0"
tomli = { version = "2.2.1", markers = "python_version < '3.11'" }
[tool.poetry.group.dev.dependencies]
pre-commit = "^4.0.1"
invoke = "^2.2.0"
bump2version = "^1.0.1"
watchdog = { version = "^6.0.0", extras = ["watchmedo"] }
[tool.poetry.group.test.dependencies]
pytest = "^8.3.3"
xdoctest = "^1.2.0"
coverage = { version = "^7.6.7", extras = ["toml"] }
pytest-cov = "^6.0.0"
pytest-playwright = "^0.7.0"
[tool.poetry.group.linters.dependencies]
isort = ">=5.13.2,<7.0.0"
black = ">=24.10,<26.0"
ruff = ">=0.9.2,<0.13.0"
[tool.poetry.group.security.dependencies]
safety = "^3.2.11"
[tool.poetry.group.typing.dependencies]
mypy = "^1.13.0"
[tool.poetry.group.docs.dependencies]
sphinx = "^8.1.3"
recommonmark = "^0.7.1"
[project.optional-dependencies]
dev = [
"pre-commit>=4.0.1",
"invoke>=2.2.0",
"bump2version>=1.0.1",
"watchdog[watchmedo]>=6.0.0",
"pip-audit>=2.9.0",
]
test = [
"pytest>=8.3.3",
"xdoctest>=1.2.0",
"coverage[toml]>=7.6.7",
"pytest-cov>=6.0.0",
"pytest-playwright>=0.7.0",
]
linters = [
"isort>=5.13.2,<7.0.0",
"black>=24.10,<26.0",
"ruff>=0.9.2,<0.13.0",
]
security = [
"safety>=3.2.11",
]
typing = [
"mypy>=1.13.0",
]
docs = [
"sphinx>=8.1.3",
"recommonmark>=0.7.1",
]
[tool.coverage.paths]
source = ["src", "*/site-packages"]
@@ -175,7 +179,7 @@ include_trailing_comma = true
force_grid_wrap = 0
use_parentheses = true
line_length = 99
known_third_party = ["invoke", "nox", "nox_poetry"]
known_third_party = ["invoke", "nox"]
[tool.black]
line-length = 99
@@ -186,7 +190,7 @@ warn_return_any = false
warn_unused_configs = true
[[tool.mypy.overrides]]
module = ["pytest.*", "invoke.*", "nox.*", "nox_poetry.*"]
module = ["pytest.*", "invoke.*", "nox.*"]
allow_redefinition = false
check_untyped_defs = true
ignore_errors = false
@@ -201,5 +205,5 @@ warn_unreachable = true
warn_no_return = true
[build-system]
requires = ["poetry>=0.12"]
build-backend = "poetry.masonry.api"
requires = ["hatchling"]
build-backend = "hatchling.build"

View File

@@ -271,7 +271,7 @@ class OpenAIBackend(AIBackend):
res: AIResponse | None = AIResponse.from_cache(listing, item_config, marketplace_config)
if res is not None:
if self.logger:
self.logger.info(
self.logger.debug(
f"""{hilight("[AI]", res.style)} {self.config.name} previously concluded {hilight(f"{res.conclusion} ({res.score}): {res.comment}", res.style)} for listing {hilight(listing.title)}."""
)
return res

View File

@@ -321,6 +321,7 @@ locale = "Spanish"
'Location is approximate' = 'La ubicación es aproximada'
"About this vehicle" = 'Descripción del vendedor'
"Seller's description" = 'Información sobre este vehículo'
"Browse Marketplace" = 'Explorar Marketplace'
[translation.zh]
locale = "Chinese"
@@ -331,3 +332,15 @@ locale = "Chinese"
'Location is approximate' = '我们只提供大概位置'
"About this vehicle" = "车辆信息"
"Seller's description" = "卖家描述"
"Browse Marketplace" = '浏览 Marketplace'
[translation.sv]
locale = "Swedish"
'Collection of Marketplace items' = 'Samling av Marketplace-objekt'
'Condition' = 'Skick'
'Description' = 'Beskrivning'
'Details' = 'Detaljer'
'Location is approximate' = 'Platsen är ungefärlig'
"About this vehicle" = "Om detta fordon"
"Seller's description" = "Säljarens beskrivning"
"Browse Marketplace" = 'Bläddra på Marketplace'

View File

@@ -473,30 +473,19 @@ class FacebookMarketplace(Marketplace):
f"""{hilight(item_config.name)} from {hilight(cname or city)}"""
+ (f" with radius={radius}" if radius else " with default radius")
)
retries = 0
while True:
self.goto_url(
marketplace_url + "&".join([f"query={quote(search_phrase)}", *options])
)
found_listings = FacebookSearchResultPage(
self.page, self.translator, self.logger
).get_listings()
time.sleep(5)
if found_listings:
break
if retries > 5:
if self.logger:
self.logger.error(
f"""{hilight("[Search]", "fail")} Failed to get search results for {search_phrase}"""
)
break
else:
retries += 1
if self.logger:
self.logger.debug(
f"""{hilight("[Search]", "info")} Retrying to get search results for {search_phrase}"""
)
self.goto_url(
marketplace_url + "&".join([f"query={quote(search_phrase)}", *options])
)
found_listings = FacebookSearchResultPage(
self.page, self.translator, self.logger
).get_listings()
time.sleep(5)
if self.logger:
self.logger.error(
f"""{hilight("[Search]", "fail")} Failed to get search results for {search_phrase} from {city}"""
)
counter.increment(CounterItem.SEARCH_PERFORMED, item_config.name)
@@ -688,6 +677,19 @@ class FacebookSearchResultPage(WebPage):
return valid_listings
def get_listings(self: "FacebookSearchResultPage") -> List[Listing]:
# if no result is found
btn = self.page.locator(f"""span:has-text('{self.translator("Browse Marketplace")}')""")
if btn.count() > 0:
if self.logger:
msg = self._parent_with_cond(
btn.first,
lambda x: len(x) == 3
and self.translator("Browse Marketplace") in (x[-1].text_content() or ""),
1,
)
self.logger.info(f'{hilight("[Retrieve]", "dim")} {msg}')
return []
# find the grid box
try:
valid_listings = (
@@ -1138,6 +1140,6 @@ def parse_listing(
except KeyboardInterrupt:
raise
except Exception:
# try next page layout
# try next page ayout
continue
return None

View File

@@ -23,7 +23,7 @@ class RegionConfig(BaseConfig):
def handle_radius(self: "RegionConfig") -> None:
if isinstance(self.radius, int):
self.radius = [self.radius]
self.radius = [self.radius] * len(self.search_city)
elif not self.radius:
self.radius = [500] * len(self.search_city)
elif len(self.radius) != len(self.search_city):

View File

@@ -3,7 +3,9 @@
Execute 'invoke --list' for guidance on using Invoke
"""
import os
import platform
import tempfile
import webbrowser
from pathlib import Path
from typing import Optional
@@ -76,42 +78,49 @@ def clean(c: Context) -> None:
@task()
def install_hooks(c: Context) -> None:
"""Install pre-commit hooks."""
_run(c, "poetry run pre-commit install")
_run(c, "uv run pre-commit install")
@task()
def hooks(c: Context) -> None:
"""Run pre-commit hooks."""
_run(c, "poetry run pre-commit run --all-files")
_run(c, "uv run pre-commit run --all-files")
@task(name="format", help={"check": "Checks if source is formatted without applying changes"})
def format_(c: Context, check: bool = False) -> None:
"""Format code."""
isort_options = ["--check-only", "--diff"] if check else []
_run(c, f"poetry run isort {' '.join(isort_options)} {PYTHON_TARGETS_STR}")
_run(c, f"uv run isort {' '.join(isort_options)} {PYTHON_TARGETS_STR}")
black_options = ["--diff", "--check"] if check else ["--quiet"]
_run(c, f"poetry run black {' '.join(black_options)} {PYTHON_TARGETS_STR}")
_run(c, f"uv run black {' '.join(black_options)} {PYTHON_TARGETS_STR}")
@task()
def ruff(c: Context) -> None:
"""Run ruff."""
_run(c, f"poetry run ruff check {PYTHON_TARGETS_STR}")
_run(c, f"uv run ruff check {PYTHON_TARGETS_STR}")
@task()
def security(c: Context) -> None:
"""Run security related checks."""
# _run(
# c,
# "poetry export --with dev --format=requirements.txt --without-hashes | "
# "poetry run safety check --stdin --full-report",
# )
with tempfile.NamedTemporaryFile(mode="w", suffix=".txt", delete=False) as f:
temp_file = f.name
try:
_run(c, f"uv export --extra dev --format requirements-txt --no-hashes > {temp_file}")
_run(
c,
f"uv run pip-audit --requirement {temp_file} --format json",
)
finally:
# Clean up
if os.path.exists(temp_file):
os.unlink(temp_file)
return None
@task(pre=[ruff, security, call(format_, check=True)])
@task(pre=[ruff, call(format_, check=True)])
def lint(c: Context) -> None:
"""Run all linting."""
@@ -119,14 +128,14 @@ def lint(c: Context) -> None:
@task()
def mypy(c: Context) -> None:
"""Run mypy."""
_run(c, f"poetry run mypy {PYTHON_TARGETS_STR}")
_run(c, f"uv run mypy {PYTHON_TARGETS_STR}")
@task()
def tests(c: Context) -> None:
"""Run tests."""
pytest_options = ["--xdoctest", "--cov", "--cov-report=", "--cov-fail-under=0"]
_run(c, f"poetry run pytest {' '.join(pytest_options)} {TEST_DIR} {SOURCE_DIR}")
_run(c, f"uv run pytest {' '.join(pytest_options)} {TEST_DIR} {SOURCE_DIR}")
@task(
@@ -138,8 +147,8 @@ def tests(c: Context) -> None:
def coverage(c: Context, fmt: str = "report", open_browser: bool = False) -> None:
"""Create coverage report."""
if any(Path().glob(".coverage.*")):
_run(c, "poetry run coverage combine")
_run(c, f"poetry run coverage {fmt} -i")
_run(c, "uv run coverage combine")
_run(c, f"uv run coverage {fmt} -i")
if fmt == "html" and open_browser:
webbrowser.open(COVERAGE_REPORT.as_uri())
@@ -158,7 +167,7 @@ def docs(c: Context, serve: bool = False, open_browser: bool = False) -> None:
if open_browser:
webbrowser.open(DOCS_INDEX.absolute().as_uri())
if serve:
_run(c, f"poetry run watchmedo shell-command -p '*.rst;*.md' -c '{build_docs}' -R -D .")
_run(c, f"uv run watchmedo shell-command -p '*.rst;*.md' -c '{build_docs}' -R -D .")
@task(
@@ -170,4 +179,4 @@ def docs(c: Context, serve: bool = False, open_browser: bool = False) -> None:
def version(c: Context, part: str, dry_run: bool = False) -> None:
"""Bump version."""
bump_options = ["--dry-run"] if dry_run else []
_run(c, f"poetry run bump2version {' '.join(bump_options)} {part}")
_run(c, f"uv run bump2version {' '.join(bump_options)} {part}")

2450
uv.lock generated Normal file
View File

File diff suppressed because it is too large Load Diff