Skip to content

Commit cbad4a9

Browse files
authored
Merge pull request #100 from hotosm/build/no-requirements-txt
Remove deprecated stdlib `cgi` usage, replace pipenv with uv, bundle requirements in pyproject.toml
2 parents d4ea8d3 + cf75949 commit cbad4a9

7 files changed

Lines changed: 1437 additions & 36 deletions

File tree

.github/workflows/continuous_integration.yml

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ jobs:
3333
name: Run unit tests & build
3434
strategy:
3535
matrix:
36-
python_version: ["3.8", "3.9", "3.10", "3.11", "3.12"]
36+
python_version: ["3.9", "3.10", "3.11", "3.12"]
3737
os: [ubuntu-latest]
3838

3939
runs-on: ${{ matrix.os }}
@@ -48,8 +48,8 @@ jobs:
4848
python-version: ${{ matrix.python_version }}
4949
cache: 'pip'
5050

51-
- name: Install dependencies
52-
run: pip install -r requirements.txt
51+
- name: Install package and dependencies
52+
run: pip install .
5353

5454
- name: Run unit tests
5555
run: ENVIRONMENT=test python -m unittest discover

README.md

Lines changed: 17 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -80,11 +80,11 @@ Contributions are more than welcome!
8080
Code style done with [precommit](https://pre-commit.com/).
8181

8282
```
83-
pip install pre-commit
83+
uv sync --group dev
8484
# if you want to have git commits trigger pre-commit, install pre-commit hook:
85-
pre-commit install
85+
uv run pre-commit install
8686
# else run manually before (re)staging your files:
87-
pre-commit run --all-files
87+
uv run pre-commit run --all-files
8888
```
8989

9090
### Cloning the project
@@ -94,24 +94,21 @@ One time action to clone and setup:
9494
```shell
9595
git clone https://github.com/opengisch/qfieldcloud-sdk-python
9696
cd qfieldcloud-sdk-python
97-
# install dev dependencies
98-
python3 -m pip install pipenv
99-
pre-commit install
100-
# install package in a virtual environment
101-
pipenv install -r requirements.txt
97+
# install dev dependencies (+ standard dependencies)
98+
uv sync --group dev
99+
uv run pre-commit install
102100
```
103101

104102
To run CLI interface for development purposes execute:
105103

106104
```shell
107-
pipenv shell # if your pipenv virtual environment is not active yet
108-
python -m qfieldcloud_sdk
105+
uv run python -m qfieldcloud_sdk
109106
```
110107

111108
To ease development, you can set a `.env` file. Therefore you can use directly the `qfieldcloud-cli` executable:
112109
```
113110
cp .env.example .env
114-
pipenv run qfieldcloud-cli
111+
uv run qfieldcloud-cli
115112
```
116113

117114
### Building the package
@@ -120,12 +117,15 @@ pipenv run qfieldcloud-cli
120117
# make sure your shell is sourced to no virtual environment
121118
deactivate
122119
# build
123-
python3 -m build
124-
# now either activate your shell with
125-
pipenv shell
120+
uv run python -m build
126121
# and install with
127-
python -m pip install . --force-reinstall
128-
# or manually ensure it's pipenv and not your global pip doing the installation
129-
pipenv run pip install . --force-reinstall
122+
uv run python -m pip install . --force-reinstall
130123
```
131124
Voilà!
125+
126+
### Running the tests
127+
128+
```shell
129+
uv sync --group dev
130+
ENVIRONMENT=test uv run python -m unittest discover -s tests
131+
```

pyproject.toml

Lines changed: 19 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ requires = [
88
build-backend = "setuptools.build_meta"
99

1010
[project]
11-
requires-python = ">=3.8"
11+
requires-python = ">=3.9"
1212
name = "qfieldcloud-sdk"
1313
description = "The official QFieldCloud SDK and CLI."
1414
authors = [
@@ -23,15 +23,25 @@ classifiers = [
2323
"Intended Audience :: Information Technology",
2424
"Operating System :: OS Independent",
2525
"Programming Language :: Python :: 3",
26-
"Programming Language :: Python :: 3.8",
2726
"Programming Language :: Python :: 3.9",
2827
"Programming Language :: Python :: 3.10",
2928
"Programming Language :: Python :: 3.11",
3029
"Programming Language :: Python :: 3.12",
3130
"Programming Language :: Python :: Implementation :: CPython",
3231
"Topic :: Scientific/Engineering :: GIS",
3332
]
34-
dynamic = ["dependencies", "readme", "version"]
33+
dependencies = [
34+
"certifi>=2023.7.22",
35+
"charset-normalizer>=3.2.0",
36+
"click>=8.1.5",
37+
"idna>=3.4",
38+
"requests>=2.31.0",
39+
"requests-toolbelt>=1.0.0",
40+
"tqdm>=4.65.0",
41+
"urllib3>=2.0.7",
42+
"pathvalidate>=3.2.1",
43+
]
44+
dynamic = ["readme", "version"]
3545

3646
[project.optional-dependencies]
3747
docs = [
@@ -41,6 +51,12 @@ docs = [
4151
"fancyboxmd~=1.1"
4252
]
4353

54+
[dependency-groups]
55+
dev = [
56+
"build>=1.2.2",
57+
"pre-commit==4.2.0",
58+
]
59+
4460
[project.scripts]
4561
qfieldcloud-cli = "qfieldcloud_sdk.cli:cli"
4662

@@ -55,7 +71,6 @@ packages = ["qfieldcloud_sdk"]
5571

5672
[tool.setuptools.dynamic]
5773
readme = {file = ["README.md"], content-type = "text/markdown"}
58-
dependencies = { file = ["requirements.txt"] }
5974

6075
[tool.setuptools-git-versioning]
6176
enabled = true

qfieldcloud_sdk/sdk.py

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,12 +5,12 @@
55
import sys
66
import requests
77
import urllib3
8-
import cgi
98

109
from enum import Enum
1110
from pathlib import Path
1211
from typing import Any, Callable, Dict, List, Optional, TypedDict, Union, cast
1312
from urllib import parse as urlparse
13+
from email.message import Message
1414

1515
from requests.adapters import HTTPAdapter, Retry
1616
from requests_toolbelt.multipart.encoder import MultipartEncoderMonitor
@@ -456,8 +456,7 @@ def get_project_seed_xlsform(
456456

457457
return None
458458

459-
_value, params = cgi.parse_header(content_disposition)
460-
filename = params.get("filename")
459+
filename = self._get_filename_from_content_disposition(content_disposition)
461460

462461
if not filename:
463462
logger.warning(
@@ -471,6 +470,14 @@ def get_project_seed_xlsform(
471470

472471
return str(path)
473472

473+
@staticmethod
474+
def _get_filename_from_content_disposition(
475+
content_disposition: str,
476+
) -> Optional[str]:
477+
message = Message()
478+
message["Content-Disposition"] = content_disposition
479+
return cast(Optional[str], message.get_filename())
480+
474481
def list_remote_files(
475482
self, project_id: str, skip_metadata: bool = True
476483
) -> List[Dict[str, Any]]:

requirements.txt

Lines changed: 0 additions & 9 deletions
This file was deleted.

tests/test_cli_client.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,17 @@ def test_paginated_list_projects_include_public(self):
2121
)
2222
self.assertTrue(0 < len(results) and len(results) <= 50)
2323

24+
def test_parse_content_disposition_filename(self):
25+
filename = Client._get_filename_from_content_disposition(
26+
'attachment; filename="seed.xlsx"'
27+
)
28+
self.assertEqual(filename, "seed.xlsx")
29+
30+
encoded_filename = Client._get_filename_from_content_disposition(
31+
"attachment; filename*=UTF-8''my%20seed.xlsx"
32+
)
33+
self.assertEqual(encoded_filename, "my seed.xlsx")
34+
2435

2536
class TestCLI(unittest.TestCase):
2637
@classmethod

0 commit comments

Comments
 (0)