├── .github ├── FUNDING.yml ├── ISSUE_TEMPLATE │ ├── bug_report.md │ ├── documentation.md │ └── feature_request.md ├── dependabot.yml ├── pull_request_template.md └── workflows │ ├── continuous_integration.yml │ └── release.yml ├── .gitignore ├── .pre-commit-config.yaml ├── .readthedocs.yaml ├── CHANGELOG.rst ├── LICENSE ├── Makefile ├── README.rst ├── arrow ├── __init__.py ├── _version.py ├── api.py ├── arrow.py ├── constants.py ├── factory.py ├── formatter.py ├── locales.py ├── parser.py ├── py.typed └── util.py ├── docs ├── Makefile ├── api-guide.rst ├── conf.py ├── getting-started.rst ├── guide.rst ├── index.rst ├── make.bat └── releases.rst ├── pyproject.toml ├── requirements ├── requirements-docs.txt ├── requirements-tests.txt └── requirements.txt ├── setup.cfg ├── tests ├── __init__.py ├── conftest.py ├── test_api.py ├── test_arrow.py ├── test_factory.py ├── test_formatter.py ├── test_locales.py ├── test_parser.py ├── test_util.py └── utils.py └── tox.ini /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | open_collective: arrow 2 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: "🐞 Bug Report" 3 | about: Find a bug? Create a report to help us improve. 4 | title: '' 5 | labels: 'bug' 6 | assignees: '' 7 | --- 8 | 9 | 14 | 15 | ## Issue Description 16 | 17 | 22 | 23 | ## System Info 24 | 25 | - 🖥 **OS name and version**: 26 | - 🐍 **Python version**: 27 | - 🏹 **Arrow version**: 28 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/documentation.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: "📚 Documentation" 3 | about: Find errors or problems in the docs (https://arrow.readthedocs.io)? 4 | title: '' 5 | labels: 'documentation' 6 | assignees: '' 7 | --- 8 | 9 | 14 | 15 | ## Issue Description 16 | 17 | 18 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: "💡 Feature Request" 3 | about: Have an idea for a new feature or improvement? 4 | title: '' 5 | labels: 'enhancement' 6 | assignees: '' 7 | --- 8 | 9 | 14 | 15 | ## Feature Request 16 | 17 | 18 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: "github-actions" 4 | directory: "/" 5 | schedule: 6 | interval: "weekly" 7 | -------------------------------------------------------------------------------- /.github/pull_request_template.md: -------------------------------------------------------------------------------- 1 | ## Pull Request Checklist 2 | 3 | Thank you for taking the time to improve Arrow! Before submitting your pull request, please check all *appropriate* boxes: 4 | 5 | 6 | - [ ] 🧪 Added **tests** for changed code. 7 | - [ ] 🛠️ All tests **pass** when run locally (run `tox` or `make test` to find out!). 8 | - [ ] 🧹 All linting checks **pass** when run locally (run `tox -e lint` or `make lint` to find out!). 9 | - [ ] 📚 Updated **documentation** for changed code. 10 | - [ ] ⏩ Code is **up-to-date** with the `master` branch. 11 | 12 | If you have *any* questions about your code changes or any of the points above, please submit your questions along with the pull request and we will try our best to help! 13 | 14 | ## Description of Changes 15 | 16 | 23 | -------------------------------------------------------------------------------- /.github/workflows/continuous_integration.yml: -------------------------------------------------------------------------------- 1 | name: tests 2 | 3 | on: 4 | pull_request: # Run on all pull requests 5 | push: # Run only on pushes to master 6 | branches: 7 | - master 8 | schedule: # Run monthly 9 | - cron: "0 0 1 * *" 10 | 11 | jobs: 12 | unit-tests: 13 | name: ${{ matrix.os }} (${{ matrix.python-version }}) 14 | runs-on: ${{ matrix.os }} 15 | strategy: 16 | fail-fast: false 17 | matrix: 18 | python-version: ["pypy-3.9", "3.8", "3.9", "3.10", "3.11", "3.12", "3.13"] 19 | os: [ubuntu-latest, macos-latest, windows-latest] 20 | exclude: 21 | # pypy3 randomly fails on Windows builds 22 | - os: windows-latest 23 | python-version: "pypy-3.9" 24 | include: 25 | - os: ubuntu-latest 26 | path: ~/.cache/pip 27 | - os: macos-latest 28 | path: ~/Library/Caches/pip 29 | - os: windows-latest 30 | path: ~\AppData\Local\pip\Cache 31 | steps: 32 | - uses: actions/checkout@v4 33 | - name: Cache pip 34 | uses: actions/cache@v4 35 | with: 36 | path: ${{ matrix.path }} 37 | key: ${{ runner.os }}-pip-${{ hashFiles('**/requirements*.txt') }} 38 | restore-keys: ${{ runner.os }}-pip- 39 | - name: Set up Python ${{ matrix.python-version }} 40 | uses: actions/setup-python@v5 41 | with: 42 | python-version: ${{ matrix.python-version }} 43 | - name: Install dependencies 44 | run: | 45 | pip install -U pip setuptools wheel 46 | pip install -U tox tox-gh-actions 47 | - name: Test with tox 48 | run: tox 49 | - name: Upload coverage to Codecov 50 | uses: codecov/codecov-action@v5 51 | with: 52 | file: coverage.xml 53 | 54 | linting: 55 | name: Linting 56 | runs-on: ubuntu-latest 57 | steps: 58 | - uses: actions/checkout@v4 59 | - uses: actions/cache@v4 60 | with: 61 | path: ~/.cache/pip 62 | key: ${{ runner.os }}-pip-${{ hashFiles('**/requirements*.txt') }} 63 | restore-keys: ${{ runner.os }}-pip- 64 | - uses: actions/cache@v4 65 | with: 66 | path: ~/.cache/pre-commit 67 | key: ${{ runner.os }}-pre-commit-${{ hashFiles('**/.pre-commit-config.yaml') }} 68 | restore-keys: ${{ runner.os }}-pre-commit- 69 | - name: Set up Python ${{ runner.python-version }} 70 | uses: actions/setup-python@v5 71 | with: 72 | python-version: "3.11" 73 | - name: Install dependencies 74 | run: | 75 | pip install -U pip setuptools wheel 76 | pip install -U tox 77 | - name: Lint code 78 | run: tox -e lint -- --show-diff-on-failure 79 | - name: Lint docs 80 | run: tox -e docs 81 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: release 2 | 3 | on: 4 | workflow_dispatch: # run manually 5 | push: # run on matching tags 6 | tags: 7 | - '*.*.*' 8 | 9 | jobs: 10 | release-to-pypi: 11 | runs-on: ubuntu-latest 12 | steps: 13 | - uses: actions/checkout@v4 14 | - uses: actions/cache@v4 15 | with: 16 | path: ~/.cache/pip 17 | key: ${{ runner.os }}-pip-${{ hashFiles('**/requirements*.txt') }} 18 | restore-keys: ${{ runner.os }}-pip- 19 | - uses: actions/setup-python@v5 20 | with: 21 | python-version: "3.11" 22 | - name: Install dependencies 23 | run: | 24 | pip install -U pip setuptools wheel 25 | pip install -U tox 26 | - name: Publish package to PyPI 27 | env: 28 | FLIT_USERNAME: __token__ 29 | FLIT_PASSWORD: ${{ secrets.PYPI_API_TOKEN }} 30 | run: tox -e publish 31 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | README.rst.new 2 | 3 | # Small entry point file for debugging tasks 4 | test.py 5 | 6 | # Byte-compiled / optimized / DLL files 7 | __pycache__/ 8 | *.py[cod] 9 | *$py.class 10 | 11 | # C extensions 12 | *.so 13 | 14 | # Distribution / packaging 15 | .Python 16 | build/ 17 | develop-eggs/ 18 | dist/ 19 | downloads/ 20 | eggs/ 21 | .eggs/ 22 | lib/ 23 | lib64/ 24 | parts/ 25 | sdist/ 26 | var/ 27 | wheels/ 28 | pip-wheel-metadata/ 29 | share/python-wheels/ 30 | *.egg-info/ 31 | .installed.cfg 32 | *.egg 33 | 34 | # PyInstaller 35 | # Usually these files are written by a python script from a template 36 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 37 | *.manifest 38 | *.spec 39 | 40 | # Installer logs 41 | pip-log.txt 42 | pip-delete-this-directory.txt 43 | 44 | # Unit test / coverage reports 45 | htmlcov/ 46 | .tox/ 47 | .nox/ 48 | .coverage 49 | .coverage.* 50 | .cache 51 | nosetests.xml 52 | coverage.xml 53 | *.cover 54 | .hypothesis/ 55 | .pytest_cache/ 56 | 57 | # Translations 58 | *.mo 59 | *.pot 60 | 61 | # Django stuff: 62 | *.log 63 | local_settings.py 64 | db.sqlite3 65 | db.sqlite3-journal 66 | 67 | # Flask stuff: 68 | instance/ 69 | .webassets-cache 70 | 71 | # Scrapy stuff: 72 | .scrapy 73 | 74 | # Sphinx documentation 75 | docs/_build/ 76 | 77 | # PyBuilder 78 | target/ 79 | 80 | # Jupyter Notebook 81 | .ipynb_checkpoints 82 | 83 | # IPython 84 | profile_default/ 85 | ipython_config.py 86 | 87 | # pyenv 88 | .python-version 89 | 90 | # celery beat schedule file 91 | celerybeat-schedule 92 | 93 | # SageMath parsed files 94 | *.sage.py 95 | 96 | # Environments 97 | .env 98 | .venv 99 | env/ 100 | venv/ 101 | ENV/ 102 | local/ 103 | env.bak/ 104 | venv.bak/ 105 | 106 | # Spyder project settings 107 | .spyderproject 108 | .spyproject 109 | 110 | # Rope project settings 111 | .ropeproject 112 | 113 | # mkdocs documentation 114 | /site 115 | 116 | # mypy 117 | .mypy_cache/ 118 | .dmypy.json 119 | dmypy.json 120 | 121 | # Pyre type checker 122 | .pyre/ 123 | 124 | # Swap 125 | [._]*.s[a-v][a-z] 126 | [._]*.sw[a-p] 127 | [._]s[a-rt-v][a-z] 128 | [._]ss[a-gi-z] 129 | [._]sw[a-p] 130 | 131 | # Session 132 | Session.vim 133 | Sessionx.vim 134 | 135 | # Temporary 136 | .netrwhist 137 | *~ 138 | # Auto-generated tag files 139 | tags 140 | # Persistent undo 141 | [._]*.un~ 142 | 143 | .idea/ 144 | .vscode/ 145 | 146 | # General 147 | .DS_Store 148 | .AppleDouble 149 | .LSOverride 150 | 151 | # Icon must end with two \r 152 | Icon 153 | 154 | 155 | # Thumbnails 156 | ._* 157 | 158 | # Files that might appear in the root of a volume 159 | .DocumentRevisions-V100 160 | .fseventsd 161 | .Spotlight-V100 162 | .TemporaryItems 163 | .Trashes 164 | .VolumeIcon.icns 165 | .com.apple.timemachine.donotpresent 166 | 167 | # Directories potentially created on remote AFP share 168 | .AppleDB 169 | .AppleDesktop 170 | Network Trash Folder 171 | Temporary Items 172 | .apdisk 173 | 174 | *~ 175 | 176 | # temporary files which can be created if a process still has a handle open of a deleted file 177 | .fuse_hidden* 178 | 179 | # KDE directory preferences 180 | .directory 181 | 182 | # Linux trash folder which might appear on any partition or disk 183 | .Trash-* 184 | 185 | # .nfs files are created when an open file is removed but is still being accessed 186 | .nfs* 187 | 188 | # Windows thumbnail cache files 189 | Thumbs.db 190 | Thumbs.db:encryptable 191 | ehthumbs.db 192 | ehthumbs_vista.db 193 | 194 | # Dump file 195 | *.stackdump 196 | 197 | # Folder config file 198 | [Dd]esktop.ini 199 | 200 | # Recycle Bin used on file shares 201 | $RECYCLE.BIN/ 202 | 203 | # Windows Installer files 204 | *.cab 205 | *.msi 206 | *.msix 207 | *.msm 208 | *.msp 209 | 210 | # Windows shortcuts 211 | *.lnk 212 | -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | default_language_version: 2 | python: python3 3 | repos: 4 | - repo: https://github.com/pre-commit/pre-commit-hooks 5 | rev: v4.5.0 6 | hooks: 7 | - id: check-ast 8 | - id: check-yaml 9 | - id: check-case-conflict 10 | - id: check-docstring-first 11 | - id: check-merge-conflict 12 | - id: check-builtin-literals 13 | - id: debug-statements 14 | - id: end-of-file-fixer 15 | - id: fix-encoding-pragma 16 | args: [--remove] 17 | - id: requirements-txt-fixer 18 | args: [requirements/requirements.txt, requirements/requirements-docs.txt, requirements/requirements-tests.txt] 19 | - id: trailing-whitespace 20 | - repo: https://github.com/timothycrosley/isort 21 | rev: 5.13.2 22 | hooks: 23 | - id: isort 24 | - repo: https://github.com/asottile/pyupgrade 25 | rev: v3.16.0 26 | hooks: 27 | - id: pyupgrade 28 | args: [--py36-plus] 29 | - repo: https://github.com/pre-commit/pygrep-hooks 30 | rev: v1.10.0 31 | hooks: 32 | - id: python-no-eval 33 | - id: python-check-blanket-noqa 34 | - id: python-check-mock-methods 35 | - id: python-use-type-annotations 36 | - id: rst-backticks 37 | - id: rst-directive-colons 38 | - id: rst-inline-touching-normal 39 | - id: text-unicode-replacement-char 40 | - repo: https://github.com/psf/black 41 | rev: 23.9.1 42 | hooks: 43 | - id: black 44 | args: [--safe, --quiet, --target-version=py36] 45 | - repo: https://github.com/pycqa/flake8 46 | rev: 6.1.0 47 | hooks: 48 | - id: flake8 49 | additional_dependencies: [flake8-bugbear,flake8-annotations] 50 | - repo: https://github.com/pre-commit/mirrors-mypy 51 | rev: v1.10.0 52 | hooks: 53 | - id: mypy 54 | additional_dependencies: [types-python-dateutil] 55 | -------------------------------------------------------------------------------- /.readthedocs.yaml: -------------------------------------------------------------------------------- 1 | # Read the Docs configuration file for Sphinx projects 2 | # See https://docs.readthedocs.io/en/stable/config-file/v2.html for details 3 | 4 | version: 2 5 | 6 | build: 7 | os: ubuntu-22.04 8 | tools: 9 | python: "3.11" 10 | 11 | # Build documentation in the "docs/" directory with Sphinx 12 | sphinx: 13 | configuration: docs/conf.py 14 | # You can configure Sphinx to use a different builder, for instance use the dirhtml builder for simpler URLs 15 | # builder: "dirhtml" 16 | # Fail on all warnings to avoid broken references 17 | # fail_on_warning: true 18 | 19 | # Optionally build your docs in additional formats such as PDF and ePub 20 | # formats: 21 | # - pdf 22 | # - epub 23 | 24 | # Optional but recommended, declare the Python requirements required 25 | # to build your documentation 26 | # See https://docs.readthedocs.io/en/stable/guides/reproducible-builds.html 27 | python: 28 | install: 29 | - requirements: requirements/requirements-docs.txt 30 | -------------------------------------------------------------------------------- /CHANGELOG.rst: -------------------------------------------------------------------------------- 1 | Changelog 2 | ========= 3 | 4 | 1.3.0 (2023-09-30) 5 | ------------------ 6 | 7 | - [ADDED] Added official support for Python 3.11 and 3.12. 8 | - [ADDED] Added dependency on ``types-python-dateutil`` to improve Arrow mypy compatibility. `PR #1102 `_ 9 | - [FIX] Updates to Italian, Romansh, Hungarian, Finish and Arabic locales. 10 | - [FIX] Handling parsing of UTC prefix in timezone strings. 11 | - [CHANGED] Update documentation to improve readability. 12 | - [CHANGED] Dropped support for Python 3.6 and 3.7, which are end-of-life. 13 | - [INTERNAL] Migrate from ``setup.py``/Twine to ``pyproject.toml``/Flit for packaging and distribution. 14 | - [INTERNAL] Adopt ``.readthedocs.yaml`` configuration file for continued ReadTheDocs support. 15 | 16 | 1.2.3 (2022-06-25) 17 | ------------------ 18 | 19 | - [NEW] Added Amharic, Armenian, Georgian, Laotian and Uzbek locales. 20 | - [FIX] Updated Danish locale and associated tests. 21 | - [INTERNAL] Small fixes to CI. 22 | 23 | 1.2.2 (2022-01-19) 24 | ------------------ 25 | 26 | - [NEW] Added Kazakh locale. 27 | - [FIX] The Belarusian, Bulgarian, Czech, Macedonian, Polish, Russian, Slovak and Ukrainian locales now support ``dehumanize``. 28 | - [FIX] Minor bug fixes and improvements to ChineseCN, Indonesian, Norwegian, and Russian locales. 29 | - [FIX] Expanded testing for multiple locales. 30 | - [INTERNAL] Started using ``xelatex`` for pdf generation in documentation. 31 | - [INTERNAL] Split requirements file into ``requirements.txt``, ``requirements-docs.txt`` and ``requirements-tests.txt``. 32 | - [INTERNAL] Added ``flake8-annotations`` package for type linting in ``pre-commit``. 33 | 34 | 1.2.1 (2021-10-24) 35 | ------------------ 36 | 37 | - [NEW] Added quarter granularity to humanize, for example: 38 | 39 | .. code-block:: python 40 | 41 | >>> import arrow 42 | >>> now = arrow.now() 43 | >>> four_month_shift = now.shift(months=4) 44 | >>> now.humanize(four_month_shift, granularity="quarter") 45 | 'a quarter ago' 46 | >>> four_month_shift.humanize(now, granularity="quarter") 47 | 'in a quarter' 48 | >>> thirteen_month_shift = now.shift(months=13) 49 | >>> thirteen_month_shift.humanize(now, granularity="quarter") 50 | 'in 4 quarters' 51 | >>> now.humanize(thirteen_month_shift, granularity="quarter") 52 | '4 quarters ago' 53 | 54 | - [NEW] Added Sinhala and Urdu locales. 55 | - [NEW] Added official support for Python 3.10. 56 | - [CHANGED] Updated Azerbaijani, Hebrew, and Serbian locales and added tests. 57 | - [CHANGED] Passing an empty granularity list to ``humanize`` now raises a ``ValueError``. 58 | 59 | 1.2.0 (2021-09-12) 60 | ------------------ 61 | 62 | - [NEW] Added Albanian, Tamil and Zulu locales. 63 | - [NEW] Added support for ``Decimal`` as input to ``arrow.get()``. 64 | - [FIX] The Estonian, Finnish, Nepali and Zulu locales now support ``dehumanize``. 65 | - [FIX] Improved validation checks when using parser tokens ``A`` and ``hh``. 66 | - [FIX] Minor bug fixes to Catalan, Cantonese, Greek and Nepali locales. 67 | 68 | 1.1.1 (2021-06-24) 69 | ------------------ 70 | 71 | - [NEW] Added Odia, Maltese, Serbian, Sami, and Luxembourgish locales. 72 | - [FIXED] All calls to ``arrow.get()`` should now properly pass the ``tzinfo`` argument to the Arrow constructor. See PR `#968 `_ for more info. 73 | - [FIXED] Humanize output is now properly truncated when a locale class overrides ``_format_timeframe()``. 74 | - [CHANGED] Renamed ``requirements.txt`` to ``requirements-dev.txt`` to prevent confusion with the dependencies in ``setup.py``. 75 | - [CHANGED] Updated Turkish locale and added tests. 76 | 77 | 1.1.0 (2021-04-26) 78 | ------------------ 79 | 80 | - [NEW] Implemented the ``dehumanize`` method for ``Arrow`` objects. This takes human readable input and uses it to perform relative time shifts, for example: 81 | 82 | .. code-block:: python 83 | 84 | >>> arw 85 | 86 | >>> arw.dehumanize("8 hours ago") 87 | 88 | >>> arw.dehumanize("in 4 days") 89 | 90 | >>> arw.dehumanize("in an hour 34 minutes 10 seconds") 91 | 92 | >>> arw.dehumanize("hace 2 años", locale="es") 93 | 94 | 95 | - [NEW] Made the start of the week adjustable when using ``span("week")``, for example: 96 | 97 | .. code-block:: python 98 | 99 | >>> arw 100 | 101 | >>> arw.isoweekday() 102 | 1 # Monday 103 | >>> arw.span("week") 104 | (, ) 105 | >>> arw.span("week", week_start=4) 106 | (, ) 107 | 108 | - [NEW] Added Croatian, Latin, Latvian, Lithuanian and Malay locales. 109 | - [FIX] Internally standardize locales and improve locale validation. Locales should now use the ISO notation of a dash (``"en-gb"``) rather than an underscore (``"en_gb"``) however this change is backward compatible. 110 | - [FIX] Correct type checking for internal locale mapping by using ``_init_subclass``. This now allows subclassing of locales, for example: 111 | 112 | .. code-block:: python 113 | 114 | >>> from arrow.locales import EnglishLocale 115 | >>> class Klingon(EnglishLocale): 116 | ... names = ["tlh"] 117 | ... 118 | >>> from arrow import locales 119 | >>> locales.get_locale("tlh") 120 | <__main__.Klingon object at 0x7f7cd1effd30> 121 | 122 | - [FIX] Correct type checking for ``arrow.get(2021, 3, 9)`` construction. 123 | - [FIX] Audited all docstrings for style, typos and outdated info. 124 | 125 | 1.0.3 (2021-03-05) 126 | ------------------ 127 | 128 | - [FIX] Updated internals to avoid issues when running ``mypy --strict``. 129 | - [FIX] Corrections to Swedish locale. 130 | - [INTERNAL] Lowered required coverage limit until ``humanize`` month tests are fixed. 131 | 132 | 1.0.2 (2021-02-28) 133 | ------------------ 134 | 135 | - [FIXED] Fixed an ``OverflowError`` that could occur when running Arrow on a 32-bit OS. 136 | 137 | 1.0.1 (2021-02-27) 138 | ------------------ 139 | 140 | - [FIXED] A ``py.typed`` file is now bundled with the Arrow package to conform to PEP 561. 141 | 142 | 1.0.0 (2021-02-26) 143 | ------------------ 144 | 145 | After 8 years we're pleased to announce Arrow v1.0. Thanks to the entire Python community for helping make Arrow the amazing package it is today! 146 | 147 | - [CHANGE] Arrow has **dropped support** for Python 2.7 and 3.5. 148 | - [CHANGE] There are multiple **breaking changes** with this release, please see the `migration guide `_ for a complete overview. 149 | - [CHANGE] Arrow is now following `semantic versioning `_. 150 | - [CHANGE] Made ``humanize`` granularity="auto" limits more accurate to reduce strange results. 151 | - [NEW] Added support for Python 3.9. 152 | - [NEW] Added a new keyword argument "exact" to ``span``, ``span_range`` and ``interval`` methods. This makes timespans begin at the start time given and not extend beyond the end time given, for example: 153 | 154 | .. code-block:: python 155 | 156 | >>> start = Arrow(2021, 2, 5, 12, 30) 157 | >>> end = Arrow(2021, 2, 5, 17, 15) 158 | >>> for r in arrow.Arrow.span_range('hour', start, end, exact=True): 159 | ... print(r) 160 | ... 161 | (, ) 162 | (, ) 163 | (, ) 164 | (, ) 165 | (, ) 166 | 167 | - [NEW] Arrow now natively supports PEP 484-style type annotations. 168 | - [FIX] Fixed handling of maximum permitted timestamp on Windows systems. 169 | - [FIX] Corrections to French, German, Japanese and Norwegian locales. 170 | - [INTERNAL] Raise more appropriate errors when string parsing fails to match. 171 | 172 | 0.17.0 (2020-10-2) 173 | ------------------- 174 | 175 | - [WARN] Arrow will **drop support** for Python 2.7 and 3.5 in the upcoming 1.0.0 release. This is the last major release to support Python 2.7 and Python 3.5. 176 | - [NEW] Arrow now properly handles imaginary datetimes during DST shifts. For example: 177 | 178 | .. code-block:: python 179 | 180 | >>> just_before = arrow.get(2013, 3, 31, 1, 55, tzinfo="Europe/Paris") 181 | >>> just_before.shift(minutes=+10) 182 | 183 | 184 | .. code-block:: python 185 | 186 | >>> before = arrow.get("2018-03-10 23:00:00", "YYYY-MM-DD HH:mm:ss", tzinfo="US/Pacific") 187 | >>> after = arrow.get("2018-03-11 04:00:00", "YYYY-MM-DD HH:mm:ss", tzinfo="US/Pacific") 188 | >>> result=[(t, t.to("utc")) for t in arrow.Arrow.range("hour", before, after)] 189 | >>> for r in result: 190 | ... print(r) 191 | ... 192 | (, ) 193 | (, ) 194 | (, ) 195 | (, ) 196 | (, ) 197 | 198 | - [NEW] Added ``humanize`` week granularity translation for Tagalog. 199 | - [CHANGE] Calls to the ``timestamp`` property now emit a ``DeprecationWarning``. In a future release, ``timestamp`` will be changed to a method to align with Python's datetime module. If you would like to continue using the property, please change your code to use the ``int_timestamp`` or ``float_timestamp`` properties instead. 200 | - [CHANGE] Expanded and improved Catalan locale. 201 | - [FIX] Fixed a bug that caused ``Arrow.range()`` to incorrectly cut off ranges in certain scenarios when using month, quarter, or year endings. 202 | - [FIX] Fixed a bug that caused day of week token parsing to be case sensitive. 203 | - [INTERNAL] A number of functions were reordered in arrow.py for better organization and grouping of related methods. This change will have no impact on usage. 204 | - [INTERNAL] A minimum tox version is now enforced for compatibility reasons. Contributors must use tox >3.18.0 going forward. 205 | 206 | 0.16.0 (2020-08-23) 207 | ------------------- 208 | 209 | - [WARN] Arrow will **drop support** for Python 2.7 and 3.5 in the upcoming 1.0.0 release. The 0.16.x and 0.17.x releases are the last to support Python 2.7 and 3.5. 210 | - [NEW] Implemented `PEP 495 `_ to handle ambiguous datetimes. This is achieved by the addition of the ``fold`` attribute for Arrow objects. For example: 211 | 212 | .. code-block:: python 213 | 214 | >>> before = Arrow(2017, 10, 29, 2, 0, tzinfo='Europe/Stockholm') 215 | 216 | >>> before.fold 217 | 0 218 | >>> before.ambiguous 219 | True 220 | >>> after = Arrow(2017, 10, 29, 2, 0, tzinfo='Europe/Stockholm', fold=1) 221 | 222 | >>> after = before.replace(fold=1) 223 | 224 | 225 | - [NEW] Added ``normalize_whitespace`` flag to ``arrow.get``. This is useful for parsing log files and/or any files that may contain inconsistent spacing. For example: 226 | 227 | .. code-block:: python 228 | 229 | >>> arrow.get("Jun 1 2005 1:33PM", "MMM D YYYY H:mmA", normalize_whitespace=True) 230 | 231 | >>> arrow.get("2013-036 \t 04:05:06Z", normalize_whitespace=True) 232 | 233 | 234 | 0.15.8 (2020-07-23) 235 | ------------------- 236 | 237 | - [WARN] Arrow will **drop support** for Python 2.7 and 3.5 in the upcoming 1.0.0 release. The 0.15.x, 0.16.x, and 0.17.x releases are the last to support Python 2.7 and 3.5. 238 | - [NEW] Added ``humanize`` week granularity translation for Czech. 239 | - [FIX] ``arrow.get`` will now pick sane defaults when weekdays are passed with particular token combinations, see `#446 `_. 240 | - [INTERNAL] Moved arrow to an organization. The repo can now be found `here `_. 241 | - [INTERNAL] Started issuing deprecation warnings for Python 2.7 and 3.5. 242 | - [INTERNAL] Added Python 3.9 to CI pipeline. 243 | 244 | 0.15.7 (2020-06-19) 245 | ------------------- 246 | 247 | - [NEW] Added a number of built-in format strings. See the `docs `_ for a complete list of supported formats. For example: 248 | 249 | .. code-block:: python 250 | 251 | >>> arw = arrow.utcnow() 252 | >>> arw.format(arrow.FORMAT_COOKIE) 253 | 'Wednesday, 27-May-2020 10:30:35 UTC' 254 | 255 | - [NEW] Arrow is now fully compatible with Python 3.9 and PyPy3. 256 | - [NEW] Added Makefile, tox.ini, and requirements.txt files to the distribution bundle. 257 | - [NEW] Added French Canadian and Swahili locales. 258 | - [NEW] Added ``humanize`` week granularity translation for Hebrew, Greek, Macedonian, Swedish, Slovak. 259 | - [FIX] ms and μs timestamps are now normalized in ``arrow.get()``, ``arrow.fromtimestamp()``, and ``arrow.utcfromtimestamp()``. For example: 260 | 261 | .. code-block:: python 262 | 263 | >>> ts = 1591161115194556 264 | >>> arw = arrow.get(ts) 265 | 266 | >>> arw.timestamp 267 | 1591161115 268 | 269 | - [FIX] Refactored and updated Macedonian, Hebrew, Korean, and Portuguese locales. 270 | 271 | 0.15.6 (2020-04-29) 272 | ------------------- 273 | 274 | - [NEW] Added support for parsing and formatting `ISO 8601 week dates `_ via a new token ``W``, for example: 275 | 276 | .. code-block:: python 277 | 278 | >>> arrow.get("2013-W29-6", "W") 279 | 280 | >>> utc=arrow.utcnow() 281 | >>> utc 282 | 283 | >>> utc.format("W") 284 | '2020-W04-4' 285 | 286 | - [NEW] Formatting with ``x`` token (microseconds) is now possible, for example: 287 | 288 | .. code-block:: python 289 | 290 | >>> dt = arrow.utcnow() 291 | >>> dt.format("x") 292 | '1585669870688329' 293 | >>> dt.format("X") 294 | '1585669870' 295 | 296 | - [NEW] Added ``humanize`` week granularity translation for German, Italian, Polish & Taiwanese locales. 297 | - [FIX] Consolidated and simplified German locales. 298 | - [INTERNAL] Moved testing suite from nosetest/Chai to pytest/pytest-mock. 299 | - [INTERNAL] Converted xunit-style setup and teardown functions in tests to pytest fixtures. 300 | - [INTERNAL] Setup GitHub Actions for CI alongside Travis. 301 | - [INTERNAL] Help support Arrow's future development by donating to the project on `Open Collective `_. 302 | 303 | 0.15.5 (2020-01-03) 304 | ------------------- 305 | 306 | - [WARN] Python 2 reached EOL on 2020-01-01. arrow will **drop support** for Python 2 in a future release to be decided (see `#739 `_). 307 | - [NEW] Added bounds parameter to ``span_range``, ``interval`` and ``span`` methods. This allows you to include or exclude the start and end values. 308 | - [NEW] ``arrow.get()`` can now create arrow objects from a timestamp with a timezone, for example: 309 | 310 | .. code-block:: python 311 | 312 | >>> arrow.get(1367900664, tzinfo=tz.gettz('US/Pacific')) 313 | 314 | 315 | - [NEW] ``humanize`` can now combine multiple levels of granularity, for example: 316 | 317 | .. code-block:: python 318 | 319 | >>> later140 = arrow.utcnow().shift(seconds=+8400) 320 | >>> later140.humanize(granularity="minute") 321 | 'in 139 minutes' 322 | >>> later140.humanize(granularity=["hour", "minute"]) 323 | 'in 2 hours and 19 minutes' 324 | 325 | - [NEW] Added Hong Kong locale (``zh_hk``). 326 | - [NEW] Added ``humanize`` week granularity translation for Dutch. 327 | - [NEW] Numbers are now displayed when using the seconds granularity in ``humanize``. 328 | - [CHANGE] ``range`` now supports both the singular and plural forms of the ``frames`` argument (e.g. day and days). 329 | - [FIX] Improved parsing of strings that contain punctuation. 330 | - [FIX] Improved behaviour of ``humanize`` when singular seconds are involved. 331 | 332 | 0.15.4 (2019-11-02) 333 | ------------------- 334 | 335 | - [FIX] Fixed an issue that caused package installs to fail on Conda Forge. 336 | 337 | 0.15.3 (2019-11-02) 338 | ------------------- 339 | 340 | - [NEW] ``factory.get()`` can now create arrow objects from a ISO calendar tuple, for example: 341 | 342 | .. code-block:: python 343 | 344 | >>> arrow.get((2013, 18, 7)) 345 | 346 | 347 | - [NEW] Added a new token ``x`` to allow parsing of integer timestamps with milliseconds and microseconds. 348 | - [NEW] Formatting now supports escaping of characters using the same syntax as parsing, for example: 349 | 350 | .. code-block:: python 351 | 352 | >>> arw = arrow.now() 353 | >>> fmt = "YYYY-MM-DD h [h] m" 354 | >>> arw.format(fmt) 355 | '2019-11-02 3 h 32' 356 | 357 | - [NEW] Added ``humanize`` week granularity translations for Chinese, Spanish and Vietnamese. 358 | - [CHANGE] Added ``ParserError`` to module exports. 359 | - [FIX] Added support for midnight at end of day. See `#703 `_ for details. 360 | - [INTERNAL] Created Travis build for macOS. 361 | - [INTERNAL] Test parsing and formatting against full timezone database. 362 | 363 | 0.15.2 (2019-09-14) 364 | ------------------- 365 | 366 | - [NEW] Added ``humanize`` week granularity translations for Portuguese and Brazilian Portuguese. 367 | - [NEW] Embedded changelog within docs and added release dates to versions. 368 | - [FIX] Fixed a bug that caused test failures on Windows only, see `#668 `_ for details. 369 | 370 | 0.15.1 (2019-09-10) 371 | ------------------- 372 | 373 | - [NEW] Added ``humanize`` week granularity translations for Japanese. 374 | - [FIX] Fixed a bug that caused Arrow to fail when passed a negative timestamp string. 375 | - [FIX] Fixed a bug that caused Arrow to fail when passed a datetime object with ``tzinfo`` of type ``StaticTzInfo``. 376 | 377 | 0.15.0 (2019-09-08) 378 | ------------------- 379 | 380 | - [NEW] Added support for DDD and DDDD ordinal date tokens. The following functionality is now possible: ``arrow.get("1998-045")``, ``arrow.get("1998-45", "YYYY-DDD")``, ``arrow.get("1998-045", "YYYY-DDDD")``. 381 | - [NEW] ISO 8601 basic format for dates and times is now supported (e.g. ``YYYYMMDDTHHmmssZ``). 382 | - [NEW] Added ``humanize`` week granularity translations for French, Russian and Swiss German locales. 383 | - [CHANGE] Timestamps of type ``str`` are no longer supported **without a format string** in the ``arrow.get()`` method. This change was made to support the ISO 8601 basic format and to address bugs such as `#447 `_. 384 | 385 | The following will NOT work in v0.15.0: 386 | 387 | .. code-block:: python 388 | 389 | >>> arrow.get("1565358758") 390 | >>> arrow.get("1565358758.123413") 391 | 392 | The following will work in v0.15.0: 393 | 394 | .. code-block:: python 395 | 396 | >>> arrow.get("1565358758", "X") 397 | >>> arrow.get("1565358758.123413", "X") 398 | >>> arrow.get(1565358758) 399 | >>> arrow.get(1565358758.123413) 400 | 401 | - [CHANGE] When a meridian token (a|A) is passed and no meridians are available for the specified locale (e.g. unsupported or untranslated) a ``ParserError`` is raised. 402 | - [CHANGE] The timestamp token (``X``) will now match float timestamps of type ``str``: ``arrow.get(“1565358758.123415”, “X”)``. 403 | - [CHANGE] Strings with leading and/or trailing whitespace will no longer be parsed without a format string. Please see `the docs `_ for ways to handle this. 404 | - [FIX] The timestamp token (``X``) will now only match on strings that **strictly contain integers and floats**, preventing incorrect matches. 405 | - [FIX] Most instances of ``arrow.get()`` returning an incorrect ``Arrow`` object from a partial parsing match have been eliminated. The following issue have been addressed: `#91 `_, `#196 `_, `#396 `_, `#434 `_, `#447 `_, `#456 `_, `#519 `_, `#538 `_, `#560 `_. 406 | 407 | 0.14.7 (2019-09-04) 408 | ------------------- 409 | 410 | - [CHANGE] ``ArrowParseWarning`` will no longer be printed on every call to ``arrow.get()`` with a datetime string. The purpose of the warning was to start a conversation about the upcoming 0.15.0 changes and we appreciate all the feedback that the community has given us! 411 | 412 | 0.14.6 (2019-08-28) 413 | ------------------- 414 | 415 | - [NEW] Added support for ``week`` granularity in ``Arrow.humanize()``. For example, ``arrow.utcnow().shift(weeks=-1).humanize(granularity="week")`` outputs "a week ago". This change introduced two new untranslated words, ``week`` and ``weeks``, to all locale dictionaries, so locale contributions are welcome! 416 | - [NEW] Fully translated the Brazilian Portuguese locale. 417 | - [CHANGE] Updated the Macedonian locale to inherit from a Slavic base. 418 | - [FIX] Fixed a bug that caused ``arrow.get()`` to ignore tzinfo arguments of type string (e.g. ``arrow.get(tzinfo="Europe/Paris")``). 419 | - [FIX] Fixed a bug that occurred when ``arrow.Arrow()`` was instantiated with a ``pytz`` tzinfo object. 420 | - [FIX] Fixed a bug that caused Arrow to fail when passed a sub-second token, that when rounded, had a value greater than 999999 (e.g. ``arrow.get("2015-01-12T01:13:15.9999995")``). Arrow should now accurately propagate the rounding for large sub-second tokens. 421 | 422 | 0.14.5 (2019-08-09) 423 | ------------------- 424 | 425 | - [NEW] Added Afrikaans locale. 426 | - [CHANGE] Removed deprecated ``replace`` shift functionality. Users looking to pass plural properties to the ``replace`` function to shift values should use ``shift`` instead. 427 | - [FIX] Fixed bug that occurred when ``factory.get()`` was passed a locale kwarg. 428 | 429 | 0.14.4 (2019-07-30) 430 | ------------------- 431 | 432 | - [FIX] Fixed a regression in 0.14.3 that prevented a tzinfo argument of type string to be passed to the ``get()`` function. Functionality such as ``arrow.get("2019072807", "YYYYMMDDHH", tzinfo="UTC")`` should work as normal again. 433 | - [CHANGE] Moved ``backports.functools_lru_cache`` dependency from ``extra_requires`` to ``install_requires`` for ``Python 2.7`` installs to fix `#495 `_. 434 | 435 | 0.14.3 (2019-07-28) 436 | ------------------- 437 | 438 | - [NEW] Added full support for Python 3.8. 439 | - [CHANGE] Added warnings for upcoming factory.get() parsing changes in 0.15.0. Please see `#612 `_ for full details. 440 | - [FIX] Extensive refactor and update of documentation. 441 | - [FIX] factory.get() can now construct from kwargs. 442 | - [FIX] Added meridians to Spanish Locale. 443 | 444 | 0.14.2 (2019-06-06) 445 | ------------------- 446 | 447 | - [CHANGE] Travis CI builds now use tox to lint and run tests. 448 | - [FIX] Fixed UnicodeDecodeError on certain locales (#600). 449 | 450 | 0.14.1 (2019-06-06) 451 | ------------------- 452 | 453 | - [FIX] Fixed ``ImportError: No module named 'dateutil'`` (#598). 454 | 455 | 0.14.0 (2019-06-06) 456 | ------------------- 457 | 458 | - [NEW] Added provisional support for Python 3.8. 459 | - [CHANGE] Removed support for EOL Python 3.4. 460 | - [FIX] Updated setup.py with modern Python standards. 461 | - [FIX] Upgraded dependencies to latest versions. 462 | - [FIX] Enabled flake8 and black on travis builds. 463 | - [FIX] Formatted code using black and isort. 464 | 465 | 0.13.2 (2019-05-30) 466 | ------------------- 467 | 468 | - [NEW] Add is_between method. 469 | - [FIX] Improved humanize behaviour for near zero durations (#416). 470 | - [FIX] Correct humanize behaviour with future days (#541). 471 | - [FIX] Documentation updates. 472 | - [FIX] Improvements to German Locale. 473 | 474 | 0.13.1 (2019-02-17) 475 | ------------------- 476 | 477 | - [NEW] Add support for Python 3.7. 478 | - [CHANGE] Remove deprecation decorators for Arrow.range(), Arrow.span_range() and Arrow.interval(), all now return generators, wrap with list() to get old behavior. 479 | - [FIX] Documentation and docstring updates. 480 | 481 | 0.13.0 (2019-01-09) 482 | ------------------- 483 | 484 | - [NEW] Added support for Python 3.6. 485 | - [CHANGE] Drop support for Python 2.6/3.3. 486 | - [CHANGE] Return generator instead of list for Arrow.range(), Arrow.span_range() and Arrow.interval(). 487 | - [FIX] Make arrow.get() work with str & tzinfo combo. 488 | - [FIX] Make sure special RegEx characters are escaped in format string. 489 | - [NEW] Added support for ZZZ when formatting. 490 | - [FIX] Stop using datetime.utcnow() in internals, use datetime.now(UTC) instead. 491 | - [FIX] Return NotImplemented instead of TypeError in arrow math internals. 492 | - [NEW] Added Estonian Locale. 493 | - [FIX] Small fixes to Greek locale. 494 | - [FIX] TagalogLocale improvements. 495 | - [FIX] Added test requirements to setup. 496 | - [FIX] Improve docs for get, now and utcnow methods. 497 | - [FIX] Correct typo in depreciation warning. 498 | 499 | 0.12.1 500 | ------ 501 | 502 | - [FIX] Allow universal wheels to be generated and reliably installed. 503 | - [FIX] Make humanize respect only_distance when granularity argument is also given. 504 | 505 | 0.12.0 506 | ------ 507 | 508 | - [FIX] Compatibility fix for Python 2.x 509 | 510 | 0.11.0 511 | ------ 512 | 513 | - [FIX] Fix grammar of ArabicLocale 514 | - [NEW] Add Nepali Locale 515 | - [FIX] Fix month name + rename AustriaLocale -> AustrianLocale 516 | - [FIX] Fix typo in Basque Locale 517 | - [FIX] Fix grammar in PortugueseBrazilian locale 518 | - [FIX] Remove pip --user-mirrors flag 519 | - [NEW] Add Indonesian Locale 520 | 521 | 0.10.0 522 | ------ 523 | 524 | - [FIX] Fix getattr off by one for quarter 525 | - [FIX] Fix negative offset for UTC 526 | - [FIX] Update arrow.py 527 | 528 | 0.9.0 529 | ----- 530 | 531 | - [NEW] Remove duplicate code 532 | - [NEW] Support gnu date iso 8601 533 | - [NEW] Add support for universal wheels 534 | - [NEW] Slovenian locale 535 | - [NEW] Slovak locale 536 | - [NEW] Romanian locale 537 | - [FIX] respect limit even if end is defined range 538 | - [FIX] Separate replace & shift functions 539 | - [NEW] Added tox 540 | - [FIX] Fix supported Python versions in documentation 541 | - [NEW] Azerbaijani locale added, locale issue fixed in Turkish. 542 | - [FIX] Format ParserError's raise message 543 | 544 | 0.8.0 545 | ----- 546 | 547 | - [] 548 | 549 | 0.7.1 550 | ----- 551 | 552 | - [NEW] Esperanto locale (batisteo) 553 | 554 | 0.7.0 555 | ----- 556 | 557 | - [FIX] Parse localized strings #228 (swistakm) 558 | - [FIX] Modify tzinfo parameter in ``get`` api #221 (bottleimp) 559 | - [FIX] Fix Czech locale (PrehistoricTeam) 560 | - [FIX] Raise TypeError when adding/subtracting non-dates (itsmeolivia) 561 | - [FIX] Fix pytz conversion error (Kudo) 562 | - [FIX] Fix overzealous time truncation in span_range (kdeldycke) 563 | - [NEW] Humanize for time duration #232 (ybrs) 564 | - [NEW] Add Thai locale (sipp11) 565 | - [NEW] Adding Belarusian (be) locale (oire) 566 | - [NEW] Search date in strings (beenje) 567 | - [NEW] Note that arrow's tokens differ from strptime's. (offby1) 568 | 569 | 0.6.0 570 | ----- 571 | 572 | - [FIX] Added support for Python 3 573 | - [FIX] Avoid truncating oversized epoch timestamps. Fixes #216. 574 | - [FIX] Fixed month abbreviations for Ukrainian 575 | - [FIX] Fix typo timezone 576 | - [FIX] A couple of dialect fixes and two new languages 577 | - [FIX] Spanish locale: ``Miercoles`` should have acute accent 578 | - [Fix] Fix Finnish grammar 579 | - [FIX] Fix typo in 'Arrow.floor' docstring 580 | - [FIX] Use read() utility to open README 581 | - [FIX] span_range for week frame 582 | - [NEW] Add minimal support for fractional seconds longer than six digits. 583 | - [NEW] Adding locale support for Marathi (mr) 584 | - [NEW] Add count argument to span method 585 | - [NEW] Improved docs 586 | 587 | 0.5.1 - 0.5.4 588 | ------------- 589 | 590 | - [FIX] test the behavior of simplejson instead of calling for_json directly (tonyseek) 591 | - [FIX] Add Hebrew Locale (doodyparizada) 592 | - [FIX] Update documentation location (andrewelkins) 593 | - [FIX] Update setup.py Development Status level (andrewelkins) 594 | - [FIX] Case insensitive month match (cshowe) 595 | 596 | 0.5.0 597 | ----- 598 | 599 | - [NEW] struct_time addition. (mhworth) 600 | - [NEW] Version grep (eirnym) 601 | - [NEW] Default to ISO 8601 format (emonty) 602 | - [NEW] Raise TypeError on comparison (sniekamp) 603 | - [NEW] Adding Macedonian(mk) locale (krisfremen) 604 | - [FIX] Fix for ISO seconds and fractional seconds (sdispater) (andrewelkins) 605 | - [FIX] Use correct Dutch wording for "hours" (wbolster) 606 | - [FIX] Complete the list of english locales (indorilftw) 607 | - [FIX] Change README to reStructuredText (nyuszika7h) 608 | - [FIX] Parse lower-cased 'h' (tamentis) 609 | - [FIX] Slight modifications to Dutch locale (nvie) 610 | 611 | 0.4.4 612 | ----- 613 | 614 | - [NEW] Include the docs in the released tarball 615 | - [NEW] Czech localization Czech localization for Arrow 616 | - [NEW] Add fa_ir to locales 617 | - [FIX] Fixes parsing of time strings with a final Z 618 | - [FIX] Fixes ISO parsing and formatting for fractional seconds 619 | - [FIX] test_fromtimestamp sp 620 | - [FIX] some typos fixed 621 | - [FIX] removed an unused import statement 622 | - [FIX] docs table fix 623 | - [FIX] Issue with specify 'X' template and no template at all to arrow.get 624 | - [FIX] Fix "import" typo in docs/index.rst 625 | - [FIX] Fix unit tests for zero passed 626 | - [FIX] Update layout.html 627 | - [FIX] In Norwegian and new Norwegian months and weekdays should not be capitalized 628 | - [FIX] Fixed discrepancy between specifying 'X' to arrow.get and specifying no template 629 | 630 | 0.4.3 631 | ----- 632 | 633 | - [NEW] Turkish locale (Emre) 634 | - [NEW] Arabic locale (Mosab Ahmad) 635 | - [NEW] Danish locale (Holmars) 636 | - [NEW] Icelandic locale (Holmars) 637 | - [NEW] Hindi locale (Atmb4u) 638 | - [NEW] Malayalam locale (Atmb4u) 639 | - [NEW] Finnish locale (Stormpat) 640 | - [NEW] Portuguese locale (Danielcorreia) 641 | - [NEW] ``h`` and ``hh`` strings are now supported (Averyonghub) 642 | - [FIX] An incorrect inflection in the Polish locale has been fixed (Avalanchy) 643 | - [FIX] ``arrow.get`` now properly handles ``Date`` (Jaapz) 644 | - [FIX] Tests are now declared in ``setup.py`` and the manifest (Pypingou) 645 | - [FIX] ``__version__`` has been added to ``__init__.py`` (Sametmax) 646 | - [FIX] ISO 8601 strings can be parsed without a separator (Ivandiguisto / Root) 647 | - [FIX] Documentation is now more clear regarding some inputs on ``arrow.get`` (Eriktaubeneck) 648 | - [FIX] Some documentation links have been fixed (Vrutsky) 649 | - [FIX] Error messages for parse errors are now more descriptive (Maciej Albin) 650 | - [FIX] The parser now correctly checks for separators in strings (Mschwager) 651 | 652 | 0.4.2 653 | ----- 654 | 655 | - [NEW] Factory ``get`` method now accepts a single ``Arrow`` argument. 656 | - [NEW] Tokens SSSS, SSSSS and SSSSSS are supported in parsing. 657 | - [NEW] ``Arrow`` objects have a ``float_timestamp`` property. 658 | - [NEW] Vietnamese locale (Iu1nguoi) 659 | - [NEW] Factory ``get`` method now accepts a list of format strings (Dgilland) 660 | - [NEW] A MANIFEST.in file has been added (Pypingou) 661 | - [NEW] Tests can be run directly from ``setup.py`` (Pypingou) 662 | - [FIX] Arrow docs now list 'day of week' format tokens correctly (Rudolphfroger) 663 | - [FIX] Several issues with the Korean locale have been resolved (Yoloseem) 664 | - [FIX] ``humanize`` now correctly returns unicode (Shvechikov) 665 | - [FIX] ``Arrow`` objects now pickle / unpickle correctly (Yoloseem) 666 | 667 | 0.4.1 668 | ----- 669 | 670 | - [NEW] Table / explanation of formatting & parsing tokens in docs 671 | - [NEW] Brazilian locale (Augusto2112) 672 | - [NEW] Dutch locale (OrangeTux) 673 | - [NEW] Italian locale (Pertux) 674 | - [NEW] Austrian locale (LeChewbacca) 675 | - [NEW] Tagalog locale (Marksteve) 676 | - [FIX] Corrected spelling and day numbers in German locale (LeChewbacca) 677 | - [FIX] Factory ``get`` method should now handle unicode strings correctly (Bwells) 678 | - [FIX] Midnight and noon should now parse and format correctly (Bwells) 679 | 680 | 0.4.0 681 | ----- 682 | 683 | - [NEW] Format-free ISO 8601 parsing in factory ``get`` method 684 | - [NEW] Support for 'week' / 'weeks' in ``span``, ``range``, ``span_range``, ``floor`` and ``ceil`` 685 | - [NEW] Support for 'weeks' in ``replace`` 686 | - [NEW] Norwegian locale (Martinp) 687 | - [NEW] Japanese locale (CortYuming) 688 | - [FIX] Timezones no longer show the wrong sign when formatted (Bean) 689 | - [FIX] Microseconds are parsed correctly from strings (Bsidhom) 690 | - [FIX] Locale day-of-week is no longer off by one (Cynddl) 691 | - [FIX] Corrected plurals of Ukrainian and Russian nouns (Catchagain) 692 | - [CHANGE] Old 0.1 ``arrow`` module method removed 693 | - [CHANGE] Dropped timestamp support in ``range`` and ``span_range`` (never worked correctly) 694 | - [CHANGE] Dropped parsing of single string as tz string in factory ``get`` method (replaced by ISO 8601) 695 | 696 | 0.3.5 697 | ----- 698 | 699 | - [NEW] French locale (Cynddl) 700 | - [NEW] Spanish locale (Slapresta) 701 | - [FIX] Ranges handle multiple timezones correctly (Ftobia) 702 | 703 | 0.3.4 704 | ----- 705 | 706 | - [FIX] Humanize no longer sometimes returns the wrong month delta 707 | - [FIX] ``__format__`` works correctly with no format string 708 | 709 | 0.3.3 710 | ----- 711 | 712 | - [NEW] Python 2.6 support 713 | - [NEW] Initial support for locale-based parsing and formatting 714 | - [NEW] ArrowFactory class, now proxied as the module API 715 | - [NEW] ``factory`` api method to obtain a factory for a custom type 716 | - [FIX] Python 3 support and tests completely ironed out 717 | 718 | 0.3.2 719 | ----- 720 | 721 | - [NEW] Python 3+ support 722 | 723 | 0.3.1 724 | ----- 725 | 726 | - [FIX] The old ``arrow`` module function handles timestamps correctly as it used to 727 | 728 | 0.3.0 729 | ----- 730 | 731 | - [NEW] ``Arrow.replace`` method 732 | - [NEW] Accept timestamps, datetimes and Arrows for datetime inputs, where reasonable 733 | - [FIX] ``range`` and ``span_range`` respect end and limit parameters correctly 734 | - [CHANGE] Arrow objects are no longer mutable 735 | - [CHANGE] Plural attribute name semantics altered: single -> absolute, plural -> relative 736 | - [CHANGE] Plural names no longer supported as properties (e.g. ``arrow.utcnow().years``) 737 | 738 | 0.2.1 739 | ----- 740 | 741 | - [NEW] Support for localized humanization 742 | - [NEW] English, Russian, Greek, Korean, Chinese locales 743 | 744 | 0.2.0 745 | ----- 746 | 747 | - **REWRITE** 748 | - [NEW] Date parsing 749 | - [NEW] Date formatting 750 | - [NEW] ``floor``, ``ceil`` and ``span`` methods 751 | - [NEW] ``datetime`` interface implementation 752 | - [NEW] ``clone`` method 753 | - [NEW] ``get``, ``now`` and ``utcnow`` API methods 754 | 755 | 0.1.6 756 | ----- 757 | 758 | - [NEW] Humanized time deltas 759 | - [NEW] ``__eq__`` implemented 760 | - [FIX] Issues with conversions related to daylight savings time resolved 761 | - [CHANGE] ``__str__`` uses ISO formatting 762 | 763 | 0.1.5 764 | ----- 765 | 766 | - **Started tracking changes** 767 | - [NEW] Parsing of ISO-formatted time zone offsets (e.g. '+02:30', '-05:00') 768 | - [NEW] Resolved some issues with timestamps and delta / Olson time zones 769 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright 2023 Chris Smith 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | .PHONY: auto test docs clean 2 | 3 | auto: build311 4 | 5 | build38: PYTHON_VER = python3.8 6 | build39: PYTHON_VER = python3.9 7 | build310: PYTHON_VER = python3.10 8 | build311: PYTHON_VER = python3.11 9 | build312: PYTHON_VER = python3.12 10 | build313: PYTHON_VER = python3.13 11 | 12 | build36 build37 build38 build39 build310 build311 build312 build313: clean 13 | $(PYTHON_VER) -m venv venv 14 | . venv/bin/activate; \ 15 | pip install -U pip setuptools wheel; \ 16 | pip install -r requirements/requirements-tests.txt; \ 17 | pip install -r requirements/requirements-docs.txt; \ 18 | pre-commit install 19 | 20 | test: 21 | rm -f .coverage coverage.xml 22 | . venv/bin/activate; \ 23 | pytest 24 | 25 | lint: 26 | . venv/bin/activate; \ 27 | pre-commit run --all-files --show-diff-on-failure 28 | 29 | clean-docs: 30 | rm -rf docs/_build 31 | 32 | docs: 33 | . venv/bin/activate; \ 34 | cd docs; \ 35 | make html 36 | 37 | live-docs: clean-docs 38 | . venv/bin/activate; \ 39 | sphinx-autobuild docs docs/_build/html 40 | 41 | clean: clean-dist 42 | rm -rf venv .pytest_cache ./**/__pycache__ 43 | rm -f .coverage coverage.xml ./**/*.pyc 44 | 45 | clean-dist: 46 | rm -rf dist build *.egg *.eggs *.egg-info 47 | 48 | build-dist: clean-dist 49 | . venv/bin/activate; \ 50 | pip install -U flit; \ 51 | flit build 52 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | Arrow: Better dates & times for Python 2 | ====================================== 3 | 4 | .. start-inclusion-marker-do-not-remove 5 | 6 | .. image:: https://github.com/arrow-py/arrow/workflows/tests/badge.svg?branch=master 7 | :alt: Build Status 8 | :target: https://github.com/arrow-py/arrow/actions?query=workflow%3Atests+branch%3Amaster 9 | 10 | .. image:: https://codecov.io/gh/arrow-py/arrow/branch/master/graph/badge.svg 11 | :alt: Coverage 12 | :target: https://codecov.io/gh/arrow-py/arrow 13 | 14 | .. image:: https://img.shields.io/pypi/v/arrow.svg 15 | :alt: PyPI Version 16 | :target: https://pypi.python.org/pypi/arrow 17 | 18 | .. image:: https://img.shields.io/pypi/pyversions/arrow.svg 19 | :alt: Supported Python Versions 20 | :target: https://pypi.python.org/pypi/arrow 21 | 22 | .. image:: https://img.shields.io/pypi/l/arrow.svg 23 | :alt: License 24 | :target: https://pypi.python.org/pypi/arrow 25 | 26 | .. image:: https://img.shields.io/badge/code%20style-black-000000.svg 27 | :alt: Code Style: Black 28 | :target: https://github.com/psf/black 29 | 30 | 31 | **Arrow** is a Python library that offers a sensible and human-friendly approach to creating, manipulating, formatting and converting dates, times and timestamps. It implements and updates the datetime type, plugging gaps in functionality and providing an intelligent module API that supports many common creation scenarios. Simply put, it helps you work with dates and times with fewer imports and a lot less code. 32 | 33 | Arrow is named after the `arrow of time `_ and is heavily inspired by `moment.js `_ and `requests `_. 34 | 35 | Why use Arrow over built-in modules? 36 | ------------------------------------ 37 | 38 | Python's standard library and some other low-level modules have near-complete date, time and timezone functionality, but don't work very well from a usability perspective: 39 | 40 | - Too many modules: datetime, time, calendar, dateutil, pytz and more 41 | - Too many types: date, time, datetime, tzinfo, timedelta, relativedelta, etc. 42 | - Timezones and timestamp conversions are verbose and unpleasant 43 | - Timezone naivety is the norm 44 | - Gaps in functionality: ISO 8601 parsing, timespans, humanization 45 | 46 | Features 47 | -------- 48 | 49 | - Fully-implemented, drop-in replacement for datetime 50 | - Support for Python 3.8+ 51 | - Timezone-aware and UTC by default 52 | - Super-simple creation options for many common input scenarios 53 | - ``shift`` method with support for relative offsets, including weeks 54 | - Format and parse strings automatically 55 | - Wide support for the `ISO 8601 `_ standard 56 | - Timezone conversion 57 | - Support for ``dateutil``, ``pytz``, and ``ZoneInfo`` tzinfo objects 58 | - Generates time spans, ranges, floors and ceilings for time frames ranging from microsecond to year 59 | - Humanize dates and times with a growing list of contributed locales 60 | - Extensible for your own Arrow-derived types 61 | - Full support for PEP 484-style type hints 62 | 63 | Quick Start 64 | ----------- 65 | 66 | Installation 67 | ~~~~~~~~~~~~ 68 | 69 | To install Arrow, use `pip `_ or `pipenv `_: 70 | 71 | .. code-block:: console 72 | 73 | $ pip install -U arrow 74 | 75 | Example Usage 76 | ~~~~~~~~~~~~~ 77 | 78 | .. code-block:: python 79 | 80 | >>> import arrow 81 | >>> arrow.get('2013-05-11T21:23:58.970460+07:00') 82 | 83 | 84 | >>> utc = arrow.utcnow() 85 | >>> utc 86 | 87 | 88 | >>> utc = utc.shift(hours=-1) 89 | >>> utc 90 | 91 | 92 | >>> local = utc.to('US/Pacific') 93 | >>> local 94 | 95 | 96 | >>> local.timestamp() 97 | 1368303838.970460 98 | 99 | >>> local.format() 100 | '2013-05-11 13:23:58 -07:00' 101 | 102 | >>> local.format('YYYY-MM-DD HH:mm:ss ZZ') 103 | '2013-05-11 13:23:58 -07:00' 104 | 105 | >>> local.humanize() 106 | 'an hour ago' 107 | 108 | >>> local.humanize(locale='ko-kr') 109 | '한시간 전' 110 | 111 | .. end-inclusion-marker-do-not-remove 112 | 113 | Documentation 114 | ------------- 115 | 116 | For full documentation, please visit `arrow.readthedocs.io `_. 117 | 118 | Contributing 119 | ------------ 120 | 121 | Contributions are welcome for both code and localizations (adding and updating locales). Begin by gaining familiarity with the Arrow library and its features. Then, jump into contributing: 122 | 123 | #. Find an issue or feature to tackle on the `issue tracker `_. Issues marked with the `"good first issue" label `_ may be a great place to start! 124 | #. Fork `this repository `_ on GitHub and begin making changes in a branch. 125 | #. Add a few tests to ensure that the bug was fixed or the feature works as expected. 126 | #. Run the entire test suite and linting checks by running one of the following commands: ``tox && tox -e lint,docs`` (if you have `tox `_ installed) **OR** ``make build39 && make test && make lint`` (if you do not have Python 3.9 installed, replace ``build39`` with the latest Python version on your system). 127 | #. Submit a pull request and await feedback 😃. 128 | 129 | If you have any questions along the way, feel free to ask them `here `_. 130 | 131 | Support Arrow 132 | ------------- 133 | 134 | `Open Collective `_ is an online funding platform that provides tools to raise money and share your finances with full transparency. It is the platform of choice for individuals and companies to make one-time or recurring donations directly to the project. If you are interested in making a financial contribution, please visit the `Arrow collective `_. 135 | -------------------------------------------------------------------------------- /arrow/__init__.py: -------------------------------------------------------------------------------- 1 | from ._version import __version__ 2 | from .api import get, now, utcnow 3 | from .arrow import Arrow 4 | from .factory import ArrowFactory 5 | from .formatter import ( 6 | FORMAT_ATOM, 7 | FORMAT_COOKIE, 8 | FORMAT_RFC822, 9 | FORMAT_RFC850, 10 | FORMAT_RFC1036, 11 | FORMAT_RFC1123, 12 | FORMAT_RFC2822, 13 | FORMAT_RFC3339, 14 | FORMAT_RFC3339_STRICT, 15 | FORMAT_RSS, 16 | FORMAT_W3C, 17 | ) 18 | from .parser import ParserError 19 | 20 | # https://mypy.readthedocs.io/en/stable/command_line.html#cmdoption-mypy-no-implicit-reexport 21 | # Mypy with --strict or --no-implicit-reexport requires an explicit reexport. 22 | __all__ = [ 23 | "__version__", 24 | "get", 25 | "now", 26 | "utcnow", 27 | "Arrow", 28 | "ArrowFactory", 29 | "FORMAT_ATOM", 30 | "FORMAT_COOKIE", 31 | "FORMAT_RFC822", 32 | "FORMAT_RFC850", 33 | "FORMAT_RFC1036", 34 | "FORMAT_RFC1123", 35 | "FORMAT_RFC2822", 36 | "FORMAT_RFC3339", 37 | "FORMAT_RFC3339_STRICT", 38 | "FORMAT_RSS", 39 | "FORMAT_W3C", 40 | "ParserError", 41 | ] 42 | -------------------------------------------------------------------------------- /arrow/_version.py: -------------------------------------------------------------------------------- 1 | __version__ = "1.3.0" 2 | -------------------------------------------------------------------------------- /arrow/api.py: -------------------------------------------------------------------------------- 1 | """ 2 | Provides the default implementation of :class:`ArrowFactory ` 3 | methods for use as a module API. 4 | 5 | """ 6 | 7 | from datetime import date, datetime 8 | from datetime import tzinfo as dt_tzinfo 9 | from time import struct_time 10 | from typing import Any, List, Optional, Tuple, Type, Union, overload 11 | 12 | from arrow.arrow import TZ_EXPR, Arrow 13 | from arrow.constants import DEFAULT_LOCALE 14 | from arrow.factory import ArrowFactory 15 | 16 | # internal default factory. 17 | _factory = ArrowFactory() 18 | 19 | # TODO: Use Positional Only Argument (https://www.python.org/dev/peps/pep-0570/) 20 | # after Python 3.7 deprecation 21 | 22 | 23 | @overload 24 | def get( 25 | *, 26 | locale: str = DEFAULT_LOCALE, 27 | tzinfo: Optional[TZ_EXPR] = None, 28 | normalize_whitespace: bool = False, 29 | ) -> Arrow: 30 | ... # pragma: no cover 31 | 32 | 33 | @overload 34 | def get( 35 | *args: int, 36 | locale: str = DEFAULT_LOCALE, 37 | tzinfo: Optional[TZ_EXPR] = None, 38 | normalize_whitespace: bool = False, 39 | ) -> Arrow: 40 | ... # pragma: no cover 41 | 42 | 43 | @overload 44 | def get( 45 | __obj: Union[ 46 | Arrow, 47 | datetime, 48 | date, 49 | struct_time, 50 | dt_tzinfo, 51 | int, 52 | float, 53 | str, 54 | Tuple[int, int, int], 55 | ], 56 | *, 57 | locale: str = DEFAULT_LOCALE, 58 | tzinfo: Optional[TZ_EXPR] = None, 59 | normalize_whitespace: bool = False, 60 | ) -> Arrow: 61 | ... # pragma: no cover 62 | 63 | 64 | @overload 65 | def get( 66 | __arg1: Union[datetime, date], 67 | __arg2: TZ_EXPR, 68 | *, 69 | locale: str = DEFAULT_LOCALE, 70 | tzinfo: Optional[TZ_EXPR] = None, 71 | normalize_whitespace: bool = False, 72 | ) -> Arrow: 73 | ... # pragma: no cover 74 | 75 | 76 | @overload 77 | def get( 78 | __arg1: str, 79 | __arg2: Union[str, List[str]], 80 | *, 81 | locale: str = DEFAULT_LOCALE, 82 | tzinfo: Optional[TZ_EXPR] = None, 83 | normalize_whitespace: bool = False, 84 | ) -> Arrow: 85 | ... # pragma: no cover 86 | 87 | 88 | def get(*args: Any, **kwargs: Any) -> Arrow: 89 | """Calls the default :class:`ArrowFactory ` ``get`` method.""" 90 | 91 | return _factory.get(*args, **kwargs) 92 | 93 | 94 | get.__doc__ = _factory.get.__doc__ 95 | 96 | 97 | def utcnow() -> Arrow: 98 | """Calls the default :class:`ArrowFactory ` ``utcnow`` method.""" 99 | 100 | return _factory.utcnow() 101 | 102 | 103 | utcnow.__doc__ = _factory.utcnow.__doc__ 104 | 105 | 106 | def now(tz: Optional[TZ_EXPR] = None) -> Arrow: 107 | """Calls the default :class:`ArrowFactory ` ``now`` method.""" 108 | 109 | return _factory.now(tz) 110 | 111 | 112 | now.__doc__ = _factory.now.__doc__ 113 | 114 | 115 | def factory(type: Type[Arrow]) -> ArrowFactory: 116 | """Returns an :class:`.ArrowFactory` for the specified :class:`Arrow ` 117 | or derived type. 118 | 119 | :param type: the type, :class:`Arrow ` or derived. 120 | 121 | """ 122 | 123 | return ArrowFactory(type) 124 | 125 | 126 | __all__ = ["get", "utcnow", "now", "factory"] 127 | -------------------------------------------------------------------------------- /arrow/constants.py: -------------------------------------------------------------------------------- 1 | """Constants used internally in arrow.""" 2 | 3 | import sys 4 | from datetime import datetime 5 | from typing import Final 6 | 7 | # datetime.max.timestamp() errors on Windows, so we must hardcode 8 | # the highest possible datetime value that can output a timestamp. 9 | # tl;dr platform-independent max timestamps are hard to form 10 | # See: https://stackoverflow.com/q/46133223 11 | try: 12 | # Get max timestamp. Works on POSIX-based systems like Linux and macOS, 13 | # but will trigger an OverflowError, ValueError, or OSError on Windows 14 | _MAX_TIMESTAMP = datetime.max.timestamp() 15 | except (OverflowError, ValueError, OSError): # pragma: no cover 16 | # Fallback for Windows and 32-bit systems if initial max timestamp call fails 17 | # Must get max value of ctime on Windows based on architecture (x32 vs x64) 18 | # https://docs.microsoft.com/en-us/cpp/c-runtime-library/reference/ctime-ctime32-ctime64-wctime-wctime32-wctime64 19 | # Note: this may occur on both 32-bit Linux systems (issue #930) along with Windows systems 20 | is_64bits = sys.maxsize > 2**32 21 | _MAX_TIMESTAMP = ( 22 | datetime(3000, 1, 1, 23, 59, 59, 999999).timestamp() 23 | if is_64bits 24 | else datetime(2038, 1, 1, 23, 59, 59, 999999).timestamp() 25 | ) 26 | 27 | MAX_TIMESTAMP: Final[float] = _MAX_TIMESTAMP 28 | MAX_TIMESTAMP_MS: Final[float] = MAX_TIMESTAMP * 1000 29 | MAX_TIMESTAMP_US: Final[float] = MAX_TIMESTAMP * 1_000_000 30 | 31 | MAX_ORDINAL: Final[int] = datetime.max.toordinal() 32 | MIN_ORDINAL: Final[int] = 1 33 | 34 | DEFAULT_LOCALE: Final[str] = "en-us" 35 | 36 | # Supported dehumanize locales 37 | DEHUMANIZE_LOCALES = { 38 | "en", 39 | "en-us", 40 | "en-gb", 41 | "en-au", 42 | "en-be", 43 | "en-jp", 44 | "en-za", 45 | "en-ca", 46 | "en-ph", 47 | "fr", 48 | "fr-fr", 49 | "fr-ca", 50 | "it", 51 | "it-it", 52 | "es", 53 | "es-es", 54 | "el", 55 | "el-gr", 56 | "ja", 57 | "ja-jp", 58 | "se", 59 | "se-fi", 60 | "se-no", 61 | "se-se", 62 | "sv", 63 | "sv-se", 64 | "fi", 65 | "fi-fi", 66 | "zh", 67 | "zh-cn", 68 | "zh-tw", 69 | "zh-hk", 70 | "nl", 71 | "nl-nl", 72 | "be", 73 | "be-by", 74 | "pl", 75 | "pl-pl", 76 | "ru", 77 | "ru-ru", 78 | "af", 79 | "bg", 80 | "bg-bg", 81 | "ua", 82 | "uk", 83 | "uk-ua", 84 | "mk", 85 | "mk-mk", 86 | "de", 87 | "de-de", 88 | "de-ch", 89 | "de-at", 90 | "nb", 91 | "nb-no", 92 | "nn", 93 | "nn-no", 94 | "pt", 95 | "pt-pt", 96 | "pt-br", 97 | "tl", 98 | "tl-ph", 99 | "vi", 100 | "vi-vn", 101 | "tr", 102 | "tr-tr", 103 | "az", 104 | "az-az", 105 | "da", 106 | "da-dk", 107 | "ml", 108 | "hi", 109 | "cs", 110 | "cs-cz", 111 | "sk", 112 | "sk-sk", 113 | "fa", 114 | "fa-ir", 115 | "mr", 116 | "ca", 117 | "ca-es", 118 | "ca-ad", 119 | "ca-fr", 120 | "ca-it", 121 | "eo", 122 | "eo-xx", 123 | "bn", 124 | "bn-bd", 125 | "bn-in", 126 | "rm", 127 | "rm-ch", 128 | "ro", 129 | "ro-ro", 130 | "sl", 131 | "sl-si", 132 | "id", 133 | "id-id", 134 | "ne", 135 | "ne-np", 136 | "ee", 137 | "et", 138 | "sw", 139 | "sw-ke", 140 | "sw-tz", 141 | "la", 142 | "la-va", 143 | "lt", 144 | "lt-lt", 145 | "ms", 146 | "ms-my", 147 | "ms-bn", 148 | "or", 149 | "or-in", 150 | "lb", 151 | "lb-lu", 152 | "zu", 153 | "zu-za", 154 | "sq", 155 | "sq-al", 156 | "ta", 157 | "ta-in", 158 | "ta-lk", 159 | "ur", 160 | "ur-pk", 161 | "ka", 162 | "ka-ge", 163 | "kk", 164 | "kk-kz", 165 | # "lo", 166 | # "lo-la", 167 | "am", 168 | "am-et", 169 | "hy-am", 170 | "hy", 171 | "uz", 172 | "uz-uz", 173 | } 174 | -------------------------------------------------------------------------------- /arrow/factory.py: -------------------------------------------------------------------------------- 1 | """ 2 | Implements the :class:`ArrowFactory ` class, 3 | providing factory methods for common :class:`Arrow ` 4 | construction scenarios. 5 | 6 | """ 7 | 8 | import calendar 9 | from datetime import date, datetime 10 | from datetime import tzinfo as dt_tzinfo 11 | from decimal import Decimal 12 | from time import struct_time 13 | from typing import Any, List, Optional, Tuple, Type, Union, overload 14 | 15 | from dateutil import tz as dateutil_tz 16 | 17 | from arrow import parser 18 | from arrow.arrow import TZ_EXPR, Arrow 19 | from arrow.constants import DEFAULT_LOCALE 20 | from arrow.util import is_timestamp, iso_to_gregorian 21 | 22 | 23 | class ArrowFactory: 24 | """A factory for generating :class:`Arrow ` objects. 25 | 26 | :param type: (optional) the :class:`Arrow `-based class to construct from. 27 | Defaults to :class:`Arrow `. 28 | 29 | """ 30 | 31 | type: Type[Arrow] 32 | 33 | def __init__(self, type: Type[Arrow] = Arrow) -> None: 34 | self.type = type 35 | 36 | @overload 37 | def get( 38 | self, 39 | *, 40 | locale: str = DEFAULT_LOCALE, 41 | tzinfo: Optional[TZ_EXPR] = None, 42 | normalize_whitespace: bool = False, 43 | ) -> Arrow: 44 | ... # pragma: no cover 45 | 46 | @overload 47 | def get( 48 | self, 49 | __obj: Union[ 50 | Arrow, 51 | datetime, 52 | date, 53 | struct_time, 54 | dt_tzinfo, 55 | int, 56 | float, 57 | str, 58 | Tuple[int, int, int], 59 | ], 60 | *, 61 | locale: str = DEFAULT_LOCALE, 62 | tzinfo: Optional[TZ_EXPR] = None, 63 | normalize_whitespace: bool = False, 64 | ) -> Arrow: 65 | ... # pragma: no cover 66 | 67 | @overload 68 | def get( 69 | self, 70 | __arg1: Union[datetime, date], 71 | __arg2: TZ_EXPR, 72 | *, 73 | locale: str = DEFAULT_LOCALE, 74 | tzinfo: Optional[TZ_EXPR] = None, 75 | normalize_whitespace: bool = False, 76 | ) -> Arrow: 77 | ... # pragma: no cover 78 | 79 | @overload 80 | def get( 81 | self, 82 | __arg1: str, 83 | __arg2: Union[str, List[str]], 84 | *, 85 | locale: str = DEFAULT_LOCALE, 86 | tzinfo: Optional[TZ_EXPR] = None, 87 | normalize_whitespace: bool = False, 88 | ) -> Arrow: 89 | ... # pragma: no cover 90 | 91 | def get(self, *args: Any, **kwargs: Any) -> Arrow: 92 | """Returns an :class:`Arrow ` object based on flexible inputs. 93 | 94 | :param locale: (optional) a ``str`` specifying a locale for the parser. Defaults to 'en-us'. 95 | :param tzinfo: (optional) a :ref:`timezone expression ` or tzinfo object. 96 | Replaces the timezone unless using an input form that is explicitly UTC or specifies 97 | the timezone in a positional argument. Defaults to UTC. 98 | :param normalize_whitespace: (optional) a ``bool`` specifying whether or not to normalize 99 | redundant whitespace (spaces, tabs, and newlines) in a datetime string before parsing. 100 | Defaults to false. 101 | 102 | Usage:: 103 | 104 | >>> import arrow 105 | 106 | **No inputs** to get current UTC time:: 107 | 108 | >>> arrow.get() 109 | 110 | 111 | **One** :class:`Arrow ` object, to get a copy. 112 | 113 | >>> arw = arrow.utcnow() 114 | >>> arrow.get(arw) 115 | 116 | 117 | **One** ``float`` or ``int``, convertible to a floating-point timestamp, to get 118 | that timestamp in UTC:: 119 | 120 | >>> arrow.get(1367992474.293378) 121 | 122 | 123 | >>> arrow.get(1367992474) 124 | 125 | 126 | **One** ISO 8601-formatted ``str``, to parse it:: 127 | 128 | >>> arrow.get('2013-09-29T01:26:43.830580') 129 | 130 | 131 | **One** ISO 8601-formatted ``str``, in basic format, to parse it:: 132 | 133 | >>> arrow.get('20160413T133656.456289') 134 | 135 | 136 | **One** ``tzinfo``, to get the current time **converted** to that timezone:: 137 | 138 | >>> arrow.get(tz.tzlocal()) 139 | 140 | 141 | **One** naive ``datetime``, to get that datetime in UTC:: 142 | 143 | >>> arrow.get(datetime(2013, 5, 5)) 144 | 145 | 146 | **One** aware ``datetime``, to get that datetime:: 147 | 148 | >>> arrow.get(datetime(2013, 5, 5, tzinfo=tz.tzlocal())) 149 | 150 | 151 | **One** naive ``date``, to get that date in UTC:: 152 | 153 | >>> arrow.get(date(2013, 5, 5)) 154 | 155 | 156 | **One** time.struct time:: 157 | 158 | >>> arrow.get(gmtime(0)) 159 | 160 | 161 | **One** iso calendar ``tuple``, to get that week date in UTC:: 162 | 163 | >>> arrow.get((2013, 18, 7)) 164 | 165 | 166 | **Two** arguments, a naive or aware ``datetime``, and a replacement 167 | :ref:`timezone expression `:: 168 | 169 | >>> arrow.get(datetime(2013, 5, 5), 'US/Pacific') 170 | 171 | 172 | **Two** arguments, a naive ``date``, and a replacement 173 | :ref:`timezone expression `:: 174 | 175 | >>> arrow.get(date(2013, 5, 5), 'US/Pacific') 176 | 177 | 178 | **Two** arguments, both ``str``, to parse the first according to the format of the second:: 179 | 180 | >>> arrow.get('2013-05-05 12:30:45 America/Chicago', 'YYYY-MM-DD HH:mm:ss ZZZ') 181 | 182 | 183 | **Two** arguments, first a ``str`` to parse and second a ``list`` of formats to try:: 184 | 185 | >>> arrow.get('2013-05-05 12:30:45', ['MM/DD/YYYY', 'YYYY-MM-DD HH:mm:ss']) 186 | 187 | 188 | **Three or more** arguments, as for the direct constructor of an ``Arrow`` object:: 189 | 190 | >>> arrow.get(2013, 5, 5, 12, 30, 45) 191 | 192 | 193 | """ 194 | 195 | arg_count = len(args) 196 | locale = kwargs.pop("locale", DEFAULT_LOCALE) 197 | tz = kwargs.get("tzinfo", None) 198 | normalize_whitespace = kwargs.pop("normalize_whitespace", False) 199 | 200 | # if kwargs given, send to constructor unless only tzinfo provided 201 | if len(kwargs) > 1: 202 | arg_count = 3 203 | 204 | # tzinfo kwarg is not provided 205 | if len(kwargs) == 1 and tz is None: 206 | arg_count = 3 207 | 208 | # () -> now, @ tzinfo or utc 209 | if arg_count == 0: 210 | if isinstance(tz, str): 211 | tz = parser.TzinfoParser.parse(tz) 212 | return self.type.now(tzinfo=tz) 213 | 214 | if isinstance(tz, dt_tzinfo): 215 | return self.type.now(tzinfo=tz) 216 | 217 | return self.type.utcnow() 218 | 219 | if arg_count == 1: 220 | arg = args[0] 221 | if isinstance(arg, Decimal): 222 | arg = float(arg) 223 | 224 | # (None) -> raises an exception 225 | if arg is None: 226 | raise TypeError("Cannot parse argument of type None.") 227 | 228 | # try (int, float) -> from timestamp @ tzinfo 229 | elif not isinstance(arg, str) and is_timestamp(arg): 230 | if tz is None: 231 | # set to UTC by default 232 | tz = dateutil_tz.tzutc() 233 | return self.type.fromtimestamp(arg, tzinfo=tz) 234 | 235 | # (Arrow) -> from the object's datetime @ tzinfo 236 | elif isinstance(arg, Arrow): 237 | return self.type.fromdatetime(arg.datetime, tzinfo=tz) 238 | 239 | # (datetime) -> from datetime @ tzinfo 240 | elif isinstance(arg, datetime): 241 | return self.type.fromdatetime(arg, tzinfo=tz) 242 | 243 | # (date) -> from date @ tzinfo 244 | elif isinstance(arg, date): 245 | return self.type.fromdate(arg, tzinfo=tz) 246 | 247 | # (tzinfo) -> now @ tzinfo 248 | elif isinstance(arg, dt_tzinfo): 249 | return self.type.now(tzinfo=arg) 250 | 251 | # (str) -> parse @ tzinfo 252 | elif isinstance(arg, str): 253 | dt = parser.DateTimeParser(locale).parse_iso(arg, normalize_whitespace) 254 | return self.type.fromdatetime(dt, tzinfo=tz) 255 | 256 | # (struct_time) -> from struct_time 257 | elif isinstance(arg, struct_time): 258 | return self.type.utcfromtimestamp(calendar.timegm(arg)) 259 | 260 | # (iso calendar) -> convert then from date @ tzinfo 261 | elif isinstance(arg, tuple) and len(arg) == 3: 262 | d = iso_to_gregorian(*arg) 263 | return self.type.fromdate(d, tzinfo=tz) 264 | 265 | else: 266 | raise TypeError(f"Cannot parse single argument of type {type(arg)!r}.") 267 | 268 | elif arg_count == 2: 269 | arg_1, arg_2 = args[0], args[1] 270 | 271 | if isinstance(arg_1, datetime): 272 | # (datetime, tzinfo/str) -> fromdatetime @ tzinfo 273 | if isinstance(arg_2, (dt_tzinfo, str)): 274 | return self.type.fromdatetime(arg_1, tzinfo=arg_2) 275 | else: 276 | raise TypeError( 277 | f"Cannot parse two arguments of types 'datetime', {type(arg_2)!r}." 278 | ) 279 | 280 | elif isinstance(arg_1, date): 281 | # (date, tzinfo/str) -> fromdate @ tzinfo 282 | if isinstance(arg_2, (dt_tzinfo, str)): 283 | return self.type.fromdate(arg_1, tzinfo=arg_2) 284 | else: 285 | raise TypeError( 286 | f"Cannot parse two arguments of types 'date', {type(arg_2)!r}." 287 | ) 288 | 289 | # (str, format) -> parse @ tzinfo 290 | elif isinstance(arg_1, str) and isinstance(arg_2, (str, list)): 291 | dt = parser.DateTimeParser(locale).parse( 292 | args[0], args[1], normalize_whitespace 293 | ) 294 | return self.type.fromdatetime(dt, tzinfo=tz) 295 | 296 | else: 297 | raise TypeError( 298 | f"Cannot parse two arguments of types {type(arg_1)!r} and {type(arg_2)!r}." 299 | ) 300 | 301 | # 3+ args -> datetime-like via constructor 302 | else: 303 | return self.type(*args, **kwargs) 304 | 305 | def utcnow(self) -> Arrow: 306 | """Returns an :class:`Arrow ` object, representing "now" in UTC time. 307 | 308 | Usage:: 309 | 310 | >>> import arrow 311 | >>> arrow.utcnow() 312 | 313 | """ 314 | 315 | return self.type.utcnow() 316 | 317 | def now(self, tz: Optional[TZ_EXPR] = None) -> Arrow: 318 | """Returns an :class:`Arrow ` object, representing "now" in the given 319 | timezone. 320 | 321 | :param tz: (optional) A :ref:`timezone expression `. Defaults to local time. 322 | 323 | Usage:: 324 | 325 | >>> import arrow 326 | >>> arrow.now() 327 | 328 | 329 | >>> arrow.now('US/Pacific') 330 | 331 | 332 | >>> arrow.now('+02:00') 333 | 334 | 335 | >>> arrow.now('local') 336 | 337 | """ 338 | 339 | if tz is None: 340 | tz = dateutil_tz.tzlocal() 341 | elif not isinstance(tz, dt_tzinfo): 342 | tz = parser.TzinfoParser.parse(tz) 343 | 344 | return self.type.now(tz) 345 | -------------------------------------------------------------------------------- /arrow/formatter.py: -------------------------------------------------------------------------------- 1 | """Provides the :class:`Arrow ` class, an improved formatter for datetimes.""" 2 | 3 | import re 4 | from datetime import datetime, timedelta 5 | from typing import Final, Optional, Pattern, cast 6 | 7 | from dateutil import tz as dateutil_tz 8 | 9 | from arrow import locales 10 | from arrow.constants import DEFAULT_LOCALE 11 | 12 | FORMAT_ATOM: Final[str] = "YYYY-MM-DD HH:mm:ssZZ" 13 | FORMAT_COOKIE: Final[str] = "dddd, DD-MMM-YYYY HH:mm:ss ZZZ" 14 | FORMAT_RFC822: Final[str] = "ddd, DD MMM YY HH:mm:ss Z" 15 | FORMAT_RFC850: Final[str] = "dddd, DD-MMM-YY HH:mm:ss ZZZ" 16 | FORMAT_RFC1036: Final[str] = "ddd, DD MMM YY HH:mm:ss Z" 17 | FORMAT_RFC1123: Final[str] = "ddd, DD MMM YYYY HH:mm:ss Z" 18 | FORMAT_RFC2822: Final[str] = "ddd, DD MMM YYYY HH:mm:ss Z" 19 | FORMAT_RFC3339: Final[str] = "YYYY-MM-DD HH:mm:ssZZ" 20 | FORMAT_RFC3339_STRICT: Final[str] = "YYYY-MM-DDTHH:mm:ssZZ" 21 | FORMAT_RSS: Final[str] = "ddd, DD MMM YYYY HH:mm:ss Z" 22 | FORMAT_W3C: Final[str] = "YYYY-MM-DD HH:mm:ssZZ" 23 | 24 | 25 | class DateTimeFormatter: 26 | # This pattern matches characters enclosed in square brackets are matched as 27 | # an atomic group. For more info on atomic groups and how to they are 28 | # emulated in Python's re library, see https://stackoverflow.com/a/13577411/2701578 29 | 30 | _FORMAT_RE: Final[Pattern[str]] = re.compile( 31 | r"(\[(?:(?=(?P[^]]))(?P=literal))*\]|YYY?Y?|MM?M?M?|Do|DD?D?D?|d?dd?d?|HH?|hh?|mm?|ss?|SS?S?S?S?S?|ZZ?Z?|a|A|X|x|W)" 32 | ) 33 | 34 | locale: locales.Locale 35 | 36 | def __init__(self, locale: str = DEFAULT_LOCALE) -> None: 37 | self.locale = locales.get_locale(locale) 38 | 39 | def format(cls, dt: datetime, fmt: str) -> str: 40 | # FIXME: _format_token() is nullable 41 | return cls._FORMAT_RE.sub( 42 | lambda m: cast(str, cls._format_token(dt, m.group(0))), fmt 43 | ) 44 | 45 | def _format_token(self, dt: datetime, token: Optional[str]) -> Optional[str]: 46 | if token and token.startswith("[") and token.endswith("]"): 47 | return token[1:-1] 48 | 49 | if token == "YYYY": 50 | return self.locale.year_full(dt.year) 51 | if token == "YY": 52 | return self.locale.year_abbreviation(dt.year) 53 | 54 | if token == "MMMM": 55 | return self.locale.month_name(dt.month) 56 | if token == "MMM": 57 | return self.locale.month_abbreviation(dt.month) 58 | if token == "MM": 59 | return f"{dt.month:02d}" 60 | if token == "M": 61 | return f"{dt.month}" 62 | 63 | if token == "DDDD": 64 | return f"{dt.timetuple().tm_yday:03d}" 65 | if token == "DDD": 66 | return f"{dt.timetuple().tm_yday}" 67 | if token == "DD": 68 | return f"{dt.day:02d}" 69 | if token == "D": 70 | return f"{dt.day}" 71 | 72 | if token == "Do": 73 | return self.locale.ordinal_number(dt.day) 74 | 75 | if token == "dddd": 76 | return self.locale.day_name(dt.isoweekday()) 77 | if token == "ddd": 78 | return self.locale.day_abbreviation(dt.isoweekday()) 79 | if token == "d": 80 | return f"{dt.isoweekday()}" 81 | 82 | if token == "HH": 83 | return f"{dt.hour:02d}" 84 | if token == "H": 85 | return f"{dt.hour}" 86 | if token == "hh": 87 | return f"{dt.hour if 0 < dt.hour < 13 else abs(dt.hour - 12):02d}" 88 | if token == "h": 89 | return f"{dt.hour if 0 < dt.hour < 13 else abs(dt.hour - 12)}" 90 | 91 | if token == "mm": 92 | return f"{dt.minute:02d}" 93 | if token == "m": 94 | return f"{dt.minute}" 95 | 96 | if token == "ss": 97 | return f"{dt.second:02d}" 98 | if token == "s": 99 | return f"{dt.second}" 100 | 101 | if token == "SSSSSS": 102 | return f"{dt.microsecond:06d}" 103 | if token == "SSSSS": 104 | return f"{dt.microsecond // 10:05d}" 105 | if token == "SSSS": 106 | return f"{dt.microsecond // 100:04d}" 107 | if token == "SSS": 108 | return f"{dt.microsecond // 1000:03d}" 109 | if token == "SS": 110 | return f"{dt.microsecond // 10000:02d}" 111 | if token == "S": 112 | return f"{dt.microsecond // 100000}" 113 | 114 | if token == "X": 115 | return f"{dt.timestamp()}" 116 | 117 | if token == "x": 118 | return f"{dt.timestamp() * 1_000_000:.0f}" 119 | 120 | if token == "ZZZ": 121 | return dt.tzname() 122 | 123 | if token in ["ZZ", "Z"]: 124 | separator = ":" if token == "ZZ" else "" 125 | tz = dateutil_tz.tzutc() if dt.tzinfo is None else dt.tzinfo 126 | # `dt` must be aware object. Otherwise, this line will raise AttributeError 127 | # https://github.com/arrow-py/arrow/pull/883#discussion_r529866834 128 | # datetime awareness: https://docs.python.org/3/library/datetime.html#aware-and-naive-objects 129 | total_minutes = int(cast(timedelta, tz.utcoffset(dt)).total_seconds() / 60) 130 | 131 | sign = "+" if total_minutes >= 0 else "-" 132 | total_minutes = abs(total_minutes) 133 | hour, minute = divmod(total_minutes, 60) 134 | 135 | return f"{sign}{hour:02d}{separator}{minute:02d}" 136 | 137 | if token in ("a", "A"): 138 | return self.locale.meridian(dt.hour, token) 139 | 140 | if token == "W": 141 | year, week, day = dt.isocalendar() 142 | return f"{year}-W{week:02d}-{day}" 143 | -------------------------------------------------------------------------------- /arrow/parser.py: -------------------------------------------------------------------------------- 1 | """Provides the :class:`Arrow ` class, a better way to parse datetime strings.""" 2 | 3 | import re 4 | from datetime import datetime, timedelta 5 | from datetime import tzinfo as dt_tzinfo 6 | from functools import lru_cache 7 | from typing import ( 8 | Any, 9 | ClassVar, 10 | Dict, 11 | Iterable, 12 | List, 13 | Literal, 14 | Match, 15 | Optional, 16 | Pattern, 17 | SupportsFloat, 18 | SupportsInt, 19 | Tuple, 20 | TypedDict, 21 | Union, 22 | cast, 23 | overload, 24 | ) 25 | 26 | from dateutil import tz 27 | 28 | from arrow import locales 29 | from arrow.constants import DEFAULT_LOCALE 30 | from arrow.util import next_weekday, normalize_timestamp 31 | 32 | 33 | class ParserError(ValueError): 34 | """ 35 | A custom exception class for handling parsing errors in the parser. 36 | 37 | Notes: 38 | This class inherits from the built-in `ValueError` class and is used to raise exceptions 39 | when an error occurs during the parsing process. 40 | """ 41 | 42 | pass 43 | 44 | 45 | # Allows for ParserErrors to be propagated from _build_datetime() 46 | # when day_of_year errors occur. 47 | # Before this, the ParserErrors were caught by the try/except in 48 | # _parse_multiformat() and the appropriate error message was not 49 | # transmitted to the user. 50 | class ParserMatchError(ParserError): 51 | """ 52 | This class is a subclass of the ParserError class and is used to raise errors that occur during the matching process. 53 | 54 | Notes: 55 | This class is part of the Arrow parser and is used to provide error handling when a parsing match fails. 56 | 57 | """ 58 | 59 | pass 60 | 61 | 62 | _WEEKDATE_ELEMENT = Union[str, bytes, SupportsInt, bytearray] 63 | 64 | _FORMAT_TYPE = Literal[ 65 | "YYYY", 66 | "YY", 67 | "MM", 68 | "M", 69 | "DDDD", 70 | "DDD", 71 | "DD", 72 | "D", 73 | "HH", 74 | "H", 75 | "hh", 76 | "h", 77 | "mm", 78 | "m", 79 | "ss", 80 | "s", 81 | "X", 82 | "x", 83 | "ZZZ", 84 | "ZZ", 85 | "Z", 86 | "S", 87 | "W", 88 | "MMMM", 89 | "MMM", 90 | "Do", 91 | "dddd", 92 | "ddd", 93 | "d", 94 | "a", 95 | "A", 96 | ] 97 | 98 | 99 | class _Parts(TypedDict, total=False): 100 | """ 101 | A dictionary that represents different parts of a datetime. 102 | 103 | :class:`_Parts` is a TypedDict that represents various components of a date or time, 104 | such as year, month, day, hour, minute, second, microsecond, timestamp, expanded_timestamp, tzinfo, 105 | am_pm, day_of_week, and weekdate. 106 | 107 | :ivar year: The year, if present, as an integer. 108 | :ivar month: The month, if present, as an integer. 109 | :ivar day_of_year: The day of the year, if present, as an integer. 110 | :ivar day: The day, if present, as an integer. 111 | :ivar hour: The hour, if present, as an integer. 112 | :ivar minute: The minute, if present, as an integer. 113 | :ivar second: The second, if present, as an integer. 114 | :ivar microsecond: The microsecond, if present, as an integer. 115 | :ivar timestamp: The timestamp, if present, as a float. 116 | :ivar expanded_timestamp: The expanded timestamp, if present, as an integer. 117 | :ivar tzinfo: The timezone info, if present, as a :class:`dt_tzinfo` object. 118 | :ivar am_pm: The AM/PM indicator, if present, as a string literal "am" or "pm". 119 | :ivar day_of_week: The day of the week, if present, as an integer. 120 | :ivar weekdate: The week date, if present, as a tuple of three integers or None. 121 | """ 122 | 123 | year: int 124 | month: int 125 | day_of_year: int 126 | day: int 127 | hour: int 128 | minute: int 129 | second: int 130 | microsecond: int 131 | timestamp: float 132 | expanded_timestamp: int 133 | tzinfo: dt_tzinfo 134 | am_pm: Literal["am", "pm"] 135 | day_of_week: int 136 | weekdate: Tuple[_WEEKDATE_ELEMENT, _WEEKDATE_ELEMENT, Optional[_WEEKDATE_ELEMENT]] 137 | 138 | 139 | class DateTimeParser: 140 | """A :class:`DateTimeParser ` object 141 | 142 | Contains the regular expressions and functions to parse and split the input strings into tokens and eventually 143 | produce a datetime that is used by :class:`Arrow ` internally. 144 | 145 | :param locale: the locale string 146 | :param cache_size: the size of the LRU cache used for regular expressions. Defaults to 0. 147 | 148 | """ 149 | 150 | _FORMAT_RE: ClassVar[Pattern[str]] = re.compile( 151 | r"(YYY?Y?|MM?M?M?|Do|DD?D?D?|d?d?d?d|HH?|hh?|mm?|ss?|S+|ZZ?Z?|a|A|x|X|W)" 152 | ) 153 | _ESCAPE_RE: ClassVar[Pattern[str]] = re.compile(r"\[[^\[\]]*\]") 154 | 155 | _ONE_OR_TWO_DIGIT_RE: ClassVar[Pattern[str]] = re.compile(r"\d{1,2}") 156 | _ONE_OR_TWO_OR_THREE_DIGIT_RE: ClassVar[Pattern[str]] = re.compile(r"\d{1,3}") 157 | _ONE_OR_MORE_DIGIT_RE: ClassVar[Pattern[str]] = re.compile(r"\d+") 158 | _TWO_DIGIT_RE: ClassVar[Pattern[str]] = re.compile(r"\d{2}") 159 | _THREE_DIGIT_RE: ClassVar[Pattern[str]] = re.compile(r"\d{3}") 160 | _FOUR_DIGIT_RE: ClassVar[Pattern[str]] = re.compile(r"\d{4}") 161 | _TZ_Z_RE: ClassVar[Pattern[str]] = re.compile(r"([\+\-])(\d{2})(?:(\d{2}))?|Z") 162 | _TZ_ZZ_RE: ClassVar[Pattern[str]] = re.compile(r"([\+\-])(\d{2})(?:\:(\d{2}))?|Z") 163 | _TZ_NAME_RE: ClassVar[Pattern[str]] = re.compile(r"\w[\w+\-/]+") 164 | # NOTE: timestamps cannot be parsed from natural language strings (by removing the ^...$) because it will 165 | # break cases like "15 Jul 2000" and a format list (see issue #447) 166 | _TIMESTAMP_RE: ClassVar[Pattern[str]] = re.compile(r"^\-?\d+\.?\d+$") 167 | _TIMESTAMP_EXPANDED_RE: ClassVar[Pattern[str]] = re.compile(r"^\-?\d+$") 168 | _TIME_RE: ClassVar[Pattern[str]] = re.compile( 169 | r"^(\d{2})(?:\:?(\d{2}))?(?:\:?(\d{2}))?(?:([\.\,])(\d+))?$" 170 | ) 171 | _WEEK_DATE_RE: ClassVar[Pattern[str]] = re.compile( 172 | r"(?P\d{4})[\-]?W(?P\d{2})[\-]?(?P\d)?" 173 | ) 174 | 175 | _BASE_INPUT_RE_MAP: ClassVar[Dict[_FORMAT_TYPE, Pattern[str]]] = { 176 | "YYYY": _FOUR_DIGIT_RE, 177 | "YY": _TWO_DIGIT_RE, 178 | "MM": _TWO_DIGIT_RE, 179 | "M": _ONE_OR_TWO_DIGIT_RE, 180 | "DDDD": _THREE_DIGIT_RE, 181 | "DDD": _ONE_OR_TWO_OR_THREE_DIGIT_RE, 182 | "DD": _TWO_DIGIT_RE, 183 | "D": _ONE_OR_TWO_DIGIT_RE, 184 | "HH": _TWO_DIGIT_RE, 185 | "H": _ONE_OR_TWO_DIGIT_RE, 186 | "hh": _TWO_DIGIT_RE, 187 | "h": _ONE_OR_TWO_DIGIT_RE, 188 | "mm": _TWO_DIGIT_RE, 189 | "m": _ONE_OR_TWO_DIGIT_RE, 190 | "ss": _TWO_DIGIT_RE, 191 | "s": _ONE_OR_TWO_DIGIT_RE, 192 | "X": _TIMESTAMP_RE, 193 | "x": _TIMESTAMP_EXPANDED_RE, 194 | "ZZZ": _TZ_NAME_RE, 195 | "ZZ": _TZ_ZZ_RE, 196 | "Z": _TZ_Z_RE, 197 | "S": _ONE_OR_MORE_DIGIT_RE, 198 | "W": _WEEK_DATE_RE, 199 | } 200 | 201 | SEPARATORS: ClassVar[List[str]] = ["-", "/", "."] 202 | 203 | locale: locales.Locale 204 | _input_re_map: Dict[_FORMAT_TYPE, Pattern[str]] 205 | 206 | def __init__(self, locale: str = DEFAULT_LOCALE, cache_size: int = 0) -> None: 207 | """ 208 | Contains the regular expressions and functions to parse and split the input strings into tokens and eventually 209 | produce a datetime that is used by :class:`Arrow ` internally. 210 | 211 | :param locale: the locale string 212 | :type locale: str 213 | :param cache_size: the size of the LRU cache used for regular expressions. Defaults to 0. 214 | :type cache_size: int 215 | """ 216 | self.locale = locales.get_locale(locale) 217 | self._input_re_map = self._BASE_INPUT_RE_MAP.copy() 218 | self._input_re_map.update( 219 | { 220 | "MMMM": self._generate_choice_re( 221 | self.locale.month_names[1:], re.IGNORECASE 222 | ), 223 | "MMM": self._generate_choice_re( 224 | self.locale.month_abbreviations[1:], re.IGNORECASE 225 | ), 226 | "Do": re.compile(self.locale.ordinal_day_re), 227 | "dddd": self._generate_choice_re( 228 | self.locale.day_names[1:], re.IGNORECASE 229 | ), 230 | "ddd": self._generate_choice_re( 231 | self.locale.day_abbreviations[1:], re.IGNORECASE 232 | ), 233 | "d": re.compile(r"[1-7]"), 234 | "a": self._generate_choice_re( 235 | (self.locale.meridians["am"], self.locale.meridians["pm"]) 236 | ), 237 | # note: 'A' token accepts both 'am/pm' and 'AM/PM' formats to 238 | # ensure backwards compatibility of this token 239 | "A": self._generate_choice_re(self.locale.meridians.values()), 240 | } 241 | ) 242 | if cache_size > 0: 243 | self._generate_pattern_re = lru_cache(maxsize=cache_size)( # type: ignore 244 | self._generate_pattern_re 245 | ) 246 | 247 | # TODO: since we support more than ISO 8601, we should rename this function 248 | # IDEA: break into multiple functions 249 | def parse_iso( 250 | self, datetime_string: str, normalize_whitespace: bool = False 251 | ) -> datetime: 252 | """ 253 | Parses a datetime string using a ISO 8601-like format. 254 | 255 | :param datetime_string: The datetime string to parse. 256 | :param normalize_whitespace: Whether to normalize whitespace in the datetime string (default is False). 257 | :type datetime_string: str 258 | :type normalize_whitespace: bool 259 | :returns: The parsed datetime object. 260 | :rtype: datetime 261 | :raises ParserError: If the datetime string is not in a valid ISO 8601-like format. 262 | 263 | Usage:: 264 | >>> import arrow.parser 265 | >>> arrow.parser.DateTimeParser().parse_iso('2021-10-12T14:30:00') 266 | datetime.datetime(2021, 10, 12, 14, 30) 267 | 268 | """ 269 | if normalize_whitespace: 270 | datetime_string = re.sub(r"\s+", " ", datetime_string.strip()) 271 | 272 | has_space_divider = " " in datetime_string 273 | has_t_divider = "T" in datetime_string 274 | 275 | num_spaces = datetime_string.count(" ") 276 | if has_space_divider and num_spaces != 1 or has_t_divider and num_spaces > 0: 277 | raise ParserError( 278 | f"Expected an ISO 8601-like string, but was given {datetime_string!r}. " 279 | "Try passing in a format string to resolve this." 280 | ) 281 | 282 | has_time = has_space_divider or has_t_divider 283 | has_tz = False 284 | 285 | # date formats (ISO 8601 and others) to test against 286 | # NOTE: YYYYMM is omitted to avoid confusion with YYMMDD (no longer part of ISO 8601, but is still often used) 287 | formats = [ 288 | "YYYY-MM-DD", 289 | "YYYY-M-DD", 290 | "YYYY-M-D", 291 | "YYYY/MM/DD", 292 | "YYYY/M/DD", 293 | "YYYY/M/D", 294 | "YYYY.MM.DD", 295 | "YYYY.M.DD", 296 | "YYYY.M.D", 297 | "YYYYMMDD", 298 | "YYYY-DDDD", 299 | "YYYYDDDD", 300 | "YYYY-MM", 301 | "YYYY/MM", 302 | "YYYY.MM", 303 | "YYYY", 304 | "W", 305 | ] 306 | 307 | if has_time: 308 | if has_space_divider: 309 | date_string, time_string = datetime_string.split(" ", 1) 310 | else: 311 | date_string, time_string = datetime_string.split("T", 1) 312 | 313 | time_parts = re.split( 314 | r"[\+\-Z]", time_string, maxsplit=1, flags=re.IGNORECASE 315 | ) 316 | 317 | time_components: Optional[Match[str]] = self._TIME_RE.match(time_parts[0]) 318 | 319 | if time_components is None: 320 | raise ParserError( 321 | "Invalid time component provided. " 322 | "Please specify a format or provide a valid time component in the basic or extended ISO 8601 time format." 323 | ) 324 | 325 | ( 326 | hours, 327 | minutes, 328 | seconds, 329 | subseconds_sep, 330 | subseconds, 331 | ) = time_components.groups() 332 | 333 | has_tz = len(time_parts) == 2 334 | has_minutes = minutes is not None 335 | has_seconds = seconds is not None 336 | has_subseconds = subseconds is not None 337 | 338 | is_basic_time_format = ":" not in time_parts[0] 339 | tz_format = "Z" 340 | 341 | # use 'ZZ' token instead since tz offset is present in non-basic format 342 | if has_tz and ":" in time_parts[1]: 343 | tz_format = "ZZ" 344 | 345 | time_sep = "" if is_basic_time_format else ":" 346 | 347 | if has_subseconds: 348 | time_string = "HH{time_sep}mm{time_sep}ss{subseconds_sep}S".format( 349 | time_sep=time_sep, subseconds_sep=subseconds_sep 350 | ) 351 | elif has_seconds: 352 | time_string = "HH{time_sep}mm{time_sep}ss".format(time_sep=time_sep) 353 | elif has_minutes: 354 | time_string = f"HH{time_sep}mm" 355 | else: 356 | time_string = "HH" 357 | 358 | if has_space_divider: 359 | formats = [f"{f} {time_string}" for f in formats] 360 | else: 361 | formats = [f"{f}T{time_string}" for f in formats] 362 | 363 | if has_time and has_tz: 364 | # Add "Z" or "ZZ" to the format strings to indicate to 365 | # _parse_token() that a timezone needs to be parsed 366 | formats = [f"{f}{tz_format}" for f in formats] 367 | 368 | return self._parse_multiformat(datetime_string, formats) 369 | 370 | def parse( 371 | self, 372 | datetime_string: str, 373 | fmt: Union[List[str], str], 374 | normalize_whitespace: bool = False, 375 | ) -> datetime: 376 | """ 377 | Parses a datetime string using a specified format. 378 | 379 | :param datetime_string: The datetime string to parse. 380 | :param fmt: The format string or list of format strings to use for parsing. 381 | :param normalize_whitespace: Whether to normalize whitespace in the datetime string (default is False). 382 | :type datetime_string: str 383 | :type fmt: Union[List[str], str] 384 | :type normalize_whitespace: bool 385 | :returns: The parsed datetime object. 386 | :rtype: datetime 387 | :raises ParserMatchError: If the datetime string does not match the specified format. 388 | 389 | Usage:: 390 | 391 | >>> import arrow.parser 392 | >>> arrow.parser.DateTimeParser().parse('2021-10-12 14:30:00', 'YYYY-MM-DD HH:mm:ss') 393 | datetime.datetime(2021, 10, 12, 14, 30) 394 | 395 | 396 | """ 397 | if normalize_whitespace: 398 | datetime_string = re.sub(r"\s+", " ", datetime_string) 399 | 400 | if isinstance(fmt, list): 401 | return self._parse_multiformat(datetime_string, fmt) 402 | 403 | try: 404 | fmt_tokens: List[_FORMAT_TYPE] 405 | fmt_pattern_re: Pattern[str] 406 | fmt_tokens, fmt_pattern_re = self._generate_pattern_re(fmt) 407 | except re.error as e: 408 | raise ParserMatchError( 409 | f"Failed to generate regular expression pattern: {e}." 410 | ) 411 | 412 | match = fmt_pattern_re.search(datetime_string) 413 | 414 | if match is None: 415 | raise ParserMatchError( 416 | f"Failed to match {fmt!r} when parsing {datetime_string!r}." 417 | ) 418 | 419 | parts: _Parts = {} 420 | for token in fmt_tokens: 421 | value: Union[Tuple[str, str, str], str] 422 | if token == "Do": 423 | value = match.group("value") 424 | elif token == "W": 425 | value = (match.group("year"), match.group("week"), match.group("day")) 426 | else: 427 | value = match.group(token) 428 | 429 | if value is None: 430 | raise ParserMatchError( 431 | f"Unable to find a match group for the specified token {token!r}." 432 | ) 433 | 434 | self._parse_token(token, value, parts) # type: ignore[arg-type] 435 | 436 | return self._build_datetime(parts) 437 | 438 | def _generate_pattern_re(self, fmt: str) -> Tuple[List[_FORMAT_TYPE], Pattern[str]]: 439 | """ 440 | Generates a regular expression pattern from a format string. 441 | 442 | :param fmt: The format string to convert into a regular expression pattern. 443 | :type fmt: str 444 | :returns: A tuple containing a list of format tokens and the corresponding regular expression pattern. 445 | :rtype: Tuple[List[_FORMAT_TYPE], Pattern[str]] 446 | :raises ParserError: If an unrecognized token is encountered in the format string. 447 | """ 448 | # fmt is a string of tokens like 'YYYY-MM-DD' 449 | # we construct a new string by replacing each 450 | # token by its pattern: 451 | # 'YYYY-MM-DD' -> '(?P\d{4})-(?P\d{2})-(?P
\d{2})' 452 | tokens: List[_FORMAT_TYPE] = [] 453 | offset = 0 454 | 455 | # Escape all special RegEx chars 456 | escaped_fmt = re.escape(fmt) 457 | 458 | # Extract the bracketed expressions to be reinserted later. 459 | escaped_fmt = re.sub(self._ESCAPE_RE, "#", escaped_fmt) 460 | 461 | # Any number of S is the same as one. 462 | # TODO: allow users to specify the number of digits to parse 463 | escaped_fmt = re.sub(r"S+", "S", escaped_fmt) 464 | 465 | escaped_data = re.findall(self._ESCAPE_RE, fmt) 466 | 467 | fmt_pattern = escaped_fmt 468 | 469 | for m in self._FORMAT_RE.finditer(escaped_fmt): 470 | token: _FORMAT_TYPE = cast(_FORMAT_TYPE, m.group(0)) 471 | try: 472 | input_re = self._input_re_map[token] 473 | except KeyError: 474 | raise ParserError(f"Unrecognized token {token!r}.") 475 | input_pattern = f"(?P<{token}>{input_re.pattern})" 476 | tokens.append(token) 477 | # a pattern doesn't have the same length as the token 478 | # it replaces! We keep the difference in the offset variable. 479 | # This works because the string is scanned left-to-right and matches 480 | # are returned in the order found by finditer. 481 | fmt_pattern = ( 482 | fmt_pattern[: m.start() + offset] 483 | + input_pattern 484 | + fmt_pattern[m.end() + offset :] 485 | ) 486 | offset += len(input_pattern) - (m.end() - m.start()) 487 | 488 | final_fmt_pattern = "" 489 | split_fmt = fmt_pattern.split(r"\#") 490 | 491 | # Due to the way Python splits, 'split_fmt' will always be longer 492 | for i in range(len(split_fmt)): 493 | final_fmt_pattern += split_fmt[i] 494 | if i < len(escaped_data): 495 | final_fmt_pattern += escaped_data[i][1:-1] 496 | 497 | # Wrap final_fmt_pattern in a custom word boundary to strictly 498 | # match the formatting pattern and filter out date and time formats 499 | # that include junk such as: blah1998-09-12 blah, blah 1998-09-12blah, 500 | # blah1998-09-12blah. The custom word boundary matches every character 501 | # that is not a whitespace character to allow for searching for a date 502 | # and time string in a natural language sentence. Therefore, searching 503 | # for a string of the form YYYY-MM-DD in "blah 1998-09-12 blah" will 504 | # work properly. 505 | # Certain punctuation before or after the target pattern such as 506 | # "1998-09-12," is permitted. For the full list of valid punctuation, 507 | # see the documentation. 508 | 509 | starting_word_boundary = ( 510 | r"(?\s])" # This is the list of punctuation that is ok before the 513 | # pattern (i.e. "It can't not be these characters before the pattern") 514 | r"(\b|^)" 515 | # The \b is to block cases like 1201912 but allow 201912 for pattern YYYYMM. The ^ was necessary to allow a 516 | # negative number through i.e. before epoch numbers 517 | ) 518 | ending_word_boundary = ( 519 | r"(?=[\,\.\;\:\?\!\"\'\`\[\]\{\}\(\)\<\>]?" # Positive lookahead stating that these punctuation marks 520 | # can appear after the pattern at most 1 time 521 | r"(?!\S))" # Don't allow any non-whitespace character after the punctuation 522 | ) 523 | bounded_fmt_pattern = r"{}{}{}".format( 524 | starting_word_boundary, final_fmt_pattern, ending_word_boundary 525 | ) 526 | 527 | return tokens, re.compile(bounded_fmt_pattern, flags=re.IGNORECASE) 528 | 529 | @overload 530 | def _parse_token( 531 | self, 532 | token: Literal[ 533 | "YYYY", 534 | "YY", 535 | "MM", 536 | "M", 537 | "DDDD", 538 | "DDD", 539 | "DD", 540 | "D", 541 | "Do", 542 | "HH", 543 | "hh", 544 | "h", 545 | "H", 546 | "mm", 547 | "m", 548 | "ss", 549 | "s", 550 | "x", 551 | ], 552 | value: Union[str, bytes, SupportsInt, bytearray], 553 | parts: _Parts, 554 | ) -> None: 555 | ... # pragma: no cover 556 | 557 | @overload 558 | def _parse_token( 559 | self, 560 | token: Literal["X"], 561 | value: Union[str, bytes, SupportsFloat, bytearray], 562 | parts: _Parts, 563 | ) -> None: 564 | ... # pragma: no cover 565 | 566 | @overload 567 | def _parse_token( 568 | self, 569 | token: Literal["MMMM", "MMM", "dddd", "ddd", "S"], 570 | value: Union[str, bytes, bytearray], 571 | parts: _Parts, 572 | ) -> None: 573 | ... # pragma: no cover 574 | 575 | @overload 576 | def _parse_token( 577 | self, 578 | token: Literal["a", "A", "ZZZ", "ZZ", "Z"], 579 | value: Union[str, bytes], 580 | parts: _Parts, 581 | ) -> None: 582 | ... # pragma: no cover 583 | 584 | @overload 585 | def _parse_token( 586 | self, 587 | token: Literal["W"], 588 | value: Tuple[_WEEKDATE_ELEMENT, _WEEKDATE_ELEMENT, Optional[_WEEKDATE_ELEMENT]], 589 | parts: _Parts, 590 | ) -> None: 591 | ... # pragma: no cover 592 | 593 | def _parse_token( 594 | self, 595 | token: Any, 596 | value: Any, 597 | parts: _Parts, 598 | ) -> None: 599 | """ 600 | Parse a token and its value, and update the `_Parts` dictionary with the parsed values. 601 | 602 | The function supports several tokens, including "YYYY", "YY", "MMMM", "MMM", "MM", "M", "DDDD", "DDD", "DD", "D", "Do", "dddd", "ddd", "HH", "H", "mm", "m", "ss", "s", "S", "X", "x", "ZZZ", "ZZ", "Z", "a", "A", and "W". Each token is matched and the corresponding value is parsed and added to the `_Parts` dictionary. 603 | 604 | :param token: The token to parse. 605 | :type token: Any 606 | :param value: The value of the token. 607 | :type value: Any 608 | :param parts: A dictionary to update with the parsed values. 609 | :type parts: _Parts 610 | :raises ParserMatchError: If the hour token value is not between 0 and 12 inclusive for tokens "a" or "A". 611 | 612 | """ 613 | if token == "YYYY": 614 | parts["year"] = int(value) 615 | 616 | elif token == "YY": 617 | value = int(value) 618 | parts["year"] = 1900 + value if value > 68 else 2000 + value 619 | 620 | elif token in ["MMMM", "MMM"]: 621 | # FIXME: month_number() is nullable 622 | parts["month"] = self.locale.month_number(value.lower()) # type: ignore[typeddict-item] 623 | 624 | elif token in ["MM", "M"]: 625 | parts["month"] = int(value) 626 | 627 | elif token in ["DDDD", "DDD"]: 628 | parts["day_of_year"] = int(value) 629 | 630 | elif token in ["DD", "D"]: 631 | parts["day"] = int(value) 632 | 633 | elif token == "Do": 634 | parts["day"] = int(value) 635 | 636 | elif token == "dddd": 637 | # locale day names are 1-indexed 638 | day_of_week = [x.lower() for x in self.locale.day_names].index( 639 | value.lower() 640 | ) 641 | parts["day_of_week"] = day_of_week - 1 642 | 643 | elif token == "ddd": 644 | # locale day abbreviations are 1-indexed 645 | day_of_week = [x.lower() for x in self.locale.day_abbreviations].index( 646 | value.lower() 647 | ) 648 | parts["day_of_week"] = day_of_week - 1 649 | 650 | elif token.upper() in ["HH", "H"]: 651 | parts["hour"] = int(value) 652 | 653 | elif token in ["mm", "m"]: 654 | parts["minute"] = int(value) 655 | 656 | elif token in ["ss", "s"]: 657 | parts["second"] = int(value) 658 | 659 | elif token == "S": 660 | # We have the *most significant* digits of an arbitrary-precision integer. 661 | # We want the six most significant digits as an integer, rounded. 662 | # IDEA: add nanosecond support somehow? Need datetime support for it first. 663 | value = value.ljust(7, "0") 664 | 665 | # floating-point (IEEE-754) defaults to half-to-even rounding 666 | seventh_digit = int(value[6]) 667 | if seventh_digit == 5: 668 | rounding = int(value[5]) % 2 669 | elif seventh_digit > 5: 670 | rounding = 1 671 | else: 672 | rounding = 0 673 | 674 | parts["microsecond"] = int(value[:6]) + rounding 675 | 676 | elif token == "X": 677 | parts["timestamp"] = float(value) 678 | 679 | elif token == "x": 680 | parts["expanded_timestamp"] = int(value) 681 | 682 | elif token in ["ZZZ", "ZZ", "Z"]: 683 | parts["tzinfo"] = TzinfoParser.parse(value) 684 | 685 | elif token in ["a", "A"]: 686 | if value in (self.locale.meridians["am"], self.locale.meridians["AM"]): 687 | parts["am_pm"] = "am" 688 | if "hour" in parts and not 0 <= parts["hour"] <= 12: 689 | raise ParserMatchError( 690 | f"Hour token value must be between 0 and 12 inclusive for token {token!r}." 691 | ) 692 | elif value in (self.locale.meridians["pm"], self.locale.meridians["PM"]): 693 | parts["am_pm"] = "pm" 694 | elif token == "W": 695 | parts["weekdate"] = value 696 | 697 | @staticmethod 698 | def _build_datetime(parts: _Parts) -> datetime: 699 | """ 700 | Build a datetime object from a dictionary of date parts. 701 | 702 | :param parts: A dictionary containing the date parts extracted from a date string. 703 | :type parts: dict 704 | :return: A datetime object representing the date and time. 705 | :rtype: datetime.datetime 706 | """ 707 | weekdate = parts.get("weekdate") 708 | 709 | if weekdate is not None: 710 | year, week = int(weekdate[0]), int(weekdate[1]) 711 | 712 | if weekdate[2] is not None: 713 | _day = int(weekdate[2]) 714 | else: 715 | # day not given, default to 1 716 | _day = 1 717 | 718 | date_string = f"{year}-{week}-{_day}" 719 | 720 | # tokens for ISO 8601 weekdates 721 | dt = datetime.strptime(date_string, "%G-%V-%u") 722 | 723 | parts["year"] = dt.year 724 | parts["month"] = dt.month 725 | parts["day"] = dt.day 726 | 727 | timestamp = parts.get("timestamp") 728 | 729 | if timestamp is not None: 730 | return datetime.fromtimestamp(timestamp, tz=tz.tzutc()) 731 | 732 | expanded_timestamp = parts.get("expanded_timestamp") 733 | 734 | if expanded_timestamp is not None: 735 | return datetime.fromtimestamp( 736 | normalize_timestamp(expanded_timestamp), 737 | tz=tz.tzutc(), 738 | ) 739 | 740 | day_of_year = parts.get("day_of_year") 741 | 742 | if day_of_year is not None: 743 | _year = parts.get("year") 744 | month = parts.get("month") 745 | if _year is None: 746 | raise ParserError( 747 | "Year component is required with the DDD and DDDD tokens." 748 | ) 749 | 750 | if month is not None: 751 | raise ParserError( 752 | "Month component is not allowed with the DDD and DDDD tokens." 753 | ) 754 | 755 | date_string = f"{_year}-{day_of_year}" 756 | try: 757 | dt = datetime.strptime(date_string, "%Y-%j") 758 | except ValueError: 759 | raise ParserError( 760 | f"The provided day of year {day_of_year!r} is invalid." 761 | ) 762 | 763 | parts["year"] = dt.year 764 | parts["month"] = dt.month 765 | parts["day"] = dt.day 766 | 767 | day_of_week: Optional[int] = parts.get("day_of_week") 768 | day = parts.get("day") 769 | 770 | # If day is passed, ignore day of week 771 | if day_of_week is not None and day is None: 772 | year = parts.get("year", 1970) 773 | month = parts.get("month", 1) 774 | day = 1 775 | 776 | # dddd => first day of week after epoch 777 | # dddd YYYY => first day of week in specified year 778 | # dddd MM YYYY => first day of week in specified year and month 779 | # dddd MM => first day after epoch in specified month 780 | next_weekday_dt = next_weekday(datetime(year, month, day), day_of_week) 781 | parts["year"] = next_weekday_dt.year 782 | parts["month"] = next_weekday_dt.month 783 | parts["day"] = next_weekday_dt.day 784 | 785 | am_pm = parts.get("am_pm") 786 | hour = parts.get("hour", 0) 787 | 788 | if am_pm == "pm" and hour < 12: 789 | hour += 12 790 | elif am_pm == "am" and hour == 12: 791 | hour = 0 792 | 793 | # Support for midnight at the end of day 794 | if hour == 24: 795 | if parts.get("minute", 0) != 0: 796 | raise ParserError("Midnight at the end of day must not contain minutes") 797 | if parts.get("second", 0) != 0: 798 | raise ParserError("Midnight at the end of day must not contain seconds") 799 | if parts.get("microsecond", 0) != 0: 800 | raise ParserError( 801 | "Midnight at the end of day must not contain microseconds" 802 | ) 803 | hour = 0 804 | day_increment = 1 805 | else: 806 | day_increment = 0 807 | 808 | # account for rounding up to 1000000 809 | microsecond = parts.get("microsecond", 0) 810 | if microsecond == 1000000: 811 | microsecond = 0 812 | second_increment = 1 813 | else: 814 | second_increment = 0 815 | 816 | increment = timedelta(days=day_increment, seconds=second_increment) 817 | 818 | return ( 819 | datetime( 820 | year=parts.get("year", 1), 821 | month=parts.get("month", 1), 822 | day=parts.get("day", 1), 823 | hour=hour, 824 | minute=parts.get("minute", 0), 825 | second=parts.get("second", 0), 826 | microsecond=microsecond, 827 | tzinfo=parts.get("tzinfo"), 828 | ) 829 | + increment 830 | ) 831 | 832 | def _parse_multiformat(self, string: str, formats: Iterable[str]) -> datetime: 833 | """ 834 | Parse a date and time string using multiple formats. 835 | 836 | Tries to parse the provided string with each format in the given `formats` 837 | iterable, returning the resulting `datetime` object if a match is found. If no 838 | format matches the string, a `ParserError` is raised. 839 | 840 | :param string: The date and time string to parse. 841 | :type string: str 842 | :param formats: An iterable of date and time format strings to try, in order. 843 | :type formats: Iterable[str] 844 | :returns: The parsed date and time. 845 | :rtype: datetime.datetime 846 | :raises ParserError: If no format matches the input string. 847 | """ 848 | _datetime: Optional[datetime] = None 849 | 850 | for fmt in formats: 851 | try: 852 | _datetime = self.parse(string, fmt) 853 | break 854 | except ParserMatchError: 855 | pass 856 | 857 | if _datetime is None: 858 | supported_formats = ", ".join(formats) 859 | raise ParserError( 860 | f"Could not match input {string!r} to any of the following formats: {supported_formats}." 861 | ) 862 | 863 | return _datetime 864 | 865 | # generates a capture group of choices separated by an OR operator 866 | @staticmethod 867 | def _generate_choice_re( 868 | choices: Iterable[str], flags: Union[int, re.RegexFlag] = 0 869 | ) -> Pattern[str]: 870 | """ 871 | Generate a regular expression pattern that matches a choice from an iterable. 872 | 873 | Takes an iterable of strings (`choices`) and returns a compiled regular expression 874 | pattern that matches any of the choices. The pattern is created by joining the 875 | choices with the '|' (OR) operator, which matches any of the enclosed patterns. 876 | 877 | :param choices: An iterable of strings to match. 878 | :type choices: Iterable[str] 879 | :param flags: Optional regular expression flags. Default is 0. 880 | :type flags: Union[int, re.RegexFlag], optional 881 | :returns: A compiled regular expression pattern that matches any of the choices. 882 | :rtype: re.Pattern[str] 883 | """ 884 | return re.compile(r"({})".format("|".join(choices)), flags=flags) 885 | 886 | 887 | class TzinfoParser: 888 | """ 889 | Parser for timezone information. 890 | """ 891 | 892 | _TZINFO_RE: ClassVar[Pattern[str]] = re.compile( 893 | r"^(?:\(UTC)*([\+\-])?(\d{2})(?:\:?(\d{2}))?" 894 | ) 895 | 896 | @classmethod 897 | def parse(cls, tzinfo_string: str) -> dt_tzinfo: 898 | """ 899 | Parse a timezone string and return a datetime timezone object. 900 | 901 | :param tzinfo_string: The timezone string to parse. 902 | :type tzinfo_string: str 903 | :returns: The parsed datetime timezone object. 904 | :rtype: datetime.timezone 905 | :raises ParserError: If the timezone string cannot be parsed. 906 | """ 907 | tzinfo: Optional[dt_tzinfo] = None 908 | 909 | if tzinfo_string == "local": 910 | tzinfo = tz.tzlocal() 911 | 912 | elif tzinfo_string in ["utc", "UTC", "Z"]: 913 | tzinfo = tz.tzutc() 914 | 915 | else: 916 | iso_match = cls._TZINFO_RE.match(tzinfo_string) 917 | 918 | if iso_match: 919 | sign: Optional[str] 920 | hours: str 921 | minutes: Union[str, int, None] 922 | sign, hours, minutes = iso_match.groups() 923 | seconds = int(hours) * 3600 + int(minutes or 0) * 60 924 | 925 | if sign == "-": 926 | seconds *= -1 927 | 928 | tzinfo = tz.tzoffset(None, seconds) 929 | 930 | else: 931 | tzinfo = tz.gettz(tzinfo_string) 932 | 933 | if tzinfo is None: 934 | raise ParserError(f"Could not parse timezone expression {tzinfo_string!r}.") 935 | 936 | return tzinfo 937 | -------------------------------------------------------------------------------- /arrow/py.typed: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/arrow-py/arrow/0fe5f065718e5af3ab47903d1bec82d87d202a63/arrow/py.typed -------------------------------------------------------------------------------- /arrow/util.py: -------------------------------------------------------------------------------- 1 | """Helpful functions used internally within arrow.""" 2 | 3 | import datetime 4 | from typing import Any, Optional, cast 5 | 6 | from dateutil.rrule import WEEKLY, rrule 7 | 8 | from arrow.constants import ( 9 | MAX_ORDINAL, 10 | MAX_TIMESTAMP, 11 | MAX_TIMESTAMP_MS, 12 | MAX_TIMESTAMP_US, 13 | MIN_ORDINAL, 14 | ) 15 | 16 | 17 | def next_weekday( 18 | start_date: Optional[datetime.date], weekday: int 19 | ) -> datetime.datetime: 20 | """Get next weekday from the specified start date. 21 | 22 | :param start_date: Datetime object representing the start date. 23 | :param weekday: Next weekday to obtain. Can be a value between 0 (Monday) and 6 (Sunday). 24 | :return: Datetime object corresponding to the next weekday after start_date. 25 | 26 | Usage:: 27 | 28 | # Get first Monday after epoch 29 | >>> next_weekday(datetime(1970, 1, 1), 0) 30 | 1970-01-05 00:00:00 31 | 32 | # Get first Thursday after epoch 33 | >>> next_weekday(datetime(1970, 1, 1), 3) 34 | 1970-01-01 00:00:00 35 | 36 | # Get first Sunday after epoch 37 | >>> next_weekday(datetime(1970, 1, 1), 6) 38 | 1970-01-04 00:00:00 39 | """ 40 | if weekday < 0 or weekday > 6: 41 | raise ValueError("Weekday must be between 0 (Monday) and 6 (Sunday).") 42 | return cast( 43 | datetime.datetime, 44 | rrule(freq=WEEKLY, dtstart=start_date, byweekday=weekday, count=1)[0], 45 | ) 46 | 47 | 48 | def is_timestamp(value: Any) -> bool: 49 | """Check if value is a valid timestamp.""" 50 | if isinstance(value, bool): 51 | return False 52 | if not isinstance(value, (int, float, str)): 53 | return False 54 | try: 55 | float(value) 56 | return True 57 | except ValueError: 58 | return False 59 | 60 | 61 | def validate_ordinal(value: Any) -> None: 62 | """Raise an exception if value is an invalid Gregorian ordinal. 63 | 64 | :param value: the input to be checked 65 | 66 | """ 67 | if isinstance(value, bool) or not isinstance(value, int): 68 | raise TypeError(f"Ordinal must be an integer (got type {type(value)}).") 69 | if not (MIN_ORDINAL <= value <= MAX_ORDINAL): 70 | raise ValueError(f"Ordinal {value} is out of range.") 71 | 72 | 73 | def normalize_timestamp(timestamp: float) -> float: 74 | """Normalize millisecond and microsecond timestamps into normal timestamps.""" 75 | if timestamp > MAX_TIMESTAMP: 76 | if timestamp < MAX_TIMESTAMP_MS: 77 | timestamp /= 1000 78 | elif timestamp < MAX_TIMESTAMP_US: 79 | timestamp /= 1_000_000 80 | else: 81 | raise ValueError(f"The specified timestamp {timestamp!r} is too large.") 82 | return timestamp 83 | 84 | 85 | # Credit to https://stackoverflow.com/a/1700069 86 | def iso_to_gregorian(iso_year: int, iso_week: int, iso_day: int) -> datetime.date: 87 | """Converts an ISO week date into a datetime object. 88 | 89 | :param iso_year: the year 90 | :param iso_week: the week number, each year has either 52 or 53 weeks 91 | :param iso_day: the day numbered 1 through 7, beginning with Monday 92 | 93 | """ 94 | 95 | if not 1 <= iso_week <= 53: 96 | raise ValueError("ISO Calendar week value must be between 1-53.") 97 | 98 | if not 1 <= iso_day <= 7: 99 | raise ValueError("ISO Calendar day value must be between 1-7") 100 | 101 | # The first week of the year always contains 4 Jan. 102 | fourth_jan = datetime.date(iso_year, 1, 4) 103 | delta = datetime.timedelta(fourth_jan.isoweekday() - 1) 104 | year_start = fourth_jan - delta 105 | gregorian = year_start + datetime.timedelta(days=iso_day - 1, weeks=iso_week - 1) 106 | 107 | return gregorian 108 | 109 | 110 | def validate_bounds(bounds: str) -> None: 111 | if bounds != "()" and bounds != "(]" and bounds != "[)" and bounds != "[]": 112 | raise ValueError( 113 | "Invalid bounds. Please select between '()', '(]', '[)', or '[]'." 114 | ) 115 | 116 | 117 | __all__ = ["next_weekday", "is_timestamp", "validate_ordinal", "iso_to_gregorian"] 118 | -------------------------------------------------------------------------------- /docs/Makefile: -------------------------------------------------------------------------------- 1 | # Minimal makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line, and also 5 | # from the environment for the first two. 6 | SPHINXOPTS ?= 7 | SPHINXBUILD ?= sphinx-build 8 | SOURCEDIR = . 9 | BUILDDIR = _build 10 | 11 | # Put it first so that "make" without argument is like "make help". 12 | help: 13 | @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 14 | 15 | .PHONY: help Makefile 16 | 17 | # Catch-all target: route all unknown targets to Sphinx using the new 18 | # "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). 19 | %: Makefile 20 | @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 21 | -------------------------------------------------------------------------------- /docs/api-guide.rst: -------------------------------------------------------------------------------- 1 | *************************************** 2 | API Guide 3 | *************************************** 4 | 5 | :mod:`arrow.arrow` 6 | ===================== 7 | 8 | .. automodule:: arrow.arrow 9 | :members: 10 | 11 | :mod:`arrow.factory` 12 | ===================== 13 | 14 | .. automodule:: arrow.factory 15 | :members: 16 | 17 | :mod:`arrow.api` 18 | ===================== 19 | 20 | .. automodule:: arrow.api 21 | :members: 22 | 23 | :mod:`arrow.locale` 24 | ===================== 25 | 26 | .. automodule:: arrow.locales 27 | :members: 28 | :undoc-members: 29 | -------------------------------------------------------------------------------- /docs/conf.py: -------------------------------------------------------------------------------- 1 | # mypy: ignore-errors 2 | # -- Path setup -------------------------------------------------------------- 3 | 4 | import os 5 | import sys 6 | 7 | sys.path.insert(0, os.path.abspath("..")) 8 | 9 | about = {} 10 | with open("../arrow/_version.py", encoding="utf-8") as f: 11 | exec(f.read(), about) 12 | 13 | # -- Project information ----------------------------------------------------- 14 | 15 | project = "Arrow 🏹" 16 | copyright = "2023, Chris Smith" 17 | author = "Chris Smith" 18 | 19 | release = about["__version__"] 20 | 21 | # -- General configuration --------------------------------------------------- 22 | 23 | extensions = [ 24 | "sphinx.ext.autodoc", 25 | "sphinx_autodoc_typehints", 26 | "sphinx_rtd_theme", 27 | ] 28 | 29 | templates_path = [] 30 | 31 | exclude_patterns = ["_build", "Thumbs.db", ".DS_Store"] 32 | 33 | master_doc = "index" 34 | source_suffix = ".rst" 35 | pygments_style = "sphinx" 36 | 37 | language = "en" 38 | 39 | # -- Options for HTML output ------------------------------------------------- 40 | 41 | html_theme = "sphinx_rtd_theme" 42 | html_theme_path = [] 43 | html_static_path = [] 44 | 45 | html_show_sourcelink = False 46 | html_show_sphinx = False 47 | html_show_copyright = True 48 | 49 | html_context = { 50 | "display_github": True, 51 | "github_user": "arrow-py", 52 | "github_repo": "arrow", 53 | "github_version": "master/docs/", 54 | } 55 | 56 | # https://sphinx-rtd-theme.readthedocs.io/en/stable/index.html 57 | html_theme_options = { 58 | "logo_only": False, 59 | "prev_next_buttons_location": "both", 60 | "style_nav_header_background": "grey", 61 | # TOC options 62 | "collapse_navigation": False, 63 | "navigation_depth": 3, 64 | } 65 | 66 | # Generate PDFs with unicode characters 67 | # https://docs.readthedocs.io/en/stable/guides/pdf-non-ascii-languages.html 68 | latex_engine = "xelatex" 69 | -------------------------------------------------------------------------------- /docs/getting-started.rst: -------------------------------------------------------------------------------- 1 | *************************************** 2 | Getting started 3 | *************************************** 4 | 5 | Assuming you have Python already, follow the guidelines below to get started with Arrow. 6 | 7 | .. include:: ../README.rst 8 | :start-after: Quick Start 9 | :end-before: end-inclusion-marker-do-not-remove 10 | -------------------------------------------------------------------------------- /docs/guide.rst: -------------------------------------------------------------------------------- 1 | *************************************** 2 | User’s Guide 3 | *************************************** 4 | 5 | 6 | Creation 7 | ~~~~~~~~ 8 | 9 | Get 'now' easily: 10 | 11 | .. code-block:: python 12 | 13 | >>> arrow.utcnow() 14 | 15 | 16 | >>> arrow.now() 17 | 18 | 19 | >>> arrow.now('US/Pacific') 20 | 21 | 22 | Create from timestamps (:code:`int` or :code:`float`): 23 | 24 | .. code-block:: python 25 | 26 | >>> arrow.get(1367900664) 27 | 28 | 29 | >>> arrow.get(1367900664.152325) 30 | 31 | 32 | Use a naive or timezone-aware datetime, or flexibly specify a timezone: 33 | 34 | .. code-block:: python 35 | 36 | >>> arrow.get(datetime.utcnow()) 37 | 38 | 39 | >>> arrow.get(datetime(2013, 5, 5), 'US/Pacific') 40 | 41 | 42 | >>> from dateutil import tz 43 | >>> arrow.get(datetime(2013, 5, 5), tz.gettz('US/Pacific')) 44 | 45 | 46 | >>> arrow.get(datetime.now(tz.gettz('US/Pacific'))) 47 | 48 | 49 | Parse from a string: 50 | 51 | .. code-block:: python 52 | 53 | >>> arrow.get('2013-05-05 12:30:45', 'YYYY-MM-DD HH:mm:ss') 54 | 55 | 56 | Search a date in a string: 57 | 58 | .. code-block:: python 59 | 60 | >>> arrow.get('June was born in May 1980', 'MMMM YYYY') 61 | 62 | 63 | Some ISO 8601 compliant strings are recognized and parsed without a format string: 64 | 65 | >>> arrow.get('2013-09-30T15:34:00.000-07:00') 66 | 67 | 68 | Arrow objects can be instantiated directly too, with the same arguments as a datetime: 69 | 70 | .. code-block:: python 71 | 72 | >>> arrow.get(2013, 5, 5) 73 | 74 | 75 | >>> arrow.Arrow(2013, 5, 5) 76 | 77 | 78 | Properties 79 | ~~~~~~~~~~ 80 | 81 | Get a datetime or timestamp representation: 82 | 83 | .. code-block:: python 84 | 85 | >>> a = arrow.utcnow() 86 | >>> a.datetime 87 | datetime.datetime(2013, 5, 7, 4, 38, 15, 447644, tzinfo=tzutc()) 88 | 89 | Get a naive datetime, and tzinfo: 90 | 91 | .. code-block:: python 92 | 93 | >>> a.naive 94 | datetime.datetime(2013, 5, 7, 4, 38, 15, 447644) 95 | 96 | >>> a.tzinfo 97 | tzutc() 98 | 99 | Get any datetime value: 100 | 101 | .. code-block:: python 102 | 103 | >>> a.year 104 | 2013 105 | 106 | Call datetime functions that return properties: 107 | 108 | .. code-block:: python 109 | 110 | >>> a.date() 111 | datetime.date(2013, 5, 7) 112 | 113 | >>> a.time() 114 | datetime.time(4, 38, 15, 447644) 115 | 116 | Replace & Shift 117 | ~~~~~~~~~~~~~~~ 118 | 119 | Get a new :class:`Arrow ` object, with altered attributes, just as you would with a datetime: 120 | 121 | .. code-block:: python 122 | 123 | >>> arw = arrow.utcnow() 124 | >>> arw 125 | 126 | 127 | >>> arw.replace(hour=4, minute=40) 128 | 129 | 130 | Or, get one with attributes shifted forward or backward: 131 | 132 | .. code-block:: python 133 | 134 | >>> arw.shift(weeks=+3) 135 | 136 | 137 | Even replace the timezone without altering other attributes: 138 | 139 | .. code-block:: python 140 | 141 | >>> arw.replace(tzinfo='US/Pacific') 142 | 143 | 144 | Move between the earlier and later moments of an ambiguous time: 145 | 146 | .. code-block:: python 147 | 148 | >>> paris_transition = arrow.Arrow(2019, 10, 27, 2, tzinfo="Europe/Paris", fold=0) 149 | >>> paris_transition 150 | 151 | >>> paris_transition.ambiguous 152 | True 153 | >>> paris_transition.replace(fold=1) 154 | 155 | 156 | Format 157 | ~~~~~~ 158 | 159 | For a list of formatting values, see :ref:`supported-tokens` 160 | 161 | .. code-block:: python 162 | 163 | >>> arrow.utcnow().format('YYYY-MM-DD HH:mm:ss ZZ') 164 | '2013-05-07 05:23:16 -00:00' 165 | 166 | Convert 167 | ~~~~~~~ 168 | 169 | Convert from UTC to other timezones by name or tzinfo: 170 | 171 | .. code-block:: python 172 | 173 | >>> utc = arrow.utcnow() 174 | >>> utc 175 | 176 | 177 | >>> utc.to('US/Pacific') 178 | 179 | 180 | >>> utc.to(tz.gettz('US/Pacific')) 181 | 182 | 183 | Or using shorthand: 184 | 185 | .. code-block:: python 186 | 187 | >>> utc.to('local') 188 | 189 | 190 | >>> utc.to('local').to('utc') 191 | 192 | 193 | 194 | Humanize 195 | ~~~~~~~~ 196 | 197 | Humanize relative to now: 198 | 199 | .. code-block:: python 200 | 201 | >>> past = arrow.utcnow().shift(hours=-1) 202 | >>> past.humanize() 203 | 'an hour ago' 204 | 205 | Or another Arrow, or datetime: 206 | 207 | .. code-block:: python 208 | 209 | >>> present = arrow.utcnow() 210 | >>> future = present.shift(hours=2) 211 | >>> future.humanize(present) 212 | 'in 2 hours' 213 | 214 | Indicate time as relative or include only the distance 215 | 216 | .. code-block:: python 217 | 218 | >>> present = arrow.utcnow() 219 | >>> future = present.shift(hours=2) 220 | >>> future.humanize(present) 221 | 'in 2 hours' 222 | >>> future.humanize(present, only_distance=True) 223 | '2 hours' 224 | 225 | 226 | Indicate a specific time granularity (or multiple): 227 | 228 | .. code-block:: python 229 | 230 | >>> present = arrow.utcnow() 231 | >>> future = present.shift(minutes=66) 232 | >>> future.humanize(present, granularity="minute") 233 | 'in 66 minutes' 234 | >>> future.humanize(present, granularity=["hour", "minute"]) 235 | 'in an hour and 6 minutes' 236 | >>> present.humanize(future, granularity=["hour", "minute"]) 237 | 'an hour and 6 minutes ago' 238 | >>> future.humanize(present, only_distance=True, granularity=["hour", "minute"]) 239 | 'an hour and 6 minutes' 240 | 241 | Support for a growing number of locales (see ``locales.py`` for supported languages): 242 | 243 | .. code-block:: python 244 | 245 | 246 | >>> future = arrow.utcnow().shift(hours=1) 247 | >>> future.humanize(a, locale='ru') 248 | 'через 2 час(а,ов)' 249 | 250 | Dehumanize 251 | ~~~~~~~~~~ 252 | 253 | Take a human readable string and use it to shift into a past time: 254 | 255 | .. code-block:: python 256 | 257 | >>> arw = arrow.utcnow() 258 | >>> arw 259 | 260 | >>> earlier = arw.dehumanize("2 days ago") 261 | >>> earlier 262 | 263 | 264 | Or use it to shift into a future time: 265 | 266 | .. code-block:: python 267 | 268 | >>> arw = arrow.utcnow() 269 | >>> arw 270 | 271 | >>> later = arw.dehumanize("in a month") 272 | >>> later 273 | 274 | 275 | Support for a growing number of locales (see ``constants.py`` for supported languages): 276 | 277 | .. code-block:: python 278 | 279 | >>> arw = arrow.utcnow() 280 | >>> arw 281 | 282 | >>> later = arw.dehumanize("एक माह बाद", locale="hi") 283 | >>> later 284 | 285 | 286 | Ranges & Spans 287 | ~~~~~~~~~~~~~~ 288 | 289 | Get the time span of any unit: 290 | 291 | .. code-block:: python 292 | 293 | >>> arrow.utcnow().span('hour') 294 | (, ) 295 | 296 | Or just get the floor and ceiling: 297 | 298 | .. code-block:: python 299 | 300 | >>> arrow.utcnow().floor('hour') 301 | 302 | 303 | >>> arrow.utcnow().ceil('hour') 304 | 305 | 306 | You can also get a range of time spans: 307 | 308 | .. code-block:: python 309 | 310 | >>> start = datetime(2013, 5, 5, 12, 30) 311 | >>> end = datetime(2013, 5, 5, 17, 15) 312 | >>> for r in arrow.Arrow.span_range('hour', start, end): 313 | ... print(r) 314 | ... 315 | (, ) 316 | (, ) 317 | (, ) 318 | (, ) 319 | (, ) 320 | 321 | Or just iterate over a range of time: 322 | 323 | .. code-block:: python 324 | 325 | >>> start = datetime(2013, 5, 5, 12, 30) 326 | >>> end = datetime(2013, 5, 5, 17, 15) 327 | >>> for r in arrow.Arrow.range('hour', start, end): 328 | ... print(repr(r)) 329 | ... 330 | 331 | 332 | 333 | 334 | 335 | 336 | .. toctree:: 337 | :maxdepth: 2 338 | 339 | Factories 340 | ~~~~~~~~~ 341 | 342 | Use factories to harness Arrow's module API for a custom Arrow-derived type. First, derive your type: 343 | 344 | .. code-block:: python 345 | 346 | >>> class CustomArrow(arrow.Arrow): 347 | ... 348 | ... def days_till_xmas(self): 349 | ... 350 | ... xmas = arrow.Arrow(self.year, 12, 25) 351 | ... 352 | ... if self > xmas: 353 | ... xmas = xmas.shift(years=1) 354 | ... 355 | ... return (xmas - self).days 356 | 357 | 358 | Then get and use a factory for it: 359 | 360 | .. code-block:: python 361 | 362 | >>> factory = arrow.ArrowFactory(CustomArrow) 363 | >>> custom = factory.utcnow() 364 | >>> custom 365 | >>> 366 | 367 | >>> custom.days_till_xmas() 368 | >>> 211 369 | 370 | .. _supported-tokens: 371 | 372 | Supported Tokens 373 | ~~~~~~~~~~~~~~~~ 374 | 375 | Use the following tokens for parsing and formatting. Note that they are **not** the same as the tokens for `strptime `_: 376 | 377 | +--------------------------------+--------------+-------------------------------------------+ 378 | | |Token |Output | 379 | +================================+==============+===========================================+ 380 | |**Year** |YYYY |2000, 2001, 2002 ... 2012, 2013 | 381 | +--------------------------------+--------------+-------------------------------------------+ 382 | | |YY |00, 01, 02 ... 12, 13 | 383 | +--------------------------------+--------------+-------------------------------------------+ 384 | |**Month** |MMMM |January, February, March ... [#t1]_ | 385 | +--------------------------------+--------------+-------------------------------------------+ 386 | | |MMM |Jan, Feb, Mar ... [#t1]_ | 387 | +--------------------------------+--------------+-------------------------------------------+ 388 | | |MM |01, 02, 03 ... 11, 12 | 389 | +--------------------------------+--------------+-------------------------------------------+ 390 | | |M |1, 2, 3 ... 11, 12 | 391 | +--------------------------------+--------------+-------------------------------------------+ 392 | |**Day of Year** |DDDD |001, 002, 003 ... 364, 365 | 393 | +--------------------------------+--------------+-------------------------------------------+ 394 | | |DDD |1, 2, 3 ... 364, 365 | 395 | +--------------------------------+--------------+-------------------------------------------+ 396 | |**Day of Month** |DD |01, 02, 03 ... 30, 31 | 397 | +--------------------------------+--------------+-------------------------------------------+ 398 | | |D |1, 2, 3 ... 30, 31 | 399 | +--------------------------------+--------------+-------------------------------------------+ 400 | | |Do |1st, 2nd, 3rd ... 30th, 31st | 401 | +--------------------------------+--------------+-------------------------------------------+ 402 | |**Day of Week** |dddd |Monday, Tuesday, Wednesday ... [#t2]_ | 403 | +--------------------------------+--------------+-------------------------------------------+ 404 | | |ddd |Mon, Tue, Wed ... [#t2]_ | 405 | +--------------------------------+--------------+-------------------------------------------+ 406 | | |d |1, 2, 3 ... 6, 7 | 407 | +--------------------------------+--------------+-------------------------------------------+ 408 | |**ISO week date** |W |2011-W05-4, 2019-W17 | 409 | +--------------------------------+--------------+-------------------------------------------+ 410 | |**Hour** |HH |00, 01, 02 ... 23, 24 | 411 | +--------------------------------+--------------+-------------------------------------------+ 412 | | |H |0, 1, 2 ... 23, 24 | 413 | +--------------------------------+--------------+-------------------------------------------+ 414 | | |hh |01, 02, 03 ... 11, 12 | 415 | +--------------------------------+--------------+-------------------------------------------+ 416 | | |h |1, 2, 3 ... 11, 12 | 417 | +--------------------------------+--------------+-------------------------------------------+ 418 | |**AM / PM** |A |AM, PM, am, pm [#t1]_ | 419 | +--------------------------------+--------------+-------------------------------------------+ 420 | | |a |am, pm [#t1]_ | 421 | +--------------------------------+--------------+-------------------------------------------+ 422 | |**Minute** |mm |00, 01, 02 ... 58, 59 | 423 | +--------------------------------+--------------+-------------------------------------------+ 424 | | |m |0, 1, 2 ... 58, 59 | 425 | +--------------------------------+--------------+-------------------------------------------+ 426 | |**Second** |ss |00, 01, 02 ... 58, 59 | 427 | +--------------------------------+--------------+-------------------------------------------+ 428 | | |s |0, 1, 2 ... 58, 59 | 429 | +--------------------------------+--------------+-------------------------------------------+ 430 | |**Sub-second** |S... |0, 02, 003, 000006, 123123123123... [#t3]_ | 431 | +--------------------------------+--------------+-------------------------------------------+ 432 | |**Timezone** |ZZZ |Asia/Baku, Europe/Warsaw, GMT ... [#t4]_ | 433 | +--------------------------------+--------------+-------------------------------------------+ 434 | | |ZZ |-07:00, -06:00 ... +06:00, +07:00, +08, Z | 435 | +--------------------------------+--------------+-------------------------------------------+ 436 | | |Z |-0700, -0600 ... +0600, +0700, +08, Z | 437 | +--------------------------------+--------------+-------------------------------------------+ 438 | |**Seconds Timestamp** |X |1381685817, 1381685817.915482 ... [#t5]_ | 439 | +--------------------------------+--------------+-------------------------------------------+ 440 | |**ms or µs Timestamp** |x |1569980330813, 1569980330813221 | 441 | +--------------------------------+--------------+-------------------------------------------+ 442 | 443 | .. rubric:: Footnotes 444 | 445 | .. [#t1] localization support for parsing and formatting 446 | .. [#t2] localization support only for formatting 447 | .. [#t3] the result is truncated to microseconds, with `half-to-even rounding `_. 448 | .. [#t4] timezone names from `tz database `_ provided via dateutil package, note that abbreviations such as MST, PDT, BRST are unlikely to parse due to ambiguity. Use the full IANA zone name instead (Asia/Shanghai, Europe/London, America/Chicago etc). 449 | .. [#t5] this token cannot be used for parsing timestamps out of natural language strings due to compatibility reasons 450 | 451 | Built-in Formats 452 | ++++++++++++++++ 453 | 454 | There are several formatting standards that are provided as built-in tokens. 455 | 456 | .. code-block:: python 457 | 458 | >>> arw = arrow.utcnow() 459 | >>> arw.format(arrow.FORMAT_ATOM) 460 | '2020-05-27 10:30:35+00:00' 461 | >>> arw.format(arrow.FORMAT_COOKIE) 462 | 'Wednesday, 27-May-2020 10:30:35 UTC' 463 | >>> arw.format(arrow.FORMAT_RSS) 464 | 'Wed, 27 May 2020 10:30:35 +0000' 465 | >>> arw.format(arrow.FORMAT_RFC822) 466 | 'Wed, 27 May 20 10:30:35 +0000' 467 | >>> arw.format(arrow.FORMAT_RFC850) 468 | 'Wednesday, 27-May-20 10:30:35 UTC' 469 | >>> arw.format(arrow.FORMAT_RFC1036) 470 | 'Wed, 27 May 20 10:30:35 +0000' 471 | >>> arw.format(arrow.FORMAT_RFC1123) 472 | 'Wed, 27 May 2020 10:30:35 +0000' 473 | >>> arw.format(arrow.FORMAT_RFC2822) 474 | 'Wed, 27 May 2020 10:30:35 +0000' 475 | >>> arw.format(arrow.FORMAT_RFC3339) 476 | '2020-05-27 10:30:35+00:00' 477 | >>> arw.format(arrow.FORMAT_W3C) 478 | '2020-05-27 10:30:35+00:00' 479 | 480 | Escaping Formats 481 | ~~~~~~~~~~~~~~~~ 482 | 483 | Tokens, phrases, and regular expressions in a format string can be escaped when parsing and formatting by enclosing them within square brackets. 484 | 485 | Tokens & Phrases 486 | ++++++++++++++++ 487 | 488 | Any `token `_ or phrase can be escaped as follows: 489 | 490 | .. code-block:: python 491 | 492 | >>> fmt = "YYYY-MM-DD h [h] m" 493 | >>> arw = arrow.get("2018-03-09 8 h 40", fmt) 494 | 495 | >>> arw.format(fmt) 496 | '2018-03-09 8 h 40' 497 | 498 | >>> fmt = "YYYY-MM-DD h [hello] m" 499 | >>> arw = arrow.get("2018-03-09 8 hello 40", fmt) 500 | 501 | >>> arw.format(fmt) 502 | '2018-03-09 8 hello 40' 503 | 504 | >>> fmt = "YYYY-MM-DD h [hello world] m" 505 | >>> arw = arrow.get("2018-03-09 8 hello world 40", fmt) 506 | 507 | >>> arw.format(fmt) 508 | '2018-03-09 8 hello world 40' 509 | 510 | This can be useful for parsing dates in different locales such as French, in which it is common to format time strings as "8 h 40" rather than "8:40". 511 | 512 | Regular Expressions 513 | +++++++++++++++++++ 514 | 515 | You can also escape regular expressions by enclosing them within square brackets. In the following example, we are using the regular expression :code:`\s+` to match any number of whitespace characters that separate the tokens. This is useful if you do not know the number of spaces between tokens ahead of time (e.g. in log files). 516 | 517 | .. code-block:: python 518 | 519 | >>> fmt = r"ddd[\s+]MMM[\s+]DD[\s+]HH:mm:ss[\s+]YYYY" 520 | >>> arrow.get("Mon Sep 08 16:41:45 2014", fmt) 521 | 522 | 523 | >>> arrow.get("Mon \tSep 08 16:41:45 2014", fmt) 524 | 525 | 526 | >>> arrow.get("Mon Sep 08 16:41:45 2014", fmt) 527 | 528 | 529 | Punctuation 530 | ~~~~~~~~~~~ 531 | 532 | Date and time formats may be fenced on either side by one punctuation character from the following list: ``, . ; : ? ! " \` ' [ ] { } ( ) < >`` 533 | 534 | .. code-block:: python 535 | 536 | >>> arrow.get("Cool date: 2019-10-31T09:12:45.123456+04:30.", "YYYY-MM-DDTHH:mm:ss.SZZ") 537 | 538 | 539 | >>> arrow.get("Tomorrow (2019-10-31) is Halloween!", "YYYY-MM-DD") 540 | 541 | 542 | >>> arrow.get("Halloween is on 2019.10.31.", "YYYY.MM.DD") 543 | 544 | 545 | >>> arrow.get("It's Halloween tomorrow (2019-10-31)!", "YYYY-MM-DD") 546 | # Raises exception because there are multiple punctuation marks following the date 547 | 548 | Redundant Whitespace 549 | ~~~~~~~~~~~~~~~~~~~~ 550 | 551 | Redundant whitespace characters (spaces, tabs, and newlines) can be normalized automatically by passing in the ``normalize_whitespace`` flag to ``arrow.get``: 552 | 553 | .. code-block:: python 554 | 555 | >>> arrow.get('\t \n 2013-05-05T12:30:45.123456 \t \n', normalize_whitespace=True) 556 | 557 | 558 | >>> arrow.get('2013-05-05 T \n 12:30:45\t123456', 'YYYY-MM-DD T HH:mm:ss S', normalize_whitespace=True) 559 | 560 | -------------------------------------------------------------------------------- /docs/index.rst: -------------------------------------------------------------------------------- 1 | Arrow: Better dates & times for Python 2 | ====================================== 3 | 4 | Release v\ |release| (`Installation`_) (`Changelog `_) 5 | 6 | `Go to repository `_ 7 | 8 | .. include:: ../README.rst 9 | :start-after: start-inclusion-marker-do-not-remove 10 | :end-before: end-inclusion-marker-do-not-remove 11 | 12 | .. toctree:: 13 | :maxdepth: 2 14 | 15 | getting-started 16 | 17 | --------------- 18 | 19 | .. toctree:: 20 | :maxdepth: 2 21 | 22 | guide 23 | 24 | --------------- 25 | 26 | .. toctree:: 27 | :maxdepth: 2 28 | 29 | api-guide 30 | 31 | --------------- 32 | 33 | .. toctree:: 34 | :maxdepth: 2 35 | 36 | releases 37 | -------------------------------------------------------------------------------- /docs/make.bat: -------------------------------------------------------------------------------- 1 | @ECHO OFF 2 | 3 | pushd %~dp0 4 | 5 | REM Command file for Sphinx documentation 6 | 7 | if "%SPHINXBUILD%" == "" ( 8 | set SPHINXBUILD=sphinx-build 9 | ) 10 | set SOURCEDIR=. 11 | set BUILDDIR=_build 12 | 13 | if "%1" == "" goto help 14 | 15 | %SPHINXBUILD% >NUL 2>NUL 16 | if errorlevel 9009 ( 17 | echo. 18 | echo.The 'sphinx-build' command was not found. Make sure you have Sphinx 19 | echo.installed, then set the SPHINXBUILD environment variable to point 20 | echo.to the full path of the 'sphinx-build' executable. Alternatively you 21 | echo.may add the Sphinx directory to PATH. 22 | echo. 23 | echo.If you don't have Sphinx installed, grab it from 24 | echo.http://sphinx-doc.org/ 25 | exit /b 1 26 | ) 27 | 28 | %SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% 29 | goto end 30 | 31 | :help 32 | %SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% 33 | 34 | :end 35 | popd 36 | -------------------------------------------------------------------------------- /docs/releases.rst: -------------------------------------------------------------------------------- 1 | *************************************** 2 | Release History 3 | *************************************** 4 | 5 | .. _releases: 6 | 7 | .. include:: ../CHANGELOG.rst 8 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | requires = ["flit_core >=3.2,<4"] 3 | build-backend = "flit_core.buildapi" 4 | 5 | [project] 6 | name = "arrow" 7 | authors = [{name = "Chris Smith", email = "crsmithdev@gmail.com"}] 8 | readme = "README.rst" 9 | license = {file = "LICENSE"} 10 | classifiers = [ 11 | "Development Status :: 5 - Production/Stable", 12 | "Intended Audience :: Developers", 13 | "Intended Audience :: Information Technology", 14 | "License :: OSI Approved :: Apache Software License", 15 | "Topic :: Software Development :: Libraries :: Python Modules", 16 | "Programming Language :: Python :: 3", 17 | "Programming Language :: Python :: 3 :: Only", 18 | "Programming Language :: Python :: 3.8", 19 | "Programming Language :: Python :: 3.9", 20 | "Programming Language :: Python :: 3.10", 21 | "Programming Language :: Python :: 3.11", 22 | "Programming Language :: Python :: 3.12", 23 | "Programming Language :: Python :: 3.13", 24 | "Operating System :: OS Independent", 25 | ] 26 | dependencies = [ 27 | "python-dateutil>=2.7.0", 28 | ] 29 | requires-python = ">=3.8" 30 | description = "Better dates & times for Python" 31 | keywords = [ 32 | "arrow", 33 | "date", 34 | "time", 35 | "datetime", 36 | "timestamp", 37 | "timezone", 38 | "humanize", 39 | ] 40 | dynamic = ["version"] 41 | 42 | [project.optional-dependencies] 43 | test = [ 44 | "backports.zoneinfo==0.2.1;python_version<'3.9'", 45 | "dateparser==1.*", 46 | "pre-commit", 47 | "pytest", 48 | "pytest-cov", 49 | "pytest-mock", 50 | "pytz==2021.1", 51 | "simplejson==3.*", 52 | ] 53 | doc = [ 54 | "doc8", 55 | "sphinx>=7.0.0", 56 | "sphinx-autobuild", 57 | "sphinx-autodoc-typehints", 58 | "sphinx_rtd_theme>=1.3.0", 59 | ] 60 | 61 | [project.urls] 62 | Documentation = "https://arrow.readthedocs.io" 63 | Source = "https://github.com/arrow-py/arrow" 64 | Issues = "https://github.com/arrow-py/arrow/issues" 65 | 66 | [tool.flit.module] 67 | name = "arrow" 68 | -------------------------------------------------------------------------------- /requirements/requirements-docs.txt: -------------------------------------------------------------------------------- 1 | -r requirements.txt 2 | doc8 3 | sphinx>=7.0.0 4 | sphinx-autobuild 5 | sphinx-autodoc-typehints 6 | sphinx_rtd_theme>=1.3.0 7 | -------------------------------------------------------------------------------- /requirements/requirements-tests.txt: -------------------------------------------------------------------------------- 1 | -r requirements.txt 2 | backports.zoneinfo==0.2.1;python_version<'3.9' 3 | dateparser==1.* 4 | pre-commit 5 | pytest 6 | pytest-cov 7 | pytest-mock 8 | pytz==2021.1 9 | simplejson==3.* 10 | types-python-dateutil>=2.8.10 11 | -------------------------------------------------------------------------------- /requirements/requirements.txt: -------------------------------------------------------------------------------- 1 | python-dateutil>=2.7.0 2 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [mypy] 2 | python_version = 3.11 3 | 4 | show_error_codes = True 5 | pretty = True 6 | 7 | allow_any_expr = True 8 | allow_any_decorated = True 9 | allow_any_explicit = True 10 | disallow_any_generics = True 11 | disallow_subclassing_any = True 12 | 13 | disallow_untyped_calls = True 14 | disallow_untyped_defs = True 15 | disallow_incomplete_defs = True 16 | disallow_untyped_decorators = True 17 | 18 | no_implicit_optional = True 19 | 20 | warn_redundant_casts = True 21 | warn_unused_ignores = True 22 | no_warn_no_return = True 23 | warn_return_any = True 24 | warn_unreachable = True 25 | 26 | strict_equality = True 27 | no_implicit_reexport = True 28 | 29 | allow_redefinition = True 30 | 31 | # Type annotations for testing code and migration files are not mandatory 32 | [mypy-*.tests.*,tests.*] 33 | ignore_errors = True 34 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/arrow-py/arrow/0fe5f065718e5af3ab47903d1bec82d87d202a63/tests/__init__.py -------------------------------------------------------------------------------- /tests/conftest.py: -------------------------------------------------------------------------------- 1 | from datetime import datetime 2 | 3 | import pytest 4 | from dateutil import tz as dateutil_tz 5 | 6 | from arrow import arrow, factory, formatter, locales, parser 7 | 8 | 9 | @pytest.fixture(scope="class") 10 | def time_utcnow(request): 11 | request.cls.arrow = arrow.Arrow.utcnow() 12 | 13 | 14 | @pytest.fixture(scope="class") 15 | def time_2013_01_01(request): 16 | request.cls.now = arrow.Arrow.utcnow() 17 | request.cls.arrow = arrow.Arrow(2013, 1, 1) 18 | request.cls.datetime = datetime(2013, 1, 1) 19 | 20 | 21 | @pytest.fixture(scope="class") 22 | def time_2013_02_03(request): 23 | request.cls.arrow = arrow.Arrow(2013, 2, 3, 12, 30, 45, 1) 24 | 25 | 26 | @pytest.fixture(scope="class") 27 | def time_2013_02_15(request): 28 | request.cls.datetime = datetime(2013, 2, 15, 3, 41, 22, 8923) 29 | request.cls.arrow = arrow.Arrow.fromdatetime(request.cls.datetime) 30 | 31 | 32 | @pytest.fixture(scope="class") 33 | def time_1975_12_25(request): 34 | request.cls.datetime = datetime( 35 | 1975, 12, 25, 14, 15, 16, tzinfo=dateutil_tz.gettz("America/New_York") 36 | ) 37 | request.cls.arrow = arrow.Arrow.fromdatetime(request.cls.datetime) 38 | 39 | 40 | @pytest.fixture(scope="class") 41 | def arrow_formatter(request): 42 | request.cls.formatter = formatter.DateTimeFormatter() 43 | 44 | 45 | @pytest.fixture(scope="class") 46 | def arrow_factory(request): 47 | request.cls.factory = factory.ArrowFactory() 48 | 49 | 50 | @pytest.fixture(scope="class") 51 | def lang_locales(request): 52 | request.cls.locales = locales._locale_map 53 | 54 | 55 | @pytest.fixture(scope="class") 56 | def lang_locale(request): 57 | # As locale test classes are prefixed with Test, we are dynamically getting the locale by the test class name. 58 | # TestEnglishLocale -> EnglishLocale 59 | name = request.cls.__name__[4:] 60 | request.cls.locale = locales.get_locale_by_class_name(name) 61 | 62 | 63 | @pytest.fixture(scope="class") 64 | def dt_parser(request): 65 | request.cls.parser = parser.DateTimeParser() 66 | 67 | 68 | @pytest.fixture(scope="class") 69 | def dt_parser_regex(request): 70 | request.cls.format_regex = parser.DateTimeParser._FORMAT_RE 71 | 72 | 73 | @pytest.fixture(scope="class") 74 | def tzinfo_parser(request): 75 | request.cls.parser = parser.TzinfoParser() 76 | -------------------------------------------------------------------------------- /tests/test_api.py: -------------------------------------------------------------------------------- 1 | import arrow 2 | 3 | 4 | class TestModule: 5 | def test_get(self, mocker): 6 | mocker.patch("arrow.api._factory.get", return_value="result") 7 | 8 | assert arrow.api.get() == "result" 9 | 10 | def test_utcnow(self, mocker): 11 | mocker.patch("arrow.api._factory.utcnow", return_value="utcnow") 12 | 13 | assert arrow.api.utcnow() == "utcnow" 14 | 15 | def test_now(self, mocker): 16 | mocker.patch("arrow.api._factory.now", tz="tz", return_value="now") 17 | 18 | assert arrow.api.now("tz") == "now" 19 | 20 | def test_factory(self): 21 | class MockCustomArrowClass(arrow.Arrow): 22 | pass 23 | 24 | result = arrow.api.factory(MockCustomArrowClass) 25 | 26 | assert isinstance(result, arrow.factory.ArrowFactory) 27 | assert isinstance(result.utcnow(), MockCustomArrowClass) 28 | -------------------------------------------------------------------------------- /tests/test_factory.py: -------------------------------------------------------------------------------- 1 | import time 2 | from datetime import date, datetime, timezone 3 | from decimal import Decimal 4 | 5 | import pytest 6 | from dateutil import tz 7 | 8 | from arrow import Arrow 9 | from arrow.parser import ParserError 10 | 11 | from .utils import assert_datetime_equality 12 | 13 | 14 | @pytest.mark.usefixtures("arrow_factory") 15 | class TestGet: 16 | def test_no_args(self): 17 | assert_datetime_equality( 18 | self.factory.get(), datetime.now(timezone.utc).replace(tzinfo=tz.tzutc()) 19 | ) 20 | 21 | def test_timestamp_one_arg_no_arg(self): 22 | no_arg = self.factory.get(1406430900).timestamp() 23 | one_arg = self.factory.get("1406430900", "X").timestamp() 24 | 25 | assert no_arg == one_arg 26 | 27 | def test_one_arg_none(self): 28 | with pytest.raises(TypeError): 29 | self.factory.get(None) 30 | 31 | def test_struct_time(self): 32 | assert_datetime_equality( 33 | self.factory.get(time.gmtime()), 34 | datetime.now(timezone.utc).replace(tzinfo=tz.tzutc()), 35 | ) 36 | 37 | def test_one_arg_timestamp(self): 38 | int_timestamp = int(time.time()) 39 | timestamp_dt = datetime.utcfromtimestamp(int_timestamp).replace( 40 | tzinfo=tz.tzutc() 41 | ) 42 | 43 | assert self.factory.get(int_timestamp) == timestamp_dt 44 | 45 | with pytest.raises(ParserError): 46 | self.factory.get(str(int_timestamp)) 47 | 48 | float_timestamp = time.time() 49 | timestamp_dt = datetime.utcfromtimestamp(float_timestamp).replace( 50 | tzinfo=tz.tzutc() 51 | ) 52 | 53 | assert self.factory.get(float_timestamp) == timestamp_dt 54 | 55 | with pytest.raises(ParserError): 56 | self.factory.get(str(float_timestamp)) 57 | 58 | # Regression test for issue #216 59 | # Python 3 raises OverflowError, Python 2 raises ValueError 60 | timestamp = 99999999999999999999999999.99999999999999999999999999 61 | with pytest.raises((OverflowError, ValueError)): 62 | self.factory.get(timestamp) 63 | 64 | def test_one_arg_expanded_timestamp(self): 65 | millisecond_timestamp = 1591328104308 66 | microsecond_timestamp = 1591328104308505 67 | 68 | # Regression test for issue #796 69 | assert self.factory.get(millisecond_timestamp) == datetime.utcfromtimestamp( 70 | 1591328104.308 71 | ).replace(tzinfo=tz.tzutc()) 72 | assert self.factory.get(microsecond_timestamp) == datetime.utcfromtimestamp( 73 | 1591328104.308505 74 | ).replace(tzinfo=tz.tzutc()) 75 | 76 | def test_one_arg_timestamp_with_tzinfo(self): 77 | timestamp = time.time() 78 | timestamp_dt = datetime.fromtimestamp(timestamp, tz=tz.tzutc()).astimezone( 79 | tz.gettz("US/Pacific") 80 | ) 81 | timezone = tz.gettz("US/Pacific") 82 | 83 | assert_datetime_equality( 84 | self.factory.get(timestamp, tzinfo=timezone), timestamp_dt 85 | ) 86 | 87 | def test_one_arg_arrow(self): 88 | arw = self.factory.utcnow() 89 | result = self.factory.get(arw) 90 | 91 | assert arw == result 92 | 93 | def test_one_arg_datetime(self): 94 | dt = datetime.now(timezone.utc).replace(tzinfo=tz.tzutc()) 95 | 96 | assert self.factory.get(dt) == dt 97 | 98 | def test_one_arg_date(self): 99 | d = date.today() 100 | dt = datetime(d.year, d.month, d.day, tzinfo=tz.tzutc()) 101 | 102 | assert self.factory.get(d) == dt 103 | 104 | def test_one_arg_tzinfo(self): 105 | self.expected = ( 106 | datetime.now(timezone.utc) 107 | .replace(tzinfo=tz.tzutc()) 108 | .astimezone(tz.gettz("US/Pacific")) 109 | ) 110 | 111 | assert_datetime_equality( 112 | self.factory.get(tz.gettz("US/Pacific")), self.expected 113 | ) 114 | 115 | # regression test for issue #658 116 | def test_one_arg_dateparser_datetime(self): 117 | dateparser = pytest.importorskip("dateparser") 118 | expected = datetime(1990, 1, 1).replace(tzinfo=tz.tzutc()) 119 | # dateparser outputs: datetime.datetime(1990, 1, 1, 0, 0, tzinfo=) 120 | parsed_date = dateparser.parse("1990-01-01T00:00:00+00:00") 121 | dt_output = self.factory.get(parsed_date)._datetime.replace(tzinfo=tz.tzutc()) 122 | assert dt_output == expected 123 | 124 | def test_kwarg_tzinfo(self): 125 | self.expected = ( 126 | datetime.now(timezone.utc) 127 | .replace(tzinfo=tz.tzutc()) 128 | .astimezone(tz.gettz("US/Pacific")) 129 | ) 130 | 131 | assert_datetime_equality( 132 | self.factory.get(tzinfo=tz.gettz("US/Pacific")), self.expected 133 | ) 134 | 135 | def test_kwarg_tzinfo_string(self): 136 | self.expected = ( 137 | datetime.now(timezone.utc) 138 | .replace(tzinfo=tz.tzutc()) 139 | .astimezone(tz.gettz("US/Pacific")) 140 | ) 141 | 142 | assert_datetime_equality(self.factory.get(tzinfo="US/Pacific"), self.expected) 143 | 144 | with pytest.raises(ParserError): 145 | self.factory.get(tzinfo="US/PacificInvalidTzinfo") 146 | 147 | def test_kwarg_normalize_whitespace(self): 148 | result = self.factory.get( 149 | "Jun 1 2005 1:33PM", 150 | "MMM D YYYY H:mmA", 151 | tzinfo=tz.tzutc(), 152 | normalize_whitespace=True, 153 | ) 154 | assert result._datetime == datetime(2005, 6, 1, 13, 33, tzinfo=tz.tzutc()) 155 | 156 | result = self.factory.get( 157 | "\t 2013-05-05T12:30:45.123456 \t \n", 158 | tzinfo=tz.tzutc(), 159 | normalize_whitespace=True, 160 | ) 161 | assert result._datetime == datetime( 162 | 2013, 5, 5, 12, 30, 45, 123456, tzinfo=tz.tzutc() 163 | ) 164 | 165 | # regression test for #944 166 | def test_one_arg_datetime_tzinfo_kwarg(self): 167 | dt = datetime(2021, 4, 29, 6) 168 | 169 | result = self.factory.get(dt, tzinfo="America/Chicago") 170 | 171 | expected = datetime(2021, 4, 29, 6, tzinfo=tz.gettz("America/Chicago")) 172 | 173 | assert_datetime_equality(result._datetime, expected) 174 | 175 | def test_one_arg_arrow_tzinfo_kwarg(self): 176 | arw = Arrow(2021, 4, 29, 6) 177 | 178 | result = self.factory.get(arw, tzinfo="America/Chicago") 179 | 180 | expected = datetime(2021, 4, 29, 6, tzinfo=tz.gettz("America/Chicago")) 181 | 182 | assert_datetime_equality(result._datetime, expected) 183 | 184 | def test_one_arg_date_tzinfo_kwarg(self): 185 | da = date(2021, 4, 29) 186 | 187 | result = self.factory.get(da, tzinfo="America/Chicago") 188 | 189 | expected = Arrow(2021, 4, 29, tzinfo=tz.gettz("America/Chicago")) 190 | 191 | assert result.date() == expected.date() 192 | assert result.tzinfo == expected.tzinfo 193 | 194 | def test_one_arg_iso_calendar_tzinfo_kwarg(self): 195 | result = self.factory.get((2004, 1, 7), tzinfo="America/Chicago") 196 | 197 | expected = Arrow(2004, 1, 4, tzinfo="America/Chicago") 198 | 199 | assert_datetime_equality(result, expected) 200 | 201 | def test_one_arg_iso_str(self): 202 | dt = datetime.now(timezone.utc) 203 | 204 | assert_datetime_equality( 205 | self.factory.get(dt.isoformat()), dt.replace(tzinfo=tz.tzutc()) 206 | ) 207 | 208 | def test_one_arg_iso_calendar(self): 209 | pairs = [ 210 | (datetime(2004, 1, 4), (2004, 1, 7)), 211 | (datetime(2008, 12, 30), (2009, 1, 2)), 212 | (datetime(2010, 1, 2), (2009, 53, 6)), 213 | (datetime(2000, 2, 29), (2000, 9, 2)), 214 | (datetime(2005, 1, 1), (2004, 53, 6)), 215 | (datetime(2010, 1, 4), (2010, 1, 1)), 216 | (datetime(2010, 1, 3), (2009, 53, 7)), 217 | (datetime(2003, 12, 29), (2004, 1, 1)), 218 | ] 219 | 220 | for pair in pairs: 221 | dt, iso = pair 222 | assert self.factory.get(iso) == self.factory.get(dt) 223 | 224 | with pytest.raises(TypeError): 225 | self.factory.get((2014, 7, 1, 4)) 226 | 227 | with pytest.raises(TypeError): 228 | self.factory.get((2014, 7)) 229 | 230 | with pytest.raises(ValueError): 231 | self.factory.get((2014, 70, 1)) 232 | 233 | with pytest.raises(ValueError): 234 | self.factory.get((2014, 7, 10)) 235 | 236 | def test_one_arg_other(self): 237 | with pytest.raises(TypeError): 238 | self.factory.get(object()) 239 | 240 | def test_one_arg_bool(self): 241 | with pytest.raises(TypeError): 242 | self.factory.get(False) 243 | 244 | with pytest.raises(TypeError): 245 | self.factory.get(True) 246 | 247 | def test_one_arg_decimal(self): 248 | result = self.factory.get(Decimal(1577836800.26843)) 249 | 250 | assert result._datetime == datetime( 251 | 2020, 1, 1, 0, 0, 0, 268430, tzinfo=tz.tzutc() 252 | ) 253 | 254 | def test_two_args_datetime_tzinfo(self): 255 | result = self.factory.get(datetime(2013, 1, 1), tz.gettz("US/Pacific")) 256 | 257 | assert result._datetime == datetime(2013, 1, 1, tzinfo=tz.gettz("US/Pacific")) 258 | 259 | def test_two_args_datetime_tz_str(self): 260 | result = self.factory.get(datetime(2013, 1, 1), "US/Pacific") 261 | 262 | assert result._datetime == datetime(2013, 1, 1, tzinfo=tz.gettz("US/Pacific")) 263 | 264 | def test_two_args_date_tzinfo(self): 265 | result = self.factory.get(date(2013, 1, 1), tz.gettz("US/Pacific")) 266 | 267 | assert result._datetime == datetime(2013, 1, 1, tzinfo=tz.gettz("US/Pacific")) 268 | 269 | def test_two_args_date_tz_str(self): 270 | result = self.factory.get(date(2013, 1, 1), "US/Pacific") 271 | 272 | assert result._datetime == datetime(2013, 1, 1, tzinfo=tz.gettz("US/Pacific")) 273 | 274 | def test_two_args_datetime_other(self): 275 | with pytest.raises(TypeError): 276 | self.factory.get(datetime.now(timezone.utc), object()) 277 | 278 | def test_two_args_date_other(self): 279 | with pytest.raises(TypeError): 280 | self.factory.get(date.today(), object()) 281 | 282 | def test_two_args_str_str(self): 283 | result = self.factory.get("2013-01-01", "YYYY-MM-DD") 284 | 285 | assert result._datetime == datetime(2013, 1, 1, tzinfo=tz.tzutc()) 286 | 287 | def test_two_args_str_tzinfo(self): 288 | result = self.factory.get("2013-01-01", tzinfo=tz.gettz("US/Pacific")) 289 | 290 | assert_datetime_equality( 291 | result._datetime, datetime(2013, 1, 1, tzinfo=tz.gettz("US/Pacific")) 292 | ) 293 | 294 | def test_two_args_twitter_format(self): 295 | # format returned by twitter API for created_at: 296 | twitter_date = "Fri Apr 08 21:08:54 +0000 2016" 297 | result = self.factory.get(twitter_date, "ddd MMM DD HH:mm:ss Z YYYY") 298 | 299 | assert result._datetime == datetime(2016, 4, 8, 21, 8, 54, tzinfo=tz.tzutc()) 300 | 301 | def test_two_args_str_list(self): 302 | result = self.factory.get("2013-01-01", ["MM/DD/YYYY", "YYYY-MM-DD"]) 303 | 304 | assert result._datetime == datetime(2013, 1, 1, tzinfo=tz.tzutc()) 305 | 306 | def test_two_args_unicode_unicode(self): 307 | result = self.factory.get("2013-01-01", "YYYY-MM-DD") 308 | 309 | assert result._datetime == datetime(2013, 1, 1, tzinfo=tz.tzutc()) 310 | 311 | def test_two_args_other(self): 312 | with pytest.raises(TypeError): 313 | self.factory.get(object(), object()) 314 | 315 | def test_three_args_with_tzinfo(self): 316 | timefmt = "YYYYMMDD" 317 | d = "20150514" 318 | 319 | assert self.factory.get(d, timefmt, tzinfo=tz.tzlocal()) == datetime( 320 | 2015, 5, 14, tzinfo=tz.tzlocal() 321 | ) 322 | 323 | def test_three_args(self): 324 | assert self.factory.get(2013, 1, 1) == datetime(2013, 1, 1, tzinfo=tz.tzutc()) 325 | 326 | def test_full_kwargs(self): 327 | assert self.factory.get( 328 | year=2016, 329 | month=7, 330 | day=14, 331 | hour=7, 332 | minute=16, 333 | second=45, 334 | microsecond=631092, 335 | ) == datetime(2016, 7, 14, 7, 16, 45, 631092, tzinfo=tz.tzutc()) 336 | 337 | def test_three_kwargs(self): 338 | assert self.factory.get(year=2016, month=7, day=14) == datetime( 339 | 2016, 7, 14, 0, 0, tzinfo=tz.tzutc() 340 | ) 341 | 342 | def test_tzinfo_string_kwargs(self): 343 | result = self.factory.get("2019072807", "YYYYMMDDHH", tzinfo="UTC") 344 | assert result._datetime == datetime(2019, 7, 28, 7, 0, 0, 0, tzinfo=tz.tzutc()) 345 | 346 | def test_insufficient_kwargs(self): 347 | with pytest.raises(TypeError): 348 | self.factory.get(year=2016) 349 | 350 | with pytest.raises(TypeError): 351 | self.factory.get(year=2016, month=7) 352 | 353 | def test_locale(self): 354 | result = self.factory.get("2010", "YYYY", locale="ja") 355 | assert result._datetime == datetime(2010, 1, 1, 0, 0, 0, 0, tzinfo=tz.tzutc()) 356 | 357 | # regression test for issue #701 358 | result = self.factory.get( 359 | "Montag, 9. September 2019, 16:15-20:00", "dddd, D. MMMM YYYY", locale="de" 360 | ) 361 | assert result._datetime == datetime(2019, 9, 9, 0, 0, 0, 0, tzinfo=tz.tzutc()) 362 | 363 | def test_locale_kwarg_only(self): 364 | res = self.factory.get(locale="ja") 365 | assert res.tzinfo == tz.tzutc() 366 | 367 | def test_locale_with_tzinfo(self): 368 | res = self.factory.get(locale="ja", tzinfo=tz.gettz("Asia/Tokyo")) 369 | assert res.tzinfo == tz.gettz("Asia/Tokyo") 370 | 371 | 372 | @pytest.mark.usefixtures("arrow_factory") 373 | class TestUtcNow: 374 | def test_utcnow(self): 375 | assert_datetime_equality( 376 | self.factory.utcnow()._datetime, 377 | datetime.now(timezone.utc).replace(tzinfo=tz.tzutc()), 378 | ) 379 | 380 | 381 | @pytest.mark.usefixtures("arrow_factory") 382 | class TestNow: 383 | def test_no_tz(self): 384 | assert_datetime_equality(self.factory.now(), datetime.now(tz.tzlocal())) 385 | 386 | def test_tzinfo(self): 387 | assert_datetime_equality( 388 | self.factory.now(tz.gettz("EST")), datetime.now(tz.gettz("EST")) 389 | ) 390 | 391 | def test_tz_str(self): 392 | assert_datetime_equality(self.factory.now("EST"), datetime.now(tz.gettz("EST"))) 393 | -------------------------------------------------------------------------------- /tests/test_formatter.py: -------------------------------------------------------------------------------- 1 | try: 2 | import zoneinfo 3 | except ImportError: 4 | from backports import zoneinfo 5 | 6 | from datetime import datetime, timezone 7 | 8 | import pytest 9 | from dateutil import tz as dateutil_tz 10 | 11 | from arrow import ( 12 | FORMAT_ATOM, 13 | FORMAT_COOKIE, 14 | FORMAT_RFC822, 15 | FORMAT_RFC850, 16 | FORMAT_RFC1036, 17 | FORMAT_RFC1123, 18 | FORMAT_RFC2822, 19 | FORMAT_RFC3339, 20 | FORMAT_RFC3339_STRICT, 21 | FORMAT_RSS, 22 | FORMAT_W3C, 23 | ) 24 | 25 | from .utils import make_full_tz_list 26 | 27 | 28 | @pytest.mark.usefixtures("arrow_formatter") 29 | class TestFormatterFormatToken: 30 | def test_format(self): 31 | dt = datetime(2013, 2, 5, 12, 32, 51) 32 | 33 | result = self.formatter.format(dt, "MM-DD-YYYY hh:mm:ss a") 34 | 35 | assert result == "02-05-2013 12:32:51 pm" 36 | 37 | def test_year(self): 38 | dt = datetime(2013, 1, 1) 39 | assert self.formatter._format_token(dt, "YYYY") == "2013" 40 | assert self.formatter._format_token(dt, "YY") == "13" 41 | 42 | def test_month(self): 43 | dt = datetime(2013, 1, 1) 44 | assert self.formatter._format_token(dt, "MMMM") == "January" 45 | assert self.formatter._format_token(dt, "MMM") == "Jan" 46 | assert self.formatter._format_token(dt, "MM") == "01" 47 | assert self.formatter._format_token(dt, "M") == "1" 48 | 49 | def test_day(self): 50 | dt = datetime(2013, 2, 1) 51 | assert self.formatter._format_token(dt, "DDDD") == "032" 52 | assert self.formatter._format_token(dt, "DDD") == "32" 53 | assert self.formatter._format_token(dt, "DD") == "01" 54 | assert self.formatter._format_token(dt, "D") == "1" 55 | assert self.formatter._format_token(dt, "Do") == "1st" 56 | 57 | assert self.formatter._format_token(dt, "dddd") == "Friday" 58 | assert self.formatter._format_token(dt, "ddd") == "Fri" 59 | assert self.formatter._format_token(dt, "d") == "5" 60 | 61 | def test_hour(self): 62 | dt = datetime(2013, 1, 1, 2) 63 | assert self.formatter._format_token(dt, "HH") == "02" 64 | assert self.formatter._format_token(dt, "H") == "2" 65 | 66 | dt = datetime(2013, 1, 1, 13) 67 | assert self.formatter._format_token(dt, "HH") == "13" 68 | assert self.formatter._format_token(dt, "H") == "13" 69 | 70 | dt = datetime(2013, 1, 1, 2) 71 | assert self.formatter._format_token(dt, "hh") == "02" 72 | assert self.formatter._format_token(dt, "h") == "2" 73 | 74 | dt = datetime(2013, 1, 1, 13) 75 | assert self.formatter._format_token(dt, "hh") == "01" 76 | assert self.formatter._format_token(dt, "h") == "1" 77 | 78 | # test that 12-hour time converts to '12' at midnight 79 | dt = datetime(2013, 1, 1, 0) 80 | assert self.formatter._format_token(dt, "hh") == "12" 81 | assert self.formatter._format_token(dt, "h") == "12" 82 | 83 | def test_minute(self): 84 | dt = datetime(2013, 1, 1, 0, 1) 85 | assert self.formatter._format_token(dt, "mm") == "01" 86 | assert self.formatter._format_token(dt, "m") == "1" 87 | 88 | def test_second(self): 89 | dt = datetime(2013, 1, 1, 0, 0, 1) 90 | assert self.formatter._format_token(dt, "ss") == "01" 91 | assert self.formatter._format_token(dt, "s") == "1" 92 | 93 | def test_sub_second(self): 94 | dt = datetime(2013, 1, 1, 0, 0, 0, 123456) 95 | assert self.formatter._format_token(dt, "SSSSSS") == "123456" 96 | assert self.formatter._format_token(dt, "SSSSS") == "12345" 97 | assert self.formatter._format_token(dt, "SSSS") == "1234" 98 | assert self.formatter._format_token(dt, "SSS") == "123" 99 | assert self.formatter._format_token(dt, "SS") == "12" 100 | assert self.formatter._format_token(dt, "S") == "1" 101 | 102 | dt = datetime(2013, 1, 1, 0, 0, 0, 2000) 103 | assert self.formatter._format_token(dt, "SSSSSS") == "002000" 104 | assert self.formatter._format_token(dt, "SSSSS") == "00200" 105 | assert self.formatter._format_token(dt, "SSSS") == "0020" 106 | assert self.formatter._format_token(dt, "SSS") == "002" 107 | assert self.formatter._format_token(dt, "SS") == "00" 108 | assert self.formatter._format_token(dt, "S") == "0" 109 | 110 | def test_timestamp(self): 111 | dt = datetime.now(tz=dateutil_tz.UTC) 112 | expected = str(dt.timestamp()) 113 | assert self.formatter._format_token(dt, "X") == expected 114 | 115 | # Must round because time.time() may return a float with greater 116 | # than 6 digits of precision 117 | expected = str(int(dt.timestamp() * 1000000)) 118 | assert self.formatter._format_token(dt, "x") == expected 119 | 120 | def test_timezone(self): 121 | dt = datetime.now(timezone.utc).replace(tzinfo=dateutil_tz.gettz("US/Pacific")) 122 | 123 | result = self.formatter._format_token(dt, "ZZ") 124 | assert result == "-07:00" or result == "-08:00" 125 | 126 | result = self.formatter._format_token(dt, "Z") 127 | assert result == "-0700" or result == "-0800" 128 | 129 | @pytest.mark.parametrize("full_tz_name", make_full_tz_list()) 130 | def test_timezone_formatter(self, full_tz_name): 131 | # This test will fail if we use "now" as date as soon as we change from/to DST 132 | dt = datetime(1986, 2, 14, tzinfo=zoneinfo.ZoneInfo("UTC")).replace( 133 | tzinfo=dateutil_tz.gettz(full_tz_name) 134 | ) 135 | abbreviation = dt.tzname() 136 | 137 | result = self.formatter._format_token(dt, "ZZZ") 138 | assert result == abbreviation 139 | 140 | def test_am_pm(self): 141 | dt = datetime(2012, 1, 1, 11) 142 | assert self.formatter._format_token(dt, "a") == "am" 143 | assert self.formatter._format_token(dt, "A") == "AM" 144 | 145 | dt = datetime(2012, 1, 1, 13) 146 | assert self.formatter._format_token(dt, "a") == "pm" 147 | assert self.formatter._format_token(dt, "A") == "PM" 148 | 149 | def test_week(self): 150 | dt = datetime(2017, 5, 19) 151 | assert self.formatter._format_token(dt, "W") == "2017-W20-5" 152 | 153 | # make sure week is zero padded when needed 154 | dt_early = datetime(2011, 1, 20) 155 | assert self.formatter._format_token(dt_early, "W") == "2011-W03-4" 156 | 157 | def test_nonsense(self): 158 | dt = datetime(2012, 1, 1, 11) 159 | assert self.formatter._format_token(dt, None) is None 160 | assert self.formatter._format_token(dt, "NONSENSE") is None 161 | 162 | def test_escape(self): 163 | assert ( 164 | self.formatter.format( 165 | datetime(2015, 12, 10, 17, 9), "MMMM D, YYYY [at] h:mma" 166 | ) 167 | == "December 10, 2015 at 5:09pm" 168 | ) 169 | 170 | assert ( 171 | self.formatter.format( 172 | datetime(2015, 12, 10, 17, 9), "[MMMM] M D, YYYY [at] h:mma" 173 | ) 174 | == "MMMM 12 10, 2015 at 5:09pm" 175 | ) 176 | 177 | assert ( 178 | self.formatter.format( 179 | datetime(1990, 11, 25), 180 | "[It happened on] MMMM Do [in the year] YYYY [a long time ago]", 181 | ) 182 | == "It happened on November 25th in the year 1990 a long time ago" 183 | ) 184 | 185 | assert ( 186 | self.formatter.format( 187 | datetime(1990, 11, 25), 188 | "[It happened on] MMMM Do [in the][ year] YYYY [a long time ago]", 189 | ) 190 | == "It happened on November 25th in the year 1990 a long time ago" 191 | ) 192 | 193 | assert ( 194 | self.formatter.format( 195 | datetime(1, 1, 1), "[I'm][ entirely][ escaped,][ weee!]" 196 | ) 197 | == "I'm entirely escaped, weee!" 198 | ) 199 | 200 | # Special RegEx characters 201 | assert ( 202 | self.formatter.format( 203 | datetime(2017, 12, 31, 2, 0), "MMM DD, YYYY |^${}().*+?<>-& h:mm A" 204 | ) 205 | == "Dec 31, 2017 |^${}().*+?<>-& 2:00 AM" 206 | ) 207 | 208 | # Escaping is atomic: brackets inside brackets are treated literally 209 | assert self.formatter.format(datetime(1, 1, 1), "[[[ ]]") == "[[ ]" 210 | 211 | 212 | @pytest.mark.usefixtures("arrow_formatter", "time_1975_12_25") 213 | class TestFormatterBuiltinFormats: 214 | def test_atom(self): 215 | assert ( 216 | self.formatter.format(self.datetime, FORMAT_ATOM) 217 | == "1975-12-25 14:15:16-05:00" 218 | ) 219 | 220 | def test_cookie(self): 221 | assert ( 222 | self.formatter.format(self.datetime, FORMAT_COOKIE) 223 | == "Thursday, 25-Dec-1975 14:15:16 EST" 224 | ) 225 | 226 | def test_rfc_822(self): 227 | assert ( 228 | self.formatter.format(self.datetime, FORMAT_RFC822) 229 | == "Thu, 25 Dec 75 14:15:16 -0500" 230 | ) 231 | 232 | def test_rfc_850(self): 233 | assert ( 234 | self.formatter.format(self.datetime, FORMAT_RFC850) 235 | == "Thursday, 25-Dec-75 14:15:16 EST" 236 | ) 237 | 238 | def test_rfc_1036(self): 239 | assert ( 240 | self.formatter.format(self.datetime, FORMAT_RFC1036) 241 | == "Thu, 25 Dec 75 14:15:16 -0500" 242 | ) 243 | 244 | def test_rfc_1123(self): 245 | assert ( 246 | self.formatter.format(self.datetime, FORMAT_RFC1123) 247 | == "Thu, 25 Dec 1975 14:15:16 -0500" 248 | ) 249 | 250 | def test_rfc_2822(self): 251 | assert ( 252 | self.formatter.format(self.datetime, FORMAT_RFC2822) 253 | == "Thu, 25 Dec 1975 14:15:16 -0500" 254 | ) 255 | 256 | def test_rfc3339(self): 257 | assert ( 258 | self.formatter.format(self.datetime, FORMAT_RFC3339) 259 | == "1975-12-25 14:15:16-05:00" 260 | ) 261 | 262 | def test_rfc3339_strict(self): 263 | assert ( 264 | self.formatter.format(self.datetime, FORMAT_RFC3339_STRICT) 265 | == "1975-12-25T14:15:16-05:00" 266 | ) 267 | 268 | def test_rss(self): 269 | assert ( 270 | self.formatter.format(self.datetime, FORMAT_RSS) 271 | == "Thu, 25 Dec 1975 14:15:16 -0500" 272 | ) 273 | 274 | def test_w3c(self): 275 | assert ( 276 | self.formatter.format(self.datetime, FORMAT_W3C) 277 | == "1975-12-25 14:15:16-05:00" 278 | ) 279 | -------------------------------------------------------------------------------- /tests/test_util.py: -------------------------------------------------------------------------------- 1 | import time 2 | from datetime import datetime, timezone 3 | 4 | import pytest 5 | 6 | from arrow import util 7 | 8 | 9 | class TestUtil: 10 | def test_next_weekday(self): 11 | # Get first Monday after epoch 12 | assert util.next_weekday(datetime(1970, 1, 1), 0) == datetime(1970, 1, 5) 13 | 14 | # Get first Tuesday after epoch 15 | assert util.next_weekday(datetime(1970, 1, 1), 1) == datetime(1970, 1, 6) 16 | 17 | # Get first Wednesday after epoch 18 | assert util.next_weekday(datetime(1970, 1, 1), 2) == datetime(1970, 1, 7) 19 | 20 | # Get first Thursday after epoch 21 | assert util.next_weekday(datetime(1970, 1, 1), 3) == datetime(1970, 1, 1) 22 | 23 | # Get first Friday after epoch 24 | assert util.next_weekday(datetime(1970, 1, 1), 4) == datetime(1970, 1, 2) 25 | 26 | # Get first Saturday after epoch 27 | assert util.next_weekday(datetime(1970, 1, 1), 5) == datetime(1970, 1, 3) 28 | 29 | # Get first Sunday after epoch 30 | assert util.next_weekday(datetime(1970, 1, 1), 6) == datetime(1970, 1, 4) 31 | 32 | # Weekdays are 0-indexed 33 | with pytest.raises(ValueError): 34 | util.next_weekday(datetime(1970, 1, 1), 7) 35 | 36 | with pytest.raises(ValueError): 37 | util.next_weekday(datetime(1970, 1, 1), -1) 38 | 39 | def test_is_timestamp(self): 40 | timestamp_float = time.time() 41 | timestamp_int = int(timestamp_float) 42 | 43 | assert util.is_timestamp(timestamp_int) 44 | assert util.is_timestamp(timestamp_float) 45 | assert util.is_timestamp(str(timestamp_int)) 46 | assert util.is_timestamp(str(timestamp_float)) 47 | 48 | assert not util.is_timestamp(True) 49 | assert not util.is_timestamp(False) 50 | 51 | class InvalidTimestamp: 52 | pass 53 | 54 | assert not util.is_timestamp(InvalidTimestamp()) 55 | 56 | full_datetime = "2019-06-23T13:12:42" 57 | assert not util.is_timestamp(full_datetime) 58 | 59 | def test_validate_ordinal(self): 60 | timestamp_float = 1607066816.815537 61 | timestamp_int = int(timestamp_float) 62 | timestamp_str = str(timestamp_int) 63 | 64 | with pytest.raises(TypeError): 65 | util.validate_ordinal(timestamp_float) 66 | with pytest.raises(TypeError): 67 | util.validate_ordinal(timestamp_str) 68 | with pytest.raises(TypeError): 69 | util.validate_ordinal(True) 70 | with pytest.raises(TypeError): 71 | util.validate_ordinal(False) 72 | 73 | with pytest.raises(ValueError): 74 | util.validate_ordinal(timestamp_int) 75 | with pytest.raises(ValueError): 76 | util.validate_ordinal(-1 * timestamp_int) 77 | with pytest.raises(ValueError): 78 | util.validate_ordinal(0) 79 | 80 | try: 81 | util.validate_ordinal(1) 82 | except (ValueError, TypeError) as exp: 83 | pytest.fail(f"Exception raised when shouldn't have ({type(exp)}).") 84 | 85 | try: 86 | util.validate_ordinal(datetime.max.toordinal()) 87 | except (ValueError, TypeError) as exp: 88 | pytest.fail(f"Exception raised when shouldn't have ({type(exp)}).") 89 | 90 | ordinal = datetime.now(timezone.utc).toordinal() 91 | ordinal_str = str(ordinal) 92 | ordinal_float = float(ordinal) + 0.5 93 | 94 | with pytest.raises(TypeError): 95 | util.validate_ordinal(ordinal_str) 96 | with pytest.raises(TypeError): 97 | util.validate_ordinal(ordinal_float) 98 | with pytest.raises(ValueError): 99 | util.validate_ordinal(-1 * ordinal) 100 | 101 | try: 102 | util.validate_ordinal(ordinal) 103 | except (ValueError, TypeError) as exp: 104 | pytest.fail(f"Exception raised when shouldn't have ({type(exp)}).") 105 | 106 | full_datetime = "2019-06-23T13:12:42" 107 | 108 | class InvalidOrdinal: 109 | pass 110 | 111 | with pytest.raises(TypeError): 112 | util.validate_ordinal(InvalidOrdinal()) 113 | with pytest.raises(TypeError): 114 | util.validate_ordinal(full_datetime) 115 | 116 | def test_normalize_timestamp(self): 117 | timestamp = 1591161115.194556 118 | millisecond_timestamp = 1591161115194 119 | microsecond_timestamp = 1591161115194556 120 | 121 | assert util.normalize_timestamp(timestamp) == timestamp 122 | assert util.normalize_timestamp(millisecond_timestamp) == 1591161115.194 123 | assert util.normalize_timestamp(microsecond_timestamp) == 1591161115.194556 124 | 125 | with pytest.raises(ValueError): 126 | util.normalize_timestamp(3e17) 127 | 128 | def test_iso_gregorian(self): 129 | with pytest.raises(ValueError): 130 | util.iso_to_gregorian(2013, 0, 5) 131 | 132 | with pytest.raises(ValueError): 133 | util.iso_to_gregorian(2013, 8, 0) 134 | -------------------------------------------------------------------------------- /tests/utils.py: -------------------------------------------------------------------------------- 1 | try: 2 | import zoneinfo 3 | except ImportError: 4 | from backports import zoneinfo 5 | from dateutil.zoneinfo import get_zonefile_instance 6 | 7 | 8 | def make_full_tz_list(): 9 | dateutil_zones = set(get_zonefile_instance().zones) 10 | zoneinfo_zones = set(zoneinfo.available_timezones()) 11 | return dateutil_zones.union(zoneinfo_zones) 12 | 13 | 14 | def assert_datetime_equality(dt1, dt2, within=10): 15 | assert dt1.tzinfo == dt2.tzinfo 16 | assert abs((dt1 - dt2).total_seconds()) < within 17 | -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | [tox] 2 | minversion = 3.18.0 3 | envlist = py{py3,38,39,310,311,312,313} 4 | skip_missing_interpreters = true 5 | 6 | [gh-actions] 7 | python = 8 | pypy-3.7: pypy3 9 | 3.8: py38 10 | 3.9: py39 11 | 3.10: py310 12 | 3.11: py311 13 | 3.12: py312 14 | 3.13: py313 15 | 16 | [testenv] 17 | deps = -r requirements/requirements-tests.txt 18 | allowlist_externals = pytest 19 | commands = pytest 20 | 21 | [testenv:lint] 22 | skip_install = true 23 | deps = pre-commit 24 | commands_pre = pre-commit install 25 | commands = pre-commit run --all-files {posargs} 26 | 27 | [testenv:docs] 28 | skip_install = true 29 | changedir = docs 30 | deps = 31 | -r requirements/requirements-tests.txt 32 | -r requirements/requirements-docs.txt 33 | allowlist_externals = make 34 | commands = 35 | doc8 index.rst ../README.rst --extension .rst --ignore D001 36 | make html SPHINXOPTS="-W --keep-going" 37 | 38 | [testenv:publish] 39 | passenv = * 40 | skip_install = true 41 | deps = 42 | -r requirements/requirements.txt 43 | flit 44 | allowlist_externals = flit 45 | commands = flit publish --setup-py 46 | 47 | [pytest] 48 | addopts = -v --cov-branch --cov=arrow --cov-fail-under=99 --cov-report=term-missing --cov-report=xml 49 | testpaths = tests 50 | 51 | [isort] 52 | line_length = 88 53 | multi_line_output = 3 54 | include_trailing_comma = true 55 | 56 | [flake8] 57 | per-file-ignores = arrow/__init__.py:F401,tests/*:ANN001,ANN201 58 | ignore = E203,E501,W503,ANN101,ANN102,ANN401 59 | --------------------------------------------------------------------------------