diff --git a/.coveragerc b/.coveragerc deleted file mode 100644 index 73ec6b72..00000000 --- a/.coveragerc +++ /dev/null @@ -1,7 +0,0 @@ -[run] -omit = pendulum/locales/*, - pendulum/_compat.py, - pendulum/__version__.py, - pendulum/_extensions/* - pendulum/parsing/iso8601.py - pendulum/utils/_compat.py diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml new file mode 100644 index 00000000..fca881b9 --- /dev/null +++ b/.github/FUNDING.yml @@ -0,0 +1 @@ +github: [sdispater] diff --git a/.github/ISSUE_TEMPLATE/---bug-report.md b/.github/ISSUE_TEMPLATE/---bug-report.md new file mode 100644 index 00000000..1f8b6180 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/---bug-report.md @@ -0,0 +1,31 @@ +--- +name: "\U0001F41E Bug Report" +about: Did you find a bug? +title: '' +labels: 'Bug' +assignees: '' + +--- + + + + +- [ ] I am on the [latest](https://github.com/sdispater/pendulum/releases/latest) Pendulum version. +- [ ] I have searched the [issues](https://github.com/sdispater/pendulum/issues) of this repo and believe that this is not a duplicate. + + + +- **OS version and name**: +- **Pendulum version**: + +## Issue + diff --git a/.github/ISSUE_TEMPLATE/---documentation.md b/.github/ISSUE_TEMPLATE/---documentation.md new file mode 100644 index 00000000..0d4ac414 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/---documentation.md @@ -0,0 +1,22 @@ +--- +name: "\U0001F4DA Documentation" +about: Did you find errors, problems, or anything unintelligible in the docs (https://pendulum.eustace.io/docs)? +title: '' +labels: 'Documentation' +assignees: '' + +--- + + + + +- [ ] I have searched the [issues](https://github.com/sdispater/pendulum/issues) of this repo and believe that this is not a duplicate. + +## Issue + diff --git a/.github/ISSUE_TEMPLATE/---everything-else.md b/.github/ISSUE_TEMPLATE/---everything-else.md new file mode 100644 index 00000000..1fc60fac --- /dev/null +++ b/.github/ISSUE_TEMPLATE/---everything-else.md @@ -0,0 +1,19 @@ +--- +name: "\U0001F5C3 Everything Else" +about: For questions and issues that do not fall in any of the other categories. This + can include questions about Pendulum's roadmap. +title: '' +labels: '' +assignees: '' + +--- + + +- [ ] I have searched the [issues](https://github.com/sdispater/pendulum/issues) of this repo and believe that this is not a duplicate. +- [ ] I have searched the [documentation](https://pendulum.eustace.io/docs/) and believe that my question is not covered. + +## Issue + diff --git a/.github/ISSUE_TEMPLATE/---feature-request.md b/.github/ISSUE_TEMPLATE/---feature-request.md new file mode 100644 index 00000000..46050556 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/---feature-request.md @@ -0,0 +1,23 @@ +--- +name: "\U0001F381 Feature Request" +about: Do you have ideas for new features and improvements? +title: '' +labels: 'Feature' +assignees: '' + +--- + + + + +- [ ] I have searched the [issues](https://github.com/sdispater/pendulum/issues) of this repo and believe that this is not a duplicate. +- [ ] I have searched the [documentation](https://pendulum.eustace.io/docs/) and believe that my question is not covered. + +## Feature Request + diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md new file mode 100644 index 00000000..4c0ce41a --- /dev/null +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -0,0 +1,15 @@ +## Pull Request Check List + + + +- [ ] Added **tests** for changed code. +- [ ] Updated **documentation** for changed code. + + diff --git a/.github/workflows/codspeed.yml b/.github/workflows/codspeed.yml new file mode 100644 index 00000000..84278a41 --- /dev/null +++ b/.github/workflows/codspeed.yml @@ -0,0 +1,46 @@ +name: codspeed + +on: + push: + branches: + - "master" + pull_request: + # `workflow_dispatch` allows CodSpeed to trigger backtest + # performance analysis in order to generate initial data. + workflow_dispatch: + +jobs: + benchmarks: + runs-on: ubuntu-22.04 + steps: + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + - uses: actions/setup-python@83679a892e2d95755f2dac6acb0bfd1e9ac5d548 # v6.1.0 + with: + python-version: "3.9" + + - name: Get full Python version + id: full-python-version + run: | + echo version=$(python -c "import sys; print('-'.join(str(v) for v in sys.version_info))") >> $GITHUB_OUTPUT + + - name: Install poetry + run: | + pipx install poetry>=2 + + - name: Configure poetry + run: poetry config virtualenvs.in-project true + + - name: Install dependencies + run: poetry install --only test --only benchmark --only build -vvv --no-root + + - name: Install pendulum and check extensions + run: | + poetry run pip install -e . -vvv + poetry run python -c 'import pendulum._pendulum' + + - name: Run benchmarks + uses: CodSpeedHQ/action@dbda7111f8ac363564b0c51b992d4ce76bb89f2f # v4.5.2 + with: + mode: simulation + token: ${{ secrets.CODSPEED_TOKEN }} + run: poetry run pytest tests/ --codspeed diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 00000000..857a06e0 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,158 @@ +name: Release + +on: + push: + tags: + - '*.*.*' + workflow_dispatch: + +jobs: + build: + name: Build on ${{ matrix.platform || matrix.os }} (${{ matrix.target }} - ${{ matrix.manylinux || 'auto' }}) + environment: release + strategy: + fail-fast: false + matrix: + os: [ubuntu, macos, windows] + target: [x86_64, aarch64] + manylinux: [auto] + include: + - os: ubuntu + platform: linux + - os: windows + ls: dir + interpreter: 3.9 3.10 3.11 3.12 3.13 3.14 pypy3.9 pypy3.10 + - os: windows + ls: dir + target: aarch64 + interpreter: 3.11 3.12 3.13 3.14 + - os: macos + target: aarch64 + interpreter: 3.9 3.10 3.11 3.12 3.13 3.14 pypy3.9 pypy3.10 + - os: ubuntu + platform: linux + target: aarch64 + # musllinux + - os: ubuntu + platform: linux + target: x86_64 + manylinux: musllinux_1_1 + - os: ubuntu + platform: linux + target: aarch64 + manylinux: musllinux_1_1 + - os: ubuntu + platform: linux + target: ppc64le + interpreter: 3.9 3.10 3.11 3.12 3.13 3.14 + - os: ubuntu + platform: linux + target: s390x + interpreter: 3.9 3.10 3.11 3.12 3.13 3.14 + + runs-on: ${{ matrix.os }}-latest + steps: + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + + - name: set up python + uses: actions/setup-python@83679a892e2d95755f2dac6acb0bfd1e9ac5d548 # v6.1.0 + with: + python-version: '3.11' + architecture: ${{ matrix.python-architecture || 'x64' }} + + - name: build wheels + uses: PyO3/maturin-action@86b9d133d34bc1b40018696f782949dac11bd380 # v1.49.4 + with: + target: ${{ matrix.target }} + manylinux: ${{ matrix.manylinux || 'auto' }} + container: ${{ matrix.container }} + args: --release --out dist --interpreter ${{ matrix.interpreter || '3.9 3.10 3.11 3.12 3.13 pypy3.9 pypy3.10' }} ${{ matrix.extra-build-args }} + rust-toolchain: stable + docker-options: -e CI + + - run: ${{ matrix.ls || 'ls -lh' }} dist/ + + - uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0 + with: + name: dist-${{ matrix.os }}-${{ matrix.target }}-${{ matrix.manylinux }} + path: dist + + build_sdist: + runs-on: ubuntu-latest + environment: release + steps: + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + - name: Build sdist + uses: PyO3/maturin-action@86b9d133d34bc1b40018696f782949dac11bd380 # v1.49.4 + with: + command: sdist + args: --out dist + - name: Upload sdist + uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0 + with: + name: dist-sdist + path: dist + + build_no_ext: + runs-on: ubuntu-latest + environment: release + steps: + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + - name: Install and configure Poetry + run: pipx install poetry + - name: Hotswap build backend for Poetry + # Maturin doesn't support building no-extension wheels, so we swap to Poetry for that + run: | + sed -i -e '/^\[build-system\]/,/^\[/{s/^requires = .*/requires = ["poetry-core>=2.0.0,<3.0.0"]/; s/^build-backend = .*/build-backend = "poetry.core.masonry.api"/}' pyproject.toml + - name: Install dependencies + run: poetry install --only main --only test --only typing --only build + - name: Run poetry build + run: poetry build -f wheel + - name: Upload no-ext wheel + uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0 + with: + name: dist-any + path: dist + + + Release: + needs: [ build, build_sdist, build_no_ext ] + if: success() + runs-on: ubuntu-latest + permissions: + id-token: write + contents: write + environment: + name: pypi + url: https://pypi.org/project/pendulum/ + steps: + - name: Checkout code + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + + - name: Download artifacts + uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7.0.0 + with: + pattern: dist* + path: dist + merge-multiple: true + + - name: Check distributions + run: | + ls -la dist + + - name: Check Version + id: check-version + run: | + [[ "${GITHUB_REF#refs/tags/}" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]] \ + || echo prerelease=true >> $GITHUB_OUTPUT + + - name: Create Release + uses: ncipollo/release-action@b7eabc95ff50cbeeedec83973935c8f306dfcd0b # v1.20.0 + with: + artifacts: "dist/*" + draft: false + prerelease: steps.check-version.outputs.prerelease == 'true' + body: "See CHANGELOG.md for details" + + - name: Publish package distributions to PyPI + uses: pypa/gh-action-pypi-publish@ed0c53931b1dc9bd32cbe73a98c7f6766f8a527e # v1.13.0 diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml new file mode 100644 index 00000000..67c408bf --- /dev/null +++ b/.github/workflows/tests.yml @@ -0,0 +1,93 @@ +name: Tests + +on: + push: + paths-ignore: + - 'docs/**' + branches: + - master + pull_request: + paths-ignore: + - 'docs/**' + branches: + - '**' + +jobs: + Linting: + name: Linting + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + - uses: actions/setup-python@83679a892e2d95755f2dac6acb0bfd1e9ac5d548 # v6.1.0 + with: + python-version: "3.11" + - name: "Install pre-commit" + run: pip install pre-commit + - name: "Install Rust toolchain" + run: rustup component add rustfmt clippy + - run: pre-commit run --all-files + + Tests: + name: ${{ matrix.os }} / ${{ matrix.python-version }} + runs-on: ${{ matrix.os }}-latest + strategy: + matrix: + os: [Ubuntu, MacOS, Windows] + python-version: ["3.9", "3.10", "3.11", "3.12", "3.13", "3.14"] + defaults: + run: + shell: bash + + steps: + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@83679a892e2d95755f2dac6acb0bfd1e9ac5d548 # v6.1.0 + with: + python-version: ${{ matrix.python-version }} + allow-prereleases: true + + - name: Get full Python version + id: full-python-version + run: | + echo version=$(python -c "import sys; print('-'.join(str(v) for v in sys.version_info))") >> $GITHUB_OUTPUT + + - name: Install poetry + run: | + pipx install poetry>=2 + + - name: Configure poetry + run: poetry config virtualenvs.in-project true + + - name: Set up cache + uses: actions/cache@9255dc7a253b0ccc959486e2bca901246202afeb # v5.0.1 + id: cache + with: + path: .venv + key: venv-${{ runner.os }}-${{ steps.full-python-version.outputs.version }}-${{ hashFiles('**/poetry.lock') }} + + - name: Ensure cache is healthy + # MacOS does not come with `timeout` command out of the box + if: steps.cache.outputs.cache-hit == 'true' && matrix.os != 'MacOS' + run: timeout 10s poetry run pip --version || rm -rf .venv + + - name: Install runtime, testing, and typing dependencies + run: poetry install --only main --only test --only typing --only build --no-root -vvv + + - name: Install project + run: poetry run maturin develop + + - name: Run type checking + run: poetry run mypy + + - name: Uninstall typing dependencies + # This ensures pendulum runs without typing_extensions installed + run: poetry sync --only main --only test --only build --no-root -vvv + + - name: Test Pure Python + run: | + PENDULUM_EXTENSIONS=0 poetry run pytest -q tests + + - name: Test + run: | + poetry run pytest -q tests diff --git a/.gitignore b/.gitignore index f1152e6f..dd3696f6 100644 --- a/.gitignore +++ b/.gitignore @@ -30,8 +30,10 @@ results.json profile.html /wheelhouse /docs/site/* -pyproject.lock +setup.py # editor -.vscode \ No newline at end of file +.vscode +/target +/rust/target diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 6b719683..98ac5c45 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,6 +1,27 @@ +ci: + autofix_prs: false + repos: -- repo: https://github.com/ambv/black - rev: stable + - repo: https://github.com/pre-commit/pre-commit-hooks + rev: v4.4.0 hooks: - - id: black - python_version: python3.6 + - id: trailing-whitespace + exclude: ^tests/.*/fixtures/.* + - id: end-of-file-fixer + exclude: ^tests/.*/fixtures/.* + - id: debug-statements + + - repo: https://github.com/astral-sh/ruff-pre-commit + rev: v0.11.2 + hooks: + - id: ruff + - id: ruff-format + + - repo: local + hooks: + - id: lint-rust + name: Lint Rust + entry: make lint-rust + types: [rust] + language: rust + pass_filenames: false diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index e0685ac5..00000000 --- a/.travis.yml +++ /dev/null @@ -1,78 +0,0 @@ -language: python - -stages: - - linting - - test - -cache: - pip: true - directories: - - $HOME/.cache/pypoetry - -jobs: - fast_finish: true - include: - - python: 2.7 - env: PENDULUM_EXTENSIONS=1 - - python: 2.7 - env: PENDULUM_EXTENSIONS=0 - - python: 3.4 - env: PENDULUM_EXTENSIONS=1 - - python: 3.4 - env: PENDULUM_EXTENSIONS=0 - - python: 3.5 - env: PENDULUM_EXTENSIONS=1 - - python: 3.5 - env: PENDULUM_EXTENSIONS=0 - - python: 3.6 - env: PENDULUM_EXTENSIONS=1 - - python: 3.6 - env: PENDULUM_EXTENSIONS=0 - - python: 3.7-dev - env: PENDULUM_EXTENSIONS=1 - - python: 3.7-dev - env: PENDULUM_EXTENSIONS=0 - - python: pypy - - python: pypy3 - - - stage: linting - python: "3.6" - install: - - pip install pre-commit - - pre-commit install-hooks - script: - - pre-commit run --all-files - allow_failures: - - python: pypy - - python: pypy3 - -before_install: - - pip install codecov - -install: - - | - if [ "$TRAVIS_PYTHON_VERSION" = "pypy" ]; then - export PYENV_ROOT="$HOME/.pyenv" - if [ -f "$PYENV_ROOT/bin/pyenv" ]; then - pushd "$PYENV_ROOT" && git pull && popd - else - rm -rf "$PYENV_ROOT" && git clone --depth 1 https://github.com/yyuu/pyenv.git "$PYENV_ROOT" - fi - export PYPY_VERSION="5.6.0" - "$PYENV_ROOT/bin/pyenv" install --skip-existing "pypy-$PYPY_VERSION" - virtualenv --python="$PYENV_ROOT/versions/pypy-$PYPY_VERSION/bin/python" "$HOME/virtualenvs/pypy-$PYPY_VERSION" - source "$HOME/virtualenvs/pypy-$PYPY_VERSION/bin/activate" - fi - - wget https://raw.githubusercontent.com/sdispater/poetry/master/get-poetry.py - - python get-poetry.py --preview - - poetry install -v - - poetry build -v - - | - if [ "$PENDULUM_EXTENSIONS" == "1" ]; then - find dist/ -iname pendulum*.whl -exec unzip -o {} 'pendulum/*' -d . \; - fi - -script: poetry run pytest --cov=pendulum --cov-config=.coveragerc tests/ -W ignore - -after_success: - - codecov diff --git a/CHANGELOG.md b/CHANGELOG.md index c4bccb2d..81183e4a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,158 @@ # Change Log +## [3.1.0] - 2025-04-19 + +### Added +- Added support for Python 3.13 [#871](https://github.com/python-pendulum/pendulum/pull/871) + +### Changed +- Removed support for Python 3.8 [#863](https://github.com/python-pendulum/pendulum/pull/863) +- Fixed pure Python wheels support [#889](https://github.com/python-pendulum/pendulum/pull/889) +- Fixed `pendulum.tz.timezones()` to use system tzdata [#801](https://github.com/python-pendulum/pendulum/pull/801) +- Fixed spelling of Kyiv [#885](https://github.com/python-pendulum/pendulum/pull/885) +- Fixed `DeprecationWarning` from `utcfromtimestamp` [#887](https://github.com/python-pendulum/pendulum/pull/887) +- Fixed parsing of invalid intervals [#843](https://github.com/python-pendulum/pendulum/pull/843) + +### Locales +- Added UA (Ukraine) locale [#793](https://github.com/python-pendulum/pendulum/pull/793) +- Added BG (Bulgarian) locale [#812](https://github.com/python-pendulum/pendulum/pull/812) +- Fixed KO (Korean) translations for `before` and `after` [#858](https://github.com/python-pendulum/pendulum/pull/858) + + +## [3.0.0] - 2023-12-16 + +### Changed + +- Relaxed dependency constraints. [#760](https://github.com/python-pendulum/pendulum/pull/760) +- The testing helpers are now optional and must be opted-in via the `test` extra. [#778](https://github.com/python-pendulum/pendulum/pull/778) + +### Fixed + +- Removed remaining mentions of periods instead of intervals. [#757](https://github.com/python-pendulum/pendulum/pull/757) +- Fixed the behavior of the `week_of_month` property for edge cases in January and December. [#774](https://github.com/python-pendulum/pendulum/pull/774) +- Fixed the handling of the `fold` attribute when deep-copying a `DateTime` instance. [#776](https://github.com/python-pendulum/pendulum/pull/776) +- Fixed errors where hours and days were not handled properly when adding durations. [#775](https://github.com/python-pendulum/pendulum/pull/775) +- Fixed errors where hours and days were not handled properly when adding durations. [#775](https://github.com/python-pendulum/pendulum/pull/775) + + +## [3.0.0b1] - 2023-10-01 + +### Added + +- Made `instance()` support all native types (date, time, datetime). [#732](https://github.com/python-pendulum/pendulum/pull/732) + +### Changed + +- Dropped support for Python 3.7. [#734](https://github.com/python-pendulum/pendulum/pull/734) +- Rewrote extensions in Rust. [#721](https://github.com/python-pendulum/pendulum/pull/721) +- Made day of week convention more consistent across the codebase. [#731](https://github.com/python-pendulum/pendulum/pull/731) + +### Fixed + +- Fixed datetime string representation to match the native library. [#733](https://github.com/python-pendulum/pendulum/pull/733) +- Fixed issues on some system when retrieving the local timezone. [#733](https://github.com/python-pendulum/pendulum/pull/733) +- Fixed DST handling in `start_of()/end_of()` methods. [#713](https://github.com/python-pendulum/pendulum/pull/713) + + +## [3.0.0a1] - 2022-11-23 + +### Added + +- Added new testing helpers to time travel. [#626](https://github.com/python-pendulum/pendulum/pull/626) + +### Changed + +- Dropped support for Python 2.7, 3.5 and 3.6. [#569](https://github.com/python-pendulum/pendulum/pull/569) +- The `Timezone` class now relies on the native `zoneinfo.ZoneInfo` class. [#569](https://github.com/python-pendulum/pendulum/pull/569) +- Renamed the `Period` class to `Interval`. [#676](https://github.com/python-pendulum/pendulum/pull/676) +- Renamed the `period` helper to `interval`. [#676](https://github.com/python-pendulum/pendulum/pull/676) +- Removed existing testing helpers: `test()` and `set_test_now()`. [#626](https://github.com/python-pendulum/pendulum/pull/626) + +### Locales + +- Added the `sk` locale. [#575](https://github.com/python-pendulum/pendulum/pull/575) +- Added the `ja` locale. [#610](https://github.com/python-pendulum/pendulum/pull/610) +- Added the `he` locale. [#585](https://github.com/python-pendulum/pendulum/pull/585) +- Added the `sv` locale. [#562](https://github.com/python-pendulum/pendulum/pull/562) + + +## [2.1.1] - 2020-07-13 + +### Fixed + +- Fixed errors where invalid timezones were matched in `from_format()` ([#374](https://github.com/python-pendulum/pendulum/pull/374)). +- Fixed errors when subtracting negative timedeltas ([#419](https://github.com/python-pendulum/pendulum/pull/419)). +- Fixed errors in total units computation for durations with years and months ([#482](https://github.com/python-pendulum/pendulum/pull/482)). +- Fixed an error where the `fold` attribute was overridden when using `replace()` ([#414](https://github.com/python-pendulum/pendulum/pull/414)). +- Fixed an error where `now()` was not returning the correct result on DST transitions ([#483](https://github.com/python-pendulum/pendulum/pull/483)). +- Fixed inconsistent typing annotation for the `parse()` function ([#452](https://github.com/python-pendulum/pendulum/pull/452)). + +### Locales + +- Added the `pl` locale ([#459](https://github.com/python-pendulum/pendulum/pull/459)). + + +## [2.1.0] - 2020-03-07 + +### Added + +- Added better typing and PEP-561 compliance ([#320](https://github.com/python-pendulum/pendulum/pull/320)). +- Added the `is_anniversary()` method as an alias of `is_birthday()` ([#298](https://github.com/python-pendulum/pendulum/pull/298)). + +### Changed + +- Dropped support for Python 3.4. +- `is_utc()` will now return `True` for any datetime with an offset of 0, similar to the behavior in the `1.*` versions ([#295](https://github.com/python-pendulum/pendulum/pull/295)) +- `Duration.in_words()` will now return `0 milliseconds` for empty durations. + +### Fixed + +- Fixed various issues with timezone transitions for some edge cases ([#321](https://github.com/python-pendulum/pendulum/pull/321), ([#350](https://github.com/python-pendulum/pendulum/pull/350))). +- Fixed out of bound detection for `nth_of("month")` ([#357](https://github.com/python-pendulum/pendulum/pull/357)). +- Fixed an error where extra text was accepted in `from_format()` ([#372](https://github.com/python-pendulum/pendulum/pull/372)). +- Fixed a recursion error when adding time to a `DateTime` with a fixed timezone ([#431](https://github.com/python-pendulum/pendulum/pull/431)). +- Fixed errors where `Period` instances were not properly compared to other classes, especially `timedelta` instances ([#427](https://github.com/python-pendulum/pendulum/pull/427)). +- Fixed deprecation warnings due to internal regexps ([#427](https://github.com/python-pendulum/pendulum/pull/427)). +- Fixed an error where the `test()` helper would not unset the test instance when an exception was raised ([#445](https://github.com/python-pendulum/pendulum/pull/445)). +- Fixed an error where the `week_of_month` attribute was not returning the correct value ([#446](https://github.com/python-pendulum/pendulum/pull/446)). +- Fixed an error in the way the `Z` ISO-8601 UTC designator was not parsed as UTC ([#448](https://github.com/python-pendulum/pendulum/pull/448)). + +### Locales + +- Added the `nl` locale. +- Added the `it` locale. +- Added the `id` locale. +- Added the `nb` locale. +- Added the `nn` locale. + + +## [2.0.5] - 2019-07-03 + +### Fixed + +- Fixed ISO week dates not being parsed properly in `from_format()`. +- Fixed loading of some timezones with empty posix spec. +- Fixed deprecation warnings. + +### Locales + +- Added RU locale. + + +## [2.0.4] - 2018-10-30 + +### Fixed + +- Fixed `from_format()` not recognizing input strings when the specified pattern had escaped elements. +- Fixed missing `x` token for string formatting. +- Fixed reading timezone files. +- Added support for parsing padded 2-digit days of the month with `from_format()` +- Fixed `from_format()` trying to parse escaped tokens. +- Fixed the `z` token timezone parsing in `from_format()` to allow underscores. +- Fixed C extensions build errors. +- Fixed `age` calculation for future dates. + + ## [2.0.3] - 2018-07-30 ### Fixed @@ -66,8 +219,16 @@ -[Unreleased]: https://github.com/sdispater/pendulum/compare/2.0.3...master -[2.0.3]: https://github.com/sdispater/pendulum/releases/tag/2.0.3 -[2.0.2]: https://github.com/sdispater/pendulum/releases/tag/2.0.2 -[2.0.1]: https://github.com/sdispater/pendulum/releases/tag/2.0.1 -[2.0.0]: https://github.com/sdispater/pendulum/releases/tag/2.0.0 +[Unreleased]: https://github.com/python-pendulum/pendulum/compare/3.1.0...master +[3.1.0]: https://github.com/python-pendulum/pendulum/releases/tag/3.1.0 +[3.0.0]: https://github.com/python-pendulum/pendulum/releases/tag/3.0.0 +[3.0.0b1]: https://github.com/python-pendulum/pendulum/releases/tag/3.0.0b1 +[3.0.0a1]: https://github.com/python-pendulum/pendulum/releases/tag/3.0.0a1 +[2.1.1]: https://github.com/python-pendulum/pendulum/releases/tag/2.1.1 +[2.1.0]: https://github.com/python-pendulum/pendulum/releases/tag/2.1.0 +[2.0.5]: https://github.com/python-pendulum/pendulum/releases/tag/2.0.5 +[2.0.4]: https://github.com/python-pendulum/pendulum/releases/tag/2.0.4 +[2.0.3]: https://github.com/python-pendulum/pendulum/releases/tag/2.0.3 +[2.0.2]: https://github.com/python-pendulum/pendulum/releases/tag/2.0.2 +[2.0.1]: https://github.com/python-pendulum/pendulum/releases/tag/2.0.1 +[2.0.0]: https://github.com/python-pendulum/pendulum/releases/tag/2.0.0 diff --git a/Makefile b/Makefile index a3a3ef44..575ab920 100644 --- a/Makefile +++ b/Makefile @@ -1,11 +1,3 @@ -# This file is part of orator -# https://github.com/sdispater/orator - -# Licensed under the MIT license: -# http://www.opensource.org/licenses/MIT-license -# Copyright (c) 2015 Sébastien Eustace - -PENDULUM_RELEASE := $$(sed -n -E "s/VERSION = '(.+)'/\1/p" pendulum/version.py) # lists all available targets list: @@ -16,49 +8,26 @@ list: # required for list no_targets__: -# install all dependencies -setup: setup-python - -# test your application (tests in the tests/ directory) -test: - @py.test --cov=pendulum --cov-config .coveragerc tests/ -sq - -release: wheels_x64 cp_wheels_x64 wheels_i686 cp_wheels_i686 wheel - -publish: - @poetry publish --no-build +lint-rust: + cd rust && cargo fmt --all -- --check + cd rust && cargo clippy --tests -- -D warnings -tar: - python setup.py sdist --formats=gztar -wheel: - @poetry build -v +format-rust: + cd rust && cargo fmt --all + cd rust && cargo clippy --tests --fix --allow-dirty -- -D warnings -wheels_x64: clean_wheels build_wheels_x64 +dev: + poetry install --only main --only test --only typing --only build --only lint + poetry run maturin develop -wheels_i686: clean_wheels build_wheels_i686 +lint: + poetry run mypy + poetry run pre-commit run --all-files -build_wheels_x64: - rm -rf wheelhouse/ - mkdir wheelhouse - docker pull quay.io/pypa/manylinux1_x86_64 - docker run --rm -v `pwd`:/io quay.io/pypa/manylinux1_x86_64 /io/build-wheels.sh - -build_wheels_i686: - rm -rf wheelhouse/ - mkdir wheelhouse - docker pull quay.io/pypa/manylinux1_i686 - docker run --rm -v `pwd`:/io quay.io/pypa/manylinux1_i686 /io/build-wheels.sh - -clean_wheels: - rm -rf wheelhouse/ - -cp_wheels_x64: - mv wheelhouse/*manylinux1_x86_64.whl dist/ - -cp_wheels_i686: - mv wheelhouse/*manylinux1_i686.whl dist/ +test: + PENDULUM_EXTENSIONS=0 poetry run pytest -q tests + poetry run pytest -q tests -# run tests against all supported python versions -tox: - @tox +clean: + rm src/pendulum/*.so diff --git a/README.rst b/README.rst index 70f59df8..63422f14 100644 --- a/README.rst +++ b/README.rst @@ -7,16 +7,13 @@ Pendulum .. image:: https://img.shields.io/pypi/l/pendulum.svg :target: https://pypi.python.org/pypi/pendulum -.. image:: https://img.shields.io/codecov/c/github/sdispater/pendulum/master.svg - :target: https://codecov.io/gh/sdispater/pendulum/branch/master - -.. image:: https://travis-ci.org/sdispater/pendulum.svg +.. image:: https://github.com/sdispater/pendulum/actions/workflows/tests.yml/badge.svg :alt: Pendulum Build status - :target: https://travis-ci.org/sdispater/pendulum + :target: https://github.com/sdispater/pendulum/actions Python datetimes made easy. -Supports Python **2.7** and **3.4+**. +Supports Python **3.9 and newer**. .. code-block:: python @@ -36,7 +33,7 @@ Supports Python **2.7** and **3.4+**. >>> past = pendulum.now().subtract(minutes=2) >>> past.diff_for_humans() - >>> '2 minutes ago' + '2 minutes ago' >>> delta = past - last_week >>> delta.hours @@ -55,6 +52,13 @@ Supports Python **2.7** and **3.4+**. '2013-03-31T03:00:00+02:00' +Resources +========= + +* `Official Website `_ +* `Documentation `_ +* `Issue Tracker `_ + Why Pendulum? ============= @@ -65,7 +69,7 @@ So it's still ``datetime`` but better. Unlike other datetime libraries for Python, Pendulum is a drop-in replacement for the standard ``datetime`` class (it inherits from it), so, basically, you can replace all your ``datetime`` -instances by ``DateTime`` instances in you code (exceptions exist for libraries that check +instances by ``DateTime`` instances in your code (exceptions exist for libraries that check the type of the objects by using the ``type`` function like ``sqlite3`` or ``PyMySQL`` for instance). It also removes the notion of naive datetimes: each ``Pendulum`` instance is timezone-aware @@ -73,55 +77,6 @@ and by default in ``UTC`` for ease of use. Pendulum also improves the standard ``timedelta`` class by providing more intuitive methods and properties. - -Why not Arrow? -============== - -Arrow is the most popular datetime library for Python right now, however its behavior -and API can be erratic and unpredictable. The ``get()`` method can receive pretty much anything -and it will try its best to return something while silently failing to handle some cases: - -.. code-block:: python - - arrow.get('2016-1-17') - # - - pendulum.parse('2016-1-17') - # - - arrow.get('20160413') - # - - pendulum.parse('20160413') - # - - arrow.get('2016-W07-5') - # - - pendulum.parse('2016-W07-5') - # - - # Working with DST - just_before = arrow.Arrow(2013, 3, 31, 1, 59, 59, 999999, 'Europe/Paris') - just_after = just_before.replace(microseconds=1) - '2013-03-31T02:00:00+02:00' - # Should be 2013-03-31T03:00:00+02:00 - - (just_after.to('utc') - just_before.to('utc')).total_seconds() - -3599.999999 - # Should be 1e-06 - - just_before = pendulum.datetime(2013, 3, 31, 1, 59, 59, 999999, 'Europe/Paris') - just_after = just_before.add(microseconds=1) - '2013-03-31T03:00:00+02:00' - - (just_after.in_timezone('utc') - just_before.in_timezone('utc')).total_seconds() - 1e-06 - -Those are a few examples showing that Arrow cannot always be trusted to have a consistent -behavior with the data you are passing to it. - - Limitations =========== @@ -169,14 +124,6 @@ a possible solution, if any: return '' if val is None else val.isoformat() -Resources -========= - -* `Official Website `_ -* `Documentation `_ -* `Issue Tracker `_ - - Contributing ============ @@ -186,7 +133,7 @@ Getting started --------------- To work on the Pendulum codebase, you'll want to clone the project locally -and install the required depedendencies via `poetry `_. +and install the required dependencies via `poetry `_. .. code-block:: bash @@ -203,7 +150,7 @@ If the locale does not exist you will need to create it by using the ``clock`` u .. code-block:: bash - ./clock locale:create + ./clock locale create It will generate a directory in ``pendulum/locales`` named after your locale, with the following structure: @@ -218,7 +165,7 @@ The ``locale.py`` file must not be modified. It contains the translations provid the CLDR database. The ``custom.py`` file is the one you want to modify. It contains the data needed -by Pendulum that are not provided by the CLDR database. You can take the `en `_ +by Pendulum that are not provided by the CLDR database. You can take the `en `_ data as a reference to see which data is needed. You should also add tests for the created or modified locale. diff --git a/appveyor.yml b/appveyor.yml deleted file mode 100644 index 55cba3cc..00000000 --- a/appveyor.yml +++ /dev/null @@ -1,34 +0,0 @@ -build: false - -environment: - matrix: - - PYTHON: "C:/Python27" - - PYTHON: "C:/Python27-x64" - - PYTHON: "C:/Python36" - - PYTHON: "C:/Python36-x64" - -cache: - - '%LocalAppData%\pip\Cache' - - '%LocalAppData%\pypoetry\Cache' - - -install: - - "SET PATH=%PYTHON%;%PYTHON%\\Scripts;%PATH%" - - # Upgrade to the latest version of pip to avoid it displaying warnings - # about it being out of date. - - "python -m pip install --disable-pip-version-check --user --upgrade pip" - - # Downloading and installing poetry - - curl -fsS -o get-poetry.py https://raw.githubusercontent.com/sdispater/poetry/master/get-poetry.py - - python get-poetry.py --preview - - # Install dependencies - - python -m pip install codecov - - poetry install -v - -test_script: - - "poetry run pytest --cov=pendulum --cov-config=.coveragerc tests/ -W ignore" - -after_test: - - "codecov" diff --git a/build-wheels.sh b/build-wheels.sh deleted file mode 100755 index 83a4d1e7..00000000 --- a/build-wheels.sh +++ /dev/null @@ -1,38 +0,0 @@ -#!/bin/bash -PYTHON_VERSIONS="cp27-cp27m cp34-cp34m cp35-cp35m cp36-cp36m cp37-cp37m" - -POETRY_PYTHON="cp37-cp37m" -POETRY_VENV="/opt/python/poetry" -echo "Create Poetry's virtualenv" -/opt/python/${POETRY_PYTHON}/bin/pip install virtualenv -/opt/python/${POETRY_PYTHON}/bin/virtualenv --python /opt/python/${POETRY_PYTHON}/bin/python ${POETRY_VENV} -${POETRY_VENV}/bin/pip install poetry --pre - -RELEASE=$(sed -n "s/__version__ = '\(.*\)'/\1/p" /io/pendulum/__version__.py) - -echo "Compile wheels" -for PYTHON in ${PYTHON_VERSIONS}; do - cd /io - /opt/python/${POETRY_PYTHON}/bin/virtualenv --python /opt/python/${PYTHON}/bin/python /opt/python/venv-${PYTHON} - . /opt/python/venv-${PYTHON}/bin/activate - ${POETRY_VENV}/bin/poetry install -v - ${POETRY_VENV}/bin/poetry build -v - mv dist/*-${RELEASE}-*-linux_*.whl wheelhouse/ - deactivate - cd - -done - -echo "Bundle external shared libraries into the wheels" -for whl in /io/wheelhouse/pendulum-${RELEASE}-*-linux_*.whl; do - auditwheel repair "$whl" -w /io/wheelhouse/ -done - -echo "Install packages and test" -for PYTHON in ${PYTHON_VERSIONS}; do - . /opt/python/venv-${PYTHON}/bin/activate - pip install pendulum==${RELEASE} --no-index --find-links /io/wheelhouse - find ./io/tests | grep -E "(__pycache__|\.pyc$)" | xargs rm -rf - pytest /io/tests -W ignore - find ./io/tests | grep -E "(__pycache__|\.pyc$)" | xargs rm -rf - deactivate -done diff --git a/build.py b/build.py deleted file mode 100644 index 299d76ba..00000000 --- a/build.py +++ /dev/null @@ -1,59 +0,0 @@ -import os -import sys - - -from distutils.core import Extension - -from distutils.errors import CCompilerError, DistutilsExecError, DistutilsPlatformError -from distutils.command.build_ext import build_ext - - -# C Extensions -with_extensions = os.getenv("PENDULUM_EXTENSIONS", None) - -if with_extensions == "1" or with_extensions is None: - with_extensions = True - -if with_extensions == "0" or hasattr(sys, "pypy_version_info"): - with_extensions = False - -extensions = [] -if with_extensions: - extensions = [ - Extension("pendulum._extensions._helpers", ["pendulum/_extensions/_helpers.c"]), - Extension("pendulum.parsing._iso8601", ["pendulum/parsing/_iso8601.c"]), - ] - - -class BuildFailed(Exception): - - pass - - -class ExtBuilder(build_ext): - # This class allows C extension building to fail. - - def run(self): - try: - build_ext.run(self) - except (DistutilsPlatformError, FileNotFoundError): - print("************************************************************") - print("Cannot compile C accelerator module, use pure python version") - print("************************************************************") - - def build_extension(self, ext): - try: - build_ext.build_extension(self, ext) - except (CCompilerError, DistutilsExecError, DistutilsPlatformError, ValueError): - print("************************************************************") - print("Cannot compile C accelerator module, use pure python version") - print("************************************************************") - - -def build(setup_kwargs): - """ - This function is mandatory in order to build the extensions. - """ - setup_kwargs.update( - {"ext_modules": extensions, "cmdclass": {"build_ext": ExtBuilder}} - ) diff --git a/clock b/clock index df73ff7d..5e026907 100755 --- a/clock +++ b/clock @@ -1,22 +1,26 @@ #!/usr/bin/env python -from __future__ import unicode_literals + +from __future__ import annotations import glob import json import os from babel.core import get_global -from babel.plural import ( - PluralRule, - compile_zero, - _GettextCompiler, - _binary_compiler, - _unary_compiler, -) -from babel.dates import tokenize_pattern, PATTERN_CHARS -from babel.localedata import load, normalize_locale, LocaleDataDict - -from cleo import Application, Command +from babel.dates import PATTERN_CHARS +from babel.dates import tokenize_pattern +from babel.localedata import LocaleDataDict +from babel.localedata import load +from babel.localedata import normalize_locale +from babel.plural import PluralRule +from babel.plural import _binary_compiler +from babel.plural import _GettextCompiler +from babel.plural import _unary_compiler +from babel.plural import compile_zero +from cleo.application import Application +from cleo.commands.command import Command +from cleo.helpers import argument + from pendulum import __version__ @@ -38,28 +42,25 @@ class _LambdaCompiler(_GettextCompiler): code = code.replace("||", "or") if method == "in": expr = self.compile(expr) - code = "(%s == %s and %s)" % (expr, expr, code) + code = f"({expr} == {expr} and {code})" return code class LocaleCreate(Command): - """ - Creates locale translations. + name = "locale create" + description = "Creates locale translations." + + arguments = [argument("locales", "Locales to dump.", optional=False, multiple=True)] + + TEMPLATE = """from .custom import translations as custom_translations - locale:create - {locales?* : Locales to dump} - """ - TEMPLATE = """# -*- coding: utf-8 -*- -from __future__ import unicode_literals \"\"\" {locale} locale file. It has been generated automatically and must not be modified directly. \"\"\" -from .custom import translations as custom_translations - locale = {{ 'plural': {plural}, @@ -69,9 +70,7 @@ locale = {{ }} """ - CUSTOM_TEMPLATE = """# -*- coding: utf-8 -*- -from __future__ import unicode_literals -\"\"\" + CUSTOM_TEMPLATE = """\"\"\" {locale} custom locale file. \"\"\" @@ -93,10 +92,10 @@ translations = {{}} normalized = normalize_locale(locale.replace("-", "_")) if not normalized: - self.error("Locale [{}] does not exist.".format(locale)) + self.line(f"Locale [{locale}] does not exist.") continue - self.line("Generating {} locale.".format(locale)) + self.line(f"Generating {locale} locale.") content = LocaleDataDict(load(normalized)) @@ -113,8 +112,8 @@ translations = {{}} data["days"] = {} for fmt, names in days.items(): data["days"][fmt] = {} - for value, name in names.items(): - data["days"][fmt][(value + 1) % 7] = name + for value, name in sorted(names.items()): + data["days"][fmt][value] = name # Getting months names months = content["months"]["format"] @@ -134,7 +133,7 @@ translations = {{}} ] data["units"] = {} for unit in units: - pattern = patterns["duration-{}".format(unit)]["long"] + pattern = patterns[f"duration-{unit}"]["long"] if "per" in pattern: del pattern["per"] @@ -159,6 +158,9 @@ translations = {{}} # Day periods data["day_periods"] = content["day_periods"]["format"]["wide"] + # Week data + data["week_data"] = content["week_data"] + result = self.TEMPLATE.format( locale=locale, plural=plural, @@ -188,13 +190,14 @@ translations = {{}} def format_dict(self, d, tab=1): s = ["{\n"] for k, v in d.items(): - if isinstance(v, (dict, LocaleDataDict)): - v = self.format_dict(v, tab + 1) - else: - v = repr(v) + v = ( + self.format_dict(v, tab + 1) + if isinstance(v, (dict, LocaleDataDict)) + else repr(v) + ) - s.append("%s%r: %s,\n" % (" " * tab, k, v)) - s.append("%s}" % (" " * (tab - 1))) + s.append(f"{' ' * tab}{k!r}: {v},\n") + s.append(f"{' ' * (tab - 1)}}}") return "".join(s) @@ -202,7 +205,7 @@ translations = {{}} to_py = _LambdaCompiler().compile result = ["lambda n: "] for tag, ast in PluralRule.parse(rule).abstract: - result.append("'%s' if %s else " % (tag, to_py(ast))) + result.append(f"'{tag}' if {to_py(ast)} else ") result.append("'other'") return "".join(result) @@ -217,59 +220,52 @@ translations = {{}} limit = PATTERN_CHARS[fieldchar] if limit and fieldnum not in limit: raise ValueError( - "Invalid length for field: %r" % (fieldchar * fieldnum) + f"Invalid length for field: {(fieldchar * fieldnum)!r}" ) result.append( self.TOKENS_MAP.get(fieldchar * fieldnum, fieldchar * fieldnum) ) else: - raise NotImplementedError("Unknown token type: %s" % tok_type) + raise NotImplementedError(f"Unknown token type: {tok_type}") return "".join(result) class LocaleRecreate(Command): - """ - Recreate existing locales. - - locale:recreate - """ + name = "locale recreate" + description = "Recreate existing locales." def handle(self): # Listing locales locales_dir = os.path.join("pendulum", "locales") locales = glob.glob(os.path.join(locales_dir, "*", "locale.py")) - locales = [os.path.basename(os.path.dirname(l)) for l in locales] + locales = [os.path.basename(os.path.dirname(locale)) for locale in locales] - self.call("locale:create", [("locales", locales)]) + self.call("locale create", "locales " + " ".join(locales)) class WindowsTzDump(Command): - """ - Dumps the mapping of Windows timezones to IANA timezones. - - windows:tz:dump - """ + name = "windows dump-timezones" + description = "Dumps the mapping of Windows timezones to IANA timezones." MAPPING_DIR = os.path.join("pendulum", "tz", "data") def handle(self): raw_tznames = get_global("windows_zone_mapping") - sorted_names = sorted(list(raw_tznames.keys())) + sorted_names = sorted(raw_tznames.keys()) tznames = {} for name in sorted_names: tznames[name] = raw_tznames[name] - mapping = json.dumps(tznames, indent=4) - mapping = "windows_timezones = " + mapping.replace('"', "'") + "\n" + mapping = json.dumps(tznames, indent=4).replace('"', "'") with open(os.path.join(self.MAPPING_DIR, "windows.py"), "w") as f: - f.write(mapping) + f.write(f"windows_timezones = {mapping}\n") -app = Application("Clock", __version__) +app = Application("clock", __version__) app.add(LocaleCreate()) app.add(LocaleRecreate()) app.add(WindowsTzDump()) diff --git a/codecov.yml b/codecov.yml deleted file mode 100644 index 3cbba287..00000000 --- a/codecov.yml +++ /dev/null @@ -1,7 +0,0 @@ -comment: false - -coverage: - status: - patch: - default: - enabled: false diff --git a/docs/docs/addition_subtraction.md b/docs/docs/addition_subtraction.md index cd55a96c..686f67f0 100644 --- a/docs/docs/addition_subtraction.md +++ b/docs/docs/addition_subtraction.md @@ -1,6 +1,6 @@ # Addition and Subtraction -To easily adding and subtracting time, you can use the `add()` and `subtract()` +To easily add and subtract time, you can use the `add()` and `subtract()` methods. Each method returns a new `DateTime` instance. @@ -79,12 +79,6 @@ Each method returns a new `DateTime` instance. '2015-04-03 12:31:43' >>> dt = dt.subtract(years=3, months=2, days=6, hours=12, minutes=31, seconds=43) '2012-01-28 00:00:00' - -# You can also add or remove a timedelta ->>> dt.add_timedelta(timedelta(hours=3, minutes=4, seconds=5)) -'2012-01-28 03:04:05' ->>> dt.sub_timedelta(timedelta(hours=3, minutes=4, seconds=5)) -'2012-01-28 00:00:00' ``` !!!note diff --git a/docs/docs/comparison.md b/docs/docs/comparison.md index bc3fb50e..6be8d0da 100644 --- a/docs/docs/comparison.md +++ b/docs/docs/comparison.md @@ -64,7 +64,7 @@ the `now()` is created in the same timezone as the instance. >>> born = pendulum.datetime(1987, 4, 23) >>> not_birthday = pendulum.datetime(2014, 9, 26) ->>> birthday = pendulum.datetime(2014, 2, 23) +>>> birthday = pendulum.datetime(2014, 4, 23) >>> past_birthday = pendulum.now().subtract(years=50) >>> born.is_birthday(not_birthday) diff --git a/docs/docs/difference.md b/docs/docs/difference.md index 3a7f0634..2653f01f 100644 --- a/docs/docs/difference.md +++ b/docs/docs/difference.md @@ -1,6 +1,6 @@ # Difference -The `diff()` method returns a [Period](#period) instance that represents the total duration +The `diff()` method returns an [Interval](#interval) instance that represents the total duration between two `DateTime` instances. This interval can be then expressed in various units. These interval methods always return *the total difference expressed* in the specified time requested. All values are truncated and not rounded. diff --git a/docs/docs/duration.md b/docs/docs/duration.md index 0801d9ea..a657d9ae 100644 --- a/docs/docs/duration.md +++ b/docs/docs/duration.md @@ -11,10 +11,10 @@ It has many improvements over the base class. ```python >>> import pendulum - >>> from datetime import datetime + >>> import datetime - >>> d1 = datetime(2012, 1, 1, 1, 2, 3, tzinfo=pytz.UTC) - >>> d2 = datetime(2011, 12, 31, 22, 2, 3, tzinfo=pytz.UTC) + >>> d1 = datetime.datetime(2012, 1, 1, 1, 2, 3, tzinfo=datetime.UTC) + >>> d2 = datetime.datetime(2011, 12, 31, 22, 2, 3, tzinfo=datetime.UTC) >>> delta = d2 - d1 >>> delta.days -1 diff --git a/docs/docs/fluent_helpers.md b/docs/docs/fluent_helpers.md index 5b020e59..2ace4910 100644 --- a/docs/docs/fluent_helpers.md +++ b/docs/docs/fluent_helpers.md @@ -1,8 +1,8 @@ # Fluent helpers -Pendulum provides helpers that returns a new instance with some attributes +Pendulum provides helpers that return a new instance with some attributes modified compared to the original instance. -However, none of these helpers, with the exception of explicitely setting the +However, none of these helpers, with the exception of explicitly setting the timezone, will change the timezone of the instance. Specifically, setting the timestamp will not set the corresponding timezone to UTC. @@ -38,8 +38,8 @@ You can also modify the timezone. >>> dt.set(tz='Europe/London') ``` -Setting the timezone just modify the timezone information without -making any conversion while `in_timezone()` (or `in_tz()`) +Setting the timezone just modifies the timezone information without +making any conversion, while `in_timezone()` (or `in_tz()`) converts the time in the appropriate timezone. ```python diff --git a/docs/docs/index.md b/docs/docs/index.md index 1d57f7b6..107e043c 100644 --- a/docs/docs/index.md +++ b/docs/docs/index.md @@ -1,17 +1,17 @@ -{!installation.md!} -{!introduction.md!} -{!instantiation.md!} -{!parsing.md!} -{!localization.md!} -{!attributes_properties.md!} -{!fluent_helpers.md!} -{!string_formatting.md!} -{!comparison.md!} -{!addition_subtraction.md!} -{!difference.md!} -{!modifiers.md!} -{!timezones.md!} -{!duration.md!} -{!period.md!} -{!testing.md!} -{!limitations.md!} +{!docs/installation.md!} +{!docs/introduction.md!} +{!docs/instantiation.md!} +{!docs/parsing.md!} +{!docs/localization.md!} +{!docs/attributes_properties.md!} +{!docs/fluent_helpers.md!} +{!docs/string_formatting.md!} +{!docs/comparison.md!} +{!docs/addition_subtraction.md!} +{!docs/difference.md!} +{!docs/modifiers.md!} +{!docs/timezones.md!} +{!docs/duration.md!} +{!docs/interval.md!} +{!docs/testing.md!} +{!docs/limitations.md!} diff --git a/docs/docs/installation.md b/docs/docs/installation.md index ea4a9afa..9f80e874 100644 --- a/docs/docs/installation.md +++ b/docs/docs/installation.md @@ -6,8 +6,28 @@ Installing `pendulum` is quite simple: $ pip install pendulum ``` -or, if you are using [poetry](https://poetry.eustace.io): +or, if you are using [poetry](https://python-poetry.org): ```bash $ poetry add pendulum ``` + +## Optional features + +Pendulum provides optional features that you must explicitly require in order to use them. + +These optional features are: + +- `test`: Provides a set of helpers to make testing easier by allowing you to control the flow of time. + +You can install them by specifying them when installing Pendulum + +```bash +$ pip install pendulum[test] +``` + +or, if you are using [poetry](https://python-poetry.org): + +```bash +$ poetry add pendulum[test] +``` diff --git a/docs/docs/instantiation.md b/docs/docs/instantiation.md index 43fa7a4d..a49c6a26 100644 --- a/docs/docs/instantiation.md +++ b/docs/docs/instantiation.md @@ -16,7 +16,7 @@ True `datetime()` sets the time to `00:00:00` if it's not specified, and the timezone (the `tz` keyword argument) to `UTC`. -It otherwise can be a `Timezone` instance or simply a string timezone value. +Otherwise it can be a `Timezone` instance or simply a string timezone value. ```python >>> import pendulum @@ -88,7 +88,7 @@ and each has their time value set to `00:00:00`. ``` Pendulum enforces timezone aware datetimes, and using them is the preferred and recommended way -of using the library, however is you really need a **naive** `DateTime` object, the `naive()` helper +of using the library. However, if you really need a **naive** `DateTime` object, the `naive()` helper is there for you. ```python @@ -133,7 +133,7 @@ and will set the timezone as well or default it to `UTC`. '1970-01-01T00:59:59+01:00' ``` -Finally, if you find yourself inheriting a `DateTime` instance, +Finally, if you find yourself inheriting a `datetime.datetime` instance, you can create a `DateTime` instance via the `instance()` function. ```python diff --git a/docs/docs/period.md b/docs/docs/interval.md similarity index 55% rename from docs/docs/period.md rename to docs/docs/interval.md index 523078ed..fc70fb3c 100644 --- a/docs/docs/period.md +++ b/docs/docs/interval.md @@ -1,6 +1,6 @@ -# Period +# Interval -When you subtract a `DateTime` instance to another, or use the `diff()` method, it will return a `Period` instance. +When you subtract a `DateTime` instance from another, or use the `diff()` method, it will return an `Interval` instance. It inherits from the [Duration](#duration) class with the added benefit that it is aware of the instances that generated it, so that it can give access to more methods and properties: @@ -10,29 +10,29 @@ instances that generated it, so that it can give access to more methods and prop >>> start = pendulum.datetime(2000, 11, 20) >>> end = pendulum.datetime(2016, 11, 5) ->>> period = end - start +>>> interval = end - start ->>> period.years +>>> interval.years 15 ->>> period.months +>>> interval.months 11 ->>> period.in_years() +>>> interval.in_years() 15 ->>> period.in_months() +>>> interval.in_months() 191 # Note that the weeks property # will change compared to the Duration class ->>> period.weeks +>>> interval.weeks 2 # 832 for the duration # However the days property will still remain the same -# to keep the compatiblity with the timedelta class ->>> period.days +# to keep the compatibility with the timedelta class +>>> interval.days 5829 ``` -Be aware that a period, just like an interval, is compatible with the `timedelta` class regarding +Be aware that an interval, just like an duration, is compatible with the `timedelta` class regarding its attributes. However, its custom attributes (like `remaining_days`) will be aware of any DST transitions that might have occurred and adjust accordingly. Let's take an example: @@ -42,42 +42,42 @@ transitions that might have occurred and adjust accordingly. Let's take an examp >>> start = pendulum.datetime(2017, 3, 7, tz='America/Toronto') >>> end = start.add(days=6) ->>> period = end - start +>>> interval = end - start # timedelta properties ->>> period.days +>>> interval.days 5 ->>> period.seconds +>>> interval.seconds 82800 -# period properties ->>> period.remaining_days +# interval properties +>>> interval.remaining_days 6 ->>> period.hours +>>> interval.hours 0 ->>> period.remaining_seconds +>>> interval.remaining_seconds 0 ``` !!!warning - Due to its nature (fixed duration between two datetimes), most arithmetic operations will - return a `Duration` instead of a `Period`. + Due to their nature (fixed duration between two datetimes), most arithmetic operations will + return a `Duration` instead of an `Interval`. ```python >>> import pendulum >>> dt1 = pendulum.datetime(2016, 8, 7, 12, 34, 56) >>> dt2 = dt1.add(days=6, seconds=34) - >>> period = pendulum.period(dt1, dt2) - >>> period * 2 + >>> interval = pendulum.interval(dt1, dt2) + >>> interval * 2 Duration(weeks=1, days=5, minutes=1, seconds=8) ``` ## Instantiation -You can create an instance by using the `period()` helper: +You can create an instance by using the `interval()` helper: ```python @@ -86,29 +86,29 @@ You can create an instance by using the `period()` helper: >>> start = pendulum.datetime(2000, 1, 1) >>> end = pendulum.datetime(2000, 1, 31) ->>> period = pendulum.period(start, end) +>>> interval = pendulum.interval(start, end) ``` -You can also make an inverted period: +You can also make an inverted interval: ```python ->>> period = pendulum.period(end, start) ->>> period.remaining_days +>>> interval = pendulum.interval(end, start) +>>> interval.remaining_days -2 ``` -If you have inverted dates but want to make sure that the period is positive, -you set the `absolute` keyword argument to `True`: +If you have inverted dates but want to make sure that the interval is positive, +you should set the `absolute` keyword argument to `True`: ```python ->>> period = pendulum.period(end, start, absolute=True) ->>> period.remaining_days +>>> interval = pendulum.interval(end, start, absolute=True) +>>> interval.remaining_days 2 ``` ## Range -If you want to iterate over a period, you can use the `range()` method: +If you want to iterate over a interval, you can use the `range()` method: ```python >>> import pendulum @@ -116,9 +116,9 @@ If you want to iterate over a period, you can use the `range()` method: >>> start = pendulum.datetime(2000, 1, 1) >>> end = pendulum.datetime(2000, 1, 10) ->>> period = pendulum.period(start, end) +>>> interval = pendulum.interval(start, end) ->>> for dt in period.range('days'): +>>> for dt in interval.range('days'): >>> print(dt) '2000-01-01T00:00:00+00:00' @@ -136,12 +136,12 @@ If you want to iterate over a period, you can use the `range()` method: !!!note Supported units for `range()` are: `years`, `months`, `weeks`, - `days`, `hours`, `minutes` and `seconds` + `days`, `hours`, `minutes`, `seconds` and `microseconds` You can pass an amount for the passed unit to control the length of the gap: ```python ->>> for dt in period.range('days', 2): +>>> for dt in interval.range('days', 2): >>> print(dt) '2000-01-01T00:00:00+00:00' @@ -151,18 +151,18 @@ You can pass an amount for the passed unit to control the length of the gap: '2000-01-09T00:00:00+00:00' ``` -You can also directly iterate over the `Period` instance, +You can also directly iterate over the `Interval` instance, the unit will be `days` in this case: ```python ->>> for dt in period: +>>> for dt in interval: >>> print(dt) ``` -You can check if a `DateTime` instance is inside a period using the `in` keyword: +You can check if a `DateTime` instance is inside a interval using the `in` keyword: ```python >>> dt = pendulum.datetime(2000, 1, 4) ->>> dt in period +>>> dt in interval True ``` diff --git a/docs/docs/introduction.md b/docs/docs/introduction.md index 95042125..0078b488 100644 --- a/docs/docs/introduction.md +++ b/docs/docs/introduction.md @@ -6,7 +6,7 @@ It provides classes that are drop-in replacements for the native ones (they inhe Special care has been taken to ensure timezones are handled correctly, and are based on the underlying `tzinfo` implementation. -For example all comparisons are done in `UTC` or in the timezone of the datetime being used. +For example, all comparisons are done in `UTC` or in the timezone of the datetime being used. ```python >>> import pendulum @@ -18,4 +18,4 @@ For example all comparisons are done in `UTC` or in the timezone of the datetime 3 ``` -The default timezone, except when using the `now()`, method will always be `UTC`. +The default timezone, except when using the `now()` method, will always be `UTC`. diff --git a/docs/docs/limitations.md b/docs/docs/limitations.md index 4470da8d..913aca1c 100644 --- a/docs/docs/limitations.md +++ b/docs/docs/limitations.md @@ -4,7 +4,7 @@ Even though the `DateTime` class is a subclass of `datetime`, there are some rare cases where it can't replace the native class directly. Here is a list (non-exhaustive) of the reported cases with a possible solution, if any: -* `sqlite3` will use the the `type()` function to determine the type of the object by default. To work around it you can register a new adapter: +* `sqlite3` will use the `type()` function to determine the type of the object by default. To work around it you can register a new adapter: ```python import pendulum @@ -13,7 +13,7 @@ Here is a list (non-exhaustive) of the reported cases with a possible solution, register_adapter(pendulum.DateTime, lambda val: val.isoformat(' ')) ``` -* `mysqlclient` (former `MySQLdb`) and `PyMySQL` will use the the `type()` function to determine the type of the object by default. To work around it you can register a new adapter: +* `mysqlclient` (former `MySQLdb`) and `PyMySQL` will use the `type()` function to determine the type of the object by default. To work around it you can register a new adapter: ```python import pendulum @@ -24,7 +24,7 @@ Here is a list (non-exhaustive) of the reported cases with a possible solution, pymysql.converters.conversions[pendulum.DateTime] = pymysql.converters.escape_datetime ``` -* `django` will use the `isoformat()` method to store datetimes in the database. However since `pendulum` is always timezone aware the offset information will always be returned by `isoformat()` raising an error, at least for MySQL databases. To work around it you can either create your own `DateTimeField` or use the previous workaround for `MySQLdb`: +* `django` will use the `isoformat()` method to store datetimes in the database. However, since `pendulum` is always timezone aware, the offset information will always be returned by `isoformat()` raising an error, at least for MySQL databases. To work around it, you can either create your own `DateTimeField` or use the previous workaround for `MySQLdb`: ```python import pendulum diff --git a/docs/docs/localization.md b/docs/docs/localization.md index b7055ed4..7560dae5 100644 --- a/docs/docs/localization.md +++ b/docs/docs/localization.md @@ -26,7 +26,7 @@ by using `pendulum.set_locale()`. ``` However, you might not want to set the locale globally. The `diff_for_humans()` -method accept a `locale` keyword argument to use a locale for a specific call. +method accepts a `locale` keyword argument to use a locale for a specific call. ```python >>> pendulum.set_locale('de') diff --git a/docs/docs/modifiers.md b/docs/docs/modifiers.md index 160db8c4..64a3a994 100644 --- a/docs/docs/modifiers.md +++ b/docs/docs/modifiers.md @@ -1,6 +1,6 @@ # Modifiers -These group of methods perform helpful modifications to a copy of the current instance. +This group of methods performs helpful modifications to a copy of the current instance. You'll notice that the `start_of()`, `next()` and `previous()` methods set the time to `00:00:00` and the `end_of()` methods set the time to `23:59:59.999999`. @@ -37,10 +37,10 @@ It returns the middle date between itself and the provided `DateTime` argument. '2019-12-31 23:59:59' >>> dt.start_of('century') -'2000-01-01 00:00:00' +'2001-01-01 00:00:00' >>> dt.end_of('century') -'2099-12-31 23:59:59' +'2100-12-31 23:59:59' >>> dt.start_of('week') '2012-01-30 00:00:00' @@ -81,6 +81,6 @@ True '2014-01-15 12:00:00' # others that are defined that are similar -# and tha accept month, quarter and year units +# and that accept month, quarter and year units # first_of(), last_of(), nth_of() ``` diff --git a/docs/docs/parsing.md b/docs/docs/parsing.md index 08c3f039..dd78fd45 100644 --- a/docs/docs/parsing.md +++ b/docs/docs/parsing.md @@ -18,10 +18,10 @@ The library natively supports the RFC 3339 format, most ISO 8601 formats and som >>> dt = pendulum.parse('1975-05-21 22:00:00') ``` -If you pass a non-standard or more complicated string, it will raise an exception so it is advised to +If you pass a non-standard or more complicated string, it will raise an exception, so it is advised to use the `from_format()` helper instead. -However, if you want the library to fallback on the [dateutil](https://dateutil.readthedocs.io) parser, +However, if you want the library to fall back on the [dateutil](https://dateutil.readthedocs.io) parser, you have to pass `strict=False`. ```python diff --git a/docs/docs/string_formatting.md b/docs/docs/string_formatting.md index 27c7a20c..91b95fc4 100644 --- a/docs/docs/string_formatting.md +++ b/docs/docs/string_formatting.md @@ -1,6 +1,6 @@ # String formatting -The `__str__` magic method is defined which allows `DateTime` instances to be printed +The `__str__` magic method is defined to allow `DateTime` instances to be printed as a pretty date string when used in a string context. The default string representation is the same as the one returned by the `isoformat()` method. @@ -108,6 +108,7 @@ The following tokens are currently supported: | ------------------------------ | ------------- | ------------------------------------------ | | **Year** | YYYY | 2000, 2001, 2002 ... 2012, 2013 | | | YY | 00, 01, 02 ... 12, 13 | +| | Y | 2000, 2001, 2002 ... 2012, 2013 | | **Quarter** | Q | 1 2 3 4 | | | Qo | 1st 2nd 3rd 4th | | **Month** | MMMM | January, February, March ... | @@ -125,8 +126,8 @@ The following tokens are currently supported: | | dd | Mo, Tu, We ... | | | d | 0, 1, 2 ... 6 | | **Days of ISO Week** | E | 1, 2, 3 ... 7 | -| **Hour** | HH | 00, 01, 02 ... 23, 24 | -| | H | 0, 1, 2 ... 23, 24 | +| **Hour** | HH | 00, 01, 02 ... 23 | +| | H | 0, 1, 2 ... 23 | | | hh | 01, 02, 03 ... 11, 12 | | | h | 1, 2, 3 ... 11, 12 | | **Minute** | mm | 00, 01, 02 ... 58, 59 | @@ -144,7 +145,7 @@ The following tokens are currently supported: | | z | Asia/Baku, Europe/Warsaw, GMT ... | | | zz | EST CST ... MST PST | | **Seconds timestamp** | X | 1381685817, 1234567890.123 | -| **Microseconds timestamp** | x | 1234567890123 | +| **Milliseconds timestamp** | x | 1234567890123 | ### Localized Formats @@ -169,6 +170,6 @@ To escape characters in format strings, you can wrap the characters in square br >>> import pendulum >>> dt = pendulum.now() ->>> dt.format('[today] dddd', formatter='alternative') +>>> dt.format('[today] dddd') 'today Sunday' ``` diff --git a/docs/docs/testing.md b/docs/docs/testing.md index 6e3da402..25aad8d6 100644 --- a/docs/docs/testing.md +++ b/docs/docs/testing.md @@ -1,58 +1,87 @@ # Testing -The testing methods allow you to set a `DateTime` instance (real or mock) to be returned -when a "now" instance is created. -The provided instance will be returned specifically under the following conditions: +Pendulum provides a few helpers to help you control the flow of time in your tests. Note that +these helpers are only available if you opted in the `test` extra during [installation](#installation). -* A call to the `now()` method, ex. `pendulum.now()`. -* When the string "now" is passed to the `parse()` method, ex. `pendulum.parse('now')` +!!!warning + If you are migrating from Pendulum 2, note that the `set_test_now()` and `test()` + helpers have been removed. + + +## Relative time travel + +You can travel in time relatively to the current time ```python >>> import pendulum -# Create testing datetime ->>> known = pendulum.datetime(2001, 5, 21, 12) +>>> now = pendulum.now() +>>> pendulum.travel(minutes=5) +>>> pendulum.now().diff_for_humans(now) +"5 minutes after" +``` -# Set the mock ->>> pendulum.set_test_now(known) +Note that once you've travelled in time the clock **keeps ticking**. If you prefer to stop the time completely +you can use the `freeze` parameter: ->>> print(pendulum.now()) -'2001-05-21T12:00:00+00:00' +```python +>>> import pendulum ->>> print(pendulum.parse('now')) -'2001-05-21T12:00:00+00:00' +>>> now = pendulum.now() +>>> pendulum.travel(minutes=5, freeze=True) +>>> pendulum.now().diff_for_humans(now) +"5 minutes after" # This will stay like this indefinitely +``` -# Clear the mock ->>> pendulum.set_test_now() ->>> print(pendulum.now()) -'2016-07-10T22:10:33.954851-05:00' +## Absolute time travel -Related methods will also returned values mocked according to the **now** instance. +Sometimes, you may want to place yourself at a specific point in time. +This is possible by using the `travel_to()` helper. This helper accepts a `DateTime` instance +that represents the point in time where you want to travel to. ```python ->>> print(pendulum.today()) -'2001-05-21T00:00:00+00:00' +>>> import pendulum + +>>> pendulum.travel_to(pendulum.yesterday()) +``` ->>> print(pendulum.tomorrow()) -'2001-05-22T00:00:00+00:00' +Similarly to `travel`, it's important to remember that, by default, the time keeps ticking so, if you prefer +stopping the time, use the `freeze` parameter: ->>> print(pendulum.yesterday()) -'2001-05-20T00:00:00+00:00' +```python +>>> import pendulum + +>>> pendulum.travel_to(pendulum.yesterday(), freeze=True) ``` -If you don't want to manually clear the mock (or you are afraid of forgetting), -you can use the provided `test()` contextmanager. +## Travelling back to the present + +Using any of the travel helpers will keep you in the past, or future, until you decide +to travel back to the present time. To do so, you may use the `travel_back()` helper. ```python >>> import pendulum ->>> known = pendulum.datetime(2001, 5, 21, 12) +>>> now = pendulum.now() +>>> pendulum.travel(minutes=5, freeze=True) +>>> pendulum.now().diff_for_humans(now) +"5 minutes after" +>>> pendulum.travel_back() +>>> pendulum.now().diff_for_humans(now) +"a few seconds after" +``` + +However, it might be cumbersome to remember to travel back so, instead, you can use any of the helpers as a context +manager: ->>> with pendulum.test(known): ->>> print(pendulum.now()) -'2001-05-21T12:00:00+00:00' +```python +>>> import pendulum ->>> print(pendulum.now()) -'2016-07-10T22:10:33.954851-05:00' +>>> now = pendulum.now() +>>> with pendulum.travel(minutes=5, freeze=True): +>>> pendulum.now().diff_for_humans(now) +"5 minutes after" +>>> pendulum.now().diff_for_humans(now) +"a few seconds after" ``` diff --git a/docs/docs/timezones.md b/docs/docs/timezones.md index 0f604020..e70034e7 100644 --- a/docs/docs/timezones.md +++ b/docs/docs/timezones.md @@ -1,6 +1,6 @@ # Timezones -Timezones are an important part of every datetime library and `pendulum` +Timezones are an important part of every datetime library, and `pendulum` tries to provide an easy and accurate system to handle them properly. !!!note @@ -67,7 +67,7 @@ adopt the proper behavior and apply the transition accordingly. >>> dt = dt.add(microseconds=1) '2013-03-31T03:00:00+02:00' >>> dt.subtract(microseconds=1) -'2013-03-31T01:59:59.999998+01:00' +'2013-03-31T01:59:59.999999+01:00' >>> dt = pendulum.datetime(2013, 10, 27, 2, 59, 59, 999999, tz='Europe/Paris', @@ -102,12 +102,12 @@ with the `in_timezone()` method. !!!warning **You should avoid using the timezone library in Python < 3.6.** - + This is due to the fact that Pendulum relies heavily on the presence of the `fold` attribute which was introduced in Python 3.6. - + The reason it works inside the Pendulum ecosystem is that it - backported the `fold` attribute in the `DateTime` class. + backports the `fold` attribute in the `DateTime` class. Like said in the introduction, you can use the timezone library directly with standard `datetime` objects but with limitations, especially @@ -134,7 +134,7 @@ by default to determine the transition rule. ``` Instead of relying on the `fold` attribute, you can use the `dst_rule` -keyword argument, this is especially useful if you want to raise errors +keyword argument. This is especially useful if you want to raise errors on non-existing and ambiguous times. ```python @@ -170,8 +170,8 @@ object, things get tricky. '2013-03-31T02:00:00+01:00' ``` -This is not what we expect, it should be `2013-03-31T03:00:00+02:00`. -This is actually easy to retrieve the proper datetime by using `convert()` +This is not what we expect. It should be `2013-03-31T03:00:00+02:00`. +It is actually easy to retrieve the proper datetime by using `convert()` again. ```python diff --git a/docs/mkdocs.yml b/docs/mkdocs.yml index ed109eaa..da64d75e 100644 --- a/docs/mkdocs.yml +++ b/docs/mkdocs.yml @@ -5,7 +5,7 @@ theme: custom_dir: theme extra: - version: 2.0 + version: 2.1 markdown_extensions: - codehilite diff --git a/pendulum/__init__.py b/pendulum/__init__.py deleted file mode 100644 index 2dcac7ac..00000000 --- a/pendulum/__init__.py +++ /dev/null @@ -1,314 +0,0 @@ -from __future__ import absolute_import - -import datetime as _datetime -from typing import Union - -from .__version__ import __version__ - -# Types -from .datetime import DateTime -from .date import Date -from .time import Time -from .duration import Duration -from .period import Period - -from .tz import timezone -from .tz import PRE_TRANSITION, POST_TRANSITION, TRANSITION_ERROR -from .tz.timezone import Timezone as _Timezone - -from .formatting import Formatter - -# Helpers -from .helpers import ( - test, - set_test_now, - has_test_now, - get_test_now, - set_locale, - get_locale, - locale, - format_diff, - week_starts_at, - week_ends_at, -) - -from .utils._compat import _HAS_FOLD - -from .tz import timezones, local_timezone, test_local_timezone, set_local_timezone, UTC - -from .parser import parse - -# Constants -from .constants import ( - MONDAY, - TUESDAY, - WEDNESDAY, - THURSDAY, - FRIDAY, - SATURDAY, - SUNDAY, - YEARS_PER_CENTURY, - YEARS_PER_DECADE, - MONTHS_PER_YEAR, - WEEKS_PER_YEAR, - DAYS_PER_WEEK, - HOURS_PER_DAY, - MINUTES_PER_HOUR, - SECONDS_PER_MINUTE, - SECONDS_PER_HOUR, - SECONDS_PER_DAY, -) - -_TEST_NOW = None -_LOCALE = "en" -_WEEK_STARTS_AT = MONDAY -_WEEK_ENDS_AT = SUNDAY - -_formatter = Formatter() - - -def _safe_timezone(obj): - # type: (Union[str, int, float, _datetime.tzinfo]) -> _Timezone - """ - Creates a timezone instance - from a string, Timezone, TimezoneInfo or integer offset. - """ - if isinstance(obj, _Timezone): - return obj - - if obj is None or obj == "local": - return local_timezone() - - if isinstance(obj, (int, float)): - obj = int(obj * 60 * 60) - elif isinstance(obj, _datetime.tzinfo): - # pytz - if hasattr(obj, "localize"): - obj = obj.zone - else: - offset = obj.utcoffset(None) - - if offset is None: - offset = _datetime.timedelta(0) - - obj = int(offset.total_seconds()) - - return timezone(obj) - - -# Public API -def datetime( - year, # type: int - month, # type: int - day, # type: int - hour=0, # type: int - minute=0, # type: int - second=0, # type: int - microsecond=0, # type: int - tz=UTC, # type: Union[str, _Timezone] - dst_rule=POST_TRANSITION, # type: str -): # type: (...) -> DateTime - """ - Creates a new DateTime instance from a specific date and time. - """ - if tz is not None: - tz = _safe_timezone(tz) - - if not _HAS_FOLD: - dt = naive(year, month, day, hour, minute, second, microsecond) - else: - dt = _datetime.datetime(year, month, day, hour, minute, second, microsecond) - if tz is not None: - dt = tz.convert(dt, dst_rule=dst_rule) - - return DateTime( - dt.year, - dt.month, - dt.day, - dt.hour, - dt.minute, - dt.second, - dt.microsecond, - tzinfo=dt.tzinfo, - fold=dt.fold, - ) - - -def local( - year, month, day, hour=0, minute=0, second=0, microsecond=0 -): # type: (int, int, int, int, int, int, int) -> DateTime - """ - Return a DateTime in the local timezone. - """ - return datetime( - year, month, day, hour, minute, second, microsecond, tz=local_timezone() - ) - - -def naive( - year, month, day, hour=0, minute=0, second=0, microsecond=0 -): # type: (int, int, int, int, int, int, int) -> DateTime - """ - Return a naive DateTime. - """ - return DateTime(year, month, day, hour, minute, second, microsecond) - - -def date(year, month, day): # type: (int, int, int) -> Date - """ - Create a new Date instance. - """ - return Date(year, month, day) - - -def time(hour, minute=0, second=0, microsecond=0): # type: (int, int, int, int) -> Time - """ - Create a new Time instance. - """ - return Time(hour, minute, second, microsecond) - - -def instance( - dt, tz=UTC # type: _datetime.datetime # type: Union[str, _Timezone, None] -): # type: (...) -> DateTime - """ - Create a DateTime instance from a datetime one. - """ - if not isinstance(dt, _datetime.datetime): - raise ValueError("instance() only accepts datetime objects.") - - if isinstance(dt, DateTime): - return dt - - tz = dt.tzinfo or tz - - # Checking for pytz/tzinfo - if isinstance(tz, _datetime.tzinfo) and not isinstance(tz, _Timezone): - # pytz - if hasattr(tz, "localize") and tz.zone: - tz = tz.zone - else: - # We have no sure way to figure out - # the timezone name, we fallback - # on a fixed offset - tz = tz.utcoffset(dt).total_seconds() / 3600 - - return datetime( - dt.year, dt.month, dt.day, dt.hour, dt.minute, dt.second, dt.microsecond, tz=tz - ) - - -def now(tz=None): # type: (Union[str, _Timezone, None]) -> DateTime - """ - Get a DateTime instance for the current date and time. - """ - if has_test_now(): - test_instance = get_test_now() - _tz = _safe_timezone(tz) - - if tz is not None and _tz != test_instance.timezone: - test_instance = test_instance.in_tz(_tz) - - return test_instance - - if tz is None or tz == "local": - dt = _datetime.datetime.now(local_timezone()) - elif tz is UTC or tz == "UTC": - dt = _datetime.datetime.now(UTC) - else: - dt = _datetime.datetime.now(UTC) - tz = _safe_timezone(tz) - dt = tz.convert(dt) - - return instance(dt, tz) - - -def today(tz="local"): # type: (Union[str, _Timezone]) -> DateTime - """ - Create a DateTime instance for today. - """ - return now(tz).start_of("day") - - -def tomorrow(tz="local"): # type: (Union[str, _Timezone]) -> DateTime - """ - Create a DateTime instance for today. - """ - return today(tz).add(days=1) - - -def yesterday(tz="local"): # type: (Union[str, _Timezone]) -> DateTime - """ - Create a DateTime instance for today. - """ - return today(tz).subtract(days=1) - - -def from_format( - string, # type: str - fmt, # type: str - tz=UTC, # type: Union[str, _Timezone] - locale=None, # type: Union[str, None] -): # type: (...) -> DateTime - """ - Creates a DateTime instance from a specific format. - """ - parts = _formatter.parse(string, fmt, now(), locale=locale) - if parts["tz"] is None: - parts["tz"] = tz - - return datetime(**parts) - - -def from_timestamp( - timestamp, tz=UTC # type: Union[int, float] # type: Union[str, _Timezone] -): # type: (...) -> DateTime - """ - Create a DateTime instance from a timestamp. - """ - dt = _datetime.datetime.utcfromtimestamp(timestamp) - - dt = datetime( - dt.year, dt.month, dt.day, dt.hour, dt.minute, dt.second, dt.microsecond - ) - - if tz is not UTC or tz != "UTC": - dt = dt.in_timezone(tz) - - return dt - - -def duration( - days=0, # type: float - seconds=0, # type: float - microseconds=0, # type: float - milliseconds=0, # type: float - minutes=0, # type: float - hours=0, # type: float - weeks=0, # type: float - years=0, # type: float - months=0, # type: float -): # type: (...) -> Duration - """ - Create a Duration instance. - """ - return Duration( - days=days, - seconds=seconds, - microseconds=microseconds, - milliseconds=milliseconds, - minutes=minutes, - hours=hours, - weeks=weeks, - years=years, - months=months, - ) - - -def period( - start, end, absolute=False # type: DateTime # type: DateTime # type: bool -): # type: (...) -> Period - """ - Create a Period instance. - """ - return Period(start, end, absolute=absolute) diff --git a/pendulum/__version__.py b/pendulum/__version__.py deleted file mode 100644 index 5fa9130a..00000000 --- a/pendulum/__version__.py +++ /dev/null @@ -1 +0,0 @@ -__version__ = "2.0.3" diff --git a/pendulum/_extensions/_helpers.c b/pendulum/_extensions/_helpers.c deleted file mode 100644 index 2b744614..00000000 --- a/pendulum/_extensions/_helpers.c +++ /dev/null @@ -1,923 +0,0 @@ -/* ------------------------------------------------------------------------- */ - -#include -#include -#include -#include -#include -#include -#include -#include - -#ifndef PyVarObject_HEAD_INIT - #define PyVarObject_HEAD_INIT(type, size) PyObject_HEAD_INIT(type) size, -#endif - -/* ------------------------------------------------------------------------- */ - -#define EPOCH_YEAR 1970 - -#define DAYS_PER_N_YEAR 365 -#define DAYS_PER_L_YEAR 366 - -#define USECS_PER_SEC 1000000 - -#define SECS_PER_MIN 60 -#define SECS_PER_HOUR (60 * SECS_PER_MIN) -#define SECS_PER_DAY (SECS_PER_HOUR * 24) - -// 400-year chunks always have 146097 days (20871 weeks). -#define DAYS_PER_400_YEARS 146097L -#define SECS_PER_400_YEARS ((int64_t)DAYS_PER_400_YEARS * (int64_t)SECS_PER_DAY) - -// The number of seconds in an aligned 100-year chunk, for those that -// do not begin with a leap year and those that do respectively. -const int64_t SECS_PER_100_YEARS[2] = { - (uint64_t)(76L * DAYS_PER_N_YEAR + 24L * DAYS_PER_L_YEAR) * SECS_PER_DAY, - (uint64_t)(75L * DAYS_PER_N_YEAR + 25L * DAYS_PER_L_YEAR) * SECS_PER_DAY -}; - -// The number of seconds in an aligned 4-year chunk, for those that -// do not begin with a leap year and those that do respectively. -const int32_t SECS_PER_4_YEARS[2] = { - (4 * DAYS_PER_N_YEAR + 0 * DAYS_PER_L_YEAR) * SECS_PER_DAY, - (3 * DAYS_PER_N_YEAR + 1 * DAYS_PER_L_YEAR) * SECS_PER_DAY -}; - -// The number of seconds in non-leap and leap years respectively. -const int32_t SECS_PER_YEAR[2] = { - DAYS_PER_N_YEAR * SECS_PER_DAY, - DAYS_PER_L_YEAR * SECS_PER_DAY -}; - -#define MONTHS_PER_YEAR 12 - -// The month lengths in non-leap and leap years respectively. -const int32_t DAYS_PER_MONTHS[2][13] = { - {-1, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31}, - {-1, 31, 29, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31} -}; - -// The day offsets of the beginning of each (1-based) month in non-leap -// and leap years respectively. -// For example, in a leap year there are 335 days before December. -const int32_t MONTHS_OFFSETS[2][14] = { - {-1, 0, 31, 59, 90, 120, 151, 181, 212, 243, 273, 304, 334, 365}, - {-1, 0, 31, 60, 91, 121, 152, 182, 213, 244, 274, 305, 335, 366} -}; - -const int DAY_OF_WEEK_TABLE[12] = { - 0, 3, 2, 5, 0, 3, 5, 1, 4, 6, 2, 4 -}; - -#define TM_SUNDAY 0 -#define TM_MONDAY 1 -#define TM_TUESDAY 2 -#define TM_WEDNESDAY 3 -#define TM_THURSDAY 4 -#define TM_FRIDAY 5 -#define TM_SATURDAY 6 - -#define TM_JANUARY 0 -#define TM_FEBRUARY 1 -#define TM_MARCH 2 -#define TM_APRIL 3 -#define TM_MAY 4 -#define TM_JUNE 5 -#define TM_JULY 6 -#define TM_AUGUST 7 -#define TM_SEPTEMBER 8 -#define TM_OCTOBER 9 -#define TM_NOVEMBER 10 -#define TM_DECEMBER 11 - -/* ------------------------------------------------------------------------- */ - -int _p(int y) { - return y + y/4 - y/100 + y /400; -} - -int _is_leap(int year) { - return year % 4 == 0 && (year % 100 != 0 || year % 400 == 0); -} - -int _is_long_year(int year) { - return (p(year) % 7 == 4) || (p(year - 1) % 7 == 3); -} - -int _week_day(int year, int month, int day) { - int y; - int w; - - y = year - (month < 3); - - w = (_p(y) + DAY_OF_WEEK_TABLE[month - 1] + day) % 7; - - if (!w) { - w = 7; - } - - return w; -} - -int _days_in_year(int year) { - if (_is_leap(year)) { - return DAYS_PER_L_YEAR; - } - - return DAYS_PER_N_YEAR; -} - -int _day_number(int year, int month, int day) { - month = (month + 9) % 12; - year = year - month / 10; - - return ( - 365 * year - + year / 4 - year / 100 + year / 400 - + (month * 306 + 5) / 10 - + (day - 1) - ); -} - -int _get_offset(PyObject *dt) { - PyObject *tzinfo; - PyObject *offset; - - tzinfo = ((PyDateTime_DateTime *)(dt))->tzinfo; - - if (tzinfo != Py_None) { - offset = PyObject_CallMethod(tzinfo, "utcoffset", "O", dt); - - return - PyDateTime_DELTA_GET_DAYS(offset) * SECS_PER_DAY - + PyDateTime_DELTA_GET_SECONDS(offset); - } - - return 0; -} - -int _has_tzinfo(PyObject *dt) { - return ((_PyDateTime_BaseTZInfo *)(dt))->hastzinfo; -} - -char* _get_tz_name(PyObject *dt) { - PyObject *tzinfo; - char *tz = ""; - - tzinfo = ((PyDateTime_DateTime *)(dt))->tzinfo; - - if (tzinfo != Py_None) { - if (PyObject_HasAttrString(tzinfo, "name")) { - // Pendulum timezone - tz = PyUnicode_AsUTF8( - PyObject_GetAttrString(tzinfo, "name") - ); - } else if (PyObject_HasAttrString(tzinfo, "zone")) { - // pytz timezone - tz = PyUnicode_AsUTF8( - PyObject_GetAttrString(tzinfo, "zone") - ); - } - } - - return tz; -} - -/* ------------------------ Custom Types ------------------------------- */ - -/* - * class Diff(): - */ -typedef struct { - PyObject_HEAD - int years; - int months; - int days; - int hours; - int minutes; - int seconds; - int microseconds; - int total_days; -} Diff; - -/* - * def __init__(self, years, months, days, hours, minutes, seconds, microseconds, total_days): - * self.years = years - * # ... -*/ -static int Diff_init(Diff *self, PyObject *args, PyObject *kwargs) { - int years; - int months; - int days; - int hours; - int minutes; - int seconds; - int microseconds; - int total_days; - - if (!PyArg_ParseTuple(args, "iiiiiii", &years, &months, &days, &hours, &minutes, &seconds, µseconds, &total_days)) - return -1; - - self->years = years; - self->months = months; - self->days = days; - self->hours = hours; - self->minutes = minutes; - self->seconds = seconds; - self->microseconds = microseconds; - self->total_days = total_days; - - return 0; -} - -/* - * def __repr__(self): - * return '{} years {} months {} days {} hours {} minutes {} seconds {} microseconds'.format( - * self.years, self.months, self.days, self.minutes, self.hours, self.seconds, self.microseconds - * ) - */ -static PyObject *Diff_repr(Diff *self) { - char repr[82] = {0}; - - sprintf( - repr, - "%d years %d months %d days %d hours %d minutes %d seconds %d microseconds", - self->years, - self->months, - self->days, - self->hours, - self->minutes, - self->seconds, - self->microseconds - ); - - return PyUnicode_FromString(repr); -} - -/* - * Instantiate new Diff_type object - * Skip overhead of calling PyObject_New and PyObject_Init. - * Directly allocate object. - */ -static PyObject *new_diff_ex(int years, int months, int days, int hours, int minutes, int seconds, int microseconds, int total_days, PyTypeObject *type) { - Diff *self = (Diff *) (type->tp_alloc(type, 0)); - - if (self != NULL) { - self->years = years; - self->months = months; - self->days = days; - self->hours = hours; - self->minutes = minutes; - self->seconds = seconds; - self->microseconds = microseconds; - self->total_days = total_days; - } - - return (PyObject *) self; -} - -/* - * Class member / class attributes - */ -static PyMemberDef Diff_members[] = { - {"years", T_INT, offsetof(Diff, years), 0, "years in diff"}, - {"months", T_INT, offsetof(Diff, months), 0, "months in diff"}, - {"days", T_INT, offsetof(Diff, days), 0, "days in diff"}, - {"hours", T_INT, offsetof(Diff, hours), 0, "hours in diff"}, - {"minutes", T_INT, offsetof(Diff, minutes), 0, "minutes in diff"}, - {"seconds", T_INT, offsetof(Diff, seconds), 0, "seconds in diff"}, - {"microseconds", T_INT, offsetof(Diff, microseconds), 0, "microseconds in diff"}, - {"total_days", T_INT, offsetof(Diff, total_days), 0, "total days in diff"}, - {NULL} -}; - -static PyTypeObject Diff_type = { - PyVarObject_HEAD_INIT(NULL, 0) - "PreciseDiff", /* tp_name */ - sizeof(Diff), /* tp_basicsize */ - 0, /* tp_itemsize */ - 0, /* tp_dealloc */ - 0, /* tp_print */ - 0, /* tp_getattr */ - 0, /* tp_setattr */ - 0, /* tp_as_async */ - (reprfunc)Diff_repr, /* tp_repr */ - 0, /* tp_as_number */ - 0, /* tp_as_sequence */ - 0, /* tp_as_mapping */ - 0, /* tp_hash */ - 0, /* tp_call */ - (reprfunc)Diff_repr, /* tp_str */ - 0, /* tp_getattro */ - 0, /* tp_setattro */ - 0, /* tp_as_buffer */ - Py_TPFLAGS_DEFAULT|Py_TPFLAGS_BASETYPE, /* tp_flags */ - "Precise difference between two datetime objects", /* tp_doc */ -}; - -#define new_diff(years, months, days, hours, minutes, seconds, microseconds, total_days) new_diff_ex(years, months, days, hours, minutes, seconds, microseconds, total_days, &Diff_type) - - -/* -------------------------- Functions --------------------------*/ - -PyObject* is_leap(PyObject *self, PyObject *args) { - PyObject *leap; - int year; - - if (!PyArg_ParseTuple(args, "i", &year)) { - PyErr_SetString( - PyExc_ValueError, "Invalid parameters" - ); - return NULL; - } - - leap = PyBool_FromLong(_is_leap(year)); - - return leap; -} - -PyObject* is_long_year(PyObject *self, PyObject *args) { - PyObject *is_long; - int year; - - if (!PyArg_ParseTuple(args, "i", &year)) { - PyErr_SetString( - PyExc_ValueError, "Invalid parameters" - ); - return NULL; - } - - is_long = PyBool_FromLong(_is_long_year(year)); - - return is_long; -} - -PyObject* week_day(PyObject *self, PyObject *args) { - PyObject *wd; - int year; - int month; - int day; - - if (!PyArg_ParseTuple(args, "iii", &year, &month, &day)) { - PyErr_SetString( - PyExc_ValueError, "Invalid parameters" - ); - return NULL; - } - - wd = PyLong_FromLong(_week_day(year, month, day)); - - return wd; -} - -PyObject* days_in_year(PyObject *self, PyObject *args) { - PyObject *ndays; - int year; - - if (!PyArg_ParseTuple(args, "i", &year)) { - PyErr_SetString( - PyExc_ValueError, "Invalid parameters" - ); - return NULL; - } - - ndays = PyLong_FromLong(_days_in_year(year)); - - return ndays; -} - -PyObject* timestamp(PyObject *self, PyObject *args) { - int64_t result; - PyObject* dt; - - if (!PyArg_ParseTuple(args, "O", &dt)) { - PyErr_SetString( - PyExc_ValueError, "Invalid parameters" - ); - return NULL; - } - - int year = (double) PyDateTime_GET_YEAR(dt); - int month = PyDateTime_GET_MONTH(dt); - int day = PyDateTime_GET_DAY(dt); - int hour = PyDateTime_DATE_GET_HOUR(dt); - int minute = PyDateTime_DATE_GET_MINUTE(dt); - int second = PyDateTime_DATE_GET_SECOND(dt); - - result = (year - 1970) * 365 + MONTHS_OFFSETS[0][month]; - result += (int) floor((double) (year - 1968) / 4); - result -= (year - 1900) / 100; - result += (year - 1600) / 400; - - if (_is_leap(year) && month < 3) { - result -= 1; - } - - result += day - 1; - result *= 24; - result += hour; - result *= 60; - result += minute; - result *= 60; - result += second; - - return PyLong_FromLong(result); -} - -PyObject* local_time(PyObject *self, PyObject *args) { - double unix_time; - int32_t utc_offset; - int32_t year; - int32_t microsecond; - int64_t seconds; - int32_t leap_year; - int64_t sec_per_100years; - int64_t sec_per_4years; - int32_t sec_per_year; - int32_t month; - int32_t day; - int32_t month_offset; - int32_t hour; - int32_t minute; - int32_t second; - - if (!PyArg_ParseTuple(args, "dii", &unix_time, &utc_offset, µsecond)) { - PyErr_SetString( - PyExc_ValueError, "Invalid parameters" - ); - return NULL; - } - - year = EPOCH_YEAR; - seconds = (int64_t) floor(unix_time); - - // Shift to a base year that is 400-year aligned. - if (seconds >= 0) { - seconds -= 10957L * SECS_PER_DAY; - year += 30; // == 2000; - } else { - seconds += (int64_t)(146097L - 10957L) * SECS_PER_DAY; - year -= 370; // == 1600; - } - - seconds += utc_offset; - - // Handle years in chunks of 400/100/4/1 - year += 400 * (seconds / SECS_PER_400_YEARS); - seconds %= SECS_PER_400_YEARS; - if (seconds < 0) { - seconds += SECS_PER_400_YEARS; - year -= 400; - } - - leap_year = 1; // 4-century aligned - - sec_per_100years = SECS_PER_100_YEARS[leap_year]; - - while (seconds >= sec_per_100years) { - seconds -= sec_per_100years; - year += 100; - leap_year = 0; // 1-century, non 4-century aligned - sec_per_100years = SECS_PER_100_YEARS[leap_year]; - } - - sec_per_4years = SECS_PER_4_YEARS[leap_year]; - while (seconds >= sec_per_4years) { - seconds -= sec_per_4years; - year += 4; - leap_year = 1; // 4-year, non century aligned - sec_per_4years = SECS_PER_4_YEARS[leap_year]; - } - - sec_per_year = SECS_PER_YEAR[leap_year]; - while (seconds >= sec_per_year) { - seconds -= sec_per_year; - year += 1; - leap_year = 0; // non 4-year aligned - sec_per_year = SECS_PER_YEAR[leap_year]; - } - - // Handle months and days - month = TM_DECEMBER + 1; - day = seconds / SECS_PER_DAY + 1; - seconds %= SECS_PER_DAY; - while (month != TM_JANUARY + 1) { - month_offset = MONTHS_OFFSETS[leap_year][month]; - if (day > month_offset) { - day -= month_offset; - break; - } - - month -= 1; - } - - // Handle hours, minutes and seconds - hour = seconds / SECS_PER_HOUR; - seconds %= SECS_PER_HOUR; - minute = seconds / SECS_PER_MIN; - second = seconds % SECS_PER_MIN; - - return Py_BuildValue("NNNNNNN", - PyLong_FromLong(year), - PyLong_FromLong(month), - PyLong_FromLong(day), - PyLong_FromLong(hour), - PyLong_FromLong(minute), - PyLong_FromLong(second), - PyLong_FromLong(microsecond) - ); -} - - -// Calculate a precise difference between two datetimes. -PyObject* precise_diff(PyObject *self, PyObject *args) { - PyObject* dt1; - PyObject* dt2; - - if (!PyArg_ParseTuple(args, "OO", &dt1, &dt2)) { - PyErr_SetString( - PyExc_ValueError, "Invalid parameters" - ); - return NULL; - } - - int year_diff = 0; - int month_diff = 0; - int day_diff = 0; - int hour_diff = 0; - int minute_diff = 0; - int second_diff = 0; - int microsecond_diff = 0; - int sign = 1; - int year; - int month; - int leap; - int days_in_last_month; - int days_in_month; - int dt1_year = PyDateTime_GET_YEAR(dt1); - int dt2_year = PyDateTime_GET_YEAR(dt2); - int dt1_month = PyDateTime_GET_MONTH(dt1); - int dt2_month = PyDateTime_GET_MONTH(dt2); - int dt1_day = PyDateTime_GET_DAY(dt1); - int dt2_day = PyDateTime_GET_DAY(dt2); - int dt1_hour = 0; - int dt2_hour = 0; - int dt1_minute = 0; - int dt2_minute = 0; - int dt1_second = 0; - int dt2_second = 0; - int dt1_microsecond = 0; - int dt2_microsecond = 0; - int dt1_total_seconds = 0; - int dt2_total_seconds = 0; - int dt1_offset = 0; - int dt2_offset = 0; - int dt1_is_datetime = PyDateTime_Check(dt1); - int dt2_is_datetime = PyDateTime_Check(dt2); - char *tz1 = ""; - char *tz2 = ""; - int in_same_tz = 0; - int total_days = ( - _day_number(dt2_year, dt2_month, dt2_day) - - _day_number(dt1_year, dt1_month, dt1_day) - ); - - // If both dates are datetimes, we check - // If we are in the same timezone - if (dt1_is_datetime && dt2_is_datetime) { - if (_has_tzinfo(dt1)) { - tz1 = _get_tz_name(dt1); - dt1_offset = _get_offset(dt1); - } - - if (_has_tzinfo(dt2)) { - tz2 = _get_tz_name(dt2); - dt2_offset = _get_offset(dt2); - } - - in_same_tz = tz1 == tz2 && strncmp(tz1, "", 1); - } - - // If we have datetimes (and not only dates) - // we get the information we need - if (dt1_is_datetime) { - dt1_hour = PyDateTime_DATE_GET_HOUR(dt1); - dt1_minute = PyDateTime_DATE_GET_MINUTE(dt1); - dt1_second = PyDateTime_DATE_GET_SECOND(dt1); - dt1_microsecond = PyDateTime_DATE_GET_MICROSECOND(dt1); - - if ((!in_same_tz && dt1_offset != 0) || total_days == 0) { - dt1_hour -= dt1_offset / SECS_PER_HOUR; - dt1_offset %= SECS_PER_HOUR; - dt1_minute -= dt1_offset / SECS_PER_MIN; - dt1_offset %= SECS_PER_MIN; - dt1_second -= dt1_offset; - - if (dt1_second < 0) { - dt1_second += 60; - dt1_minute -= 1; - } else if (dt1_second > 60) { - dt1_second -= 60; - dt1_minute += 1; - } - - if (dt1_minute < 0) { - dt1_minute += 60; - dt1_hour -= 1; - } else if (dt1_minute > 60) { - dt1_minute -= 60; - dt1_hour += 1; - } - - if (dt1_hour < 0) { - dt1_hour += 24; - dt1_day -= 1; - } else if (dt1_hour > 24) { - dt1_hour -= 24; - dt1_day += 1; - } - } - - dt1_total_seconds = ( - dt1_hour * SECS_PER_HOUR - + dt1_minute * SECS_PER_MIN - + dt1_second - ); - } - - if (dt2_is_datetime) { - dt2_hour = PyDateTime_DATE_GET_HOUR(dt2); - dt2_minute = PyDateTime_DATE_GET_MINUTE(dt2); - dt2_second = PyDateTime_DATE_GET_SECOND(dt2); - dt2_microsecond = PyDateTime_DATE_GET_MICROSECOND(dt2); - - if ((!in_same_tz && dt2_offset != 0) || total_days == 0) { - dt2_hour -= dt2_offset / SECS_PER_HOUR; - dt2_offset %= SECS_PER_HOUR; - dt2_minute -= dt2_offset / SECS_PER_MIN; - dt2_offset %= SECS_PER_MIN; - dt2_second -= dt2_offset; - - if (dt2_second < 0) { - dt2_second += 60; - dt2_minute -= 1; - } else if (dt2_second > 60) { - dt2_second -= 60; - dt2_minute += 1; - } - - if (dt2_minute < 0) { - dt2_minute += 60; - dt2_hour -= 1; - } else if (dt2_minute > 60) { - dt2_minute -= 60; - dt2_hour += 1; - } - - if (dt2_hour < 0) { - dt2_hour += 24; - dt2_day -= 1; - } else if (dt2_hour > 24) { - dt2_hour -= 24; - dt2_day += 1; - } - } - - dt2_total_seconds = ( - dt2_hour * SECS_PER_HOUR - + dt2_minute * SECS_PER_MIN - + dt2_second - ); - } - - // Direct comparison between two datetimes does not work - // so we need to check by properties - int dt1_gt_dt2 = ( - dt1_year > dt2_year - || (dt1_year == dt2_year && dt1_month > dt2_month) - || ( - dt1_year == dt2_year - && dt1_month == dt2_month - && dt1_day > dt2_day - ) - || ( - dt1_year == dt2_year - && dt1_month == dt2_month - && dt1_day == dt2_day - && dt1_total_seconds > dt2_total_seconds - ) - || ( - dt1_year == dt2_year - && dt1_month == dt2_month - && dt1_day == dt2_day - && dt1_total_seconds == dt2_total_seconds - && dt1_microsecond > dt2_microsecond - ) - ); - - if (dt1_gt_dt2) { - PyObject* temp; - temp = dt1; - dt1 = dt2; - dt2 = temp; - sign = -1; - - // Retrieving properties - dt1_year = PyDateTime_GET_YEAR(dt1); - dt2_year = PyDateTime_GET_YEAR(dt2); - dt1_month = PyDateTime_GET_MONTH(dt1); - dt2_month = PyDateTime_GET_MONTH(dt2); - dt1_day = PyDateTime_GET_DAY(dt1); - dt2_day = PyDateTime_GET_DAY(dt2); - - if (dt2_is_datetime) { - dt1_hour = PyDateTime_DATE_GET_HOUR(dt1); - dt1_minute = PyDateTime_DATE_GET_MINUTE(dt1); - dt1_second = PyDateTime_DATE_GET_SECOND(dt1); - dt1_microsecond = PyDateTime_DATE_GET_MICROSECOND(dt1); - } - - if (dt1_is_datetime) { - dt2_hour = PyDateTime_DATE_GET_HOUR(dt2); - dt2_minute = PyDateTime_DATE_GET_MINUTE(dt2); - dt2_second = PyDateTime_DATE_GET_SECOND(dt2); - dt2_microsecond = PyDateTime_DATE_GET_MICROSECOND(dt2); - } - - total_days = ( - _day_number(dt2_year, dt2_month, dt2_day) - - _day_number(dt1_year, dt1_month, dt1_day) - ); - } - - year_diff = dt2_year - dt1_year; - month_diff = dt2_month - dt1_month; - day_diff = dt2_day - dt1_day; - hour_diff = dt2_hour - dt1_hour; - minute_diff = dt2_minute - dt1_minute; - second_diff = dt2_second - dt1_second; - microsecond_diff = dt2_microsecond - dt1_microsecond; - - if (microsecond_diff < 0) { - microsecond_diff += 1e6; - second_diff -= 1; - } - - if (second_diff < 0) { - second_diff += 60; - minute_diff -= 1; - } - - if (minute_diff < 0) { - minute_diff += 60; - hour_diff -= 1; - } - - if (hour_diff < 0) { - hour_diff += 24; - day_diff -= 1; - } - - if (day_diff < 0) { - // If we have a difference in days, - // we have to check if they represent months - year = dt2_year; - month = dt2_month; - - if (month == 1) { - month = 12; - year -= 1; - } else { - month -= 1; - } - - leap = _is_leap(year); - - days_in_last_month = DAYS_PER_MONTHS[leap][month]; - days_in_month = DAYS_PER_MONTHS[_is_leap(dt2_year)][dt2_month]; - - if (day_diff < days_in_month - days_in_last_month) { - // We don't have a full month, we calculate days - if (days_in_last_month < dt1_day) { - day_diff += dt1_day; - } else { - day_diff += days_in_last_month; - } - } else if (day_diff == days_in_month - days_in_last_month) { - // We have exactly a full month - // We remove the days difference - // and add one to the months difference - day_diff = 0; - month_diff += 1; - } else { - // We have a full month - day_diff += days_in_last_month; - } - - month_diff -= 1; - } - - if (month_diff < 0) { - month_diff += 12; - year_diff -= 1; - } - - return new_diff( - year_diff * sign, - month_diff * sign, - day_diff * sign, - hour_diff * sign, - minute_diff * sign, - second_diff * sign, - microsecond_diff * sign, - total_days * sign - ); -} - -/* ------------------------------------------------------------------------- */ - -static PyMethodDef helpers_methods[] = { - { - "is_leap", - (PyCFunction) is_leap, - METH_VARARGS, - PyDoc_STR("Checks if a year is a leap year.") - }, - { - "is_long_year", - (PyCFunction) is_long_year, - METH_VARARGS, - PyDoc_STR("Checks if a year is a long year.") - }, - { - "week_day", - (PyCFunction) week_day, - METH_VARARGS, - PyDoc_STR("Returns the weekday number.") - }, - { - "days_in_year", - (PyCFunction) days_in_year, - METH_VARARGS, - PyDoc_STR("Returns the number of days in the given year.") - }, - { - "timestamp", - (PyCFunction) timestamp, - METH_VARARGS, - PyDoc_STR("Returns the timestamp of the given datetime.") - }, - { - "local_time", - (PyCFunction) local_time, - METH_VARARGS, - PyDoc_STR("Returns a UNIX time as a broken down time for a particular transition type.") - }, - { - "precise_diff", - (PyCFunction) precise_diff, - METH_VARARGS, - PyDoc_STR("Calculate a precise difference between two datetimes.") - }, - {NULL} -}; - -/* ------------------------------------------------------------------------- */ - -static struct PyModuleDef moduledef = { - PyModuleDef_HEAD_INIT, - "_helpers", - NULL, - -1, - helpers_methods, - NULL, - NULL, - NULL, - NULL, -}; - -PyMODINIT_FUNC -PyInit__helpers(void) -{ - PyObject *module; - - PyDateTime_IMPORT; - - module = PyModule_Create(&moduledef); - - if (module == NULL) - return NULL; - - // Diff declaration - Diff_type.tp_new = PyType_GenericNew; - Diff_type.tp_members = Diff_members; - Diff_type.tp_init = (initproc)Diff_init; - - if (PyType_Ready(&Diff_type) < 0) - return NULL; - - PyModule_AddObject(module, "PreciseDiff", (PyObject *)&Diff_type); - - return module; -} diff --git a/pendulum/exceptions.py b/pendulum/exceptions.py deleted file mode 100644 index 02a89f3f..00000000 --- a/pendulum/exceptions.py +++ /dev/null @@ -1,6 +0,0 @@ -from .parsing.exceptions import ParserError - - -class PendulumException(Exception): - - pass diff --git a/pendulum/formatting/__init__.py b/pendulum/formatting/__init__.py deleted file mode 100644 index 856321af..00000000 --- a/pendulum/formatting/__init__.py +++ /dev/null @@ -1,4 +0,0 @@ -from .formatter import Formatter - - -__all__ = ["Formatter"] diff --git a/pendulum/helpers.py b/pendulum/helpers.py deleted file mode 100644 index d69f3c86..00000000 --- a/pendulum/helpers.py +++ /dev/null @@ -1,179 +0,0 @@ -from __future__ import absolute_import - -import pendulum - -from math import copysign -from datetime import datetime, date, timedelta -from contextlib import contextmanager -from typing import Union - - -try: - from ._extensions._helpers import ( - local_time, - precise_diff, - is_leap, - is_long_year, - week_day, - days_in_year, - timestamp, - ) -except ImportError: - from ._extensions.helpers import ( - local_time, - precise_diff, - is_leap, - is_long_year, - week_day, - days_in_year, - timestamp, - ) - -from .constants import DAYS_PER_MONTHS -from .formatting.difference_formatter import DifferenceFormatter -from .locales.locale import Locale - - -difference_formatter = DifferenceFormatter() - - -def add_duration( - dt, # type: Union[datetime, date] - years=0, # type: int - months=0, # type: int - weeks=0, # type: int - days=0, # type: int - hours=0, # type: int - minutes=0, # type: int - seconds=0, # type: int - microseconds=0, -): # type: (...) -> Union[datetime, date] - """ - Adds a duration to a date/datetime instance. - """ - days += weeks * 7 - - if ( - isinstance(dt, date) - and not isinstance(dt, datetime) - and any([hours, minutes, seconds, microseconds]) - ): - raise RuntimeError("Time elements cannot be added to a date instance.") - - # Normalizing - if abs(microseconds) > 999999: - s = _sign(microseconds) - div, mod = divmod(microseconds * s, 1000000) - microseconds = mod * s - seconds += div * s - - if abs(seconds) > 59: - s = _sign(seconds) - div, mod = divmod(seconds * s, 60) - seconds = mod * s - minutes += div * s - - if abs(minutes) > 59: - s = _sign(minutes) - div, mod = divmod(minutes * s, 60) - minutes = mod * s - hours += div * s - - if abs(hours) > 23: - s = _sign(hours) - div, mod = divmod(hours * s, 24) - hours = mod * s - days += div * s - - if abs(months) > 11: - s = _sign(months) - div, mod = divmod(months * s, 12) - months = mod * s - years += div * s - - year = dt.year + years - month = dt.month - - if months: - month += months - if month > 12: - year += 1 - month -= 12 - elif month < 1: - year -= 1 - month += 12 - - day = min(DAYS_PER_MONTHS[int(is_leap(year))][month], dt.day) - - dt = dt.replace(year=year, month=month, day=day) - - return dt + timedelta( - days=days, - hours=hours, - minutes=minutes, - seconds=seconds, - microseconds=microseconds, - ) - - -def format_diff(diff, is_now=True, absolute=False, locale=None): - if locale is None: - locale = get_locale() - - return difference_formatter.format(diff, is_now, absolute, locale) - - -def _sign(x): - return int(copysign(1, x)) - - -# Global helpers - - -@contextmanager -def test(mock): - set_test_now(mock) - - yield - - set_test_now() - - -def set_test_now(test_now=None): - pendulum._TEST_NOW = test_now - - -def get_test_now(): # type: () -> pendulum.DateTime - return pendulum._TEST_NOW - - -def has_test_now(): # type: () -> bool - return pendulum._TEST_NOW is not None - - -def locale(name): - return Locale.load(name) - - -def set_locale(name): - locale(name) - - pendulum._LOCALE = name - - -def get_locale(): - return pendulum._LOCALE - - -def week_starts_at(wday): - if wday < pendulum.SUNDAY or wday > pendulum.SATURDAY: - raise ValueError("Invalid week day as start of week.") - - pendulum._WEEK_STARTS_AT = wday - - -def week_ends_at(wday): - if wday < pendulum.SUNDAY or wday > pendulum.SATURDAY: - raise ValueError("Invalid week day as start of week.") - - pendulum._WEEK_ENDS_AT = wday diff --git a/pendulum/locales/fr/__init__.py b/pendulum/locales/fr/__init__.py deleted file mode 100644 index 40a96afc..00000000 --- a/pendulum/locales/fr/__init__.py +++ /dev/null @@ -1 +0,0 @@ -# -*- coding: utf-8 -*- diff --git a/pendulum/locales/locale.py b/pendulum/locales/locale.py deleted file mode 100644 index 84429112..00000000 --- a/pendulum/locales/locale.py +++ /dev/null @@ -1,100 +0,0 @@ -# -*- coding: utf-8 -*- - -import os -import re - -from importlib import import_module - -from pendulum.utils._compat import basestring -from pendulum.utils._compat import decode - - -class Locale: - """ - Represent a specific locale. - """ - - _cache = {} - - def __init__(self, locale, data): - self._locale = locale - self._data = data - self._key_cache = {} - - @classmethod - def load(cls, locale): - if isinstance(locale, Locale): - return locale - - locale = cls.normalize_locale(locale) - if locale in cls._cache: - return cls._cache[locale] - - # Checking locale existence - actual_locale = locale - locale_path = os.path.join(os.path.dirname(__file__), actual_locale) - while not os.path.exists(locale_path): - if actual_locale == locale: - raise ValueError("Locale [{}] does not exist.".format(locale)) - - actual_locale = actual_locale.split("_")[0] - - m = import_module("pendulum.locales.{}.locale".format(actual_locale)) - - cls._cache[locale] = cls(locale, m.locale) - - return cls._cache[locale] - - @classmethod - def normalize_locale(cls, locale): - m = re.match("([a-z]{2})[-_]([a-z]{2})", locale, re.I) - if m: - return "{}_{}".format(m.group(1).lower(), m.group(2).lower()) - else: - return locale.lower() - - def get(self, key, default=None): - if key in self._key_cache: - return self._key_cache[key] - - parts = key.split(".") - try: - result = self._data[parts[0]] - for part in parts[1:]: - result = result[part] - except KeyError: - result = default - - if isinstance(result, basestring): - result = decode(result) - - self._key_cache[key] = result - - return self._key_cache[key] - - def translation(self, key): - return self.get("translations.{}".format(key)) - - def plural(self, number): - return decode(self._data["plural"](number)) - - def ordinal(self, number): - return decode(self._data["ordinal"](number)) - - def ordinalize(self, number): - ordinal = self.get("custom.ordinal.{}".format(self.ordinal(number))) - - if not ordinal: - return decode("{}".format(number)) - - return decode("{}{}".format(number, ordinal)) - - def match_translation(self, key, value): - translations = self.translation(key) - if value not in translations.values(): - return None - - return {v: k for k, v in translations.items()}[value] - - def __repr__(self): - return "{}('{}')".format(self.__class__.__name__, self._locale) diff --git a/pendulum/mixins/__init__.py b/pendulum/mixins/__init__.py deleted file mode 100644 index 40a96afc..00000000 --- a/pendulum/mixins/__init__.py +++ /dev/null @@ -1 +0,0 @@ -# -*- coding: utf-8 -*- diff --git a/pendulum/parsing/_iso8601.c b/pendulum/parsing/_iso8601.c deleted file mode 100644 index 1387332c..00000000 --- a/pendulum/parsing/_iso8601.c +++ /dev/null @@ -1,1352 +0,0 @@ -/* ------------------------------------------------------------------------- */ - -#include -#include -#include -#include -#include -#include -#include - -#ifndef PyVarObject_HEAD_INIT -#define PyVarObject_HEAD_INIT(type, size) PyObject_HEAD_INIT(type) size, -#endif - - -/* ------------------------------------------------------------------------- */ - -#define EPOCH_YEAR 1970 - -#define DAYS_PER_N_YEAR 365 -#define DAYS_PER_L_YEAR 366 - -#define USECS_PER_SEC 1000000 - -#define SECS_PER_MIN 60 -#define SECS_PER_HOUR (60 * SECS_PER_MIN) -#define SECS_PER_DAY (SECS_PER_HOUR * 24) - -// 400-year chunks always have 146097 days (20871 weeks). -#define DAYS_PER_400_YEARS 146097L -#define SECS_PER_400_YEARS ((int64_t)DAYS_PER_400_YEARS * (int64_t)SECS_PER_DAY) - -// The number of seconds in an aligned 100-year chunk, for those that -// do not begin with a leap year and those that do respectively. -const int64_t SECS_PER_100_YEARS[2] = { - (uint64_t)(76L * DAYS_PER_N_YEAR + 24L * DAYS_PER_L_YEAR) * SECS_PER_DAY, - (uint64_t)(75L * DAYS_PER_N_YEAR + 25L * DAYS_PER_L_YEAR) * SECS_PER_DAY -}; - -// The number of seconds in an aligned 4-year chunk, for those that -// do not begin with a leap year and those that do respectively. -const int32_t SECS_PER_4_YEARS[2] = { - (4 * DAYS_PER_N_YEAR + 0 * DAYS_PER_L_YEAR) * SECS_PER_DAY, - (3 * DAYS_PER_N_YEAR + 1 * DAYS_PER_L_YEAR) * SECS_PER_DAY -}; - -// The number of seconds in non-leap and leap years respectively. -const int32_t SECS_PER_YEAR[2] = { - DAYS_PER_N_YEAR * SECS_PER_DAY, - DAYS_PER_L_YEAR * SECS_PER_DAY -}; - -#define MONTHS_PER_YEAR 12 - -// The month lengths in non-leap and leap years respectively. -const int32_t DAYS_PER_MONTHS[2][13] = { - {-1, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31}, - {-1, 31, 29, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31} -}; - -// The day offsets of the beginning of each (1-based) month in non-leap -// and leap years respectively. -// For example, in a leap year there are 335 days before December. -const int32_t MONTHS_OFFSETS[2][14] = { - {-1, 0, 31, 59, 90, 120, 151, 181, 212, 243, 273, 304, 334, 365}, - {-1, 0, 31, 60, 91, 121, 152, 182, 213, 244, 274, 305, 335, 366} -}; - -const int DAY_OF_WEEK_TABLE[12] = { - 0, 3, 2, 5, 0, 3, 5, 1, 4, 6, 2, 4 -}; - -#define TM_SUNDAY 0 -#define TM_MONDAY 1 -#define TM_TUESDAY 2 -#define TM_WEDNESDAY 3 -#define TM_THURSDAY 4 -#define TM_FRIDAY 5 -#define TM_SATURDAY 6 - -#define TM_JANUARY 0 -#define TM_FEBRUARY 1 -#define TM_MARCH 2 -#define TM_APRIL 3 -#define TM_MAY 4 -#define TM_JUNE 5 -#define TM_JULY 6 -#define TM_AUGUST 7 -#define TM_SEPTEMBER 8 -#define TM_OCTOBER 9 -#define TM_NOVEMBER 10 -#define TM_DECEMBER 11 - -// Parsing errors -const int PARSER_INVALID_ISO8601 = 0; -const int PARSER_INVALID_DATE = 1; -const int PARSER_INVALID_TIME = 2; -const int PARSER_INVALID_WEEK_DATE = 3; -const int PARSER_INVALID_WEEK_NUMBER = 4; -const int PARSER_INVALID_WEEKDAY_NUMBER = 5; -const int PARSER_INVALID_ORDINAL_DAY_FOR_YEAR = 6; -const int PARSER_INVALID_MONTH_OR_DAY = 7; -const int PARSER_INVALID_MONTH = 8; -const int PARSER_INVALID_DAY_FOR_MONTH = 9; -const int PARSER_INVALID_HOUR = 10; -const int PARSER_INVALID_MINUTE = 11; -const int PARSER_INVALID_SECOND = 12; -const int PARSER_INVALID_SUBSECOND = 13; -const int PARSER_INVALID_TZ_OFFSET = 14; -const int PARSER_INVALID_DURATION = 15; -const int PARSER_INVALID_DURATION_FLOAT_YEAR_MONTH_NOT_SUPPORTED = 16; - -const char PARSER_ERRORS[17][80] = { - "Invalid ISO 8601 string", - "Invalid date", - "Invalid time", - "Invalid week date", - "Invalid week number", - "Invalid weekday number", - "Invalid ordinal day for year", - "Invalid month and/or day", - "Invalid month", - "Invalid day for month", - "Invalid hour", - "Invalid minute", - "Invalid second", - "Invalid subsecond", - "Invalid timezone offset", - "Invalid duration", - "Float years and months are not supported" -}; - -/* ------------------------------------------------------------------------- */ - - -int p(int y) { - return y + y/4 - y/100 + y/400; -} - -int is_leap(int year) { - return year % 4 == 0 && (year % 100 != 0 || year % 400 == 0); -} - -int week_day(int year, int month, int day) { - int y; - int w; - - y = year - (month < 3); - - w = (p(y) + DAY_OF_WEEK_TABLE[month - 1] + day) % 7; - - if (!w) { - w = 7; - } - - return w; -} - -int days_in_year(int year) { - if (is_leap(year)) { - return DAYS_PER_L_YEAR; - } - - return DAYS_PER_N_YEAR; -} - -int is_long_year(int year) { - return (p(year) % 7 == 4) || (p(year - 1) % 7 == 3); -} - - -/* ------------------------ Custom Types ------------------------------- */ - - -/* - * class FixedOffset(tzinfo): - */ -typedef struct { - PyObject_HEAD - int offset; -} FixedOffset; - -/* - * def __init__(self, offset): - * self.offset = offset -*/ -static int FixedOffset_init(FixedOffset *self, PyObject *args, PyObject *kwargs) { - int offset; - if (!PyArg_ParseTuple(args, "i", &offset)) - return -1; - - self->offset = offset; - return 0; -} - -/* - * def utcoffset(self, dt): - * return timedelta(seconds=self.offset * 60) - */ -static PyObject *FixedOffset_utcoffset(FixedOffset *self, PyObject *args) { - return PyDelta_FromDSU(0, self->offset, 0); -} - -/* - * def dst(self, dt): - * return timedelta(seconds=self.offset * 60) - */ -static PyObject *FixedOffset_dst(FixedOffset *self, PyObject *args) { - return PyDelta_FromDSU(0, self->offset, 0); -} - -/* - * def tzname(self, dt): - * sign = '+' - * if self.offset < 0: - * sign = '-' - * return "%s%d:%d" % (sign, self.offset / 60, self.offset % 60) - */ -static PyObject *FixedOffset_tzname(FixedOffset *self, PyObject *args) { - char tzname[7] = {0}; - char sign = '+'; - int offset = self->offset; - - if (offset < 0) { - sign = '-'; - offset *= -1; - } - - sprintf( - tzname, - "%c%02d:%02d", - sign, - offset / SECS_PER_HOUR, - offset / SECS_PER_MIN % SECS_PER_MIN - ); - - return PyUnicode_FromString(tzname); -} - -/* - * def __repr__(self): - * return self.tzname() - */ -static PyObject *FixedOffset_repr(FixedOffset *self) { - return FixedOffset_tzname(self, NULL); -} - -/* - * Class member / class attributes - */ -static PyMemberDef FixedOffset_members[] = { - {"offset", T_INT, offsetof(FixedOffset, offset), 0, "UTC offset"}, - {NULL} -}; - -/* - * Class methods - */ -static PyMethodDef FixedOffset_methods[] = { - {"utcoffset", (PyCFunction)FixedOffset_utcoffset, METH_VARARGS, ""}, - {"dst", (PyCFunction)FixedOffset_dst, METH_VARARGS, ""}, - {"tzname", (PyCFunction)FixedOffset_tzname, METH_VARARGS, ""}, - {NULL} -}; - -static PyTypeObject FixedOffset_type = { - PyVarObject_HEAD_INIT(NULL, 0) - "FixedOffset_type", /* tp_name */ - sizeof(FixedOffset), /* tp_basicsize */ - 0, /* tp_itemsize */ - 0, /* tp_dealloc */ - 0, /* tp_print */ - 0, /* tp_getattr */ - 0, /* tp_setattr */ - 0, /* tp_as_async */ - (reprfunc)FixedOffset_repr, /* tp_repr */ - 0, /* tp_as_number */ - 0, /* tp_as_sequence */ - 0, /* tp_as_mapping */ - 0, /* tp_hash */ - 0, /* tp_call */ - (reprfunc)FixedOffset_repr, /* tp_str */ - 0, /* tp_getattro */ - 0, /* tp_setattro */ - 0, /* tp_as_buffer */ - Py_TPFLAGS_DEFAULT|Py_TPFLAGS_BASETYPE, /* tp_flags */ - "TZInfo with fixed offset", /* tp_doc */ -}; - -/* - * Instantiate new FixedOffset_type object - * Skip overhead of calling PyObject_New and PyObject_Init. - * Directly allocate object. - */ -static PyObject *new_fixed_offset_ex(int offset, PyTypeObject *type) { - FixedOffset *self = (FixedOffset *) (type->tp_alloc(type, 0)); - - if (self != NULL) - self->offset = offset; - - return (PyObject *) self; -} - -#define new_fixed_offset(offset) new_fixed_offset_ex(offset, &FixedOffset_type) - - -/* - * class Duration(): - */ -typedef struct { - PyObject_HEAD - int years; - int months; - int weeks; - int days; - int hours; - int minutes; - int seconds; - int microseconds; -} Duration; - -/* - * def __init__(self, years, months, days, hours, minutes, seconds, microseconds): - * self.years = years - * # ... -*/ -static int Duration_init(Duration *self, PyObject *args, PyObject *kwargs) { - int years; - int months; - int weeks; - int days; - int hours; - int minutes; - int seconds; - int microseconds; - - if (!PyArg_ParseTuple(args, "iiiiiiii", &years, &months, &weeks, &days, &hours, &minutes, &seconds, µseconds)) - return -1; - - self->years = years; - self->months = months; - self->weeks = weeks; - self->days = days; - self->hours = hours; - self->minutes = minutes; - self->seconds = seconds; - self->microseconds = microseconds; - - return 0; -} - -/* - * def __repr__(self): - * return '{} years {} months {} days {} hours {} minutes {} seconds {} microseconds'.format( - * self.years, self.months, self.days, self.minutes, self.hours, self.seconds, self.microseconds - * ) - */ -static PyObject *Duration_repr(Duration *self) { - char repr[82] = {0}; - - sprintf( - repr, - "%d years %d months %d weeks %d days %d hours %d minutes %d seconds %d microseconds", - self->years, - self->months, - self->weeks, - self->days, - self->hours, - self->minutes, - self->seconds, - self->microseconds - ); - - return PyUnicode_FromString(repr); -} - -/* - * Instantiate new Duration_type object - * Skip overhead of calling PyObject_New and PyObject_Init. - * Directly allocate object. - */ -static PyObject *new_duration_ex(int years, int months, int weeks, int days, int hours, int minutes, int seconds, int microseconds, PyTypeObject *type) { - Duration *self = (Duration *) (type->tp_alloc(type, 0)); - - if (self != NULL) { - self->years = years; - self->months = months; - self->weeks = weeks; - self->days = days; - self->hours = hours; - self->minutes = minutes; - self->seconds = seconds; - self->microseconds = microseconds; - } - - return (PyObject *) self; -} - -/* - * Class member / class attributes - */ -static PyMemberDef Duration_members[] = { - {"years", T_INT, offsetof(Duration, years), 0, "years in duration"}, - {"months", T_INT, offsetof(Duration, months), 0, "months in duration"}, - {"weeks", T_INT, offsetof(Duration, weeks), 0, "weeks in duration"}, - {"days", T_INT, offsetof(Duration, days), 0, "days in duration"}, - {"remaining_days", T_INT, offsetof(Duration, days), 0, "days in duration"}, - {"hours", T_INT, offsetof(Duration, hours), 0, "hours in duration"}, - {"minutes", T_INT, offsetof(Duration, minutes), 0, "minutes in duration"}, - {"seconds", T_INT, offsetof(Duration, seconds), 0, "seconds in duration"}, - {"remaining_seconds", T_INT, offsetof(Duration, seconds), 0, "seconds in duration"}, - {"microseconds", T_INT, offsetof(Duration, microseconds), 0, "microseconds in duration"}, - {NULL} -}; - -static PyTypeObject Duration_type = { - PyVarObject_HEAD_INIT(NULL, 0) - "Duration", /* tp_name */ - sizeof(Duration), /* tp_basicsize */ - 0, /* tp_itemsize */ - 0, /* tp_dealloc */ - 0, /* tp_print */ - 0, /* tp_getattr */ - 0, /* tp_setattr */ - 0, /* tp_as_async */ - (reprfunc)Duration_repr, /* tp_repr */ - 0, /* tp_as_number */ - 0, /* tp_as_sequence */ - 0, /* tp_as_mapping */ - 0, /* tp_hash */ - 0, /* tp_call */ - (reprfunc)Duration_repr, /* tp_str */ - 0, /* tp_getattro */ - 0, /* tp_setattro */ - 0, /* tp_as_buffer */ - Py_TPFLAGS_DEFAULT|Py_TPFLAGS_BASETYPE, /* tp_flags */ - "Duration", /* tp_doc */ -}; - -#define new_duration(years, months, weeks, days, hours, minutes, seconds, microseconds) new_duration_ex(years, months, weeks, days, hours, minutes, seconds, microseconds, &Duration_type) - -typedef struct { - int is_date; - int is_time; - int is_datetime; - int is_duration; - int is_period; - int ambiguous; - int year; - int month; - int day; - int hour; - int minute; - int second; - int microsecond; - int offset; - int has_offset; - int years; - int months; - int weeks; - int days; - int hours; - int minutes; - int seconds; - int microseconds; - int error; -} Parsed; - - -Parsed* new_parsed() { - Parsed *parsed; - - if((parsed = malloc(sizeof *parsed)) != NULL) { - parsed->is_date = 0; - parsed->is_time = 0; - parsed->is_datetime = 0; - parsed->is_duration = 0; - parsed->is_period = 0; - - parsed->ambiguous = 0; - parsed->year = 0; - parsed->month = 1; - parsed->day = 1; - parsed->hour = 0; - parsed->minute = 0; - parsed->second = 0; - parsed->microsecond = 0; - parsed->offset = 0; - parsed->has_offset = 0; - - parsed->years = 0; - parsed->months = 0; - parsed->weeks = 0; - parsed->days = 0; - parsed->hours = 0; - parsed->minutes = 0; - parsed->seconds = 0; - parsed->microseconds = 0; - - parsed->error = -1; - } - - return parsed; -} - - -/* -------------------------- Functions --------------------------*/ - -Parsed* _parse_iso8601_datetime(char *str, Parsed *parsed) { - char* c; - int monthday = 0; - int week = 0; - int weekday = 1; - int ordinal; - int tz_sign = 0; - int leap = 0; - int separators = 0; - int time = 0; - int has_hour = 0; - int i; - int j; - - // Assuming date only for now - parsed->is_date = 1; - - c = str; - - for (i = 0; i < 4; i++) { - if (*c >= '0' && *c <= '9') { - parsed->year = 10 * parsed->year + *c++ - '0'; - } else { - parsed->error = PARSER_INVALID_ISO8601; - - return NULL; - } - } - - leap = is_leap(parsed->year); - - // Optional separator - if (*c == '-') { - separators++; - c++; - } - - // Checking for week dates - if (*c == 'W') { - c++; - - i = 0; - while (*c != '\0' && *c != ' ' && *c != 'T') { - if (*c == '-') { - separators++; - c++; - continue; - } - - week = 10 * week + *c++ - '0'; - - i++; - } - - switch (i) { - case 2: - // Only week number - break; - case 3: - // Week with weekday - if (!(separators == 0 || separators == 2)) { - // We should have 2 or no separator - parsed->error = PARSER_INVALID_WEEK_DATE; - - return NULL; - } - - weekday = week % 10; - week /= 10; - - break; - default: - // Any other case is wrong - parsed->error = PARSER_INVALID_WEEK_DATE; - - return NULL; - } - - // Checks - if (week > 53 || week > 52 && !is_long_year(parsed->year)) { - parsed->error = PARSER_INVALID_WEEK_NUMBER; - - return NULL; - } - - if (weekday > 7) { - parsed->error = PARSER_INVALID_WEEKDAY_NUMBER; - - return NULL; - } - - // Calculating ordinal day - ordinal = week * 7 + weekday - (week_day(parsed->year, 1, 4) + 3); - - if (ordinal < 1) { - // Previous year - ordinal += days_in_year(parsed->year - 1); - parsed->year -= 1; - leap = is_leap(parsed->year); - } - - if (ordinal > days_in_year(parsed->year)) { - // Next year - ordinal -= days_in_year(parsed->year); - parsed->year += 1; - leap = is_leap(parsed->year); - } - - for (j = 1; j < 14; j++) { - if (ordinal <= MONTHS_OFFSETS[leap][j]) { - parsed->day = ordinal - MONTHS_OFFSETS[leap][j - 1]; - parsed->month = j - 1; - - break; - } - } - } else { - // At this point we need to check the number - // of characters until the end of the date part - // (or the end of the string). - // - // If two, we have only a month if there is a separator, it may be a time otherwise. - // If three, we have an ordinal date. - // If four, we have a complete date - i = 0; - while (*c != '\0' && *c != ' ' && *c != 'T') { - if (*c == '-') { - separators++; - c++; - continue; - } - - if (!(*c >= '0' && *c <='9')) { - parsed->error = PARSER_INVALID_DATE; - - return NULL; - } - - monthday = 10 * monthday + *c++ - '0'; - - i++; - } - - switch (i) { - case 0: - // No month/day specified (only a year) - break; - case 2: - if (!separators) { - // The date looks like 201207 - // which is invalid for a date - // But it might be a time in the form hhmmss - parsed->ambiguous = 1; - } - - parsed->month = monthday; - break; - case 3: - // Ordinal day - if (separators > 1) { - parsed->error = PARSER_INVALID_DATE; - - return NULL; - } - - if (monthday < 1 || monthday > MONTHS_OFFSETS[leap][13]) { - parsed->error = PARSER_INVALID_ORDINAL_DAY_FOR_YEAR; - - return NULL; - } - - for (j = 1; j < 14; j++) { - if (monthday <= MONTHS_OFFSETS[leap][j]) { - parsed->day = monthday - MONTHS_OFFSETS[leap][j - 1]; - parsed->month = j - 1; - - break; - } - } - - break; - case 4: - // Month and day - parsed->month = monthday / 100; - parsed->day = monthday % 100; - - break; - default: - parsed->error = PARSER_INVALID_MONTH_OR_DAY; - - return NULL; - } - } - - // Checks - if (separators && !monthday && !week) { - parsed->error = PARSER_INVALID_DATE; - - return NULL; - } - - if (parsed->month > 12) { - parsed->error = PARSER_INVALID_MONTH; - - return NULL; - } - - if (parsed->day > DAYS_PER_MONTHS[leap][parsed->month]) { - parsed->error = PARSER_INVALID_DAY_FOR_MONTH; - - return NULL; - } - - separators = 0; - if (*c == 'T' || *c == ' ') { - if (parsed->ambiguous) { - parsed->error = PARSER_INVALID_DATE; - - return NULL; - } - - // We have time so we have a datetime - parsed->is_datetime = 1; - parsed->is_date = 0; - - c++; - - // Grabbing time information - i = 0; - while (*c != '\0' && *c != '.' && *c != ',' && *c != 'Z' && *c != '+' && *c != '-') { - if (*c == ':') { - separators++; - c++; - continue; - } - - if (!(*c >= '0' && *c <='9')) { - parsed->error = PARSER_INVALID_TIME; - - return NULL; - } - - time = 10 * time + *c++ - '0'; - i++; - } - - switch (i) { - case 2: - // Hours only - if (separators > 0) { - // Extraneous separators - parsed->error = PARSER_INVALID_TIME; - - return NULL; - } - - parsed->hour = time; - has_hour = 1; - break; - case 4: - // Hours and minutes - if (separators > 1) { - // Extraneous separators - parsed->error = PARSER_INVALID_TIME; - - return NULL; - } - - parsed->hour = time / 100; - parsed->minute = time % 100; - has_hour = 1; - break; - case 6: - // Hours, minutes and seconds - if (!(separators == 0 || separators == 2)) { - // We should have either two separators or none - parsed->error = PARSER_INVALID_TIME; - - return NULL; - } - - parsed->hour = time / 10000; - parsed->minute = time / 100 % 100; - parsed->second = time % 100; - has_hour = 1; - break; - default: - // Any other case is wrong - parsed->error = PARSER_INVALID_TIME; - - return NULL; - } - - // Checks - if (parsed->hour > 23) { - parsed->error = PARSER_INVALID_HOUR; - - return NULL; - } - - if (parsed->minute > 59) { - parsed->error = PARSER_INVALID_MINUTE; - - return NULL; - } - - if (parsed->second > 59) { - parsed->error = PARSER_INVALID_SECOND; - - return NULL; - } - - // Subsecond - if (*c == '.' || *c == ',') { - c++; - - time = 0; - i = 0; - while (*c != '\0' && *c != 'Z' && *c != '+' && *c != '-') { - if (!(*c >= '0' && *c <='9')) { - parsed->error = PARSER_INVALID_SUBSECOND; - - return NULL; - } - - time = 10 * time + *c++ - '0'; - i++; - } - - // adjust to microseconds - if (i > 6) { - parsed->microsecond = time / pow(10, i - 6); - } else if (i <= 6) { - parsed->microsecond = time * pow(10, 6 - i); - } - } - - // Timezone - if (*c == 'Z') { - parsed->has_offset = 1; - c++; - } else if (*c == '+' || *c == '-') { - tz_sign = 1; - if (*c == '-') { - tz_sign = -1; - } - - parsed->has_offset = 1; - c++; - - i = 0; - time = 0; - separators = 0; - while (*c != '\0') { - if (*c == ':') { - separators++; - c++; - continue; - } - - if (!(*c >= '0' && *c <= '9')) { - parsed->error = PARSER_INVALID_TZ_OFFSET; - - return NULL; - } - - time = 10 * time + *c++ - '0'; - i++; - } - - switch (i) { - case 2: - // hh Format - if (separators) { - // Extraneous separators - parsed->error = PARSER_INVALID_TZ_OFFSET; - - return NULL; - } - - parsed->offset = tz_sign * (time * 3600); - break; - case 4: - // hhmm Format - if (separators > 1) { - // Extraneous separators - parsed->error = PARSER_INVALID_TZ_OFFSET; - - return NULL; - } - - parsed->offset = tz_sign * ((time / 100 * 3600) + (time % 100 * 60)); - break; - default: - // Wrong format - parsed->error = PARSER_INVALID_TZ_OFFSET; - - return NULL; - } - } - } - - // At this point we should be at the end of the string - // If not, the string is invalid - if (*c != '\0') { - parsed->error = PARSER_INVALID_ISO8601; - - return NULL; - } - - return parsed; -} - - -Parsed* _parse_iso8601_duration(char *str, Parsed *parsed) { - char* c; - int value = 0; - int grabbed = 0; - int in_time = 0; - int in_fraction = 0; - int fraction_length = 0; - int has_fractional = 0; - int fraction = 0; - int has_ymd = 0; - int has_week = 0; - int has_year = 0; - int has_month = 0; - int has_day = 0; - int has_hour = 0; - int has_minute = 0; - int has_second = 0; - - c = str; - - // Removing P operator - c++; - - parsed->is_duration = 1; - - for (; *c != '\0'; c++) { - switch (*c) { - case 'Y': - if (!grabbed || in_time || has_week || has_ymd) { - // No value grabbed - parsed->error = PARSER_INVALID_DURATION; - - return NULL; - } - - if (fraction) { - parsed->error = PARSER_INVALID_DURATION_FLOAT_YEAR_MONTH_NOT_SUPPORTED; - - return NULL; - } - - parsed->years = value; - - grabbed = 0; - value = 0; - fraction = 0; - in_fraction = 0; - has_ymd = 1; - has_year = 1; - - break; - case 'M': - if (!grabbed || has_week) { - // No value grabbed - parsed->error = PARSER_INVALID_DURATION; - - return NULL; - } - - if (in_time) { - if (has_second) { - parsed->error = PARSER_INVALID_DURATION; - - return NULL; - } - - if (has_fractional) { - parsed->error = PARSER_INVALID_DURATION; - - return NULL; - } - - parsed->minutes = value; - if (fraction) { - parsed->seconds = fraction * 6; - has_fractional = 1; - } - - has_minute = 1; - } else { - if (fraction) { - parsed->error = PARSER_INVALID_DURATION_FLOAT_YEAR_MONTH_NOT_SUPPORTED; - - return NULL; - } - - if (has_month || has_day) { - parsed->error = PARSER_INVALID_DURATION; - - return NULL; - } - - parsed->months = value; - has_ymd = 1; - has_month = 1; - } - - grabbed = 0; - value = 0; - fraction = 0; - in_fraction = 0; - - break; - case 'D': - if (!grabbed || in_time || has_week) { - // No value grabbed - parsed->error = PARSER_INVALID_DURATION; - - return NULL; - } - - if (has_day) { - parsed->error = PARSER_INVALID_DURATION; - - return NULL; - } - - parsed->days = value; - if (fraction) { - parsed->hours = fraction * 2.4; - has_fractional = 1; - } - - grabbed = 0; - value = 0; - fraction = 0; - in_fraction = 0; - has_ymd = 1; - has_day = 1; - - break; - case 'T': - if (grabbed) { - parsed->error = PARSER_INVALID_DURATION; - - return NULL; - } - - in_time = 1; - - break; - case 'H': - if (!grabbed || !in_time || has_week) { - // No value grabbed - parsed->error = PARSER_INVALID_DURATION; - - return NULL; - } - - if (has_hour || has_second || has_minute) { - parsed->error = PARSER_INVALID_DURATION; - - return NULL; - } - - if (has_fractional) { - parsed->error = PARSER_INVALID_DURATION; - - return NULL; - } - - parsed->hours = value; - if (fraction) { - parsed->minutes = fraction * 6; - has_fractional = 1; - } - - grabbed = 0; - value = 0; - fraction = 0; - in_fraction = 0; - has_hour = 1; - - break; - case 'S': - if (!grabbed || !in_time || has_week) { - // No value grabbed - parsed->error = PARSER_INVALID_DURATION; - - return NULL; - } - - if (has_second) { - parsed->error = PARSER_INVALID_DURATION; - - return NULL; - } - - if (has_fractional) { - parsed->error = PARSER_INVALID_DURATION; - - return NULL; - } - - if (fraction) { - parsed->seconds = value; - if (fraction_length > 6) { - parsed->microseconds = fraction / pow(10, fraction_length - 6); - } else { - parsed->microseconds = fraction * pow(10, 6 - fraction_length); - } - has_fractional = 1; - } else { - parsed->seconds = value; - } - - grabbed = 0; - value = 0; - fraction = 0; - in_fraction = 0; - has_second = 1; - - break; - case 'W': - if (!grabbed || in_time || has_ymd) { - // No value grabbed - parsed->error = PARSER_INVALID_DURATION; - - return NULL; - } - - parsed->weeks = value; - if (fraction) { - float days; - days = fraction * 0.7; - parsed->hours = (int) ((days - (int) days) * 24); - parsed->days = (int) days; - } - - grabbed = 0; - value = 0; - fraction = 0; - in_fraction = 0; - has_week = 1; - - break; - case '.': - if (!grabbed || has_fractional) { - // No value grabbed - parsed->error = PARSER_INVALID_DURATION; - - return NULL; - } - - in_fraction = 1; - - break; - case ',': - if (!grabbed || has_fractional) { - // No value grabbed - parsed->error = PARSER_INVALID_DURATION; - - return NULL; - } - - in_fraction = 1; - - break; - default: - if (*c >= '0' && *c <='9') { - if (in_fraction) { - fraction = 10 * fraction + *c - '0'; - fraction_length++; - } else { - value = 10 * value + *c - '0'; - grabbed = 1; - } - break; - } - - parsed->error = PARSER_INVALID_DURATION; - - return NULL; - } - } - - return parsed; -} - - -PyObject* parse_iso8601(PyObject *self, PyObject *args) { - char* str; - PyObject *obj; - PyObject *tzinfo; - Parsed *parsed = new_parsed(); - - if (!PyArg_ParseTuple(args, "s", &str)) { - PyErr_SetString( - PyExc_ValueError, "Invalid parameters" - ); - return NULL; - } - - if (*str == 'P') { - // Duration (or interval) - if (_parse_iso8601_duration(str, parsed) == NULL) { - PyErr_SetString( - PyExc_ValueError, PARSER_ERRORS[parsed->error] - ); - - return NULL; - } - } else if (_parse_iso8601_datetime(str, parsed) == NULL) { - PyErr_SetString( - PyExc_ValueError, PARSER_ERRORS[parsed->error] - ); - - return NULL; - } - - if (parsed->is_date) { - // Date only - if (parsed->ambiguous) { - // We can "safely" assume that the ambiguous - // date was actually a time in the form hhmmss - parsed->hour = parsed->year / 100; - parsed->minute = parsed->year % 100; - parsed->second = parsed->month; - - obj = PyDateTimeAPI->Time_FromTime( - parsed->hour, parsed->minute, parsed->second, parsed->microsecond, - Py_BuildValue(""), - PyDateTimeAPI->TimeType - ); - } else { - obj = PyDateTimeAPI->Date_FromDate( - parsed->year, parsed->month, parsed->day, - PyDateTimeAPI->DateType - ); - } - } else if (parsed->is_datetime) { - if (!parsed->has_offset) { - tzinfo = Py_BuildValue(""); - } else { - tzinfo = new_fixed_offset(parsed->offset); - } - - obj = PyDateTimeAPI->DateTime_FromDateAndTime( - parsed->year, - parsed->month, - parsed->day, - parsed->hour, - parsed->minute, - parsed->second, - parsed->microsecond, - tzinfo, - PyDateTimeAPI->DateTimeType - ); - - Py_DECREF(tzinfo); - } else if (parsed->is_duration) { - obj = new_duration( - parsed->years, parsed->months, parsed->weeks, parsed->days, - parsed->hours, parsed->minutes, parsed->seconds, parsed->microseconds - ); - } else { - return NULL; - } - - free(parsed); - - return obj; -} - - -/* ------------------------------------------------------------------------- */ - -static PyMethodDef helpers_methods[] = { - { - "parse_iso8601", - (PyCFunction) parse_iso8601, - METH_VARARGS, - PyDoc_STR("Parses a ISO8601 string into a tuple.") - }, - {NULL} -}; - - -/* ------------------------------------------------------------------------- */ - -static struct PyModuleDef moduledef = { - PyModuleDef_HEAD_INIT, - "_iso8601", - NULL, - -1, - helpers_methods, - NULL, - NULL, - NULL, - NULL, -}; - -PyMODINIT_FUNC -PyInit__iso8601(void) -{ - PyObject *module; - - PyDateTime_IMPORT; - - module = PyModule_Create(&moduledef); - - if (module == NULL) - return NULL; - - // FixedOffset declaration - FixedOffset_type.tp_new = PyType_GenericNew; - FixedOffset_type.tp_base = PyDateTimeAPI->TZInfoType; - FixedOffset_type.tp_methods = FixedOffset_methods; - FixedOffset_type.tp_members = FixedOffset_members; - FixedOffset_type.tp_init = (initproc)FixedOffset_init; - - if (PyType_Ready(&FixedOffset_type) < 0) - return NULL; - - // Duration declaration - Duration_type.tp_new = PyType_GenericNew; - Duration_type.tp_members = Duration_members; - Duration_type.tp_init = (initproc)Duration_init; - - if (PyType_Ready(&Duration_type) < 0) - return NULL; - - Py_INCREF(&FixedOffset_type); - Py_INCREF(&Duration_type); - - PyModule_AddObject(module, "TZFixedOffset", (PyObject *)&FixedOffset_type); - PyModule_AddObject(module, "Duration", (PyObject *)&Duration_type); - - return module; -} diff --git a/pendulum/parsing/iso8601.py b/pendulum/parsing/iso8601.py deleted file mode 100644 index 38e42f33..00000000 --- a/pendulum/parsing/iso8601.py +++ /dev/null @@ -1,445 +0,0 @@ -from __future__ import division - -import re -import datetime - -from ..constants import ( - HOURS_PER_DAY, - MINUTES_PER_HOUR, - SECONDS_PER_MINUTE, - MONTHS_OFFSETS, -) -from ..helpers import week_day, days_in_year, is_leap, is_long_year -from ..tz.timezone import FixedTimezone -from ..duration import Duration -from .exceptions import ParserError - - -ISO8601_DT = re.compile( - # Date (optional) - "^" - "(?P" - " (?P" # Classic date (YYYY-MM-DD) or ordinal (YYYY-DDD) - " (?P\d{4})" # Year - " (?P" - " (?P-)?(?P\d{2})" # Month (optional) - " ((?P-)?(?P\d{1,2}))?" # Day (optional) - " )?" - " )" - " |" - " (?P" # Calendar date (2016-W05 or 2016-W05-5) - " (?P\d{4})" # Year - " (?P-)?" # Separator (optional) - " W" # W separator - " (?P\d{2})" # Week number - " (?P-)?" # Separator (optional) - " (?P\d)?" # Weekday (optional) - " )" - ")?" - # Time (optional) - "(?P