diff --git a/.ci/run b/.ci/run index 303de20..c881818 100755 --- a/.ci/run +++ b/.ci/run @@ -11,6 +11,8 @@ if ! command -v sudo; then } fi +# --parallel-live to show outputs while it's running +tox_cmd='run-parallel --parallel-live' if [ -n "${CI-}" ]; then # install OS specific stuff here case "$OSTYPE" in @@ -20,7 +22,8 @@ if [ -n "${CI-}" ]; then ;; cygwin* | msys* | win*) # windows - : + # ugh. parallel stuff seems super flaky under windows, some random failures, "file used by other process" and crap like that + tox_cmd='run' ;; *) # must be linux? @@ -29,12 +32,5 @@ if [ -n "${CI-}" ]; then esac fi - -PY_BIN="python3" -# some systems might have python pointing to python3 -if ! command -v python3 &> /dev/null; then - PY_BIN="python" -fi - -"$PY_BIN" -m pip install --user tox -"$PY_BIN" -m tox +# NOTE: expects uv installed +uv tool run --with tox-uv tox $tox_cmd "$@" diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 5ff9270..e24cd73 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -6,51 +6,112 @@ on: branches: '*' tags: 'v[0-9]+.*' # only trigger on 'release' tags for PyPi # Ideally I would put this in the pypi job... but github syntax doesn't allow for regexes there :shrug: - # P.S. fuck made up yaml DSLs. - pull_request: # needed to trigger on others' PRs + + # Needed to trigger on others' PRs. # Note that people who fork it need to go to "Actions" tab on their fork and click "I understand my workflows, go ahead and enable them". - workflow_dispatch: # needed to trigger workflows manually - # todo cron? + pull_request: + + # Needed to trigger workflows manually. + workflow_dispatch: + inputs: + debug_enabled: + type: boolean + description: 'Run the build with tmate debugging enabled (https://github.com/marketplace/actions/debugging-with-tmate)' + required: false + default: false + + schedule: + - cron: '31 18 * * 5' # run every Friday jobs: build: strategy: + fail-fast: false matrix: platform: [ubuntu-latest, macos-latest, windows-latest] - python-version: ['3.7', '3.8', '3.9', '3.10', '3.11'] - # 3.11 on windows has this bug, lxml setup fails - #https://bugs.launchpad.net/lxml/+bug/1977998 - exclude: [{platform: windows-latest, python-version: '3.11'}] + python-version: ['3.9', '3.10', '3.11', '3.12', '3.13'] # vvv just an example of excluding stuff from matrix # exclude: [{platform: macos-latest, python-version: '3.6'}] runs-on: ${{ matrix.platform }} + # useful for 'optional' pipelines + # continue-on-error: ${{ matrix.platform == 'windows-latest' }} + steps: # ugh https://github.com/actions/toolkit/blob/main/docs/commands.md#path-manipulation - run: echo "$HOME/.local/bin" >> $GITHUB_PATH - - if: ${{ matrix.platform == 'macos-latest' && matrix.python-version == '3.11' }} - # hmm somehow only seems necessary for 3.11 on osx?? - run: echo "$HOME/Library/Python/${{ matrix.python-version }}/bin" >> $GITHUB_PATH - - uses: actions/setup-python@v4 + - uses: actions/checkout@v4 with: - python-version: ${{ matrix.python-version }} + submodules: recursive + fetch-depth: 0 # nicer to have all git history when debugging/for tests - - uses: actions/checkout@v3 + - uses: actions/setup-python@v5 with: - submodules: recursive + python-version: ${{ matrix.python-version }} + + - uses: astral-sh/setup-uv@v5 + with: + enable-cache: false # we don't have lock files, so can't use them as cache key - # uncomment for SSH debugging - # - uses: mxschmitt/action-tmate@v3 + - uses: mxschmitt/action-tmate@v3 + if: ${{ github.event_name == 'workflow_dispatch' && inputs.debug_enabled }} # explicit bash command is necessary for Windows CI runner, otherwise it thinks it's cmd... - run: bash .ci/run + env: + # only compute lxml coverage on ubuntu; it crashes on windows + CI_MYPY_COVERAGE: ${{ matrix.platform == 'ubuntu-latest' && '--cobertura-xml-report .coverage.mypy' || '' }} - if: matrix.platform == 'ubuntu-latest' # no need to compute coverage for other platforms - uses: actions/upload-artifact@v3 + uses: codecov/codecov-action@v5 + with: + fail_ci_if_error: true # default false + token: ${{ secrets.CODECOV_TOKEN }} + flags: mypy-${{ matrix.python-version }} + files: .coverage.mypy/cobertura.xml + + + pypi: + # Do not run it for PRs/cron schedule etc. + # NOTE: release tags are guarded by on: push: tags on the top. + if: github.event_name == 'push' && (startsWith(github.event.ref, 'refs/tags/') || (github.event.ref == format('refs/heads/{0}', github.event.repository.master_branch))) + # Ugh, I tried using matrix or something to explicitly generate only test pypi or prod pypi pipelines. + # But github actions is so shit, it's impossible to do any logic at all, e.g. doesn't support conditional matrix, if/else statements for variables etc. + + needs: [build] # add all other jobs here + + runs-on: ubuntu-latest + + permissions: + # necessary for Trusted Publishing + id-token: write + + steps: + # ugh https://github.com/actions/toolkit/blob/main/docs/commands.md#path-manipulation + - run: echo "$HOME/.local/bin" >> $GITHUB_PATH + + - uses: actions/checkout@v4 with: - name: .coverage.mypy_${{ matrix.platform }}_${{ matrix.python-version }} - path: .coverage.mypy/ + submodules: recursive + fetch-depth: 0 # pull all commits to correctly infer vcs version + + - uses: actions/setup-python@v5 + with: + python-version: '3.12' + + - uses: astral-sh/setup-uv@v5 + with: + enable-cache: false # we don't have lock files, so can't use them as cache key + + - name: 'release to test pypi' + # always deploy merged master to test pypi + if: github.event.ref == format('refs/heads/{0}', github.event.repository.master_branch) + run: .ci/release-uv --use-test-pypi + - name: 'release to prod pypi' + # always deploy tags to release pypi + if: startsWith(github.event.ref, 'refs/tags/') + run: .ci/release-uv diff --git a/conftest.py b/conftest.py new file mode 100644 index 0000000..627def8 --- /dev/null +++ b/conftest.py @@ -0,0 +1,58 @@ +# this is a hack to monkey patch pytest so it handles tests inside namespace packages without __init__.py properly +# without it, pytest can't discover the package root for some reason +# also see https://github.com/karlicoss/pytest_namespace_pkgs for more + +import os +import pathlib +from typing import Optional + +import _pytest.main +import _pytest.pathlib + +# we consider all dirs in repo/ to be namespace packages +root_dir = pathlib.Path(__file__).absolute().parent.resolve() / 'src' +assert root_dir.exists(), root_dir + +# TODO assert it contains package name?? maybe get it via setuptools.. + +namespace_pkg_dirs = [str(d) for d in root_dir.iterdir() if d.is_dir()] + +# resolve_package_path is called from _pytest.pathlib.import_path +# takes a full abs path to the test file and needs to return the path to the 'root' package on the filesystem +resolve_pkg_path_orig = _pytest.pathlib.resolve_package_path + + +def resolve_package_path(path: pathlib.Path) -> Optional[pathlib.Path]: + result = path # search from the test file upwards + for parent in result.parents: + if str(parent) in namespace_pkg_dirs: + return parent + if os.name == 'nt': + # ??? for some reason on windows it is trying to call this against conftest? but not on linux/osx + if path.name == 'conftest.py': + return resolve_pkg_path_orig(path) + raise RuntimeError("Couldn't determine path for ", path) + + +# NOTE: seems like it's not necessary anymore? +# keeping it for now just in case +# after https://github.com/pytest-dev/pytest/pull/13426 we should be able to remove the whole conftest +# _pytest.pathlib.resolve_package_path = resolve_package_path + + +# without patching, the orig function returns just a package name for some reason +# (I think it's used as a sort of fallback) +# so we need to point it at the absolute path properly +# not sure what are the consequences.. maybe it wouldn't be able to run against installed packages? not sure.. +search_pypath_orig = _pytest.main.search_pypath + + +def search_pypath(module_name: str) -> str: + mpath = root_dir / module_name.replace('.', os.sep) + if not mpath.is_dir(): + mpath = mpath.with_suffix('.py') + assert mpath.exists(), mpath # just in case + return str(mpath) + + +_pytest.main.search_pypath = search_pypath # ty: ignore[invalid-assignment] diff --git a/mypy.ini b/mypy.ini index 0f8386f..7b1e535 100644 --- a/mypy.ini +++ b/mypy.ini @@ -1,9 +1,17 @@ [mypy] pretty = True show_error_context = True -show_error_codes = True +show_column_numbers = True +show_error_end = True + check_untyped_defs = True -namespace_packages = True + +# see https://mypy.readthedocs.io/en/stable/error_code_list2.html +warn_redundant_casts = True +strict_equality = True +warn_unused_ignores = True +enable_error_code = deprecated,redundant-expr,possibly-undefined,truthy-bool,truthy-iterable,ignore-without-code,unused-awaitable + # an example of suppressing # [mypy-my.config.repos.pdfannots.pdfannots] diff --git a/src/endoexport/export.py b/old/export.py old mode 100755 new mode 100644 similarity index 100% rename from src/endoexport/export.py rename to old/export.py diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..19e25bd --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,69 @@ +# see https://github.com/karlicoss/pymplate for up-to-date reference +[project] +dynamic = ["version"] # version is managed by build backend +name = "endoexport" +dependencies = [] +requires-python = ">=3.9" + +## these need to be set if you're planning to upload to pypi +# description = "TODO" +# license = {file = "LICENSE"} +# authors = [ +# {name = "Dima Gerasimov (@karlicoss)", email = "karlicoss@gmail.com"}, +# ] +# maintainers = [ +# {name = "Dima Gerasimov (@karlicoss)", email = "karlicoss@gmail.com"}, +# ] +# +# [project.urls] +# Homepage = "https://github.com/karlicoss/pymplate" +## + + +[project.optional-dependencies] +dal = [ + # I'm using some upstream unmerged changes, so unfortunately need my own fork + "endoapi @ git+https://github.com/karlicoss/endoapi.git", +] +optional = [ + "orjson", # faster json processing + "colorlog", + "ijson", # faster iterative json processing +] +[dependency-groups] +# TODO: not sure, on the one hand could just use 'standard' dev dependency group +# On the other hand, it's a bit annoying that it's always included by default? +# To make sure it's not included, need to use `uv run --exact --no-default-groups ...` +testing = [ + "pytest", + "ruff", + "mypy", + "lxml", # for mypy html coverage + "ty>=0.0.1a16", + + "numpy", # for fake data generation + "matplotlib", # for DAL demo + "types-pytz", + "pandas-stubs", +] + + +[build-system] +requires = ["hatchling", "hatch-vcs"] +build-backend = "hatchling.build" + +# unfortunately have to duplicate project name here atm, see https://github.com/pypa/hatch/issues/1894 +[tool.hatch.build.targets.wheel] +packages = ["src/endoexport"] + +[tool.hatch.version] +source = "vcs" + +[tool.hatch.version.raw-options] +version_scheme = "python-simplified-semver" +local_scheme = "dirty-tag" + +[tool.hatch.metadata] +# needed to allow direct git url for "instapaper" dependency +# NOTE: tool.uv.sources also works, but it would only work with uv, not pip +allow-direct-references = true diff --git a/pytest.ini b/pytest.ini index 20c3704..226488b 100644 --- a/pytest.ini +++ b/pytest.ini @@ -1,7 +1,14 @@ [pytest] # discover files that don't follow test_ naming. Useful to keep tests along with the source code python_files = *.py + +# this setting only impacts package/module naming under pytest, not the discovery +consider_namespace_packages = true + addopts = + # prevent pytest cache from being created... it craps into project dir and I never use it anyway + -p no:cacheprovider + # -rap to print tests summary even when they are successful -rap --verbose diff --git a/ruff.toml b/ruff.toml new file mode 100644 index 0000000..d531a23 --- /dev/null +++ b/ruff.toml @@ -0,0 +1,147 @@ +lint.extend-select = [ + "F", # flakes rules -- default, but extend just in case + "E", # pycodestyle -- default, but extend just in case + "W", # various warnings + + "B", # 'bugbear' set -- various possible bugs + "C4", # flake8-comprehensions -- unnecessary list/map/dict calls + "COM", # trailing commas + "EXE", # various checks wrt executable files + "I", # sort imports + "ICN", # various import conventions + "FBT", # detect use of boolean arguments + "FURB", # various rules + "PERF", # various potential performance speedups + "PD", # pandas rules + "PIE", # 'misc' lints + "PLC", # pylint convention rules + "PLR", # pylint refactor rules + "PLW", # pylint warnings + "PT", # pytest stuff + "PYI", # various type hinting rules + "RET", # early returns + "RUF", # various ruff-specific rules + "TID", # various imports suggestions + "TRY", # various exception handling rules + "UP", # detect deprecated python stdlib stuff + "FA", # suggest using from __future__ import annotations + "PTH", # pathlib migration + "ARG", # unused argument checks + "A", # builtin shadowing + "G", # logging stuff + + # "ALL", # uncomment this to check for new rules! +] + +# Preserve types, even if a file imports `from __future__ import annotations` +# we need this for cachew to work with HPI types on 3.9 +# can probably remove after 3.10? +lint.pyupgrade.keep-runtime-typing = true + +lint.ignore = [ + "D", # annoying nags about docstrings + "N", # pep naming + "TCH", # type checking rules, mostly just suggests moving imports under TYPE_CHECKING + "S", # bandit (security checks) -- tends to be not very useful, lots of nitpicks + "DTZ", # datetimes checks -- complaining about missing tz and mostly false positives + "FIX", # complains about fixmes/todos -- annoying + "TD", # complains about todo formatting -- too annoying + "ANN", # missing type annotations? seems way to strict though + "EM" , # suggests assigning all exception messages into a variable first... pretty annoying + +### too opinionated style checks + "E501", # too long lines + "E702", # Multiple statements on one line (semicolon) + "E731", # assigning lambda instead of using def + "E741", # Ambiguous variable name: `l` + "E742", # Ambiguous class name: `O + "E401", # Multiple imports on one line + "F403", # import *` used; unable to detect undefined names +### + +### + "E722", # Do not use bare `except` ## Sometimes it's useful for defensive imports and that sort of thing.. + "F811", # Redefinition of unused # this gets in the way of pytest fixtures (e.g. in cachew) + +## might be nice .. but later and I don't wanna make it strict + "E402", # Module level import not at top of file + + "RUF100", # unused noqa -- handle later + "RUF012", # mutable class attrs should be annotated with ClassVar... ugh pretty annoying for user configs + +### these are just nitpicky, we usually know better + "PLR0911", # too many return statements + "PLR0912", # too many branches + "PLR0913", # too many function arguments + "PLR0915", # too many statements + "PLR1714", # consider merging multiple comparisons + "PLR2044", # line with empty comment + "PLR5501", # use elif instead of else if + "PLR2004", # magic value in comparison -- super annoying in tests +### + "PLR0402", # import X.Y as Y -- TODO maybe consider enabling it, but double check + + "B009", # calling gettattr with constant attribute -- this is useful to convince mypy + "B010", # same as above, but setattr + "B011", # complains about assert False + "B017", # pytest.raises(Exception) + "B023", # seems to result in false positives? + "B028", # suggest using explicit stacklevel? TODO double check later, but not sure it's useful + + # complains about useless pass, but has sort of a false positive if the function has a docstring? + # this is common for click entrypoints (e.g. in __main__), so disable + "PIE790", + + # a bit too annoying, offers to convert for loops to list comprehension + # , which may heart readability + "PERF401", + + # suggests no using exception in for loops + # we do use this technique a lot, plus in 3.11 happy path exception handling is "zero-cost" + "PERF203", + + "RET504", # unnecessary assignment before returning -- that can be useful for readability + "RET505", # unnecessary else after return -- can hurt readability + + "PLW0603", # global variable update.. we usually know why we are doing this + "PLW2901", # for loop variable overwritten, usually this is intentional + + "PT011", # pytest raises should is too broad + "PT012", # pytest raises should contain a single statement + + "COM812", # trailing comma missing -- mostly just being annoying with long multiline strings + + "PD901", # generic variable name df + + "TRY003", # suggests defining exception messages in exception class -- kinda annoying + "TRY004", # prefer TypeError -- don't see the point + "TRY201", # raise without specifying exception name -- sometimes hurts readability + "TRY400", # TODO double check this, might be useful + "TRY401", # redundant exception in logging.exception call? TODO double check, might result in excessive logging + + "PGH", # TODO force error code in mypy instead? although it also has blanket noqa rule + + "TID252", # Prefer absolute imports over relative imports from parent modules + + "UP038", # suggests using | (union) in isisntance checks.. but it results in slower code + + ## too annoying + "T20", # just complains about prints and pprints + "Q", # flake quotes, too annoying + "C90", # some complexity checking + "G004", # logging statement uses f string + "ERA001", # commented out code + "SLF001", # private member accessed + "BLE001", # do not catch 'blind' Exception + "INP001", # complains about implicit namespace packages + "SIM", # some if statements crap + "RSE102", # complains about missing parens in exceptions + ## + + "PLC0415", # "imports should be at the top level" -- not realistic +] + + +extend-exclude = [ + "old/", +] diff --git a/setup.py b/setup.py deleted file mode 100644 index 695fc60..0000000 --- a/setup.py +++ /dev/null @@ -1,66 +0,0 @@ -# see https://github.com/karlicoss/pymplate for up-to-date reference - - -from setuptools import setup, find_namespace_packages # type: ignore - - -COMMON_DEPS = [ - # I'm using some upstream unmerged changes, so unfortunately need my own fork - 'endoapi @ git+https://github.com/karlicoss/endoapi.git', - # TODO hmm maybe I need to rename the setup.py for my fork? to make it less confusing - # uncomment to test against the filesystem package - # 'endoapi @ git+file://DUMMY/path/to/endoapi', -] - - -EXPORT_DEPS = COMMON_DEPS - -DAL_DEPS = COMMON_DEPS - -ALL_DEPS = sorted({*EXPORT_DEPS, *DAL_DEPS}) - - -def main() -> None: - # works with both ordinary and namespace packages - pkgs = find_namespace_packages('src') - pkg = min(pkgs) # lexicographically smallest is the correct one usually? - setup( - name=pkg, - use_scm_version={ - 'version_scheme': 'python-simplified-semver', - 'local_scheme': 'dirty-tag', - }, - setup_requires=['setuptools_scm'], - - # otherwise mypy won't work - # https://mypy.readthedocs.io/en/stable/installed_packages.html#making-pep-561-compatible-packages - zip_safe=False, - - packages=pkgs, - package_dir={'': 'src'}, - # necessary so that package works with mypy - package_data={pkg: ['py.typed']}, - - ## ^^^ this should be mostly automatic and not requiring any changes - - install_requires=ALL_DEPS, - extras_require={ - 'dal': DAL_DEPS, - 'export': EXPORT_DEPS, - 'testing': [ - 'pytest', - 'numpy', # for data generator - ], - 'linting': [ - 'pytest', - 'mypy', - 'lxml', # lxml for mypy coverage report - 'orjson', # optional packages - ], - }, - ) - - -if __name__ == '__main__': - main() - diff --git a/src/endoexport/__init__.py b/src/endoexport/__init__.py deleted file mode 100644 index dba6ae1..0000000 --- a/src/endoexport/__init__.py +++ /dev/null @@ -1,7 +0,0 @@ -# NOTE: without __init__.py/__init__.pyi, mypy behaves weird. -# see https://github.com/python/mypy/issues/8584 and the related discussions -# sometime it's kinda valuable to have namespace package and not have __init__.py though, - -# TLDR: you're better off having dimmy pyi, or alternatively you can use 'mypy -p src' (but that's a bit dirty?) - -# todo not sure how it behaves when installed? diff --git a/src/endoexport/dal.py b/src/endoexport/dal.py old mode 100755 new mode 100644 index 2dd638d..743caaa --- a/src/endoexport/dal.py +++ b/src/endoexport/dal.py @@ -1,17 +1,19 @@ -#!/usr/bin/env python3 -from datetime import timedelta, datetime, timezone as TZ +from __future__ import annotations + import json +from collections.abc import Iterable, Sequence +from datetime import datetime, timedelta +from datetime import timezone as TZ from pathlib import Path -from typing import Any, Dict, NamedTuple, Sequence, Union, List, TypeVar, Iterable, Optional - -from .exporthelpers import dal_helper, logging_helper -from .exporthelpers.dal_helper import Res, PathIsh, Json +from typing import Optional import endoapi.endomondo from endoapi.endomondo import Point +from .exporthelpers import dal_helper, logging_helper +from .exporthelpers.dal_helper import Json, PathIsh, Res -logger = logging_helper.logger('endoexport.dal', level='debug') +logger = logging_helper.make_logger(__name__, level='debug') class Workout(endoapi.endomondo.Workout): @@ -68,25 +70,26 @@ def workouts(self) -> Iterable[Res[Workout]]: class FakeData: - def __init__(self, seed: int=0) -> None: + def __init__(self, seed: int = 0) -> None: self.seed = seed - import numpy as np # type: ignore + import numpy as np + self.gen = np.random.default_rng(seed=self.seed) self.id = 0 - # hr is sort of a random walk?? probably not very accurate, but whatever # also keep within certain boundaries? - self.cur_avg_hr = 160.0 + self.cur_avg_hr = 160.0 self.avg_distance_km = 10 self.avg_duration_min = 40 # todo would be nice to separate parameters and the state - import pytz # type: ignore + import pytz + self.tz = pytz.timezone('America/New_York') self.first_day = datetime.strptime('20100101', '%Y%m%d') # todo gaussian distribution?? - + @property def today(self) -> datetime: return self.first_day + timedelta(days=self.id) @@ -96,14 +99,15 @@ def today(self) -> datetime: def generate_one(self) -> Json: G = self.gen D = timedelta + def N(mean, sigma): - return max(G.normal(mean, sigma), 0) # meh + return max(G.normal(mean, sigma), 0) # meh def ntd(mean, sigma): # 'normal' timedelta minutes return D(minutes=int(N(mean, sigma))) - start = self.today + D(hours=10) # todo randomize + start = self.today + D(hours=10) # todo randomize distance = N(self.avg_distance_km, 0.5) duration = ntd(self.avg_duration_min, 10) @@ -111,20 +115,23 @@ def fmtdt(x: datetime) -> str: return x.astimezone(TZ.utc).strftime('%Y-%m-%d %H:%M:%S UTC') ### - points = [dict( - # todo lat, lnt, alt?? - time=fmtdt(start + D(i)), - hr=G.normal(self.cur_avg_hr, 20), - ) for i in range(0, duration // D(seconds=1), 3)] - d = dict( - id=self.id, - sport=0, # running - start_time=fmtdt(start), - duration=duration // D(seconds=1), - points=points, - speed_avg=distance / (duration / D(hours=1)), - heart_rate_avg=self.cur_avg_hr, - ) + points = [ + { + # todo lat, lnt, alt?? + 'time': fmtdt(start + D(i)), + 'hr': G.normal(self.cur_avg_hr, 20), + } + for i in range(0, duration // D(seconds=1), 3) + ] + d = { + 'id': self.id, + 'sport': 0, # running + 'start_time': fmtdt(start), + 'duration': duration // D(seconds=1), + 'points': points, + 'speed_avg': distance / (duration / D(hours=1)), + 'heart_rate_avg': self.cur_avg_hr, + } ### self.id += 1 @@ -153,30 +160,33 @@ def test(tmp_path: Path) -> None: def demo(dao: DAL) -> None: - import pandas as pd # type: ignore - import matplotlib.pyplot as plt # type: ignore + import matplotlib.pyplot as plt + import pandas as pd # TODO split errors properly? move it to dal_helper? # todo or add some basic error handlign stuff to dal_helper? - workouts: List[Workout] = [w for w in dao.workouts() if not isinstance(w, Exception)] + workouts: list[Workout] = [w for w in dao.workouts() if not isinstance(w, Exception)] print(f"Parsed {len(workouts)} workouts") - df = pd.DataFrame({ - 'dt' : w.start_time, - 'calories': w.calories, - 'sport' : w.sport, - } for w in workouts) - df.set_index('dt', inplace=True) + df = pd.DataFrame( + { + 'dt': w.start_time, + 'calories': w.calories, + 'sport': w.sport, + } + for w in workouts + ) + df = df.set_index('dt') print(df) - cal = df.resample('D').sum() # index by days - cal = cal.rolling('30D').mean() # use moving average to smooth out the plot + cal = df.resample('D').sum() # index by days + cal = cal.rolling('30D').mean() # use moving average to smooth out the plot cal.plot(title='Exercise (30 days moving average)') plt.figure() breakdown = df.groupby('sport')['calories'].sum() - breakdown.plot.pie(title='Breakdown of calories burnt by sport') + breakdown.plot.pie(title='Breakdown of calories burnt by sport') # type: ignore[call-overload] plt.show() diff --git a/src/endoexport/exporthelpers b/src/endoexport/exporthelpers index 9b9133a..c1eaf86 160000 --- a/src/endoexport/exporthelpers +++ b/src/endoexport/exporthelpers @@ -1 +1 @@ -Subproject commit 9b9133ac407fa4ecc516e419120780f34d2cd5e6 +Subproject commit c1eaf86e0c70655bdcd3d736b35ff03e3eca8c22 diff --git a/tox.ini b/tox.ini index 5d0a55f..61e735a 100644 --- a/tox.ini +++ b/tox.ini @@ -1,32 +1,76 @@ [tox] -minversion = 3.5 +minversion = 3.21 # relies on the correct version of Python installed -envlist = tests,mypy +envlist = ruff,tests,mypy,ty # https://github.com/tox-dev/tox/issues/20#issuecomment-247788333 # hack to prevent .tox from crapping to the project directory -toxworkdir={env:TOXWORKDIR_BASE:}{toxinidir}/.tox +toxworkdir = {env:TOXWORKDIR_BASE:}{toxinidir}/.tox [testenv] -passenv = +# TODO how to get package name from setuptools? +package_name = "endoexport" +pass_env = # useful for tests to know they are running under ci - CI - CI_* + CI + CI_* # respect user's cache dirs to prevent tox from crapping into project dir - MYPY_CACHE_DIR - PYTHONPYCACHEPREFIX + PYTHONPYCACHEPREFIX + MYPY_CACHE_DIR + RUFF_CACHE_DIR + +set_env = +# do not add current working directory to pythonpath +# generally this is more robust and safer, prevents weird issues later on + PYTHONSAFEPATH=1 + +# default is 'editable', in which tox builds wheel first for some reason? not sure if makes much sense +package = uv-editable + + +[testenv:ruff] +skip_install = true +dependency_groups = testing +commands = + {envpython} -m ruff check \ + {posargs} + [testenv:tests] +dependency_groups = testing +extras = + dal + optional commands = - pip install -e .[testing] # posargs allow test filtering, e.g. tox ... -- -k test_name - python -m pytest src {posargs} + {envpython} -m pytest \ + --pyargs {[testenv]package_name} \ + {posargs} + [testenv:mypy] +dependency_groups = testing +extras = + dal + optional +deps = # any other dependencies (if needed) +commands = + {envpython} -m mypy --no-install-types \ + -p {[testenv]package_name} \ + --txt-report .coverage.mypy \ + --html-report .coverage.mypy \ + # this is for github actions to upload to codecov.io + # sadly xml coverage crashes on windows... so we need to disable it + {env:CI_MYPY_COVERAGE} \ + {posargs} + + +[testenv:ty] +dependency_groups = testing +extras = + dal + optional +deps = # any other dependencies (if needed) commands = - pip install -e .[linting] - python -m mypy --install-types --non-interactive \ - src \ - # txt report is a bit more convenient to view on CI - --txt-report .coverage.mypy \ - --html-report .coverage.mypy \ - {posargs} + {envpython} -m ty \ + check \ + {posargs} diff --git a/ty.toml b/ty.toml new file mode 100644 index 0000000..4f72141 --- /dev/null +++ b/ty.toml @@ -0,0 +1,4 @@ +[src] +exclude = [ + "old/", +]