diff --git a/.gitignore b/.gitignore index 800a0e31d..fa9792536 100644 --- a/.gitignore +++ b/.gitignore @@ -1,22 +1,154 @@ +# Byte-compiled / optimized / DLL files *.py[cod] -__pycache__ +*$py.class +__pycache__/ -*.cache -*.egg-info -*.pdf -*.sqlite +# C extensions +*.so + +# Distribution / packaging +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +pip-wheel-metadata/ +share/python-wheels/ +*.egg-info/ +.installed.cfg +*.egg +MANIFEST -# Development and build files +# PyInstaller +*.manifest +*.spec + +# Unit test / coverage reports +htmlcov/ +.tox/ +.nox/ .coverage -.pytest_cache -build +.coverage.* +.cache +nosetests.xml coverage.xml -dist -doc/cache.sqlite -doc/_build +*.cover +*.py,cover +.hypothesis/ +.pytest_cache/ prof/ -htmlcov -.eggs/ -# Editors -.vscode/settings.json +# Translations +*.mo +*.pot + +# Django stuff: +*.log +local_settings.py +db.sqlite3 +db.sqlite3-journal + +# Flask stuff: +instance/ +.webassets-cache + +# Scrapy stuff: +.scrapy + +# Sphinx documentation +doc/_build/ +doc/cache.sqlite + +# PyBuilder +target/ + +# Jupyter Notebook +.ipynb_checkpoints + +# IPython +profile_default/ +ipython_config.py + +# pyenv +.python-version + +# pipenv +Pipfile.lock + +# PEP 582 +__pypackages__/ + +# Celery stuff +celerybeat-schedule +celerybeat.pid + +# SageMath parsed files +*.sage.py + +# Environments +.env +.venv +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ + +# Spyder project settings +.spyderproject +.spyproject + +# Rope project settings +.ropeproject + +# mkdocs documentation +/site + +# mypy +.mypy_cache/ +.dmypy.json +dmypy.json + +# Pyre type checker +.pyre/ + +# pytype static type analyzer +.pytype/ + +# Cython debug symbols +cython_debug/ + +# IDEs and editors +.idea/ +.vscode/ +*.swp +*.swo +*~ +.project +.pydevproject +.settings/ +*.sublime-project +*.sublime-workspace + +# OS specific +.DS_Store +.DS_Store? +._* +.Spotlight-V100 +.Trashes +ehthumbs.db +Thumbs.db +Desktop.ini + +# Project specific +*.pdf +*.sqlite +*.cache diff --git a/doc/install.rst b/doc/install.rst index fe60ca7ec..56ef93b62 100644 --- a/doc/install.rst +++ b/doc/install.rst @@ -17,7 +17,7 @@ pandaSDMX is a pure `Python `_ package requiring Python 3.9 pandaSDMX also depends on: - `pandas `_ for data structures, -- `pydantic `_ to implement the IM, +- `pydantic 2.x `_ to implement the IM, - `requests `_ for HTTP requests, and - `lxml `_ for XML processing. diff --git a/doc/whatsnew.rst b/doc/whatsnew.rst index 7809fab03..b1c75fce8 100644 --- a/doc/whatsnew.rst +++ b/doc/whatsnew.rst @@ -1,11 +1,16 @@ :tocdepth: 1 -What's new? -=========== - - -v1.10.0 (2023-02-25) -------------------------- +What's new? +=========== + +Unreleased +---------- + +* Update dependency to pydantic 2.x and run the internal model layer via + ``pydantic.v1`` compatibility imports. + +v1.10.0 (2023-02-25) +------------------------- * update ESTAT config to support new API diff --git a/pandasdmx/__init__.py b/pandasdmx/__init__.py index 6847df89a..2f54588c5 100644 --- a/pandasdmx/__init__.py +++ b/pandasdmx/__init__.py @@ -20,7 +20,7 @@ "to_xml", ] -__version__ = "1.10.0" +__version__ = "1.10.2" #: Top-level logger. diff --git a/pandasdmx/source/__init__.py b/pandasdmx/source/__init__.py index 74b9a5eda..874fb1c5a 100644 --- a/pandasdmx/source/__init__.py +++ b/pandasdmx/source/__init__.py @@ -1,4 +1,7 @@ -from pydantic import HttpUrl +try: + from pydantic.v1 import HttpUrl +except Exception: + from pydantic import HttpUrl from enum import Enum from importlib import import_module, resources import json diff --git a/pandasdmx/tests/test_model.py b/pandasdmx/tests/test_model.py index 6254d97d1..226e79ed2 100644 --- a/pandasdmx/tests/test_model.py +++ b/pandasdmx/tests/test_model.py @@ -1,6 +1,9 @@ # TODO test str() and repr() implementations -import pydantic +try: + import pydantic.v1 as pydantic +except Exception: + import pydantic import pytest from pytest import raises diff --git a/pandasdmx/util.py b/pandasdmx/util.py index d4f662058..5ef679144 100644 --- a/pandasdmx/util.py +++ b/pandasdmx/util.py @@ -5,11 +5,20 @@ from functools import lru_cache from typing import Any, Dict, Mapping, Tuple, TypeVar, Union -import pydantic import requests -from pydantic import Field, ValidationError, validator -from pydantic.class_validators import make_generic_validator -from pydantic.typing import get_origin # type: ignore [attr-defined] +try: + import pydantic.v1 as pydantic + from pydantic.v1 import Field, ValidationError, validator + from pydantic.v1.class_validators import make_generic_validator + from pydantic.v1.typing import get_origin +except Exception: + import pydantic + from pydantic import Field, ValidationError, validator + from pydantic.class_validators import make_generic_validator + try: + from pydantic.typing import get_origin # type: ignore [attr-defined] + except Exception: # pragma: no cover + from typing import get_origin try: import requests_cache diff --git a/pandasdmx/writer/pandas.py b/pandasdmx/writer/pandas.py index 327e54cca..3adff5c04 100644 --- a/pandasdmx/writer/pandas.py +++ b/pandasdmx/writer/pandas.py @@ -485,31 +485,33 @@ def _get_attrs(): # Unstack all but the time dimension and convert other_dims = list(filter(lambda d: d != param["dim"], df.index.names)) df = df.unstack(other_dims) - df.index = pd.to_datetime(df.index) + df.index = pd.to_datetime(df.index, format='ISO8601') if param["freq"]: # Determine frequency string, Dimension, or Attribute - try: - # pandas version prior to 1.1.0 - prefix_mapping = pd.offsets.prefix_mapping - except AttributeError: - # pandas version >= 1.1.0 - # See also issue #35482 in the pandas-dev repo - prefix_mapping = pd._libs.tslibs.offsets.prefix_mapping + from pandas.tseries.frequencies import to_offset + freq = param["freq"] - if isinstance(freq, str) and freq not in prefix_mapping: - # ID of a Dimension or Attribute - for component in chain(_get_dims(), _get_attrs()): - if component.id == freq: - freq = component - break - - # No named dimension in the DSD; but perhaps on the df - if isinstance(freq, str): - if freq in df.columns.names: - freq = Dimension(id=freq) - else: - raise ValueError(freq) + if isinstance(freq, str): + # First, try to interpret as a pandas frequency string + try: + to_offset(freq) + # Valid pandas frequency string, use as-is + except (ValueError, KeyError): + # Not a valid pandas frequency; try as Dimension or Attribute ID + found = False + for component in chain(_get_dims(), _get_attrs()): + if component.id == freq: + freq = component + found = True + break + + # No named dimension in the DSD; but perhaps on the df + if not found: + if freq in df.columns.names: + freq = Dimension(id=freq) + else: + raise ValueError(freq) if isinstance(freq, Dimension): # Retrieve Dimension values from pd.MultiIndex level diff --git a/pyproject.toml b/pyproject.toml index de1ed28df..3fc78322f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -13,8 +13,8 @@ requires = [ "requests >=2.26", "lxml >= 4.8", "pandas >= 1.3", - "pydantic >=1.9.2, < 2.0"] -requires-python = ">=3.9.6,<3.12" + "pydantic >=2.12, < 3.0"] +requires-python = ">=3.9.6,<3.13" keywords = "statistics, SDMX, pandas, data, economics, science" classifiers = [ "Intended Audience :: Developers", @@ -42,4 +42,4 @@ test = ["pytest >= 5", [tool.flit.sdist] include = ["LICENSE", 'README.rst'] -exclude = ['pandasdmx/tests'] \ No newline at end of file +exclude = ['pandasdmx/tests'] diff --git a/requirements.txt b/requirements.txt deleted file mode 100644 index fe931d5d2..000000000 --- a/requirements.txt +++ /dev/null @@ -1,2 +0,0 @@ -codecov -pytest-cov