Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
41 changes: 41 additions & 0 deletions .github/actions/test-and-coverage/action.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
name: Test and coverage
description: Set up Python, run checks, and upload coverage to Codecov.

inputs:
codecov-token:
description: Token used to upload coverage to Codecov.
required: true

runs:
using: composite
steps:
- name: 🐍 Set up Python
uses: actions/setup-python@v6.2.0
with:
python-version-file: .python-version
cache: pip
cache-dependency-path: pyproject.toml

- name: 📥 Install dependencies
shell: bash
run: |
sudo apt-get update
sudo apt-get install -y libkrb5-dev

- name: 🏗 Install module
shell: bash
run: pip install .[tests]

- name: 🧶 Lint
shell: bash
run: ruff check --output-format=github .

- name: 🧪 Run pytest
shell: bash
run: pytest

- name: ⬆️ Upload coverage to Codecov
uses: codecov/codecov-action@v7
with:
token: ${{ inputs.codecov-token }}
files: ./cov.xml
20 changes: 18 additions & 2 deletions .github/copilot-instructions.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,21 @@
# palletjack - Agent Onboarding Guide

## Commits
## Project Structure

Use conventional commits as outlined here: https://github.com/agrc/release-composite-action/blob/main/README.md#commits
Palletjack updates ArcGIS Online feature services from external data using an extract-transform-load workflow. Keep source-specific loading in `extract.py`, DataFrame cleanup and geocoding in `transform.py`, and ArcGIS service updates in `load.py`. Put shared helpers, such as retry behavior, in `utils.py`.

Preserve pandas DataFrame interfaces. Use type hints and Google-style docstrings for public APIs. Follow the existing module-level logging pattern: `logger = logging.getLogger(__name__)`.

## Python Environment and Validation

Use the Conda environment named `palletjack` for all Python commands, tests, and build validation. Python 3.11 through 3.14 are supported.

Run focused tests for changed behavior with `pytest tests/test_<module>.py`, then run `pytest` when the change affects shared or public behavior. Run `ruff check .` before completion. Do not introduce unconfigured formatters or type checkers.

Tests use `pytest-mock`; prefer `mocker.patch(..., autospec=True)` where practical. Use standard assertions and `pandas.testing` helpers when comparing DataFrames.

## Documentation and Commits

Keep public documentation in Google-style docstrings, which are rendered with pdoc3. Update README or docs examples when changing the public API.

Use conventional commits as outlined here: https://github.com/agrc/release-composite-action/blob/main/README.md#commits. Use a relevant scope when helpful, such as `fix(extract):`, `feat(load):`, or `docs:`.
28 changes: 3 additions & 25 deletions .github/workflows/pull_request.yml
Original file line number Diff line number Diff line change
Expand Up @@ -20,29 +20,7 @@ jobs:
with:
show-progress: false

- name: 🐍 Set up Python
uses: actions/setup-python@v6.2.0
- name: 🧪 Test and upload coverage
uses: ./.github/actions/test-and-coverage
with:
python-version-file: .python-version
cache: pip
cache-dependency-path: setup.py

- name: 📥 Install dependencies
run: |
sudo apt-get update
sudo apt-get install -y libkrb5-dev

- name: 🏗 Install module
run: pip install .[tests]

- name: 🧶 Lint
run: ruff check --output-format=github .

- name: 🧪 Run pytest
run: pytest

- name: ⬆️ Upload coverage to Codecov
uses: codecov/codecov-action@v7
with:
token: ${{ secrets.CODECOV_TOKEN }}
files: ./cov.xml
codecov-token: ${{ secrets.CODECOV_TOKEN }}
15 changes: 15 additions & 0 deletions .github/workflows/push.yml
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,22 @@
cancel-in-progress: true

jobs:
update-base-coverage:
name: Update base code coverage
runs-on: ubuntu-latest

steps:
- name: ⬇️ Set up code
uses: actions/checkout@v7
with:
show-progress: false

- name: 🧪 Test and upload coverage
uses: ./.github/actions/test-and-coverage
with:
codecov-token: ${{ secrets.CODECOV_TOKEN }}

release-please:

Check warning

Code scanning / CodeQL

Workflow does not contain permissions Medium

Actions job or workflow does not limit the permissions of the GITHUB_TOKEN. Consider setting an explicit permissions block, using the following as a minimal starting point: {contents: read}
name: Create release
runs-on: ubuntu-latest
permissions:
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ jobs:
with:
python-version-file: .python-version
cache: pip
cache-dependency-path: setup.py
cache-dependency-path: pyproject.toml

- name: 📦 Build package
run: pipx run build
Expand Down
17 changes: 11 additions & 6 deletions .vscode/settings.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
{
"cSpell.words": [
"addopts",
"agol",
"AGRC",
"arcgis",
Expand All @@ -8,6 +9,7 @@
"astype",
"authed",
"auths",
"autospec",
"caplog",
"casefolded",
"castable",
Expand Down Expand Up @@ -37,6 +39,7 @@
"Geocoder",
"geodatabase",
"geodataframe",
"geodatasets",
"geojsons",
"geopandas",
"getbuffer",
Expand All @@ -54,15 +57,19 @@
"itertuples",
"keyscan",
"knownhosts",
"libkrb",
"localpath",
"mapserv",
"minval",
"minversion",
"nans",
"ndarray",
"nojekyll",
"norecursedirs",
"oids",
"Overwriter",
"palletjack",
"paramiko",
"PARENTOBJECTID",
"pdoc",
"PGSQL",
Expand All @@ -74,33 +81,31 @@
"pyogrio",
"pypa",
"pypi",
"pyproject",
"pysftp",
"pytest",
"reclassifier",
"Reproject",
"sedf",
"setuptools",
"sftploader",
"SOQL",
"sqlalchemy",
"subsetted",
"subsetting",
"testpaths",
"trycount",
"ugrc",
"ujson",
"unclassed",
"unconfigured",
"upserted",
"upserting",
"upserts",
"webmap",
"wkid",
"yapf"
],
"coverage-gutters.highlightdark": "rgb(61, 153, 112, .05)",
"coverage-gutters.noHighlightDark": "rgb(255, 65, 54, .05)",
"coverage-gutters.partialHighlightDark": "rgb(255, 133, 27, .05)",
"coverage-gutters.showGutterCoverage": true,
"coverage-gutters.showLineCoverage": true,
"coverage-gutters.showRulerCoverage": false,
"editor.codeActionsOnSave": {
"source.organizeImports": "explicit"
},
Expand Down
14 changes: 7 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ Pallet jack: [forklift's](https://www.github.com/agrc/forklift) little brother.

## Dependencies

`palletjack` relies on the dependencies listed in `setup.py`. These are all available on PyPI and can be installed in most environments, including Google Cloud Functions.
`palletjack` relies on the dependencies listed in `pyproject.toml`. These are all available on PyPI and can be installed in most environments, including Google Cloud Functions.

The `arcgis` library does all the heavy lifting for spatial data. If the `arcpy` library is not available (such as in a cloud function), it relies on `shapely` for its geometry engine.

Expand All @@ -34,20 +34,20 @@ The `arcgis` library does all the heavy lifting for spatial data. If the `arcpy`

#: Load the data from a Google Sheet
gsheet_extractor = extract.GSheetLoader(path_to_service_account_json)
sheet_df = gsheet_extractor.load_specific_worksheet_into_dataframe(sheet_id, 'title of desired sheet', by_title=True)
sheet_df = gsheet_extractor.load_specific_worksheet_into_dataframe(sheet_id, "title of desired sheet", by_title=True)

#: Convert the data to points using lat/long fields, clean for uploading
spatial_df = pd.DataFrame.spatial.from_xy(input_df, x_column='longitude', y_column='latitude')
spatial_df = pd.DataFrame.spatial.from_xy(input_df, x_column="longitude", y_column="latitude")
renamed_df = transform.DataCleaning.rename_dataframe_columns_for_agol(spatial_df)
cleaned_df = transform.DataCleaning.switch_to_nullable_int(renamed_df, ['an_int_field_with_null_values'])
cleaned_df = transform.DataCleaning.switch_to_nullable_int(renamed_df, ["an_int_field_with_null_values"])

#: Truncate the existing feature service data and load the new data
gis = arcgis.gis.GIS('my_agol_org_url', 'username', 'super-duper-secure-password')
updater = load.ServiceUpdater(gis, 'feature_service_item_id')
gis = arcgis.gis.GIS("my_agol_org_url", "username", "super-duper-secure-password")
updater = load.ServiceUpdater(gis, "feature_service_item_id")
updates = updater.truncate_and_load(cleaned_df)

#: It even works with stand-alone tables!
table_updater = load.TableUpdater(gis, 'table_service_item_id', service_type='table')
table_updater = load.TableUpdater(gis, "table_service_item_id", service_type="table")
table_updates = table_updater.truncate_and_load(cleaned_df)
```

Expand Down
4 changes: 2 additions & 2 deletions docs/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -57,9 +57,9 @@ palletjack takes full advantage of python's built-in [`logging`](https://docs.py
The client can get a reference to the palletjack logger and add their handlers, formatters, etc to it alongside its own logger:

```python
myapp_logger = logging.getLogger('my_app')
myapp_logger = logging.getLogger("my_app")
myapp_logger.setLevel(logging.INFO)
palletjack_logger = logging.getLogger('palletjack')
palletjack_logger = logging.getLogger("palletjack")
palletjack_logger.setLevel(logging.INFO)
#: set up handlers and formatters
#: ...
Expand Down
3 changes: 2 additions & 1 deletion docs/examples.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
import arcgis
import pandas as pd
from arcgis.features import GeoAccessor, GeoSeriesAccessor

from palletjack import extract, load, transform, utils


Expand Down Expand Up @@ -122,5 +123,5 @@ def download_from_sftp_update_agol_reclassify_map():
#: Try to clean up the tempdir (we don't use a context manager); print any errors as a heads up
try:
tempdir.cleanup()
except Exception as error:
except OSError as error:
print(error)
55 changes: 55 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,8 +1,63 @@
[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"

[project]
name = "ugrc-palletjack"
version = "6.0.4"
description = "Updating AGOL feature services with data from external tables."
readme = { file = "README.md", content-type = "text/markdown" }
requires-python = ">=3.11,<3.15"
authors = [{ name = "UGRC Developers", email = "ugrc-developers@utah.gov" }]
classifiers = [
"Development Status :: 5 - Production/Stable",
"Intended Audience :: Developers",
"License :: OSI Approved :: MIT License",
"Topic :: Utilities",
]
keywords = ["gis"]
dependencies = [
"arcgis>=2.3,<2.4.3",
"geopandas>=0.14,<1.2",
"geodatasets>=2023.12,<=2026.5.1",
"pg8000>=1.29,<1.32",
"psycopg2-binary==2.9.*",
"pygsheets==2.0.*",
"pysftp==0.2.9",
"setuptools>=80,<83",
"paramiko>=3.0,<6.0",
"SQLAlchemy>=1.4,<2.1",
]

[project.urls]
Homepage = "https://github.com/agrc/palletjack"
"Issue Tracker" = "https://github.com/agrc/palletjack/issues"

[project.optional-dependencies]
tests = [
"pdoc3>=0.10,<0.12",
"pytest-cov>=3,<8",
"pytest-instafail~=0.5",
"pytest-mock>=3.10,<3.16",
"pytest-watch~=4.2",
"pytest>=6,<10",
"requests-mock==1.*",
"ruff==0.*",
]

[tool.hatch.build.targets.wheel]
packages = ["src/palletjack"]

[tool.hatch.build]
exclude = ["/.github", "/.vscode"]

[tool.ruff]
line-length = 120
lint.ignore = ["E501"]

[tool.ruff.lint.per-file-ignores]
"tests/*.py" = ["F841"]

[tool.pytest.ini_options]
minversion = "6.0"
testpaths = ["tests", "src"]
Expand Down
Loading