From dcfc51347f7ddfb419f26bc3ccc5012c99f07abd Mon Sep 17 00:00:00 2001 From: lindsay stevens Date: Fri, 1 Nov 2024 22:06:39 +1100 Subject: [PATCH] chg: update dependencies, add job to test older pydantic - pydantic dependency assumes latest version in the range. - added actions job tests with the lower bound as well. - probably most likely package to cause conflicts for users. - requests not tested this way because often its releases are for security patches. - e.g. 2.32.0 and 2.32.1 were yanked for security reasons - includes formatting changes via ruff update --- .github/workflows/verify.yml | 7 +++++ docs/examples/2022-10-pyodk-webinar.ipynb | 17 +++++----- docs/examples/basic-analysis-pandas.ipynb | 36 +++++++++++++--------- docs/examples/beyond-library-methods.ipynb | 6 ++-- docs/examples/working-with-repeats.ipynb | 30 +++++++++++------- pyodk/_endpoints/auth.py | 2 +- pyodk/_utils/session.py | 2 +- pyproject.toml | 8 ++--- 8 files changed, 66 insertions(+), 42 deletions(-) diff --git a/.github/workflows/verify.yml b/.github/workflows/verify.yml index ad84ec7..82da1f4 100644 --- a/.github/workflows/verify.yml +++ b/.github/workflows/verify.yml @@ -41,6 +41,12 @@ jobs: matrix: python: ['3.12'] os: [ubuntu-latest, macos-latest, windows-latest] + pydantic: ['true'] + # Test pydantic at lower boundary of requirement compatibility spec. + include: + - python: '3.12' + os: ubuntu-latest + pydantic: 'pip install pydantic==2.6.4' steps: - uses: actions/checkout@v4 - name: Set up Python @@ -58,6 +64,7 @@ jobs: - name: Install dependencies. run: | python -m pip install --upgrade pip + ${{ matrix.pydantic }} pip install -e .[dev,docs] pip list diff --git a/docs/examples/2022-10-pyodk-webinar.ipynb b/docs/examples/2022-10-pyodk-webinar.ipynb index 5e29954..1504f69 100644 --- a/docs/examples/2022-10-pyodk-webinar.ipynb +++ b/docs/examples/2022-10-pyodk-webinar.ipynb @@ -371,10 +371,11 @@ } ], "source": [ - "json = client.submissions.get_table(form_id='participants')['value']\n", - "\n", "import pandas as pd\n", - "df = pd.json_normalize(json, sep='-')\n", + "\n", + "json = client.submissions.get_table(form_id=\"participants\")[\"value\"]\n", + "\n", + "df = pd.json_normalize(json, sep=\"-\")\n", "df.head(3)" ] }, @@ -416,7 +417,7 @@ } ], "source": [ - "df.pets.value_counts().plot(kind='pie')" + "df.pets.value_counts().plot(kind=\"pie\")" ] }, { @@ -445,8 +446,8 @@ } ], "source": [ - "df['height_code'] = df.height_units.astype('category').cat.codes\n", - "df['pets_code'] = df.pets.astype('category').cat.codes\n", + "df[\"height_code\"] = df.height_units.astype(\"category\").cat.codes\n", + "df[\"pets_code\"] = df.pets.astype(\"category\").cat.codes\n", "df.pets_code.corr(df.height_code)" ] }, @@ -529,7 +530,7 @@ } ], "source": [ - "df['__system-submitterName'].value_counts().plot(kind='bar', rot=45)" + "df[\"__system-submitterName\"].value_counts().plot(kind=\"bar\", rot=45)" ] }, { @@ -547,7 +548,7 @@ "metadata": {}, "outputs": [], "source": [ - "df = df.drop(df.filter(regex = 'note_'), axis = 1)" + "df = df.drop(df.filter(regex=\"note_\"), axis=1)" ] }, { diff --git a/docs/examples/basic-analysis-pandas.ipynb b/docs/examples/basic-analysis-pandas.ipynb index e14bd84..e7949ea 100644 --- a/docs/examples/basic-analysis-pandas.ipynb +++ b/docs/examples/basic-analysis-pandas.ipynb @@ -36,8 +36,8 @@ "metadata": {}, "outputs": [], "source": [ - "from pyodk.client import Client\n", - "import pandas as pd" + "import pandas as pd\n", + "from pyodk.client import Client" ] }, { @@ -228,9 +228,9 @@ ], "source": [ "with Client() as client:\n", - " submissions = client.submissions.get_table(form_id='fav_color')\n", - " df = pd.json_normalize(data=submissions['value'], sep='/')\n", - " \n", + " submissions = client.submissions.get_table(form_id=\"fav_color\")\n", + " df = pd.json_normalize(data=submissions[\"value\"], sep=\"/\")\n", + "\n", "df.head(3)" ] }, @@ -272,9 +272,11 @@ } ], "source": [ - "colors = {'g':'Green', 'o':'Orange', 'r':'Red', 'y':'Yellow'}\n", - "df['favorite_color_labels'] = df['favorite_color'].map(colors)\n", - "df['favorite_color_labels'].value_counts().plot(kind='bar', title='Count of favorite colors', xlabel=\"Color\", ylabel=\"Count\", rot=0)" + "colors = {\"g\": \"Green\", \"o\": \"Orange\", \"r\": \"Red\", \"y\": \"Yellow\"}\n", + "df[\"favorite_color_labels\"] = df[\"favorite_color\"].map(colors)\n", + "df[\"favorite_color_labels\"].value_counts().plot(\n", + " kind=\"bar\", title=\"Count of favorite colors\", xlabel=\"Color\", ylabel=\"Count\", rot=0\n", + ")" ] }, { @@ -313,7 +315,9 @@ } ], "source": [ - "df['__system/reviewState'].value_counts().plot(kind='pie', title='Submission review state', ylabel='', rot=0)" + "df[\"__system/reviewState\"].value_counts().plot(\n", + " kind=\"pie\", title=\"Submission review state\", ylabel=\"\", rot=0\n", + ")" ] }, { @@ -357,13 +361,17 @@ "import geopandas\n", "from geopandas import GeoDataFrame\n", "\n", - "world = geopandas.read_file(geopandas.datasets.get_path('naturalearth_lowres'))\n", - "base = world.plot(color='lightgrey', figsize=(15, 10))\n", + "world = geopandas.read_file(geopandas.datasets.get_path(\"naturalearth_lowres\"))\n", + "base = world.plot(color=\"lightgrey\", figsize=(15, 10))\n", "base.set_axis_off()\n", "\n", - "locations = pd.DataFrame(df['location/coordinates'].dropna().to_list(), columns=['x', 'y', 'alt'])\n", - "geodf = GeoDataFrame(locations, geometry=geopandas.points_from_xy(locations['x'], locations['y']))\n", - "geodf.plot(ax=base, marker='o', color='blue', markersize=15)" + "locations = pd.DataFrame(\n", + " df[\"location/coordinates\"].dropna().to_list(), columns=[\"x\", \"y\", \"alt\"]\n", + ")\n", + "geodf = GeoDataFrame(\n", + " locations, geometry=geopandas.points_from_xy(locations[\"x\"], locations[\"y\"])\n", + ")\n", + "geodf.plot(ax=base, marker=\"o\", color=\"blue\", markersize=15)" ] }, { diff --git a/docs/examples/beyond-library-methods.ipynb b/docs/examples/beyond-library-methods.ipynb index a618ec5..44aad90 100644 --- a/docs/examples/beyond-library-methods.ipynb +++ b/docs/examples/beyond-library-methods.ipynb @@ -168,7 +168,7 @@ } ], "source": [ - "response = client.get('/projects/7/forms/favorite_color/versions/2012060405.xlsx')\n", + "response = client.get(\"/projects/7/forms/favorite_color/versions/2012060405.xlsx\")\n", "response.content[:150]" ] }, @@ -188,7 +188,7 @@ "outputs": [], "source": [ "if response.status_code == 200:\n", - " with open('2012060405.xlsx', 'wb') as f:\n", + " with open(\"2012060405.xlsx\", \"wb\") as f:\n", " f.write(response.content)" ] }, @@ -245,7 +245,7 @@ } ], "source": [ - "r = client.post('/projects/7/app-users', json={'displayName': 'Lab Tech'})\n", + "r = client.post(\"/projects/7/app-users\", json={\"displayName\": \"Lab Tech\"})\n", "r.json()" ] }, diff --git a/docs/examples/working-with-repeats.ipynb b/docs/examples/working-with-repeats.ipynb index c2bb55e..e2ef5a0 100644 --- a/docs/examples/working-with-repeats.ipynb +++ b/docs/examples/working-with-repeats.ipynb @@ -31,11 +31,13 @@ "metadata": {}, "outputs": [], "source": [ - "from pyodk.client import Client\n", "import pandas as pd\n", + "from pyodk.client import Client\n", "\n", "client = Client().open()\n", - "submissions = client.submissions.get_table(form_id='simple_repeat', filter=\"__system/reviewState ne 'rejected'\")" + "submissions = client.submissions.get_table(\n", + " form_id=\"simple_repeat\", filter=\"__system/reviewState ne 'rejected'\"\n", + ")" ] }, { @@ -195,7 +197,7 @@ } ], "source": [ - "subs_df = pd.json_normalize(data=submissions['value'], sep='/')\n", + "subs_df = pd.json_normalize(data=submissions[\"value\"], sep=\"/\")\n", "subs_df.head(3)" ] }, @@ -288,8 +290,12 @@ } ], "source": [ - "repeats = client.submissions.get_table(form_id='simple_repeat', table_name='Submissions.observation', filter=\"__system/reviewState ne 'rejected'\")\n", - "obs_df = pd.json_normalize(data=repeats['value'], sep='/')\n", + "repeats = client.submissions.get_table(\n", + " form_id=\"simple_repeat\",\n", + " table_name=\"Submissions.observation\",\n", + " filter=\"__system/reviewState ne 'rejected'\",\n", + ")\n", + "obs_df = pd.json_normalize(data=repeats[\"value\"], sep=\"/\")\n", "obs_df.head(3)" ] }, @@ -511,7 +517,7 @@ } ], "source": [ - "subs_df.set_index('__id', inplace=True)\n", + "subs_df.set_index(\"__id\", inplace=True)\n", "joined_df = obs_df.join(subs_df, on=\"__Submissions-id\")\n", "joined_df.head(4)" ] @@ -629,7 +635,7 @@ } ], "source": [ - "grouped = joined_df.groupby(['name', 'thing']).sum('count')[['count']]\n", + "grouped = joined_df.groupby([\"name\", \"thing\"]).sum(\"count\")[[\"count\"]]\n", "grouped" ] }, @@ -651,7 +657,7 @@ } ], "source": [ - "ax = grouped.unstack(level=0).plot(kind='bar')\n", + "ax = grouped.unstack(level=0).plot(kind=\"bar\")\n", "ax.set_xlabel(\"Sighting\")\n", "ax.set_ylabel(\"Total count\")\n", "ax.legend().set_title(\"Data collector\")" @@ -842,12 +848,14 @@ "\n", "client = Client().open()\n", "# The expand option includes all repeats in the json returned by get_table\n", - "submissions = client.submissions.get_table(form_id='simple_repeat', expand='*', filter=\"__system/reviewState ne 'rejected'\")\n", + "submissions = client.submissions.get_table(\n", + " form_id=\"simple_repeat\", expand=\"*\", filter=\"__system/reviewState ne 'rejected'\"\n", + ")\n", "# Flatten all repeats for each submission returned\n", - "df = pd.DataFrame([flatten_json(x) for x in submissions['value']])\n", + "df = pd.DataFrame([flatten_json(x) for x in submissions[\"value\"]])\n", "\n", "# Drop system columns so we can see more of the repeats\n", - "df.drop(list(df.filter(regex = '__system')), axis = 1, inplace = True)\n", + "df.drop(list(df.filter(regex=\"__system\")), axis=1, inplace=True)\n", "df.head(3)" ] } diff --git a/pyodk/_endpoints/auth.py b/pyodk/_endpoints/auth.py index da4b070..28d6783 100644 --- a/pyodk/_endpoints/auth.py +++ b/pyodk/_endpoints/auth.py @@ -12,7 +12,7 @@ class AuthService: def __init__(self, session: "Session", cache_path: str | None = None) -> None: - self.session: "Session" = session + self.session: Session = session self.cache_path: str = cache_path def verify_token(self, token: str) -> str: diff --git a/pyodk/_utils/session.py b/pyodk/_utils/session.py index becef8e..fb19bfc 100644 --- a/pyodk/_utils/session.py +++ b/pyodk/_utils/session.py @@ -64,7 +64,7 @@ def send(self, request, **kwargs): class Auth(AuthBase): def __init__(self, session: "Session", username: str, password: str, cache_path: str): - self.session: "Session" = session + self.session: Session = session self.username: str = username self.password: str = password self.service: AuthService = AuthService(session=session, cache_path=cache_path) diff --git a/pyproject.toml b/pyproject.toml index bf3d9c8..588d11c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -8,16 +8,16 @@ description = "The official Python library for ODK 🐍" readme = "README.md" requires-python = ">=3.12" dependencies = [ - "requests==2.32.0", # HTTP with Central + "requests==2.32.3", # HTTP with Central "toml==0.10.2", # Configuration files - "pydantic==2.6.4", # Data validation + "pydantic>=2.6.4,<2.9.3", # Data validation ] [project.optional-dependencies] # Install with `pip install pyodk[dev]`. dev = [ - "ruff==0.3.4", # Format and lint - "openpyxl==3.1.2", # Create test XLSX files + "ruff==0.7.1", # Format and lint + "openpyxl==3.1.5", # Create test XLSX files "xlwt==1.3.0", # Create test XLS files ] docs = [