Delete stupid
This commit is contained in:
parent
12cf2e8247
commit
8aa0d204f4
1
.gitignore
vendored
1
.gitignore
vendored
@ -232,3 +232,4 @@ bootstrap-custom.min.css
|
||||
launchSettings.json
|
||||
/VpnDetectionPrivate.js
|
||||
/Plugins/ScriptPlugins/VpnDetectionPrivate.js
|
||||
**/Master/env_master
|
@ -1 +0,0 @@
|
||||
pip
|
@ -1,140 +0,0 @@
|
||||
Metadata-Version: 2.1
|
||||
Name: APScheduler
|
||||
Version: 3.5.3
|
||||
Summary: In-process task scheduler with Cron-like capabilities
|
||||
Home-page: https://github.com/agronholm/apscheduler
|
||||
Author: Alex Grönholm
|
||||
Author-email: apscheduler@nextday.fi
|
||||
License: MIT
|
||||
Keywords: scheduling cron
|
||||
Platform: UNKNOWN
|
||||
Classifier: Development Status :: 5 - Production/Stable
|
||||
Classifier: Intended Audience :: Developers
|
||||
Classifier: License :: OSI Approved :: MIT License
|
||||
Classifier: Programming Language :: Python
|
||||
Classifier: Programming Language :: Python :: 2.7
|
||||
Classifier: Programming Language :: Python :: 3
|
||||
Classifier: Programming Language :: Python :: 3.4
|
||||
Classifier: Programming Language :: Python :: 3.5
|
||||
Classifier: Programming Language :: Python :: 3.6
|
||||
Provides-Extra: testing
|
||||
Provides-Extra: rethinkdb
|
||||
Provides-Extra: redis
|
||||
Provides-Extra: tornado
|
||||
Provides-Extra: sqlalchemy
|
||||
Provides-Extra: mongodb
|
||||
Provides-Extra: zookeeper
|
||||
Provides-Extra: gevent
|
||||
Provides-Extra: asyncio
|
||||
Provides-Extra: twisted
|
||||
Requires-Dist: setuptools (>=0.7)
|
||||
Requires-Dist: six (>=1.4.0)
|
||||
Requires-Dist: pytz
|
||||
Requires-Dist: tzlocal (>=1.2)
|
||||
Requires-Dist: futures; python_version == "2.7"
|
||||
Requires-Dist: funcsigs; python_version == "2.7"
|
||||
Provides-Extra: asyncio
|
||||
Requires-Dist: trollius; (python_version == "2.7") and extra == 'asyncio'
|
||||
Provides-Extra: gevent
|
||||
Requires-Dist: gevent; extra == 'gevent'
|
||||
Provides-Extra: mongodb
|
||||
Requires-Dist: pymongo (>=2.8); extra == 'mongodb'
|
||||
Provides-Extra: redis
|
||||
Requires-Dist: redis; extra == 'redis'
|
||||
Provides-Extra: rethinkdb
|
||||
Requires-Dist: rethinkdb; extra == 'rethinkdb'
|
||||
Provides-Extra: sqlalchemy
|
||||
Requires-Dist: sqlalchemy (>=0.8); extra == 'sqlalchemy'
|
||||
Provides-Extra: testing
|
||||
Requires-Dist: pytest (<3.7); extra == 'testing'
|
||||
Requires-Dist: pytest-cov; extra == 'testing'
|
||||
Requires-Dist: pytest-tornado5; extra == 'testing'
|
||||
Provides-Extra: testing
|
||||
Requires-Dist: pytest-asyncio (<0.6.0); (python_version != "2.7") and extra == 'testing'
|
||||
Provides-Extra: testing
|
||||
Requires-Dist: mock; (python_version == "2.7") and extra == 'testing'
|
||||
Provides-Extra: tornado
|
||||
Requires-Dist: tornado (>=4.3); extra == 'tornado'
|
||||
Provides-Extra: twisted
|
||||
Requires-Dist: twisted; extra == 'twisted'
|
||||
Provides-Extra: zookeeper
|
||||
Requires-Dist: kazoo; extra == 'zookeeper'
|
||||
|
||||
.. image:: https://travis-ci.org/agronholm/apscheduler.svg?branch=master
|
||||
:target: https://travis-ci.org/agronholm/apscheduler
|
||||
:alt: Build Status
|
||||
.. image:: https://coveralls.io/repos/github/agronholm/apscheduler/badge.svg?branch=master
|
||||
:target: https://coveralls.io/github/agronholm/apscheduler?branch=master
|
||||
:alt: Code Coverage
|
||||
|
||||
Advanced Python Scheduler (APScheduler) is a Python library that lets you schedule your Python code
|
||||
to be executed later, either just once or periodically. You can add new jobs or remove old ones on
|
||||
the fly as you please. If you store your jobs in a database, they will also survive scheduler
|
||||
restarts and maintain their state. When the scheduler is restarted, it will then run all the jobs
|
||||
it should have run while it was offline [#f1]_.
|
||||
|
||||
Among other things, APScheduler can be used as a cross-platform, application specific replacement
|
||||
to platform specific schedulers, such as the cron daemon or the Windows task scheduler. Please
|
||||
note, however, that APScheduler is **not** a daemon or service itself, nor does it come with any
|
||||
command line tools. It is primarily meant to be run inside existing applications. That said,
|
||||
APScheduler does provide some building blocks for you to build a scheduler service or to run a
|
||||
dedicated scheduler process.
|
||||
|
||||
APScheduler has three built-in scheduling systems you can use:
|
||||
|
||||
* Cron-style scheduling (with optional start/end times)
|
||||
* Interval-based execution (runs jobs on even intervals, with optional start/end times)
|
||||
* One-off delayed execution (runs jobs once, on a set date/time)
|
||||
|
||||
You can mix and match scheduling systems and the backends where the jobs are stored any way you
|
||||
like. Supported backends for storing jobs include:
|
||||
|
||||
* Memory
|
||||
* `SQLAlchemy <http://www.sqlalchemy.org/>`_ (any RDBMS supported by SQLAlchemy works)
|
||||
* `MongoDB <http://www.mongodb.org/>`_
|
||||
* `Redis <http://redis.io/>`_
|
||||
* `RethinkDB <https://www.rethinkdb.com/>`_
|
||||
* `ZooKeeper <https://zookeeper.apache.org/>`_
|
||||
|
||||
APScheduler also integrates with several common Python frameworks, like:
|
||||
|
||||
* `asyncio <http://docs.python.org/3.4/library/asyncio.html>`_ (:pep:`3156`)
|
||||
* `gevent <http://www.gevent.org/>`_
|
||||
* `Tornado <http://www.tornadoweb.org/>`_
|
||||
* `Twisted <http://twistedmatrix.com/>`_
|
||||
* `Qt <http://qt-project.org/>`_ (using either
|
||||
`PyQt <http://www.riverbankcomputing.com/software/pyqt/intro>`_ or
|
||||
`PySide <http://qt-project.org/wiki/PySide>`_)
|
||||
|
||||
.. [#f1] The cutoff period for this is also configurable.
|
||||
|
||||
|
||||
Documentation
|
||||
-------------
|
||||
|
||||
Documentation can be found `here <http://readthedocs.org/docs/apscheduler/en/latest/>`_.
|
||||
|
||||
|
||||
Source
|
||||
------
|
||||
|
||||
The source can be browsed at `Github <https://github.com/agronholm/apscheduler>`_.
|
||||
|
||||
|
||||
Reporting bugs
|
||||
--------------
|
||||
|
||||
A `bug tracker <https://github.com/agronholm/apscheduler/issues>`_ is provided by Github.
|
||||
|
||||
|
||||
Getting help
|
||||
------------
|
||||
|
||||
If you have problems or other questions, you can either:
|
||||
|
||||
* Ask in the `apscheduler <https://gitter.im/apscheduler/Lobby>`_ room on Gitter
|
||||
* Ask on the `APScheduler Google group <http://groups.google.com/group/apscheduler>`_, or
|
||||
* Ask on `StackOverflow <http://stackoverflow.com/questions/tagged/apscheduler>`_ and tag your
|
||||
question with the ``apscheduler`` tag
|
||||
|
||||
|
@ -1,82 +0,0 @@
|
||||
APScheduler-3.5.3.dist-info/METADATA,sha256=oh5F2iHBxfcQx9t9JCzOCz8lwQHtyflRhrh3wMpsi9Y,5426
|
||||
APScheduler-3.5.3.dist-info/RECORD,,
|
||||
APScheduler-3.5.3.dist-info/WHEEL,sha256=gduuPyBvFJQSQ0zdyxF7k0zynDXbIbvg5ZBHoXum5uk,110
|
||||
APScheduler-3.5.3.dist-info/entry_points.txt,sha256=7RgkYN_OYyCUQtIGhj-UNcelnIjsNm7nC9rogdMQh3U,1148
|
||||
APScheduler-3.5.3.dist-info/top_level.txt,sha256=O3oMCWxG-AHkecUoO6Ze7-yYjWrttL95uHO8-RFdYvE,12
|
||||
apscheduler/__init__.py,sha256=qFEK2ysRBcLiYmm3deyJJ1avUOugaM_nCGHMD42WMBw,380
|
||||
apscheduler/events.py,sha256=CQsWk1KgCdpEfXm6H8s08HWExJ_getgfoCe2BGUQRKo,3571
|
||||
apscheduler/job.py,sha256=S8Q-uVtG1PKsZpdYdraN2xb7K12vdJnV2oi2PIZp7fU,11008
|
||||
apscheduler/util.py,sha256=cXkSLpJAI5mB6Tyr5-WRjHjuQDKCYjRnLZ94f2VUGGg,13113
|
||||
apscheduler/executors/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
||||
apscheduler/executors/asyncio.py,sha256=HR8SmX-IJnN2FprdtdX65glYhRXbWurzkocKLyQt2xQ,2111
|
||||
apscheduler/executors/base.py,sha256=hogiMc_t-huw6BMod0HEeY2FhRNmAAUyNNuBHvIX31M,5336
|
||||
apscheduler/executors/base_py3.py,sha256=s_4siAjBHrr7JZnm64VVow9zyvs2JBc-VRPkPuDeBTI,1775
|
||||
apscheduler/executors/debug.py,sha256=15_ogSBzl8RRCfBYDnkIV2uMH8cLk1KImYmBa_NVGpc,573
|
||||
apscheduler/executors/gevent.py,sha256=aulrNmoefyBgrOkH9awRhFiXIDnSCnZ4U0o0_JXIXgc,777
|
||||
apscheduler/executors/pool.py,sha256=q9TC6KzwWI9tpLNxQhdrKRWFtsN5dmx_Vegu23BV-Sk,1672
|
||||
apscheduler/executors/tornado.py,sha256=BlWrZ0MUoHBloIup7YgXGzxN5DWFI6V7s23jpx42Kmk,1747
|
||||
apscheduler/executors/twisted.py,sha256=bRoU0C4BoVcS6_BjKD5wfUs0IJpGkmLsRAcMH2rJJss,778
|
||||
apscheduler/jobstores/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
||||
apscheduler/jobstores/base.py,sha256=DXzSW9XscueHZHMvy1qFiG-vYqUl_MMv0n0uBSZWXGo,4523
|
||||
apscheduler/jobstores/memory.py,sha256=ZxWiKsqfsCHFvac-6X9BztuhnuSxlOYi1dhT6g-pjQo,3655
|
||||
apscheduler/jobstores/mongodb.py,sha256=e9KNzPFrjiVpiM3iPT_c0ONxZQT70VCF2rDXW0-22zk,5296
|
||||
apscheduler/jobstores/redis.py,sha256=ImiEL7aTMEUy8zNZpbtZEpshFA_JabP6Q7iwzKlWxgs,5437
|
||||
apscheduler/jobstores/rethinkdb.py,sha256=1X8PKyB3MQJlbhzwWmNO6POvsOJXjrPenMF6BT3e_MA,5683
|
||||
apscheduler/jobstores/sqlalchemy.py,sha256=Bz2DfcyqWPY-OTiT6Td22lPdGZeu6NnFORTwC4H1jLg,6118
|
||||
apscheduler/jobstores/zookeeper.py,sha256=BzyqZ08XIDcbu5frQWGmDVEHAEScNxjt8oML6Tty8j8,6406
|
||||
apscheduler/schedulers/__init__.py,sha256=jM63xA_K7GSToBenhsz-SCcqfhk1pdEVb6ajwoO5Kqg,406
|
||||
apscheduler/schedulers/asyncio.py,sha256=0j0mcDpf-zI_vQHcUCZZtBfEEZEiocEOZ767efIZ5YM,2082
|
||||
apscheduler/schedulers/background.py,sha256=dGX0T0z6T6HzZHG7njWgp90SFHpetZ4ZBUV2gGOSqoc,1505
|
||||
apscheduler/schedulers/base.py,sha256=Oya7QHCEdya7AvtyhU-b9kn9-WOJcbvbAuIyQj6zU_w,42858
|
||||
apscheduler/schedulers/blocking.py,sha256=c-5YR-dKn3D82tPt38t50KGPJrAiC852v8ai2Vwanmg,924
|
||||
apscheduler/schedulers/gevent.py,sha256=csPBvV75FGcboXXsdex6fCD7J54QgBddYNdWj62ZO9g,1031
|
||||
apscheduler/schedulers/qt.py,sha256=9fFJEr3zbJXuki1vTEdFG3IEg1TCzze1vhnlAeLP0Uo,1254
|
||||
apscheduler/schedulers/tornado.py,sha256=D9Vaq3Ee9EFiXa1jDy9tedI048gR_YT_LAFUWqO_uEw,1926
|
||||
apscheduler/schedulers/twisted.py,sha256=D5EBjjMRtMBxy0_aAURcULAI8Ky2IvCTr9tK9sO1rYk,1844
|
||||
apscheduler/triggers/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
||||
apscheduler/triggers/base.py,sha256=WMo5f2g14fjO5VzpIxFQtk47Z9VEUDDPSxjoPL9FGSQ,1837
|
||||
apscheduler/triggers/combining.py,sha256=WTEnaEkBHysF1009sCvBaQa99hiy9l5Oz-hHyjy3jv8,3473
|
||||
apscheduler/triggers/date.py,sha256=RrfB1PNO9G9e91p1BOf-y_TseVHQQR-KJPhNdPpAHcU,1705
|
||||
apscheduler/triggers/interval.py,sha256=LiIunGOd96yaiAceG1XGP8eY3JxSyHDWCipVhQWMzDU,4381
|
||||
apscheduler/triggers/cron/__init__.py,sha256=a8ASzvM7ci-djOI2jIL2XErL6zEx4Wr1012aD1XJw_w,9246
|
||||
apscheduler/triggers/cron/expressions.py,sha256=hu1kq0mKvivIw7U0D0Nnrbuk3q01dCuhZ7SHRPw6qhI,9184
|
||||
apscheduler/triggers/cron/fields.py,sha256=NWPClh1NgSOpTlJ3sm1TXM_ViC2qJGKWkd_vg0xsw7o,3510
|
||||
APScheduler-3.5.3.dist-info/INSTALLER,sha256=zuuue4knoyJ-UwPPXg8fezS7VCrXJQrAP7zeNuwvFQg,4
|
||||
apscheduler/executors/__pycache__/asyncio.cpython-36.pyc,,
|
||||
apscheduler/executors/__pycache__/base.cpython-36.pyc,,
|
||||
apscheduler/executors/__pycache__/base_py3.cpython-36.pyc,,
|
||||
apscheduler/executors/__pycache__/debug.cpython-36.pyc,,
|
||||
apscheduler/executors/__pycache__/gevent.cpython-36.pyc,,
|
||||
apscheduler/executors/__pycache__/pool.cpython-36.pyc,,
|
||||
apscheduler/executors/__pycache__/tornado.cpython-36.pyc,,
|
||||
apscheduler/executors/__pycache__/twisted.cpython-36.pyc,,
|
||||
apscheduler/executors/__pycache__/__init__.cpython-36.pyc,,
|
||||
apscheduler/jobstores/__pycache__/base.cpython-36.pyc,,
|
||||
apscheduler/jobstores/__pycache__/memory.cpython-36.pyc,,
|
||||
apscheduler/jobstores/__pycache__/mongodb.cpython-36.pyc,,
|
||||
apscheduler/jobstores/__pycache__/redis.cpython-36.pyc,,
|
||||
apscheduler/jobstores/__pycache__/rethinkdb.cpython-36.pyc,,
|
||||
apscheduler/jobstores/__pycache__/sqlalchemy.cpython-36.pyc,,
|
||||
apscheduler/jobstores/__pycache__/zookeeper.cpython-36.pyc,,
|
||||
apscheduler/jobstores/__pycache__/__init__.cpython-36.pyc,,
|
||||
apscheduler/schedulers/__pycache__/asyncio.cpython-36.pyc,,
|
||||
apscheduler/schedulers/__pycache__/background.cpython-36.pyc,,
|
||||
apscheduler/schedulers/__pycache__/base.cpython-36.pyc,,
|
||||
apscheduler/schedulers/__pycache__/blocking.cpython-36.pyc,,
|
||||
apscheduler/schedulers/__pycache__/gevent.cpython-36.pyc,,
|
||||
apscheduler/schedulers/__pycache__/qt.cpython-36.pyc,,
|
||||
apscheduler/schedulers/__pycache__/tornado.cpython-36.pyc,,
|
||||
apscheduler/schedulers/__pycache__/twisted.cpython-36.pyc,,
|
||||
apscheduler/schedulers/__pycache__/__init__.cpython-36.pyc,,
|
||||
apscheduler/triggers/cron/__pycache__/expressions.cpython-36.pyc,,
|
||||
apscheduler/triggers/cron/__pycache__/fields.cpython-36.pyc,,
|
||||
apscheduler/triggers/cron/__pycache__/__init__.cpython-36.pyc,,
|
||||
apscheduler/triggers/__pycache__/base.cpython-36.pyc,,
|
||||
apscheduler/triggers/__pycache__/combining.cpython-36.pyc,,
|
||||
apscheduler/triggers/__pycache__/date.cpython-36.pyc,,
|
||||
apscheduler/triggers/__pycache__/interval.cpython-36.pyc,,
|
||||
apscheduler/triggers/__pycache__/__init__.cpython-36.pyc,,
|
||||
apscheduler/__pycache__/events.cpython-36.pyc,,
|
||||
apscheduler/__pycache__/job.cpython-36.pyc,,
|
||||
apscheduler/__pycache__/util.cpython-36.pyc,,
|
||||
apscheduler/__pycache__/__init__.cpython-36.pyc,,
|
@ -1,6 +0,0 @@
|
||||
Wheel-Version: 1.0
|
||||
Generator: bdist_wheel (0.31.1)
|
||||
Root-Is-Purelib: true
|
||||
Tag: py2-none-any
|
||||
Tag: py3-none-any
|
||||
|
@ -1,24 +0,0 @@
|
||||
[apscheduler.executors]
|
||||
asyncio = apscheduler.executors.asyncio:AsyncIOExecutor [asyncio]
|
||||
debug = apscheduler.executors.debug:DebugExecutor
|
||||
gevent = apscheduler.executors.gevent:GeventExecutor [gevent]
|
||||
processpool = apscheduler.executors.pool:ProcessPoolExecutor
|
||||
threadpool = apscheduler.executors.pool:ThreadPoolExecutor
|
||||
tornado = apscheduler.executors.tornado:TornadoExecutor [tornado]
|
||||
twisted = apscheduler.executors.twisted:TwistedExecutor [twisted]
|
||||
|
||||
[apscheduler.jobstores]
|
||||
memory = apscheduler.jobstores.memory:MemoryJobStore
|
||||
mongodb = apscheduler.jobstores.mongodb:MongoDBJobStore [mongodb]
|
||||
redis = apscheduler.jobstores.redis:RedisJobStore [redis]
|
||||
rethinkdb = apscheduler.jobstores.rethinkdb:RethinkDBJobStore [rethinkdb]
|
||||
sqlalchemy = apscheduler.jobstores.sqlalchemy:SQLAlchemyJobStore [sqlalchemy]
|
||||
zookeeper = apscheduler.jobstores.zookeeper:ZooKeeperJobStore [zookeeper]
|
||||
|
||||
[apscheduler.triggers]
|
||||
and = apscheduler.triggers.combining:AndTrigger
|
||||
cron = apscheduler.triggers.cron:CronTrigger
|
||||
date = apscheduler.triggers.date:DateTrigger
|
||||
interval = apscheduler.triggers.interval:IntervalTrigger
|
||||
or = apscheduler.triggers.combining:OrTrigger
|
||||
|
@ -1 +0,0 @@
|
||||
apscheduler
|
@ -1 +0,0 @@
|
||||
pip
|
@ -1,31 +0,0 @@
|
||||
Copyright © 2010 by the Pallets team.
|
||||
|
||||
Some rights reserved.
|
||||
|
||||
Redistribution and use in source and binary forms of the software as
|
||||
well as documentation, with or without modification, are permitted
|
||||
provided that the following conditions are met:
|
||||
|
||||
* Redistributions of source code must retain the above copyright notice,
|
||||
this list of conditions and the following disclaimer.
|
||||
|
||||
* Redistributions in binary form must reproduce the above copyright
|
||||
notice, this list of conditions and the following disclaimer in the
|
||||
documentation and/or other materials provided with the distribution.
|
||||
|
||||
* Neither the name of the copyright holder nor the names of its
|
||||
contributors may be used to endorse or promote products derived from
|
||||
this software without specific prior written permission.
|
||||
|
||||
THIS SOFTWARE AND DOCUMENTATION IS PROVIDED BY THE COPYRIGHT HOLDERS AND
|
||||
CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING,
|
||||
BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND
|
||||
FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
|
||||
COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
|
||||
INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
|
||||
NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF
|
||||
USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
|
||||
ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
||||
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
|
||||
THIS SOFTWARE AND DOCUMENTATION, EVEN IF ADVISED OF THE POSSIBILITY OF
|
||||
SUCH DAMAGE.
|
@ -1,130 +0,0 @@
|
||||
Metadata-Version: 2.1
|
||||
Name: Flask
|
||||
Version: 1.0.2
|
||||
Summary: A simple framework for building complex web applications.
|
||||
Home-page: https://www.palletsprojects.com/p/flask/
|
||||
Author: Armin Ronacher
|
||||
Author-email: armin.ronacher@active-4.com
|
||||
Maintainer: Pallets team
|
||||
Maintainer-email: contact@palletsprojects.com
|
||||
License: BSD
|
||||
Project-URL: Documentation, http://flask.pocoo.org/docs/
|
||||
Project-URL: Code, https://github.com/pallets/flask
|
||||
Project-URL: Issue tracker, https://github.com/pallets/flask/issues
|
||||
Platform: any
|
||||
Classifier: Development Status :: 5 - Production/Stable
|
||||
Classifier: Environment :: Web Environment
|
||||
Classifier: Framework :: Flask
|
||||
Classifier: Intended Audience :: Developers
|
||||
Classifier: License :: OSI Approved :: BSD License
|
||||
Classifier: Operating System :: OS Independent
|
||||
Classifier: Programming Language :: Python
|
||||
Classifier: Programming Language :: Python :: 2
|
||||
Classifier: Programming Language :: Python :: 2.7
|
||||
Classifier: Programming Language :: Python :: 3
|
||||
Classifier: Programming Language :: Python :: 3.4
|
||||
Classifier: Programming Language :: Python :: 3.5
|
||||
Classifier: Programming Language :: Python :: 3.6
|
||||
Classifier: Topic :: Internet :: WWW/HTTP :: Dynamic Content
|
||||
Classifier: Topic :: Internet :: WWW/HTTP :: WSGI :: Application
|
||||
Classifier: Topic :: Software Development :: Libraries :: Application Frameworks
|
||||
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
||||
Provides-Extra: dev
|
||||
Provides-Extra: docs
|
||||
Provides-Extra: dotenv
|
||||
Requires-Dist: Werkzeug (>=0.14)
|
||||
Requires-Dist: Jinja2 (>=2.10)
|
||||
Requires-Dist: itsdangerous (>=0.24)
|
||||
Requires-Dist: click (>=5.1)
|
||||
Provides-Extra: dev
|
||||
Requires-Dist: pytest (>=3); extra == 'dev'
|
||||
Requires-Dist: coverage; extra == 'dev'
|
||||
Requires-Dist: tox; extra == 'dev'
|
||||
Requires-Dist: sphinx; extra == 'dev'
|
||||
Requires-Dist: pallets-sphinx-themes; extra == 'dev'
|
||||
Requires-Dist: sphinxcontrib-log-cabinet; extra == 'dev'
|
||||
Provides-Extra: docs
|
||||
Requires-Dist: sphinx; extra == 'docs'
|
||||
Requires-Dist: pallets-sphinx-themes; extra == 'docs'
|
||||
Requires-Dist: sphinxcontrib-log-cabinet; extra == 'docs'
|
||||
Provides-Extra: dotenv
|
||||
Requires-Dist: python-dotenv; extra == 'dotenv'
|
||||
|
||||
Flask
|
||||
=====
|
||||
|
||||
Flask is a lightweight `WSGI`_ web application framework. It is designed
|
||||
to make getting started quick and easy, with the ability to scale up to
|
||||
complex applications. It began as a simple wrapper around `Werkzeug`_
|
||||
and `Jinja`_ and has become one of the most popular Python web
|
||||
application frameworks.
|
||||
|
||||
Flask offers suggestions, but doesn't enforce any dependencies or
|
||||
project layout. It is up to the developer to choose the tools and
|
||||
libraries they want to use. There are many extensions provided by the
|
||||
community that make adding new functionality easy.
|
||||
|
||||
|
||||
Installing
|
||||
----------
|
||||
|
||||
Install and update using `pip`_:
|
||||
|
||||
.. code-block:: text
|
||||
|
||||
pip install -U Flask
|
||||
|
||||
|
||||
A Simple Example
|
||||
----------------
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
from flask import Flask
|
||||
|
||||
app = Flask(__name__)
|
||||
|
||||
@app.route('/')
|
||||
def hello():
|
||||
return 'Hello, World!'
|
||||
|
||||
.. code-block:: text
|
||||
|
||||
$ FLASK_APP=hello.py flask run
|
||||
* Serving Flask app "hello"
|
||||
* Running on http://127.0.0.1:5000/ (Press CTRL+C to quit)
|
||||
|
||||
|
||||
Donate
|
||||
------
|
||||
|
||||
The Pallets organization develops and supports Flask and the libraries
|
||||
it uses. In order to grow the community of contributors and users, and
|
||||
allow the maintainers to devote more time to the projects, `please
|
||||
donate today`_.
|
||||
|
||||
.. _please donate today: https://psfmember.org/civicrm/contribute/transact?reset=1&id=20
|
||||
|
||||
|
||||
Links
|
||||
-----
|
||||
|
||||
* Website: https://www.palletsprojects.com/p/flask/
|
||||
* Documentation: http://flask.pocoo.org/docs/
|
||||
* License: `BSD <https://github.com/pallets/flask/blob/master/LICENSE>`_
|
||||
* Releases: https://pypi.org/project/Flask/
|
||||
* Code: https://github.com/pallets/flask
|
||||
* Issue tracker: https://github.com/pallets/flask/issues
|
||||
* Test status:
|
||||
|
||||
* Linux, Mac: https://travis-ci.org/pallets/flask
|
||||
* Windows: https://ci.appveyor.com/project/pallets/flask
|
||||
|
||||
* Test coverage: https://codecov.io/gh/pallets/flask
|
||||
|
||||
.. _WSGI: https://wsgi.readthedocs.io
|
||||
.. _Werkzeug: https://www.palletsprojects.com/p/werkzeug/
|
||||
.. _Jinja: https://www.palletsprojects.com/p/jinja/
|
||||
.. _pip: https://pip.pypa.io/en/stable/quickstart/
|
||||
|
||||
|
@ -1,48 +0,0 @@
|
||||
Flask-1.0.2.dist-info/LICENSE.txt,sha256=ziEXA3AIuaiUn1qe4cd1XxCESWTYrk4TjN7Qb06J3l8,1575
|
||||
Flask-1.0.2.dist-info/METADATA,sha256=iA5tiNWzTtgCVe80aTZGNWsckj853fJyfvHs9U-WZRk,4182
|
||||
Flask-1.0.2.dist-info/RECORD,,
|
||||
Flask-1.0.2.dist-info/WHEEL,sha256=J3CsTk7Mf2JNUyhImI-mjX-fmI4oDjyiXgWT4qgZiCE,110
|
||||
Flask-1.0.2.dist-info/entry_points.txt,sha256=gBLA1aKg0OYR8AhbAfg8lnburHtKcgJLDU52BBctN0k,42
|
||||
Flask-1.0.2.dist-info/top_level.txt,sha256=dvi65F6AeGWVU0TBpYiC04yM60-FX1gJFkK31IKQr5c,6
|
||||
flask/__init__.py,sha256=qq8lK6QQbxJALf1igz7qsvUwOTAoKvFGfdLm7jPNsso,1673
|
||||
flask/__main__.py,sha256=pgIXrHhxM5MAMvgzAqWpw_t6AXZ1zG38us4JRgJKtxk,291
|
||||
flask/_compat.py,sha256=UDFGhosh6mOdNB-4evKPuneHum1OpcAlwTNJCRm0irQ,2892
|
||||
flask/app.py,sha256=ahpe3T8w98rQd_Er5d7uDxK57S1nnqGQx3V3hirBovU,94147
|
||||
flask/blueprints.py,sha256=Cyhl_x99tgwqEZPtNDJUFneAfVJxWfEU4bQA7zWS6VU,18331
|
||||
flask/cli.py,sha256=30QYAO10Do9LbZYCLgfI_xhKjASdLopL8wKKVUGS2oA,29442
|
||||
flask/config.py,sha256=kznUhj4DLYxsTF_4kfDG8GEHto1oZG_kqblyrLFtpqQ,9951
|
||||
flask/ctx.py,sha256=leFzS9fzmo0uaLCdxpHc5_iiJZ1H0X_Ig4yPCOvT--g,16224
|
||||
flask/debughelpers.py,sha256=1ceC-UyqZTd4KsJkf0OObHPsVt5R3T6vnmYhiWBjV-w,6479
|
||||
flask/globals.py,sha256=pGg72QW_-4xUfsI33I5L_y76c21AeqfSqXDcbd8wvXU,1649
|
||||
flask/helpers.py,sha256=YCl8D1plTO1evEYP4KIgaY3H8Izww5j4EdgRJ89oHTw,40106
|
||||
flask/logging.py,sha256=qV9h0vt7NIRkKM9OHDWndzO61E5CeBMlqPJyTt-W2Wc,2231
|
||||
flask/sessions.py,sha256=2XHV4ASREhSEZ8bsPQW6pNVNuFtbR-04BzfKg0AfvHo,14452
|
||||
flask/signals.py,sha256=BGQbVyCYXnzKK2DVCzppKFyWN1qmrtW1QMAYUs-1Nr8,2211
|
||||
flask/templating.py,sha256=FDfWMbpgpC3qObW8GGXRAVrkHFF8K4CHOJymB1wvULI,4914
|
||||
flask/testing.py,sha256=XD3gWNvLUV8dqVHwKd9tZzsj81fSHtjOphQ1wTNtlMs,9379
|
||||
flask/views.py,sha256=Wy-_WkUVtCfE2zCXYeJehNgHuEtviE4v3HYfJ--MpbY,5733
|
||||
flask/wrappers.py,sha256=1Z9hF5-hXQajn_58XITQFRY8efv3Vy3uZ0avBfZu6XI,7511
|
||||
flask/json/__init__.py,sha256=Ns1Hj805XIxuBMh2z0dYnMVfb_KUgLzDmP3WoUYaPhw,10729
|
||||
flask/json/tag.py,sha256=9ehzrmt5k7hxf7ZEK0NOs3swvQyU9fWNe-pnYe69N60,8223
|
||||
../../Scripts/flask.exe,sha256=ljrIXQzU8f6egd4aaMAsVCEY1T9OUzAT7Epn4Xl52NA,102766
|
||||
Flask-1.0.2.dist-info/INSTALLER,sha256=zuuue4knoyJ-UwPPXg8fezS7VCrXJQrAP7zeNuwvFQg,4
|
||||
flask/json/__pycache__/tag.cpython-36.pyc,,
|
||||
flask/json/__pycache__/__init__.cpython-36.pyc,,
|
||||
flask/__pycache__/app.cpython-36.pyc,,
|
||||
flask/__pycache__/blueprints.cpython-36.pyc,,
|
||||
flask/__pycache__/cli.cpython-36.pyc,,
|
||||
flask/__pycache__/config.cpython-36.pyc,,
|
||||
flask/__pycache__/ctx.cpython-36.pyc,,
|
||||
flask/__pycache__/debughelpers.cpython-36.pyc,,
|
||||
flask/__pycache__/globals.cpython-36.pyc,,
|
||||
flask/__pycache__/helpers.cpython-36.pyc,,
|
||||
flask/__pycache__/logging.cpython-36.pyc,,
|
||||
flask/__pycache__/sessions.cpython-36.pyc,,
|
||||
flask/__pycache__/signals.cpython-36.pyc,,
|
||||
flask/__pycache__/templating.cpython-36.pyc,,
|
||||
flask/__pycache__/testing.cpython-36.pyc,,
|
||||
flask/__pycache__/views.cpython-36.pyc,,
|
||||
flask/__pycache__/wrappers.cpython-36.pyc,,
|
||||
flask/__pycache__/_compat.cpython-36.pyc,,
|
||||
flask/__pycache__/__init__.cpython-36.pyc,,
|
||||
flask/__pycache__/__main__.cpython-36.pyc,,
|
@ -1,6 +0,0 @@
|
||||
Wheel-Version: 1.0
|
||||
Generator: bdist_wheel (0.31.0)
|
||||
Root-Is-Purelib: true
|
||||
Tag: py2-none-any
|
||||
Tag: py3-none-any
|
||||
|
@ -1,3 +0,0 @@
|
||||
[console_scripts]
|
||||
flask = flask.cli:main
|
||||
|
@ -1 +0,0 @@
|
||||
flask
|
@ -1,33 +0,0 @@
|
||||
Metadata-Version: 1.1
|
||||
Name: Flask-JWT
|
||||
Version: 0.3.2
|
||||
Summary: JWT token authentication for Flask apps
|
||||
Home-page: https://github.com/mattupstate/flask-jwt
|
||||
Author: Matt Wright
|
||||
Author-email: matt@nobien.net
|
||||
License: MIT
|
||||
Description:
|
||||
Flask-JWT
|
||||
=========
|
||||
|
||||
Flask-JWT is a Flask extension that adds basic Json Web Token features to any application.
|
||||
|
||||
Resources
|
||||
---------
|
||||
|
||||
* `Documentation <http://packages.python.org/Flask-JWT/>`_
|
||||
* `Issue Tracker <https://github.com/mattupstate/flask-jwt/issues>`_
|
||||
* `Source <https://github.com/mattupstate/flask-jwt>`_
|
||||
* `Development Version
|
||||
<https://github.com/mattupstate/flask-jwt/raw/develop#egg=Flask-JWT-dev>`_
|
||||
|
||||
|
||||
Platform: any
|
||||
Classifier: Development Status :: 4 - Beta
|
||||
Classifier: Environment :: Web Environment
|
||||
Classifier: Intended Audience :: Developers
|
||||
Classifier: License :: OSI Approved :: MIT License
|
||||
Classifier: Operating System :: OS Independent
|
||||
Classifier: Programming Language :: Python
|
||||
Classifier: Topic :: Internet :: WWW/HTTP :: Dynamic Content
|
||||
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
@ -1,19 +0,0 @@
|
||||
CHANGES
|
||||
LICENSE
|
||||
MANIFEST.in
|
||||
README.rst
|
||||
requirements-dev.txt
|
||||
requirements.txt
|
||||
setup.cfg
|
||||
setup.py
|
||||
tox.ini
|
||||
Flask_JWT.egg-info/PKG-INFO
|
||||
Flask_JWT.egg-info/SOURCES.txt
|
||||
Flask_JWT.egg-info/dependency_links.txt
|
||||
Flask_JWT.egg-info/not-zip-safe
|
||||
Flask_JWT.egg-info/requires.txt
|
||||
Flask_JWT.egg-info/top_level.txt
|
||||
flask_jwt/__init__.py
|
||||
tests/conftest.py
|
||||
tests/conftest.pyc
|
||||
tests/test_jwt.py
|
@ -1 +0,0 @@
|
||||
|
@ -1,8 +0,0 @@
|
||||
..\flask_jwt\__init__.py
|
||||
..\flask_jwt\__pycache__\__init__.cpython-36.pyc
|
||||
PKG-INFO
|
||||
SOURCES.txt
|
||||
dependency_links.txt
|
||||
not-zip-safe
|
||||
requires.txt
|
||||
top_level.txt
|
@ -1 +0,0 @@
|
||||
|
@ -1,2 +0,0 @@
|
||||
Flask>=0.9
|
||||
PyJWT<1.5.0,>=1.4.0
|
@ -1 +0,0 @@
|
||||
flask_jwt
|
@ -1,25 +0,0 @@
|
||||
Metadata-Version: 2.1
|
||||
Name: Flask-JWT-Extended
|
||||
Version: 3.8.1
|
||||
Summary: Extended JWT integration with Flask
|
||||
Home-page: https://github.com/vimalloc/flask-jwt-extended
|
||||
Author: Landon Gilbert-Bland
|
||||
Author-email: landogbland@gmail.com
|
||||
License: MIT
|
||||
Description: Extended JWT integration with Flask
|
||||
Keywords: flask,jwt,json web token
|
||||
Platform: any
|
||||
Classifier: Development Status :: 5 - Production/Stable
|
||||
Classifier: Environment :: Web Environment
|
||||
Classifier: Intended Audience :: Developers
|
||||
Classifier: License :: OSI Approved :: MIT License
|
||||
Classifier: Operating System :: OS Independent
|
||||
Classifier: Programming Language :: Python
|
||||
Classifier: Programming Language :: Python :: 2.7
|
||||
Classifier: Programming Language :: Python :: 3
|
||||
Classifier: Programming Language :: Python :: 3.4
|
||||
Classifier: Programming Language :: Python :: 3.5
|
||||
Classifier: Programming Language :: Python :: 3.6
|
||||
Classifier: Topic :: Internet :: WWW/HTTP :: Dynamic Content
|
||||
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
||||
Provides-Extra: asymmetric_crypto
|
@ -1,20 +0,0 @@
|
||||
LICENSE
|
||||
MANIFEST.in
|
||||
README.md
|
||||
requirements.txt
|
||||
setup.cfg
|
||||
setup.py
|
||||
Flask_JWT_Extended.egg-info/PKG-INFO
|
||||
Flask_JWT_Extended.egg-info/SOURCES.txt
|
||||
Flask_JWT_Extended.egg-info/dependency_links.txt
|
||||
Flask_JWT_Extended.egg-info/not-zip-safe
|
||||
Flask_JWT_Extended.egg-info/requires.txt
|
||||
Flask_JWT_Extended.egg-info/top_level.txt
|
||||
flask_jwt_extended/__init__.py
|
||||
flask_jwt_extended/config.py
|
||||
flask_jwt_extended/default_callbacks.py
|
||||
flask_jwt_extended/exceptions.py
|
||||
flask_jwt_extended/jwt_manager.py
|
||||
flask_jwt_extended/tokens.py
|
||||
flask_jwt_extended/utils.py
|
||||
flask_jwt_extended/view_decorators.py
|
@ -1 +0,0 @@
|
||||
|
@ -1,22 +0,0 @@
|
||||
..\flask_jwt_extended\__init__.py
|
||||
..\flask_jwt_extended\__pycache__\__init__.cpython-36.pyc
|
||||
..\flask_jwt_extended\__pycache__\config.cpython-36.pyc
|
||||
..\flask_jwt_extended\__pycache__\default_callbacks.cpython-36.pyc
|
||||
..\flask_jwt_extended\__pycache__\exceptions.cpython-36.pyc
|
||||
..\flask_jwt_extended\__pycache__\jwt_manager.cpython-36.pyc
|
||||
..\flask_jwt_extended\__pycache__\tokens.cpython-36.pyc
|
||||
..\flask_jwt_extended\__pycache__\utils.cpython-36.pyc
|
||||
..\flask_jwt_extended\__pycache__\view_decorators.cpython-36.pyc
|
||||
..\flask_jwt_extended\config.py
|
||||
..\flask_jwt_extended\default_callbacks.py
|
||||
..\flask_jwt_extended\exceptions.py
|
||||
..\flask_jwt_extended\jwt_manager.py
|
||||
..\flask_jwt_extended\tokens.py
|
||||
..\flask_jwt_extended\utils.py
|
||||
..\flask_jwt_extended\view_decorators.py
|
||||
PKG-INFO
|
||||
SOURCES.txt
|
||||
dependency_links.txt
|
||||
not-zip-safe
|
||||
requires.txt
|
||||
top_level.txt
|
@ -1 +0,0 @@
|
||||
|
@ -1,6 +0,0 @@
|
||||
Werkzeug>=0.14
|
||||
Flask
|
||||
PyJWT
|
||||
|
||||
[asymmetric_crypto]
|
||||
cryptography
|
@ -1 +0,0 @@
|
||||
flask_jwt_extended
|
@ -1,3 +0,0 @@
|
||||
UNKNOWN
|
||||
|
||||
|
@ -1 +0,0 @@
|
||||
pip
|
@ -1,31 +0,0 @@
|
||||
Metadata-Version: 2.0
|
||||
Name: Flask-RESTful
|
||||
Version: 0.3.6
|
||||
Summary: Simple framework for creating REST APIs
|
||||
Home-page: https://www.github.com/flask-restful/flask-restful/
|
||||
Author: Twilio API Team
|
||||
Author-email: help@twilio.com
|
||||
License: BSD
|
||||
Platform: any
|
||||
Classifier: Framework :: Flask
|
||||
Classifier: Programming Language :: Python :: 2
|
||||
Classifier: Programming Language :: Python :: 2.6
|
||||
Classifier: Programming Language :: Python :: 2.7
|
||||
Classifier: Programming Language :: Python :: 3
|
||||
Classifier: Programming Language :: Python :: 3.3
|
||||
Classifier: Programming Language :: Python :: 3.4
|
||||
Classifier: Programming Language :: Python :: 3.5
|
||||
Classifier: Programming Language :: Python :: 3.6
|
||||
Classifier: License :: OSI Approved :: BSD License
|
||||
Requires-Dist: Flask (>=0.8)
|
||||
Requires-Dist: aniso8601 (>=0.82)
|
||||
Requires-Dist: pytz
|
||||
Requires-Dist: six (>=1.3.0)
|
||||
Provides-Extra: docs
|
||||
Requires-Dist: sphinx; extra == 'docs'
|
||||
Provides-Extra: paging
|
||||
Requires-Dist: pycrypto (>=2.6); extra == 'paging'
|
||||
|
||||
UNKNOWN
|
||||
|
||||
|
@ -1,29 +0,0 @@
|
||||
Flask_RESTful-0.3.6.dist-info/DESCRIPTION.rst,sha256=OCTuuN6LcWulhHS3d5rfjdsQtW22n7HENFRh6jC6ego,10
|
||||
Flask_RESTful-0.3.6.dist-info/METADATA,sha256=THyvrIu4SUuC9fqpfDBP-m6kdZcesFu2-Gm2oxgJnWM,985
|
||||
Flask_RESTful-0.3.6.dist-info/RECORD,,
|
||||
Flask_RESTful-0.3.6.dist-info/WHEEL,sha256=o2k-Qa-RMNIJmUdIc7KU6VWR_ErNRbWNlxDIpl7lm34,110
|
||||
Flask_RESTful-0.3.6.dist-info/metadata.json,sha256=1zKAyvYdpplkpX_NJU8vKCZ9nvy0Y_paxRiMJXCNiXU,1178
|
||||
Flask_RESTful-0.3.6.dist-info/top_level.txt,sha256=lNpWPlejgBAtMhCUwz_FTyJH12ul1mBZ-Uv3ZK1HiGg,14
|
||||
flask_restful/__init__.py,sha256=dpZuahv5GMo2Rof4OWkGpQvIhnJIeOakL_1EHkkzUA4,28510
|
||||
flask_restful/__version__.py,sha256=-RHjzBqfdsudCsVzQ9u-AoX7n-_90gcO1RlvPEVk7d4,45
|
||||
flask_restful/fields.py,sha256=9cbc0vXaGzt1Ur8gUf3sHlMjNSy1qOSV_qvi-3YQAL8,13051
|
||||
flask_restful/inputs.py,sha256=AOBF_1BpB8snY3XJT_vca_uWYzkCP2nla01lFU7xqLE,9114
|
||||
flask_restful/paging.py,sha256=H_nL-UlViLSHJhS7NbHJurHMwzxv7IsaAnuTC4L5Kec,1207
|
||||
flask_restful/reqparse.py,sha256=I9qxtSqjVzrDTxQtfSHM4yGx3XsiVoa-wMPlA1mod9s,13475
|
||||
flask_restful/representations/__init__.py,sha256=AbpHGcgLb-kRsJGnwFEktk7uzpZOCcBY74-YBdrKVGs,1
|
||||
flask_restful/representations/json.py,sha256=swKwnbt7v2ioHfHkqhqbzIu_yrcP0ComlSl49IGFJOo,873
|
||||
flask_restful/utils/__init__.py,sha256=Qh5pyCIT2dfHmrUdS6lsMbBLjZmAhz1fl7vWyJ_n4XQ,719
|
||||
flask_restful/utils/cors.py,sha256=cZiqaHhIn0w66spRoSIdC-jIn4X_b6OlVms5eGF4Ess,2084
|
||||
flask_restful/utils/crypto.py,sha256=q3PBvAYMJYybbqqQlKNF_Pqeyo9h3x5jFJuVqtEA5bA,996
|
||||
Flask_RESTful-0.3.6.dist-info/INSTALLER,sha256=zuuue4knoyJ-UwPPXg8fezS7VCrXJQrAP7zeNuwvFQg,4
|
||||
flask_restful/representations/__pycache__/json.cpython-36.pyc,,
|
||||
flask_restful/representations/__pycache__/__init__.cpython-36.pyc,,
|
||||
flask_restful/utils/__pycache__/cors.cpython-36.pyc,,
|
||||
flask_restful/utils/__pycache__/crypto.cpython-36.pyc,,
|
||||
flask_restful/utils/__pycache__/__init__.cpython-36.pyc,,
|
||||
flask_restful/__pycache__/fields.cpython-36.pyc,,
|
||||
flask_restful/__pycache__/inputs.cpython-36.pyc,,
|
||||
flask_restful/__pycache__/paging.cpython-36.pyc,,
|
||||
flask_restful/__pycache__/reqparse.cpython-36.pyc,,
|
||||
flask_restful/__pycache__/__init__.cpython-36.pyc,,
|
||||
flask_restful/__pycache__/__version__.cpython-36.pyc,,
|
@ -1,6 +0,0 @@
|
||||
Wheel-Version: 1.0
|
||||
Generator: bdist_wheel (0.29.0)
|
||||
Root-Is-Purelib: true
|
||||
Tag: py2-none-any
|
||||
Tag: py3-none-any
|
||||
|
@ -1 +0,0 @@
|
||||
{"classifiers": ["Framework :: Flask", "Programming Language :: Python :: 2", "Programming Language :: Python :: 2.6", "Programming Language :: Python :: 2.7", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3.3", "Programming Language :: Python :: 3.4", "Programming Language :: Python :: 3.5", "Programming Language :: Python :: 3.6", "License :: OSI Approved :: BSD License"], "extensions": {"python.details": {"contacts": [{"email": "help@twilio.com", "name": "Twilio API Team", "role": "author"}], "document_names": {"description": "DESCRIPTION.rst"}, "project_urls": {"Home": "https://www.github.com/flask-restful/flask-restful/"}}}, "extras": ["docs", "paging"], "generator": "bdist_wheel (0.29.0)", "license": "BSD", "metadata_version": "2.0", "name": "Flask-RESTful", "platform": "any", "run_requires": [{"requires": ["Flask (>=0.8)", "aniso8601 (>=0.82)", "pytz", "six (>=1.3.0)"]}, {"extra": "paging", "requires": ["pycrypto (>=2.6)"]}, {"extra": "docs", "requires": ["sphinx"]}], "summary": "Simple framework for creating REST APIs", "test_requires": [{"requires": ["Flask-RESTful[paging]", "blinker", "mock (>=0.8)"]}], "version": "0.3.6"}
|
@ -1 +0,0 @@
|
||||
flask_restful
|
@ -1,37 +0,0 @@
|
||||
|
||||
Jinja2
|
||||
~~~~~~
|
||||
|
||||
Jinja2 is a template engine written in pure Python. It provides a
|
||||
`Django`_ inspired non-XML syntax but supports inline expressions and
|
||||
an optional `sandboxed`_ environment.
|
||||
|
||||
Nutshell
|
||||
--------
|
||||
|
||||
Here a small example of a Jinja template::
|
||||
|
||||
{% extends 'base.html' %}
|
||||
{% block title %}Memberlist{% endblock %}
|
||||
{% block content %}
|
||||
<ul>
|
||||
{% for user in users %}
|
||||
<li><a href="{{ user.url }}">{{ user.username }}</a></li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
{% endblock %}
|
||||
|
||||
Philosophy
|
||||
----------
|
||||
|
||||
Application logic is for the controller but don't try to make the life
|
||||
for the template designer too hard by giving him too few functionality.
|
||||
|
||||
For more informations visit the new `Jinja2 webpage`_ and `documentation`_.
|
||||
|
||||
.. _sandboxed: https://en.wikipedia.org/wiki/Sandbox_(computer_security)
|
||||
.. _Django: https://www.djangoproject.com/
|
||||
.. _Jinja2 webpage: http://jinja.pocoo.org/
|
||||
.. _documentation: http://jinja.pocoo.org/2/documentation/
|
||||
|
||||
|
@ -1 +0,0 @@
|
||||
pip
|
@ -1,31 +0,0 @@
|
||||
Copyright (c) 2009 by the Jinja Team, see AUTHORS for more details.
|
||||
|
||||
Some rights reserved.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without
|
||||
modification, are permitted provided that the following conditions are
|
||||
met:
|
||||
|
||||
* Redistributions of source code must retain the above copyright
|
||||
notice, this list of conditions and the following disclaimer.
|
||||
|
||||
* Redistributions in binary form must reproduce the above
|
||||
copyright notice, this list of conditions and the following
|
||||
disclaimer in the documentation and/or other materials provided
|
||||
with the distribution.
|
||||
|
||||
* The names of the contributors may not be used to endorse or
|
||||
promote products derived from this software without specific
|
||||
prior written permission.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
|
||||
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
|
||||
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
|
||||
A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
|
||||
OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
|
||||
SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
|
||||
LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
|
||||
DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
|
||||
THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
||||
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
@ -1,68 +0,0 @@
|
||||
Metadata-Version: 2.0
|
||||
Name: Jinja2
|
||||
Version: 2.10
|
||||
Summary: A small but fast and easy to use stand-alone template engine written in pure python.
|
||||
Home-page: http://jinja.pocoo.org/
|
||||
Author: Armin Ronacher
|
||||
Author-email: armin.ronacher@active-4.com
|
||||
License: BSD
|
||||
Description-Content-Type: UNKNOWN
|
||||
Platform: UNKNOWN
|
||||
Classifier: Development Status :: 5 - Production/Stable
|
||||
Classifier: Environment :: Web Environment
|
||||
Classifier: Intended Audience :: Developers
|
||||
Classifier: License :: OSI Approved :: BSD License
|
||||
Classifier: Operating System :: OS Independent
|
||||
Classifier: Programming Language :: Python
|
||||
Classifier: Programming Language :: Python :: 2
|
||||
Classifier: Programming Language :: Python :: 2.6
|
||||
Classifier: Programming Language :: Python :: 2.7
|
||||
Classifier: Programming Language :: Python :: 3
|
||||
Classifier: Programming Language :: Python :: 3.3
|
||||
Classifier: Programming Language :: Python :: 3.4
|
||||
Classifier: Programming Language :: Python :: 3.5
|
||||
Classifier: Programming Language :: Python :: 3.6
|
||||
Classifier: Topic :: Internet :: WWW/HTTP :: Dynamic Content
|
||||
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
||||
Classifier: Topic :: Text Processing :: Markup :: HTML
|
||||
Requires-Dist: MarkupSafe (>=0.23)
|
||||
Provides-Extra: i18n
|
||||
Requires-Dist: Babel (>=0.8); extra == 'i18n'
|
||||
|
||||
|
||||
Jinja2
|
||||
~~~~~~
|
||||
|
||||
Jinja2 is a template engine written in pure Python. It provides a
|
||||
`Django`_ inspired non-XML syntax but supports inline expressions and
|
||||
an optional `sandboxed`_ environment.
|
||||
|
||||
Nutshell
|
||||
--------
|
||||
|
||||
Here a small example of a Jinja template::
|
||||
|
||||
{% extends 'base.html' %}
|
||||
{% block title %}Memberlist{% endblock %}
|
||||
{% block content %}
|
||||
<ul>
|
||||
{% for user in users %}
|
||||
<li><a href="{{ user.url }}">{{ user.username }}</a></li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
{% endblock %}
|
||||
|
||||
Philosophy
|
||||
----------
|
||||
|
||||
Application logic is for the controller but don't try to make the life
|
||||
for the template designer too hard by giving him too few functionality.
|
||||
|
||||
For more informations visit the new `Jinja2 webpage`_ and `documentation`_.
|
||||
|
||||
.. _sandboxed: https://en.wikipedia.org/wiki/Sandbox_(computer_security)
|
||||
.. _Django: https://www.djangoproject.com/
|
||||
.. _Jinja2 webpage: http://jinja.pocoo.org/
|
||||
.. _documentation: http://jinja.pocoo.org/2/documentation/
|
||||
|
||||
|
@ -1,63 +0,0 @@
|
||||
Jinja2-2.10.dist-info/DESCRIPTION.rst,sha256=b5ckFDoM7vVtz_mAsJD4OPteFKCqE7beu353g4COoYI,978
|
||||
Jinja2-2.10.dist-info/LICENSE.txt,sha256=JvzUNv3Io51EiWrAPm8d_SXjhJnEjyDYvB3Tvwqqils,1554
|
||||
Jinja2-2.10.dist-info/METADATA,sha256=18EgU8zR6-av-0-5y_gXebzK4GnBB_76lALUsl-6QHM,2258
|
||||
Jinja2-2.10.dist-info/RECORD,,
|
||||
Jinja2-2.10.dist-info/WHEEL,sha256=kdsN-5OJAZIiHN-iO4Rhl82KyS0bDWf4uBwMbkNafr8,110
|
||||
Jinja2-2.10.dist-info/entry_points.txt,sha256=NdzVcOrqyNyKDxD09aERj__3bFx2paZhizFDsKmVhiA,72
|
||||
Jinja2-2.10.dist-info/metadata.json,sha256=NPUJ9TMBxVQAv_kTJzvU8HwmP-4XZvbK9mz6_4YUVl4,1473
|
||||
Jinja2-2.10.dist-info/top_level.txt,sha256=PkeVWtLb3-CqjWi1fO29OCbj55EhX_chhKrCdrVe_zs,7
|
||||
jinja2/__init__.py,sha256=xJHjaMoy51_KXn1wf0cysH6tUUifUxZCwSOfcJGEYZw,2614
|
||||
jinja2/_compat.py,sha256=xP60CE5Qr8FTYcDE1f54tbZLKGvMwYml4-8T7Q4KG9k,2596
|
||||
jinja2/_identifier.py,sha256=W1QBSY-iJsyt6oR_nKSuNNCzV95vLIOYgUNPUI1d5gU,1726
|
||||
jinja2/asyncfilters.py,sha256=cTDPvrS8Hp_IkwsZ1m9af_lr5nHysw7uTa5gV0NmZVE,4144
|
||||
jinja2/asyncsupport.py,sha256=UErQ3YlTLaSjFb94P4MVn08-aVD9jJxty2JVfMRb-1M,7878
|
||||
jinja2/bccache.py,sha256=nQldx0ZRYANMyfvOihRoYFKSlUdd5vJkS7BjxNwlOZM,12794
|
||||
jinja2/compiler.py,sha256=BqC5U6JxObSRhblyT_a6Tp5GtEU5z3US1a4jLQaxxgo,65386
|
||||
jinja2/constants.py,sha256=uwwV8ZUhHhacAuz5PTwckfsbqBaqM7aKfyJL7kGX5YQ,1626
|
||||
jinja2/debug.py,sha256=WTVeUFGUa4v6ReCsYv-iVPa3pkNB75OinJt3PfxNdXs,12045
|
||||
jinja2/defaults.py,sha256=Em-95hmsJxIenDCZFB1YSvf9CNhe9rBmytN3yUrBcWA,1400
|
||||
jinja2/environment.py,sha256=VnkAkqw8JbjZct4tAyHlpBrka2vqB-Z58RAP-32P1ZY,50849
|
||||
jinja2/exceptions.py,sha256=_Rj-NVi98Q6AiEjYQOsP8dEIdu5AlmRHzcSNOPdWix4,4428
|
||||
jinja2/ext.py,sha256=atMQydEC86tN1zUsdQiHw5L5cF62nDbqGue25Yiu3N4,24500
|
||||
jinja2/filters.py,sha256=yOAJk0MsH-_gEC0i0U6NweVQhbtYaC-uE8xswHFLF4w,36528
|
||||
jinja2/idtracking.py,sha256=2GbDSzIvGArEBGLkovLkqEfmYxmWsEf8c3QZwM4uNsw,9197
|
||||
jinja2/lexer.py,sha256=ySEPoXd1g7wRjsuw23uimS6nkGN5aqrYwcOKxCaVMBQ,28559
|
||||
jinja2/loaders.py,sha256=xiTuURKAEObyym0nU8PCIXu_Qp8fn0AJ5oIADUUm-5Q,17382
|
||||
jinja2/meta.py,sha256=fmKHxkmZYAOm9QyWWy8EMd6eefAIh234rkBMW2X4ZR8,4340
|
||||
jinja2/nativetypes.py,sha256=_sJhS8f-8Q0QMIC0dm1YEdLyxEyoO-kch8qOL5xUDfE,7308
|
||||
jinja2/nodes.py,sha256=L10L_nQDfubLhO3XjpF9qz46FSh2clL-3e49ogVlMmA,30853
|
||||
jinja2/optimizer.py,sha256=MsdlFACJ0FRdPtjmCAdt7JQ9SGrXFaDNUaslsWQaG3M,1722
|
||||
jinja2/parser.py,sha256=lPzTEbcpTRBLw8ii6OYyExHeAhaZLMA05Hpv4ll3ULk,35875
|
||||
jinja2/runtime.py,sha256=DHdD38Pq8gj7uWQC5usJyWFoNWL317A9AvXOW_CLB34,27755
|
||||
jinja2/sandbox.py,sha256=TVyZHlNqqTzsv9fv2NvJNmSdWRHTguhyMHdxjWms32U,16708
|
||||
jinja2/tests.py,sha256=iJQLwbapZr-EKquTG_fVOVdwHUUKf3SX9eNkjQDF8oU,4237
|
||||
jinja2/utils.py,sha256=q24VupGZotQ-uOyrJxCaXtDWhZC1RgsQG7kcdmjck2Q,20629
|
||||
jinja2/visitor.py,sha256=JD1H1cANA29JcntFfN5fPyqQxB4bI4wC00BzZa-XHks,3316
|
||||
Jinja2-2.10.dist-info/INSTALLER,sha256=zuuue4knoyJ-UwPPXg8fezS7VCrXJQrAP7zeNuwvFQg,4
|
||||
jinja2/__pycache__/asyncfilters.cpython-36.pyc,,
|
||||
jinja2/__pycache__/asyncsupport.cpython-36.pyc,,
|
||||
jinja2/__pycache__/bccache.cpython-36.pyc,,
|
||||
jinja2/__pycache__/compiler.cpython-36.pyc,,
|
||||
jinja2/__pycache__/constants.cpython-36.pyc,,
|
||||
jinja2/__pycache__/debug.cpython-36.pyc,,
|
||||
jinja2/__pycache__/defaults.cpython-36.pyc,,
|
||||
jinja2/__pycache__/environment.cpython-36.pyc,,
|
||||
jinja2/__pycache__/exceptions.cpython-36.pyc,,
|
||||
jinja2/__pycache__/ext.cpython-36.pyc,,
|
||||
jinja2/__pycache__/filters.cpython-36.pyc,,
|
||||
jinja2/__pycache__/idtracking.cpython-36.pyc,,
|
||||
jinja2/__pycache__/lexer.cpython-36.pyc,,
|
||||
jinja2/__pycache__/loaders.cpython-36.pyc,,
|
||||
jinja2/__pycache__/meta.cpython-36.pyc,,
|
||||
jinja2/__pycache__/nativetypes.cpython-36.pyc,,
|
||||
jinja2/__pycache__/nodes.cpython-36.pyc,,
|
||||
jinja2/__pycache__/optimizer.cpython-36.pyc,,
|
||||
jinja2/__pycache__/parser.cpython-36.pyc,,
|
||||
jinja2/__pycache__/runtime.cpython-36.pyc,,
|
||||
jinja2/__pycache__/sandbox.cpython-36.pyc,,
|
||||
jinja2/__pycache__/tests.cpython-36.pyc,,
|
||||
jinja2/__pycache__/utils.cpython-36.pyc,,
|
||||
jinja2/__pycache__/visitor.cpython-36.pyc,,
|
||||
jinja2/__pycache__/_compat.cpython-36.pyc,,
|
||||
jinja2/__pycache__/_identifier.cpython-36.pyc,,
|
||||
jinja2/__pycache__/__init__.cpython-36.pyc,,
|
@ -1,6 +0,0 @@
|
||||
Wheel-Version: 1.0
|
||||
Generator: bdist_wheel (0.30.0)
|
||||
Root-Is-Purelib: true
|
||||
Tag: py2-none-any
|
||||
Tag: py3-none-any
|
||||
|
@ -1,4 +0,0 @@
|
||||
|
||||
[babel.extractors]
|
||||
jinja2 = jinja2.ext:babel_extract[i18n]
|
||||
|
@ -1 +0,0 @@
|
||||
{"classifiers": ["Development Status :: 5 - Production/Stable", "Environment :: Web Environment", "Intended Audience :: Developers", "License :: OSI Approved :: BSD License", "Operating System :: OS Independent", "Programming Language :: Python", "Programming Language :: Python :: 2", "Programming Language :: Python :: 2.6", "Programming Language :: Python :: 2.7", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3.3", "Programming Language :: Python :: 3.4", "Programming Language :: Python :: 3.5", "Programming Language :: Python :: 3.6", "Topic :: Internet :: WWW/HTTP :: Dynamic Content", "Topic :: Software Development :: Libraries :: Python Modules", "Topic :: Text Processing :: Markup :: HTML"], "description_content_type": "UNKNOWN", "extensions": {"python.details": {"contacts": [{"email": "armin.ronacher@active-4.com", "name": "Armin Ronacher", "role": "author"}], "document_names": {"description": "DESCRIPTION.rst", "license": "LICENSE.txt"}, "project_urls": {"Home": "http://jinja.pocoo.org/"}}, "python.exports": {"babel.extractors": {"jinja2": "jinja2.ext:babel_extract [i18n]"}}}, "extras": ["i18n"], "generator": "bdist_wheel (0.30.0)", "license": "BSD", "metadata_version": "2.0", "name": "Jinja2", "run_requires": [{"extra": "i18n", "requires": ["Babel (>=0.8)"]}, {"requires": ["MarkupSafe (>=0.23)"]}], "summary": "A small but fast and easy to use stand-alone template engine written in pure python.", "version": "2.10"}
|
@ -1 +0,0 @@
|
||||
jinja2
|
@ -1,133 +0,0 @@
|
||||
Metadata-Version: 1.1
|
||||
Name: MarkupSafe
|
||||
Version: 1.0
|
||||
Summary: Implements a XML/HTML/XHTML Markup safe string for Python
|
||||
Home-page: http://github.com/pallets/markupsafe
|
||||
Author: Armin Ronacher
|
||||
Author-email: armin.ronacher@active-4.com
|
||||
License: BSD
|
||||
Description: MarkupSafe
|
||||
==========
|
||||
|
||||
Implements a unicode subclass that supports HTML strings:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
>>> from markupsafe import Markup, escape
|
||||
>>> escape("<script>alert(document.cookie);</script>")
|
||||
Markup(u'<script>alert(document.cookie);</script>')
|
||||
>>> tmpl = Markup("<em>%s</em>")
|
||||
>>> tmpl % "Peter > Lustig"
|
||||
Markup(u'<em>Peter > Lustig</em>')
|
||||
|
||||
If you want to make an object unicode that is not yet unicode
|
||||
but don't want to lose the taint information, you can use the
|
||||
``soft_unicode`` function. (On Python 3 you can also use ``soft_str`` which
|
||||
is a different name for the same function).
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
>>> from markupsafe import soft_unicode
|
||||
>>> soft_unicode(42)
|
||||
u'42'
|
||||
>>> soft_unicode(Markup('foo'))
|
||||
Markup(u'foo')
|
||||
|
||||
HTML Representations
|
||||
--------------------
|
||||
|
||||
Objects can customize their HTML markup equivalent by overriding
|
||||
the ``__html__`` function:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
>>> class Foo(object):
|
||||
... def __html__(self):
|
||||
... return '<strong>Nice</strong>'
|
||||
...
|
||||
>>> escape(Foo())
|
||||
Markup(u'<strong>Nice</strong>')
|
||||
>>> Markup(Foo())
|
||||
Markup(u'<strong>Nice</strong>')
|
||||
|
||||
Silent Escapes
|
||||
--------------
|
||||
|
||||
Since MarkupSafe 0.10 there is now also a separate escape function
|
||||
called ``escape_silent`` that returns an empty string for ``None`` for
|
||||
consistency with other systems that return empty strings for ``None``
|
||||
when escaping (for instance Pylons' webhelpers).
|
||||
|
||||
If you also want to use this for the escape method of the Markup
|
||||
object, you can create your own subclass that does that:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
from markupsafe import Markup, escape_silent as escape
|
||||
|
||||
class SilentMarkup(Markup):
|
||||
__slots__ = ()
|
||||
|
||||
@classmethod
|
||||
def escape(cls, s):
|
||||
return cls(escape(s))
|
||||
|
||||
New-Style String Formatting
|
||||
---------------------------
|
||||
|
||||
Starting with MarkupSafe 0.21 new style string formats from Python 2.6 and
|
||||
3.x are now fully supported. Previously the escape behavior of those
|
||||
functions was spotty at best. The new implementations operates under the
|
||||
following algorithm:
|
||||
|
||||
1. if an object has an ``__html_format__`` method it is called as
|
||||
replacement for ``__format__`` with the format specifier. It either
|
||||
has to return a string or markup object.
|
||||
2. if an object has an ``__html__`` method it is called.
|
||||
3. otherwise the default format system of Python kicks in and the result
|
||||
is HTML escaped.
|
||||
|
||||
Here is how you can implement your own formatting:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
class User(object):
|
||||
|
||||
def __init__(self, id, username):
|
||||
self.id = id
|
||||
self.username = username
|
||||
|
||||
def __html_format__(self, format_spec):
|
||||
if format_spec == 'link':
|
||||
return Markup('<a href="/user/{0}">{1}</a>').format(
|
||||
self.id,
|
||||
self.__html__(),
|
||||
)
|
||||
elif format_spec:
|
||||
raise ValueError('Invalid format spec')
|
||||
return self.__html__()
|
||||
|
||||
def __html__(self):
|
||||
return Markup('<span class=user>{0}</span>').format(self.username)
|
||||
|
||||
And to format that user:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
>>> user = User(1, 'foo')
|
||||
>>> Markup('<p>User: {0:link}').format(user)
|
||||
Markup(u'<p>User: <a href="/user/1"><span class=user>foo</span></a>')
|
||||
|
||||
Markupsafe supports Python 2.6, 2.7 and Python 3.3 and higher.
|
||||
|
||||
Platform: UNKNOWN
|
||||
Classifier: Development Status :: 5 - Production/Stable
|
||||
Classifier: Environment :: Web Environment
|
||||
Classifier: Intended Audience :: Developers
|
||||
Classifier: License :: OSI Approved :: BSD License
|
||||
Classifier: Operating System :: OS Independent
|
||||
Classifier: Programming Language :: Python
|
||||
Classifier: Programming Language :: Python :: 3
|
||||
Classifier: Topic :: Internet :: WWW/HTTP :: Dynamic Content
|
||||
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
||||
Classifier: Topic :: Text Processing :: Markup :: HTML
|
@ -1,18 +0,0 @@
|
||||
AUTHORS
|
||||
CHANGES
|
||||
LICENSE
|
||||
MANIFEST.in
|
||||
README.rst
|
||||
setup.cfg
|
||||
setup.py
|
||||
tests.py
|
||||
MarkupSafe.egg-info/PKG-INFO
|
||||
MarkupSafe.egg-info/SOURCES.txt
|
||||
MarkupSafe.egg-info/dependency_links.txt
|
||||
MarkupSafe.egg-info/not-zip-safe
|
||||
MarkupSafe.egg-info/top_level.txt
|
||||
markupsafe/__init__.py
|
||||
markupsafe/_compat.py
|
||||
markupsafe/_constants.py
|
||||
markupsafe/_native.py
|
||||
markupsafe/_speedups.c
|
@ -1 +0,0 @@
|
||||
|
@ -1,15 +0,0 @@
|
||||
..\markupsafe\__init__.py
|
||||
..\markupsafe\__pycache__\__init__.cpython-36.pyc
|
||||
..\markupsafe\__pycache__\_compat.cpython-36.pyc
|
||||
..\markupsafe\__pycache__\_constants.cpython-36.pyc
|
||||
..\markupsafe\__pycache__\_native.cpython-36.pyc
|
||||
..\markupsafe\_compat.py
|
||||
..\markupsafe\_constants.py
|
||||
..\markupsafe\_native.py
|
||||
..\markupsafe\_speedups.c
|
||||
..\markupsafe\_speedups.cp36-win_amd64.pyd
|
||||
PKG-INFO
|
||||
SOURCES.txt
|
||||
dependency_links.txt
|
||||
not-zip-safe
|
||||
top_level.txt
|
@ -1 +0,0 @@
|
||||
|
@ -1 +0,0 @@
|
||||
markupsafe
|
@ -1,50 +0,0 @@
|
||||
# PyJWT
|
||||
|
||||
[![travis-status-image]][travis]
|
||||
[![appveyor-status-image]][appveyor]
|
||||
[![pypi-version-image]][pypi]
|
||||
[![coveralls-status-image]][coveralls]
|
||||
[![docs-status-image]][docs]
|
||||
|
||||
A Python implementation of [RFC 7519][jwt-spec].
|
||||
Original implementation was written by [@progrium][progrium].
|
||||
|
||||
## Installing
|
||||
|
||||
```
|
||||
$ pip install PyJWT
|
||||
```
|
||||
|
||||
## Usage
|
||||
|
||||
```python
|
||||
>>> import jwt
|
||||
>>> encoded = jwt.encode({'some': 'payload'}, 'secret', algorithm='HS256')
|
||||
'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzb21lIjoicGF5bG9hZCJ9.4twFt5NiznN84AWoo1d7KO1T_yoc0Z6XOpOVswacPZg'
|
||||
|
||||
>>> jwt.decode(encoded, 'secret', algorithms=['HS256'])
|
||||
{'some': 'payload'}
|
||||
```
|
||||
|
||||
## Tests
|
||||
|
||||
You can run tests from the project root after cloning with:
|
||||
|
||||
```
|
||||
$ python setup.py test
|
||||
```
|
||||
|
||||
[travis-status-image]: https://secure.travis-ci.org/jpadilla/pyjwt.svg?branch=master
|
||||
[travis]: http://travis-ci.org/jpadilla/pyjwt?branch=master
|
||||
[appveyor-status-image]: https://ci.appveyor.com/api/projects/status/h8nt70aqtwhht39t?svg=true
|
||||
[appveyor]: https://ci.appveyor.com/project/jpadilla/pyjwt
|
||||
[pypi-version-image]: https://img.shields.io/pypi/v/pyjwt.svg
|
||||
[pypi]: https://pypi.python.org/pypi/pyjwt
|
||||
[coveralls-status-image]: https://coveralls.io/repos/jpadilla/pyjwt/badge.svg?branch=master
|
||||
[coveralls]: https://coveralls.io/r/jpadilla/pyjwt?branch=master
|
||||
[docs-status-image]: https://readthedocs.org/projects/pyjwt/badge/?version=latest
|
||||
[docs]: http://pyjwt.readthedocs.org
|
||||
[jwt-spec]: https://tools.ietf.org/html/rfc7519
|
||||
[progrium]: https://github.com/progrium
|
||||
|
||||
|
@ -1 +0,0 @@
|
||||
pip
|
@ -1,82 +0,0 @@
|
||||
Metadata-Version: 2.0
|
||||
Name: PyJWT
|
||||
Version: 1.4.2
|
||||
Summary: JSON Web Token implementation in Python
|
||||
Home-page: http://github.com/jpadilla/pyjwt
|
||||
Author: José Padilla
|
||||
Author-email: hello@jpadilla.com
|
||||
License: MIT
|
||||
Keywords: jwt json web token security signing
|
||||
Platform: UNKNOWN
|
||||
Classifier: Development Status :: 5 - Production/Stable
|
||||
Classifier: Intended Audience :: Developers
|
||||
Classifier: Natural Language :: English
|
||||
Classifier: License :: OSI Approved :: MIT License
|
||||
Classifier: Programming Language :: Python
|
||||
Classifier: Programming Language :: Python :: 2.6
|
||||
Classifier: Programming Language :: Python :: 2.7
|
||||
Classifier: Programming Language :: Python :: 3.3
|
||||
Classifier: Programming Language :: Python :: 3.4
|
||||
Classifier: Programming Language :: Python :: 3.5
|
||||
Classifier: Topic :: Utilities
|
||||
Provides-Extra: crypto
|
||||
Requires-Dist: cryptography; extra == 'crypto'
|
||||
Provides-Extra: flake8
|
||||
Requires-Dist: flake8; extra == 'flake8'
|
||||
Requires-Dist: flake8-import-order; extra == 'flake8'
|
||||
Requires-Dist: pep8-naming; extra == 'flake8'
|
||||
Provides-Extra: test
|
||||
Requires-Dist: pytest (==2.7.3); extra == 'test'
|
||||
Requires-Dist: pytest-cov; extra == 'test'
|
||||
Requires-Dist: pytest-runner; extra == 'test'
|
||||
|
||||
# PyJWT
|
||||
|
||||
[![travis-status-image]][travis]
|
||||
[![appveyor-status-image]][appveyor]
|
||||
[![pypi-version-image]][pypi]
|
||||
[![coveralls-status-image]][coveralls]
|
||||
[![docs-status-image]][docs]
|
||||
|
||||
A Python implementation of [RFC 7519][jwt-spec].
|
||||
Original implementation was written by [@progrium][progrium].
|
||||
|
||||
## Installing
|
||||
|
||||
```
|
||||
$ pip install PyJWT
|
||||
```
|
||||
|
||||
## Usage
|
||||
|
||||
```python
|
||||
>>> import jwt
|
||||
>>> encoded = jwt.encode({'some': 'payload'}, 'secret', algorithm='HS256')
|
||||
'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzb21lIjoicGF5bG9hZCJ9.4twFt5NiznN84AWoo1d7KO1T_yoc0Z6XOpOVswacPZg'
|
||||
|
||||
>>> jwt.decode(encoded, 'secret', algorithms=['HS256'])
|
||||
{'some': 'payload'}
|
||||
```
|
||||
|
||||
## Tests
|
||||
|
||||
You can run tests from the project root after cloning with:
|
||||
|
||||
```
|
||||
$ python setup.py test
|
||||
```
|
||||
|
||||
[travis-status-image]: https://secure.travis-ci.org/jpadilla/pyjwt.svg?branch=master
|
||||
[travis]: http://travis-ci.org/jpadilla/pyjwt?branch=master
|
||||
[appveyor-status-image]: https://ci.appveyor.com/api/projects/status/h8nt70aqtwhht39t?svg=true
|
||||
[appveyor]: https://ci.appveyor.com/project/jpadilla/pyjwt
|
||||
[pypi-version-image]: https://img.shields.io/pypi/v/pyjwt.svg
|
||||
[pypi]: https://pypi.python.org/pypi/pyjwt
|
||||
[coveralls-status-image]: https://coveralls.io/repos/jpadilla/pyjwt/badge.svg?branch=master
|
||||
[coveralls]: https://coveralls.io/r/jpadilla/pyjwt?branch=master
|
||||
[docs-status-image]: https://readthedocs.org/projects/pyjwt/badge/?version=latest
|
||||
[docs]: http://pyjwt.readthedocs.org
|
||||
[jwt-spec]: https://tools.ietf.org/html/rfc7519
|
||||
[progrium]: https://github.com/progrium
|
||||
|
||||
|
@ -1,33 +0,0 @@
|
||||
jwt/__init__.py,sha256=7NJDM0Wdyc8Jr_LuDyPwmS3y7NJghrFZ6WXWy-2qfrc,738
|
||||
jwt/__main__.py,sha256=93alATh8EI-GuR1-CiYzGwtjbyFllWDRGhEBPHDsFqU,3587
|
||||
jwt/algorithms.py,sha256=bixXU8k0oqPOHbnQ3sm4ne-ofWn3tFloTeSL7iifDiM,8677
|
||||
jwt/api_jws.py,sha256=YK6ODhpxH99mDcmdt4cVs8WPN0YTLLr8-_22q-WONuc,6760
|
||||
jwt/api_jwt.py,sha256=5Qmyc9Y0JXrjdBuY8OuiFdiCcHDgjF799ZbxP7Ef9ZQ,6709
|
||||
jwt/compat.py,sha256=7-3RAohhsEDwndWt1OyXaV4-HsF3QQnO_F-BddLSfa8,1317
|
||||
jwt/exceptions.py,sha256=63QgVtqVgRHdVAi0NqRO0s13opKdKZmwUGzW_CyPw4c,841
|
||||
jwt/utils.py,sha256=DsCtJn-c_q35NtTcDI3EVgsOJTWqHErQaOLPElBItio,1566
|
||||
jwt/contrib/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
||||
jwt/contrib/algorithms/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
||||
jwt/contrib/algorithms/py_ecdsa.py,sha256=tSTUrwx-u14DJcqAChRzJG-wf7bEY2Gv2hI5xSZZNjk,1771
|
||||
jwt/contrib/algorithms/pycrypto.py,sha256=M3nH1Rrk6yb6aPGo6zT4EI_MvPUM4vhO1EwC-uX9JAo,1250
|
||||
PyJWT-1.4.2.dist-info/DESCRIPTION.rst,sha256=0qxBo0Hjlq25hSJbVkeg0h2pt1_S39LrMuEiPQbEmtI,1507
|
||||
PyJWT-1.4.2.dist-info/entry_points.txt,sha256=DtGQ73zJer9SDGHdWhcHaianJpovcNUQkoqLa_8KObw,43
|
||||
PyJWT-1.4.2.dist-info/METADATA,sha256=kTVDjNG00RK_sbMRoISEmBnRV7Vvpb0GhJt4IEPHP-A,2690
|
||||
PyJWT-1.4.2.dist-info/metadata.json,sha256=MDnukon_DIPRItFf5RMwbx4sBvaagLxLgXtoV6eeA48,1418
|
||||
PyJWT-1.4.2.dist-info/RECORD,,
|
||||
PyJWT-1.4.2.dist-info/top_level.txt,sha256=RP5DHNyJbMq2ka0FmfTgoSaQzh7e3r5XuCWCO8a00k8,4
|
||||
PyJWT-1.4.2.dist-info/WHEEL,sha256=AvR0WeTpDaxT645bl5FQxUK6NPsTls2ttpcGJg3j1Xg,110
|
||||
../../Scripts/jwt.exe,sha256=aPrULChdCm_xQarigRU2UmnZjYBpn6ucPgW12sGdm2A,102769
|
||||
PyJWT-1.4.2.dist-info/INSTALLER,sha256=zuuue4knoyJ-UwPPXg8fezS7VCrXJQrAP7zeNuwvFQg,4
|
||||
jwt/contrib/algorithms/__pycache__/pycrypto.cpython-36.pyc,,
|
||||
jwt/contrib/algorithms/__pycache__/py_ecdsa.cpython-36.pyc,,
|
||||
jwt/contrib/algorithms/__pycache__/__init__.cpython-36.pyc,,
|
||||
jwt/contrib/__pycache__/__init__.cpython-36.pyc,,
|
||||
jwt/__pycache__/algorithms.cpython-36.pyc,,
|
||||
jwt/__pycache__/api_jws.cpython-36.pyc,,
|
||||
jwt/__pycache__/api_jwt.cpython-36.pyc,,
|
||||
jwt/__pycache__/compat.cpython-36.pyc,,
|
||||
jwt/__pycache__/exceptions.cpython-36.pyc,,
|
||||
jwt/__pycache__/utils.cpython-36.pyc,,
|
||||
jwt/__pycache__/__init__.cpython-36.pyc,,
|
||||
jwt/__pycache__/__main__.cpython-36.pyc,,
|
@ -1,6 +0,0 @@
|
||||
Wheel-Version: 1.0
|
||||
Generator: bdist_wheel (0.24.0)
|
||||
Root-Is-Purelib: true
|
||||
Tag: py2-none-any
|
||||
Tag: py3-none-any
|
||||
|
@ -1,3 +0,0 @@
|
||||
[console_scripts]
|
||||
jwt = jwt.__main__:main
|
||||
|
@ -1 +0,0 @@
|
||||
{"license": "MIT", "name": "PyJWT", "metadata_version": "2.0", "generator": "bdist_wheel (0.24.0)", "test_requires": [{"requires": ["pytest (==2.7.3)", "pytest-cov", "pytest-runner"]}], "summary": "JSON Web Token implementation in Python", "run_requires": [{"requires": ["pytest (==2.7.3)", "pytest-cov", "pytest-runner"], "extra": "test"}, {"requires": ["cryptography"], "extra": "crypto"}, {"requires": ["flake8", "flake8-import-order", "pep8-naming"], "extra": "flake8"}], "version": "1.4.2", "extensions": {"python.details": {"project_urls": {"Home": "http://github.com/jpadilla/pyjwt"}, "document_names": {"description": "DESCRIPTION.rst"}, "contacts": [{"role": "author", "email": "hello@jpadilla.com", "name": "Jos\u00e9 Padilla"}]}, "python.commands": {"wrap_console": {"jwt": "jwt.__main__:main"}}, "python.exports": {"console_scripts": {"jwt": "jwt.__main__:main"}}}, "keywords": ["jwt", "json", "web", "token", "security", "signing"], "classifiers": ["Development Status :: 5 - Production/Stable", "Intended Audience :: Developers", "Natural Language :: English", "License :: OSI Approved :: MIT License", "Programming Language :: Python", "Programming Language :: Python :: 2.6", "Programming Language :: Python :: 2.7", "Programming Language :: Python :: 3.3", "Programming Language :: Python :: 3.4", "Programming Language :: Python :: 3.5", "Topic :: Utilities"], "extras": ["crypto", "flake8", "test"]}
|
@ -1 +0,0 @@
|
||||
jwt
|
@ -1,80 +0,0 @@
|
||||
Werkzeug
|
||||
========
|
||||
|
||||
Werkzeug is a comprehensive `WSGI`_ web application library. It began as
|
||||
a simple collection of various utilities for WSGI applications and has
|
||||
become one of the most advanced WSGI utility libraries.
|
||||
|
||||
It includes:
|
||||
|
||||
* An interactive debugger that allows inspecting stack traces and source
|
||||
code in the browser with an interactive interpreter for any frame in
|
||||
the stack.
|
||||
* A full-featured request object with objects to interact with headers,
|
||||
query args, form data, files, and cookies.
|
||||
* A response object that can wrap other WSGI applications and handle
|
||||
streaming data.
|
||||
* A routing system for matching URLs to endpoints and generating URLs
|
||||
for endpoints, with an extensible system for capturing variables from
|
||||
URLs.
|
||||
* HTTP utilities to handle entity tags, cache control, dates, user
|
||||
agents, cookies, files, and more.
|
||||
* A threaded WSGI server for use while developing applications locally.
|
||||
* A test client for simulating HTTP requests during testing without
|
||||
requiring running a server.
|
||||
|
||||
Werkzeug is Unicode aware and doesn't enforce any dependencies. It is up
|
||||
to the developer to choose a template engine, database adapter, and even
|
||||
how to handle requests. It can be used to build all sorts of end user
|
||||
applications such as blogs, wikis, or bulletin boards.
|
||||
|
||||
`Flask`_ wraps Werkzeug, using it to handle the details of WSGI while
|
||||
providing more structure and patterns for defining powerful
|
||||
applications.
|
||||
|
||||
|
||||
Installing
|
||||
----------
|
||||
|
||||
Install and update using `pip`_:
|
||||
|
||||
.. code-block:: text
|
||||
|
||||
pip install -U Werkzeug
|
||||
|
||||
|
||||
A Simple Example
|
||||
----------------
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
from werkzeug.wrappers import Request, Response
|
||||
|
||||
@Request.application
|
||||
def application(request):
|
||||
return Response('Hello, World!')
|
||||
|
||||
if __name__ == '__main__':
|
||||
from werkzeug.serving import run_simple
|
||||
run_simple('localhost', 4000, application)
|
||||
|
||||
|
||||
Links
|
||||
-----
|
||||
|
||||
* Website: https://www.palletsprojects.com/p/werkzeug/
|
||||
* Releases: https://pypi.org/project/Werkzeug/
|
||||
* Code: https://github.com/pallets/werkzeug
|
||||
* Issue tracker: https://github.com/pallets/werkzeug/issues
|
||||
* Test status:
|
||||
|
||||
* Linux, Mac: https://travis-ci.org/pallets/werkzeug
|
||||
* Windows: https://ci.appveyor.com/project/davidism/werkzeug
|
||||
|
||||
* Test coverage: https://codecov.io/gh/pallets/werkzeug
|
||||
|
||||
.. _WSGI: https://wsgi.readthedocs.io/en/latest/
|
||||
.. _Flask: https://www.palletsprojects.com/p/flask/
|
||||
.. _pip: https://pip.pypa.io/en/stable/quickstart/
|
||||
|
||||
|
@ -1 +0,0 @@
|
||||
pip
|
@ -1,31 +0,0 @@
|
||||
Copyright © 2007 by the Pallets team.
|
||||
|
||||
Some rights reserved.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without
|
||||
modification, are permitted provided that the following conditions are
|
||||
met:
|
||||
|
||||
* Redistributions of source code must retain the above copyright notice,
|
||||
this list of conditions and the following disclaimer.
|
||||
|
||||
* Redistributions in binary form must reproduce the above copyright
|
||||
notice, this list of conditions and the following disclaimer in the
|
||||
documentation and/or other materials provided with the distribution.
|
||||
|
||||
* Neither the name of the copyright holder nor the names of its
|
||||
contributors may be used to endorse or promote products derived from
|
||||
this software without specific prior written permission.
|
||||
|
||||
THIS SOFTWARE AND DOCUMENTATION IS PROVIDED BY THE COPYRIGHT HOLDERS AND
|
||||
CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING,
|
||||
BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND
|
||||
FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
|
||||
COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
|
||||
INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
|
||||
NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF
|
||||
USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
|
||||
ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
||||
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
|
||||
THIS SOFTWARE AND DOCUMENTATION, EVEN IF ADVISED OF THE POSSIBILITY OF
|
||||
SUCH DAMAGE.
|
@ -1,116 +0,0 @@
|
||||
Metadata-Version: 2.0
|
||||
Name: Werkzeug
|
||||
Version: 0.14.1
|
||||
Summary: The comprehensive WSGI web application library.
|
||||
Home-page: https://www.palletsprojects.org/p/werkzeug/
|
||||
Author: Armin Ronacher
|
||||
Author-email: armin.ronacher@active-4.com
|
||||
License: BSD
|
||||
Description-Content-Type: UNKNOWN
|
||||
Platform: any
|
||||
Classifier: Development Status :: 5 - Production/Stable
|
||||
Classifier: Environment :: Web Environment
|
||||
Classifier: Intended Audience :: Developers
|
||||
Classifier: License :: OSI Approved :: BSD License
|
||||
Classifier: Operating System :: OS Independent
|
||||
Classifier: Programming Language :: Python
|
||||
Classifier: Programming Language :: Python :: 2
|
||||
Classifier: Programming Language :: Python :: 2.6
|
||||
Classifier: Programming Language :: Python :: 2.7
|
||||
Classifier: Programming Language :: Python :: 3
|
||||
Classifier: Programming Language :: Python :: 3.3
|
||||
Classifier: Programming Language :: Python :: 3.4
|
||||
Classifier: Programming Language :: Python :: 3.5
|
||||
Classifier: Programming Language :: Python :: 3.6
|
||||
Classifier: Topic :: Internet :: WWW/HTTP :: Dynamic Content
|
||||
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
||||
Provides-Extra: dev
|
||||
Requires-Dist: coverage; extra == 'dev'
|
||||
Requires-Dist: pytest; extra == 'dev'
|
||||
Requires-Dist: sphinx; extra == 'dev'
|
||||
Requires-Dist: tox; extra == 'dev'
|
||||
Provides-Extra: termcolor
|
||||
Requires-Dist: termcolor; extra == 'termcolor'
|
||||
Provides-Extra: watchdog
|
||||
Requires-Dist: watchdog; extra == 'watchdog'
|
||||
|
||||
Werkzeug
|
||||
========
|
||||
|
||||
Werkzeug is a comprehensive `WSGI`_ web application library. It began as
|
||||
a simple collection of various utilities for WSGI applications and has
|
||||
become one of the most advanced WSGI utility libraries.
|
||||
|
||||
It includes:
|
||||
|
||||
* An interactive debugger that allows inspecting stack traces and source
|
||||
code in the browser with an interactive interpreter for any frame in
|
||||
the stack.
|
||||
* A full-featured request object with objects to interact with headers,
|
||||
query args, form data, files, and cookies.
|
||||
* A response object that can wrap other WSGI applications and handle
|
||||
streaming data.
|
||||
* A routing system for matching URLs to endpoints and generating URLs
|
||||
for endpoints, with an extensible system for capturing variables from
|
||||
URLs.
|
||||
* HTTP utilities to handle entity tags, cache control, dates, user
|
||||
agents, cookies, files, and more.
|
||||
* A threaded WSGI server for use while developing applications locally.
|
||||
* A test client for simulating HTTP requests during testing without
|
||||
requiring running a server.
|
||||
|
||||
Werkzeug is Unicode aware and doesn't enforce any dependencies. It is up
|
||||
to the developer to choose a template engine, database adapter, and even
|
||||
how to handle requests. It can be used to build all sorts of end user
|
||||
applications such as blogs, wikis, or bulletin boards.
|
||||
|
||||
`Flask`_ wraps Werkzeug, using it to handle the details of WSGI while
|
||||
providing more structure and patterns for defining powerful
|
||||
applications.
|
||||
|
||||
|
||||
Installing
|
||||
----------
|
||||
|
||||
Install and update using `pip`_:
|
||||
|
||||
.. code-block:: text
|
||||
|
||||
pip install -U Werkzeug
|
||||
|
||||
|
||||
A Simple Example
|
||||
----------------
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
from werkzeug.wrappers import Request, Response
|
||||
|
||||
@Request.application
|
||||
def application(request):
|
||||
return Response('Hello, World!')
|
||||
|
||||
if __name__ == '__main__':
|
||||
from werkzeug.serving import run_simple
|
||||
run_simple('localhost', 4000, application)
|
||||
|
||||
|
||||
Links
|
||||
-----
|
||||
|
||||
* Website: https://www.palletsprojects.com/p/werkzeug/
|
||||
* Releases: https://pypi.org/project/Werkzeug/
|
||||
* Code: https://github.com/pallets/werkzeug
|
||||
* Issue tracker: https://github.com/pallets/werkzeug/issues
|
||||
* Test status:
|
||||
|
||||
* Linux, Mac: https://travis-ci.org/pallets/werkzeug
|
||||
* Windows: https://ci.appveyor.com/project/davidism/werkzeug
|
||||
|
||||
* Test coverage: https://codecov.io/gh/pallets/werkzeug
|
||||
|
||||
.. _WSGI: https://wsgi.readthedocs.io/en/latest/
|
||||
.. _Flask: https://www.palletsprojects.com/p/flask/
|
||||
.. _pip: https://pip.pypa.io/en/stable/quickstart/
|
||||
|
||||
|
@ -1,97 +0,0 @@
|
||||
Werkzeug-0.14.1.dist-info/DESCRIPTION.rst,sha256=rOCN36jwsWtWsTpqPG96z7FMilB5qI1CIARSKRuUmz8,2452
|
||||
Werkzeug-0.14.1.dist-info/LICENSE.txt,sha256=xndz_dD4m269AF9l_Xbl5V3tM1N3C1LoZC2PEPxWO-8,1534
|
||||
Werkzeug-0.14.1.dist-info/METADATA,sha256=FbfadrPdJNUWAxMOKxGUtHe5R3IDSBKYYmAz3FvI3uY,3872
|
||||
Werkzeug-0.14.1.dist-info/RECORD,,
|
||||
Werkzeug-0.14.1.dist-info/WHEEL,sha256=GrqQvamwgBV4nLoJe0vhYRSWzWsx7xjlt74FT0SWYfE,110
|
||||
Werkzeug-0.14.1.dist-info/metadata.json,sha256=4489UTt6HBp2NQil95-pBkjU4Je93SMHvMxZ_rjOpqA,1452
|
||||
Werkzeug-0.14.1.dist-info/top_level.txt,sha256=QRyj2VjwJoQkrwjwFIOlB8Xg3r9un0NtqVHQF-15xaw,9
|
||||
werkzeug/__init__.py,sha256=NR0d4n_-U9BLVKlOISean3zUt2vBwhvK-AZE6M0sC0k,6842
|
||||
werkzeug/_compat.py,sha256=8c4U9o6A_TR9nKCcTbpZNxpqCXcXDVIbFawwKM2s92c,6311
|
||||
werkzeug/_internal.py,sha256=GhEyGMlsSz_tYjsDWO9TG35VN7304MM8gjKDrXLEdVc,13873
|
||||
werkzeug/_reloader.py,sha256=AyPphcOHPbu6qzW0UbrVvTDJdre5WgpxbhIJN_TqzUc,9264
|
||||
werkzeug/datastructures.py,sha256=3IgNKNqrz-ZjmAG7y3YgEYK-enDiMT_b652PsypWcYg,90080
|
||||
werkzeug/exceptions.py,sha256=3wp95Hqj9FqV8MdikV99JRcHse_fSMn27V8tgP5Hw2c,20505
|
||||
werkzeug/filesystem.py,sha256=hHWeWo_gqLMzTRfYt8-7n2wWcWUNTnDyudQDLOBEICE,2175
|
||||
werkzeug/formparser.py,sha256=mUuCwjzjb8_E4RzrAT2AioLuZSYpqR1KXTK6LScRYzA,21722
|
||||
werkzeug/http.py,sha256=RQg4MJuhRv2isNRiEh__Phh09ebpfT3Kuu_GfrZ54_c,40079
|
||||
werkzeug/local.py,sha256=QdQhWV5L8p1Y1CJ1CDStwxaUs24SuN5aebHwjVD08C8,14553
|
||||
werkzeug/posixemulation.py,sha256=xEF2Bxc-vUCPkiu4IbfWVd3LW7DROYAT-ExW6THqyzw,3519
|
||||
werkzeug/routing.py,sha256=2JVtdSgxKGeANy4Z_FP-dKESvKtkYGCZ1J2fARCLGCY,67214
|
||||
werkzeug/script.py,sha256=DwaVDcXdaOTffdNvlBdLitxWXjKaRVT32VbhDtljFPY,11365
|
||||
werkzeug/security.py,sha256=0m107exslz4QJLWQCpfQJ04z3re4eGHVggRvrQVAdWc,9193
|
||||
werkzeug/serving.py,sha256=A0flnIJHufdn2QJ9oeuHfrXwP3LzP8fn3rNW6hbxKUg,31926
|
||||
werkzeug/test.py,sha256=XmECSmnpASiYQTct4oMiWr0LT5jHWCtKqnpYKZd2ui8,36100
|
||||
werkzeug/testapp.py,sha256=3HQRW1sHZKXuAjCvFMet4KXtQG3loYTFnvn6LWt-4zI,9396
|
||||
werkzeug/urls.py,sha256=dUeLg2IeTm0WLmSvFeD4hBZWGdOs-uHudR5-t8n9zPo,36771
|
||||
werkzeug/useragents.py,sha256=BhYMf4cBTHyN4U0WsQedePIocmNlH_34C-UwqSThGCc,5865
|
||||
werkzeug/utils.py,sha256=BrY1j0DHQ8RTb0K1StIobKuMJhN9SQQkWEARbrh2qpk,22972
|
||||
werkzeug/websocket.py,sha256=PpSeDxXD_0UsPAa5hQhQNM6mxibeUgn8lA8eRqiS0vM,11344
|
||||
werkzeug/wrappers.py,sha256=kbyL_aFjxELwPgMwfNCYjKu-CR6kNkh-oO8wv3GXbk8,84511
|
||||
werkzeug/wsgi.py,sha256=1Nob-aeChWQf7MsiicO8RZt6J90iRzEcik44ev9Qu8s,49347
|
||||
werkzeug/contrib/__init__.py,sha256=f7PfttZhbrImqpr5Ezre8CXgwvcGUJK7zWNpO34WWrw,623
|
||||
werkzeug/contrib/atom.py,sha256=qqfJcfIn2RYY-3hO3Oz0aLq9YuNubcPQ_KZcNsDwVJo,15575
|
||||
werkzeug/contrib/cache.py,sha256=xBImHNj09BmX_7kC5NUCx8f_l4L8_O7zi0jCL21UZKE,32163
|
||||
werkzeug/contrib/fixers.py,sha256=gR06T-w71ur-tHQ_31kP_4jpOncPJ4Wc1dOqTvYusr8,10179
|
||||
werkzeug/contrib/iterio.py,sha256=RlqDvGhz0RneTpzE8dVc-yWCUv4nkPl1jEc_EDp2fH0,10814
|
||||
werkzeug/contrib/jsrouting.py,sha256=QTmgeDoKXvNK02KzXgx9lr3cAH6fAzpwF5bBdPNvJPs,8564
|
||||
werkzeug/contrib/limiter.py,sha256=iS8-ahPZ-JLRnmfIBzxpm7O_s3lPsiDMVWv7llAIDCI,1334
|
||||
werkzeug/contrib/lint.py,sha256=Mj9NeUN7s4zIUWeQOAVjrmtZIcl3Mm2yDe9BSIr9YGE,12558
|
||||
werkzeug/contrib/profiler.py,sha256=ISwCWvwVyGpDLRBRpLjo_qUWma6GXYBrTAco4PEQSHY,5151
|
||||
werkzeug/contrib/securecookie.py,sha256=uWMyHDHY3lkeBRiCSayGqWkAIy4a7xAbSE_Hln9ecqc,12196
|
||||
werkzeug/contrib/sessions.py,sha256=39LVNvLbm5JWpbxM79WC2l87MJFbqeISARjwYbkJatw,12577
|
||||
werkzeug/contrib/testtools.py,sha256=G9xN-qeihJlhExrIZMCahvQOIDxdL9NiX874jiiHFMs,2453
|
||||
werkzeug/contrib/wrappers.py,sha256=v7OYlz7wQtDlS9fey75UiRZ1IkUWqCpzbhsLy4k14Hw,10398
|
||||
werkzeug/debug/__init__.py,sha256=uSn9BqCZ5E3ySgpoZtundpROGsn-uYvZtSFiTfAX24M,17452
|
||||
werkzeug/debug/console.py,sha256=n3-dsKk1TsjnN-u4ZgmuWCU_HO0qw5IA7ttjhyyMM6I,5607
|
||||
werkzeug/debug/repr.py,sha256=bKqstDYGfECpeLerd48s_hxuqK4b6UWnjMu3d_DHO8I,9340
|
||||
werkzeug/debug/tbtools.py,sha256=rBudXCmkVdAKIcdhxANxgf09g6kQjJWW9_5bjSpr4OY,18451
|
||||
werkzeug/debug/shared/FONT_LICENSE,sha256=LwAVEI1oYnvXiNMT9SnCH_TaLCxCpeHziDrMg0gPkAI,4673
|
||||
werkzeug/debug/shared/console.png,sha256=bxax6RXXlvOij_KeqvSNX0ojJf83YbnZ7my-3Gx9w2A,507
|
||||
werkzeug/debug/shared/debugger.js,sha256=PKPVYuyO4SX1hkqLOwCLvmIEO5154WatFYaXE-zIfKI,6264
|
||||
werkzeug/debug/shared/jquery.js,sha256=7LkWEzqTdpEfELxcZZlS6wAx5Ff13zZ83lYO2_ujj7g,95957
|
||||
werkzeug/debug/shared/less.png,sha256=-4-kNRaXJSONVLahrQKUxMwXGm9R4OnZ9SxDGpHlIR4,191
|
||||
werkzeug/debug/shared/more.png,sha256=GngN7CioHQoV58rH6ojnkYi8c_qED2Aka5FO5UXrReY,200
|
||||
werkzeug/debug/shared/source.png,sha256=RoGcBTE4CyCB85GBuDGTFlAnUqxwTBiIfDqW15EpnUQ,818
|
||||
werkzeug/debug/shared/style.css,sha256=IEO0PC2pWmh2aEyGCaN--txuWsRCliuhlbEhPDFwh0A,6270
|
||||
werkzeug/debug/shared/ubuntu.ttf,sha256=1eaHFyepmy4FyDvjLVzpITrGEBu_CZYY94jE0nED1c0,70220
|
||||
Werkzeug-0.14.1.dist-info/INSTALLER,sha256=zuuue4knoyJ-UwPPXg8fezS7VCrXJQrAP7zeNuwvFQg,4
|
||||
werkzeug/contrib/__pycache__/atom.cpython-36.pyc,,
|
||||
werkzeug/contrib/__pycache__/cache.cpython-36.pyc,,
|
||||
werkzeug/contrib/__pycache__/fixers.cpython-36.pyc,,
|
||||
werkzeug/contrib/__pycache__/iterio.cpython-36.pyc,,
|
||||
werkzeug/contrib/__pycache__/jsrouting.cpython-36.pyc,,
|
||||
werkzeug/contrib/__pycache__/limiter.cpython-36.pyc,,
|
||||
werkzeug/contrib/__pycache__/lint.cpython-36.pyc,,
|
||||
werkzeug/contrib/__pycache__/profiler.cpython-36.pyc,,
|
||||
werkzeug/contrib/__pycache__/securecookie.cpython-36.pyc,,
|
||||
werkzeug/contrib/__pycache__/sessions.cpython-36.pyc,,
|
||||
werkzeug/contrib/__pycache__/testtools.cpython-36.pyc,,
|
||||
werkzeug/contrib/__pycache__/wrappers.cpython-36.pyc,,
|
||||
werkzeug/contrib/__pycache__/__init__.cpython-36.pyc,,
|
||||
werkzeug/debug/__pycache__/console.cpython-36.pyc,,
|
||||
werkzeug/debug/__pycache__/repr.cpython-36.pyc,,
|
||||
werkzeug/debug/__pycache__/tbtools.cpython-36.pyc,,
|
||||
werkzeug/debug/__pycache__/__init__.cpython-36.pyc,,
|
||||
werkzeug/__pycache__/datastructures.cpython-36.pyc,,
|
||||
werkzeug/__pycache__/exceptions.cpython-36.pyc,,
|
||||
werkzeug/__pycache__/filesystem.cpython-36.pyc,,
|
||||
werkzeug/__pycache__/formparser.cpython-36.pyc,,
|
||||
werkzeug/__pycache__/http.cpython-36.pyc,,
|
||||
werkzeug/__pycache__/local.cpython-36.pyc,,
|
||||
werkzeug/__pycache__/posixemulation.cpython-36.pyc,,
|
||||
werkzeug/__pycache__/routing.cpython-36.pyc,,
|
||||
werkzeug/__pycache__/script.cpython-36.pyc,,
|
||||
werkzeug/__pycache__/security.cpython-36.pyc,,
|
||||
werkzeug/__pycache__/serving.cpython-36.pyc,,
|
||||
werkzeug/__pycache__/test.cpython-36.pyc,,
|
||||
werkzeug/__pycache__/testapp.cpython-36.pyc,,
|
||||
werkzeug/__pycache__/urls.cpython-36.pyc,,
|
||||
werkzeug/__pycache__/useragents.cpython-36.pyc,,
|
||||
werkzeug/__pycache__/utils.cpython-36.pyc,,
|
||||
werkzeug/__pycache__/websocket.cpython-36.pyc,,
|
||||
werkzeug/__pycache__/wrappers.cpython-36.pyc,,
|
||||
werkzeug/__pycache__/wsgi.cpython-36.pyc,,
|
||||
werkzeug/__pycache__/_compat.cpython-36.pyc,,
|
||||
werkzeug/__pycache__/_internal.cpython-36.pyc,,
|
||||
werkzeug/__pycache__/_reloader.cpython-36.pyc,,
|
||||
werkzeug/__pycache__/__init__.cpython-36.pyc,,
|
@ -1,6 +0,0 @@
|
||||
Wheel-Version: 1.0
|
||||
Generator: bdist_wheel (0.26.0)
|
||||
Root-Is-Purelib: true
|
||||
Tag: py2-none-any
|
||||
Tag: py3-none-any
|
||||
|
@ -1 +0,0 @@
|
||||
{"generator": "bdist_wheel (0.26.0)", "summary": "The comprehensive WSGI web application library.", "classifiers": ["Development Status :: 5 - Production/Stable", "Environment :: Web Environment", "Intended Audience :: Developers", "License :: OSI Approved :: BSD License", "Operating System :: OS Independent", "Programming Language :: Python", "Programming Language :: Python :: 2", "Programming Language :: Python :: 2.6", "Programming Language :: Python :: 2.7", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3.3", "Programming Language :: Python :: 3.4", "Programming Language :: Python :: 3.5", "Programming Language :: Python :: 3.6", "Topic :: Internet :: WWW/HTTP :: Dynamic Content", "Topic :: Software Development :: Libraries :: Python Modules"], "description_content_type": "UNKNOWN", "extensions": {"python.details": {"project_urls": {"Home": "https://www.palletsprojects.org/p/werkzeug/"}, "contacts": [{"email": "armin.ronacher@active-4.com", "name": "Armin Ronacher", "role": "author"}], "document_names": {"description": "DESCRIPTION.rst", "license": "LICENSE.txt"}}}, "license": "BSD", "metadata_version": "2.0", "name": "Werkzeug", "platform": "any", "extras": ["dev", "termcolor", "watchdog"], "run_requires": [{"requires": ["coverage", "pytest", "sphinx", "tox"], "extra": "dev"}, {"requires": ["termcolor"], "extra": "termcolor"}, {"requires": ["watchdog"], "extra": "watchdog"}], "version": "0.14.1"}
|
@ -1 +0,0 @@
|
||||
werkzeug
|
@ -1,384 +0,0 @@
|
||||
===========
|
||||
aniso8601
|
||||
===========
|
||||
|
||||
----------------------------------
|
||||
Another ISO 8601 parser for Python
|
||||
----------------------------------
|
||||
|
||||
Features
|
||||
========
|
||||
* Pure Python implementation
|
||||
* Python 3 support
|
||||
* Logical behavior
|
||||
|
||||
- Parse a time, get a `datetime.time <http://docs.python.org/2/library/datetime.html#datetime.time>`_
|
||||
- Parse a date, get a `datetime.date <http://docs.python.org/2/library/datetime.html#datetime.date>`_
|
||||
- Parse a datetime, get a `datetime.datetime <http://docs.python.org/2/library/datetime.html#datetime.datetime>`_
|
||||
- Parse a duration, get a `datetime.timedelta <http://docs.python.org/2/library/datetime.html#datetime.timedelta>`_
|
||||
- Parse an interval, get a tuple of dates or datetimes
|
||||
- Parse a repeating interval, get a date or datetime `generator <http://www.python.org/dev/peps/pep-0255/>`_
|
||||
|
||||
* UTC offset represented as fixed-offset tzinfo
|
||||
* Optional `dateutil.relativedelta <http://dateutil.readthedocs.io/en/latest/relativedelta.html>`_ support for calendar accuracy
|
||||
* No regular expressions
|
||||
|
||||
Installation
|
||||
============
|
||||
|
||||
The recommended installation method is to use pip::
|
||||
|
||||
$ pip install aniso8601
|
||||
|
||||
Alternatively, you can download the source (git repository hosted at `Bitbucket <https://bitbucket.org/nielsenb/aniso8601>`_) and install directly::
|
||||
|
||||
$ python setup.py install
|
||||
|
||||
Use
|
||||
===
|
||||
|
||||
Parsing datetimes
|
||||
-----------------
|
||||
|
||||
To parse a typical ISO 8601 datetime string::
|
||||
|
||||
>>> import aniso8601
|
||||
>>> aniso8601.parse_datetime('1977-06-10T12:00:00Z')
|
||||
datetime.datetime(1977, 6, 10, 12, 0, tzinfo=+0:00:00 UTC)
|
||||
|
||||
Alternative delimiters can be specified, for example, a space::
|
||||
|
||||
>>> aniso8601.parse_datetime('1977-06-10 12:00:00Z', delimiter=' ')
|
||||
datetime.datetime(1977, 6, 10, 12, 0, tzinfo=+0:00:00 UTC)
|
||||
|
||||
UTC offsets are supported::
|
||||
|
||||
>>> aniso8601.parse_datetime('1979-06-05T08:00:00-08:00')
|
||||
datetime.datetime(1979, 6, 5, 8, 0, tzinfo=-8:00:00 UTC)
|
||||
|
||||
If a UTC offset is not specified, the returned datetime will be naive::
|
||||
|
||||
>>> aniso8601.parse_datetime('1983-01-22T08:00:00')
|
||||
datetime.datetime(1983, 1, 22, 8, 0)
|
||||
|
||||
Leap seconds are currently not supported and attempting to parse one raises a :code:`LeapSecondError`::
|
||||
|
||||
>>> aniso8601.parse_datetime('2018-03-06T23:59:60')
|
||||
Traceback (most recent call last):
|
||||
File "<stdin>", line 1, in <module>
|
||||
File "aniso8601/time.py", line 127, in parse_datetime
|
||||
timepart = parse_time(isotimestr)
|
||||
File "aniso8601/time.py", line 110, in parse_time
|
||||
return _parse_time_naive(timestr)
|
||||
File "aniso8601/time.py", line 140, in _parse_time_naive
|
||||
return _RESOLUTION_MAP[get_time_resolution(timestr)](timestr)
|
||||
File "aniso8601/time.py", line 214, in _parse_second_time
|
||||
raise LeapSecondError('Leap seconds are not supported.')
|
||||
aniso8601.exceptions.LeapSecondError: Leap seconds are not supported.
|
||||
|
||||
Parsing dates
|
||||
-------------
|
||||
|
||||
To parse a date represented in an ISO 8601 string::
|
||||
|
||||
>>> import aniso8601
|
||||
>>> aniso8601.parse_date('1984-04-23')
|
||||
datetime.date(1984, 4, 23)
|
||||
|
||||
Basic format is supported as well::
|
||||
|
||||
>>> aniso8601.parse_date('19840423')
|
||||
datetime.date(1984, 4, 23)
|
||||
|
||||
To parse a date using the ISO 8601 week date format::
|
||||
|
||||
>>> aniso8601.parse_date('1986-W38-1')
|
||||
datetime.date(1986, 9, 15)
|
||||
|
||||
To parse an ISO 8601 ordinal date::
|
||||
|
||||
>>> aniso8601.parse_date('1988-132')
|
||||
datetime.date(1988, 5, 11)
|
||||
|
||||
Parsing times
|
||||
-------------
|
||||
|
||||
To parse a time formatted as an ISO 8601 string::
|
||||
|
||||
>>> import aniso8601
|
||||
>>> aniso8601.parse_time('11:31:14')
|
||||
datetime.time(11, 31, 14)
|
||||
|
||||
As with all of the above, basic format is supported::
|
||||
|
||||
>>> aniso8601.parse_time('113114')
|
||||
datetime.time(11, 31, 14)
|
||||
|
||||
A UTC offset can be specified for times::
|
||||
|
||||
>>> aniso8601.parse_time('17:18:19-02:30')
|
||||
datetime.time(17, 18, 19, tzinfo=-2:30:00 UTC)
|
||||
>>> aniso8601.parse_time('171819Z')
|
||||
datetime.time(17, 18, 19, tzinfo=+0:00:00 UTC)
|
||||
|
||||
Reduced accuracy is supported::
|
||||
|
||||
>>> aniso8601.parse_time('21:42')
|
||||
datetime.time(21, 42)
|
||||
>>> aniso8601.parse_time('22')
|
||||
datetime.time(22, 0)
|
||||
|
||||
A decimal fraction is always allowed on the lowest order element of an ISO 8601 formatted time::
|
||||
|
||||
>>> aniso8601.parse_time('22:33.5')
|
||||
datetime.time(22, 33, 30)
|
||||
>>> aniso8601.parse_time('23.75')
|
||||
datetime.time(23, 45)
|
||||
|
||||
Leap seconds are currently not supported and attempting to parse one raises a :code:`LeapSecondError`::
|
||||
|
||||
>>> aniso8601.parse_time('23:59:60')
|
||||
Traceback (most recent call last):
|
||||
File "<stdin>", line 1, in <module>
|
||||
File "aniso8601/time.py", line 110, in parse_time
|
||||
return _parse_time_naive(timestr)
|
||||
File "aniso8601/time.py", line 140, in _parse_time_naive
|
||||
return _RESOLUTION_MAP[get_time_resolution(timestr)](timestr)
|
||||
File "aniso8601/time.py", line 214, in _parse_second_time
|
||||
raise LeapSecondError('Leap seconds are not supported.')
|
||||
aniso8601.exceptions.LeapSecondError: Leap seconds are not supported.
|
||||
|
||||
Parsing durations
|
||||
-----------------
|
||||
|
||||
To parse a duration formatted as an ISO 8601 string::
|
||||
|
||||
>>> import aniso8601
|
||||
>>> aniso8601.parse_duration('P1Y2M3DT4H54M6S')
|
||||
datetime.timedelta(428, 17646)
|
||||
|
||||
Reduced accuracy is supported::
|
||||
|
||||
>>> aniso8601.parse_duration('P1Y')
|
||||
datetime.timedelta(365)
|
||||
|
||||
A decimal fraction is allowed on the lowest order element::
|
||||
|
||||
>>> aniso8601.parse_duration('P1YT3.5M')
|
||||
datetime.timedelta(365, 210)
|
||||
|
||||
The decimal fraction can be specified with a comma instead of a full-stop::
|
||||
|
||||
>>> aniso8601.parse_duration('P1YT3,5M')
|
||||
datetime.timedelta(365, 210)
|
||||
|
||||
Parsing a duration from a combined date and time is supported as well::
|
||||
|
||||
>>> aniso8601.parse_duration('P0001-01-02T01:30:5')
|
||||
datetime.timedelta(397, 5405)
|
||||
|
||||
The above treat years as 365 days and months as 30 days. If calendar level accuracy is required, the relative keyword argument can be used if `python-dateutil <https://pypi.python.org/pypi/python-dateutil>`_ is installed::
|
||||
|
||||
>>> import aniso8601
|
||||
>>> from datetime import date
|
||||
>>> one_month = aniso8601.parse_duration('P1M', relative=True)
|
||||
>>> print one_month
|
||||
relativedelta(months=+1)
|
||||
>>> date(2003,1,27) + one_month
|
||||
datetime.date(2003, 2, 27)
|
||||
>>> date(2003,1,31) + one_month
|
||||
datetime.date(2003, 2, 28)
|
||||
>>> date(2003,1,31) + two_months
|
||||
datetime.date(2003, 3, 31)
|
||||
|
||||
Since a relative fractional month or year is not logical, a :code:`RelativeValueError` is raised when attempting to parse a duration with :code:`relative=True` and fractional month or year::
|
||||
|
||||
>>> aniso8601.parse_duration('P2.1Y', relative=True)
|
||||
Traceback (most recent call last):
|
||||
File "<stdin>", line 1, in <module>
|
||||
File "aniso8601/duration.py", line 29, in parse_duration
|
||||
return _parse_duration_prescribed(isodurationstr, relative)
|
||||
File "aniso8601/duration.py", line 73, in _parse_duration_prescribed
|
||||
raise RelativeValueError('Fractional months and years are not defined for relative intervals.')
|
||||
aniso8601.exceptions.RelativeValueError: Fractional months and years are not defined for relative intervals.
|
||||
|
||||
If :code:`relative=True` is set without python-dateutil available, a :code:`RuntimeError` is raised::
|
||||
|
||||
>>> aniso8601.parse_duration('P1M', relative=True)
|
||||
Traceback (most recent call last):
|
||||
File "<stdin>", line 1, in <module>
|
||||
File "aniso8601/duration.py", line 29, in parse_duration
|
||||
return _parse_duration_prescribed(isodurationstr, relative)
|
||||
File "aniso8601/duration.py", line 77, in _parse_duration_prescribed
|
||||
raise RuntimeError('dateutil must be installed for relative duration support.')
|
||||
RuntimeError: dateutil must be installed for relative duration support
|
||||
|
||||
Parsing intervals
|
||||
-----------------
|
||||
|
||||
To parse an interval specified by a start and end::
|
||||
|
||||
>>> import aniso8601
|
||||
>>> aniso8601.parse_interval('2007-03-01T13:00:00/2008-05-11T15:30:00')
|
||||
(datetime.datetime(2007, 3, 1, 13, 0), datetime.datetime(2008, 5, 11, 15, 30))
|
||||
|
||||
Intervals specified by a start time and a duration are supported::
|
||||
|
||||
>>> aniso8601.parse_interval('2007-03-01T13:00:00Z/P1Y2M10DT2H30M')
|
||||
(datetime.datetime(2007, 3, 1, 13, 0, tzinfo=+0:00:00 UTC), datetime.datetime(2008, 5, 9, 15, 30, tzinfo=+0:00:00 UTC))
|
||||
|
||||
A duration can also be specified by a duration and end time::
|
||||
|
||||
>>> aniso8601.parse_interval('P1M/1981-04-05')
|
||||
(datetime.date(1981, 4, 5), datetime.date(1981, 3, 6))
|
||||
|
||||
Notice that the result of the above parse is not in order from earliest to latest. If sorted intervals are required, simply use the :code:`sorted` keyword as shown below::
|
||||
|
||||
>>> sorted(aniso8601.parse_interval('P1M/1981-04-05'))
|
||||
[datetime.date(1981, 3, 6), datetime.date(1981, 4, 5)]
|
||||
|
||||
The end of an interval is given as a datetime when required to maintain the resolution specified by a duration, even if the duration start is given as a date::
|
||||
|
||||
>>> aniso8601.parse_interval('2014-11-12/PT4H54M6.5S')
|
||||
(datetime.date(2014, 11, 12), datetime.datetime(2014, 11, 12, 4, 54, 6, 500000))
|
||||
|
||||
Repeating intervals are supported as well, and return a generator::
|
||||
|
||||
>>> aniso8601.parse_repeating_interval('R3/1981-04-05/P1D')
|
||||
<generator object date_generator at 0x7f698cdefc80>
|
||||
>>> list(aniso8601.parse_repeating_interval('R3/1981-04-05/P1D'))
|
||||
[datetime.date(1981, 4, 5), datetime.date(1981, 4, 6), datetime.date(1981, 4, 7)]
|
||||
|
||||
Repeating intervals are allowed to go in the reverse direction::
|
||||
|
||||
>>> list(aniso8601.parse_repeating_interval('R2/PT1H2M/1980-03-05T01:01:00'))
|
||||
[datetime.datetime(1980, 3, 5, 1, 1), datetime.datetime(1980, 3, 4, 23, 59)]
|
||||
|
||||
Unbounded intervals are also allowed (Python 2)::
|
||||
|
||||
>>> result = aniso8601.parse_repeating_interval('R/PT1H2M/1980-03-05T01:01:00')
|
||||
>>> result.next()
|
||||
datetime.datetime(1980, 3, 5, 1, 1)
|
||||
>>> result.next()
|
||||
datetime.datetime(1980, 3, 4, 23, 59)
|
||||
|
||||
or for Python 3::
|
||||
|
||||
>>> result = aniso8601.parse_repeating_interval('R/PT1H2M/1980-03-05T01:01:00')
|
||||
>>> next(result)
|
||||
datetime.datetime(1980, 3, 5, 1, 1)
|
||||
>>> next(result)
|
||||
datetime.datetime(1980, 3, 4, 23, 59)
|
||||
|
||||
Note that you should never try to convert a generator produced by an unbounded interval to a list::
|
||||
|
||||
>>> list(aniso8601.parse_repeating_interval('R/PT1H2M/1980-03-05T01:01:00'))
|
||||
Traceback (most recent call last):
|
||||
File "<stdin>", line 1, in <module>
|
||||
File "aniso8601/interval.py", line 161, in _date_generator_unbounded
|
||||
currentdate += timedelta
|
||||
OverflowError: date value out of range
|
||||
|
||||
The above treat years as 365 days and months as 30 days. If calendar level accuracy is required, the relative keyword argument can be used if `python-dateutil <https://pypi.python.org/pypi/python-dateutil>`_ is installed::
|
||||
|
||||
>>> aniso8601.parse_interval('2003-01-27/P1M', relative=True)
|
||||
(datetime.date(2003, 1, 27), datetime.date(2003, 2, 27))
|
||||
>>> aniso8601.parse_interval('2003-01-31/P1M', relative=True)
|
||||
(datetime.date(2003, 1, 31), datetime.date(2003, 2, 28))
|
||||
>>> aniso8601.parse_interval('P1Y/2001-02-28', relative=True)
|
||||
(datetime.date(2001, 2, 28), datetime.date(2000, 2, 28)
|
||||
|
||||
Fractional years and months do not make sense for relative intervals. A :code:`RelativeValueError` is raised when attempting to parse an interval with :code:`relative=True` and a fractional month or year::
|
||||
|
||||
>>> aniso8601.parse_interval('P1.1Y/2001-02-28', relative=True)
|
||||
Traceback (most recent call last):
|
||||
File "<stdin>", line 1, in <module>
|
||||
File "aniso8601/interval.py", line 37, in parse_interval
|
||||
interval_parts = _parse_interval_parts(isointervalstr, intervaldelimiter, datetimedelimiter, relative)
|
||||
File "aniso8601/interval.py", line 89, in _parse_interval_parts
|
||||
duration = parse_duration(firstpart, relative=relative)
|
||||
File "aniso8601/duration.py", line 29, in parse_duration
|
||||
return _parse_duration_prescribed(isodurationstr, relative)
|
||||
File "aniso8601/duration.py", line 73, in _parse_duration_prescribed
|
||||
raise RelativeValueError('Fractional months and years are not defined for relative intervals.')
|
||||
aniso8601.exceptions.RelativeValueError: Fractional months and years are not defined for relative intervals.
|
||||
|
||||
If :code:`relative=True` is set without python-dateutil available, a :code:`RuntimeError` is raised::
|
||||
|
||||
>>> aniso8601.parse_interval('2003-01-27/P1M', relative=True)
|
||||
Traceback (most recent call last):
|
||||
File "<stdin>", line 1, in <module>
|
||||
File "aniso8601/interval.py", line 37, in parse_interval
|
||||
interval_parts = _parse_interval_parts(isointervalstr, intervaldelimiter, datetimedelimiter, relative)
|
||||
File "aniso8601/interval.py", line 108, in _parse_interval_parts
|
||||
duration = parse_duration(secondpart, relative=relative)
|
||||
File "aniso8601/duration.py", line 29, in parse_duration
|
||||
return _parse_duration_prescribed(isodurationstr, relative)
|
||||
File "aniso8601/duration.py", line 77, in _parse_duration_prescribed
|
||||
raise RuntimeError('dateutil must be installed for relative duration support.')
|
||||
RuntimeError: dateutil must be installed for relative duration support.
|
||||
|
||||
Date and time resolution
|
||||
------------------------
|
||||
|
||||
In some situations, it may be useful to figure out the resolution provided by an ISO 8601 date or time string. Two functions are provided for this purpose.
|
||||
|
||||
To get the resolution of a ISO 8601 time string::
|
||||
|
||||
>>> aniso8601.get_time_resolution('11:31:14') == aniso8601.resolution.TimeResolution.Seconds
|
||||
True
|
||||
>>> aniso8601.get_time_resolution('11:31') == aniso8601.resolution.TimeResolution.Minutes
|
||||
True
|
||||
>>> aniso8601.get_time_resolution('11') == aniso8601.resolution.TimeResolution.Hours
|
||||
True
|
||||
|
||||
Similarly, for an ISO 8601 date string::
|
||||
|
||||
>>> aniso8601.get_date_resolution('1981-04-05') == aniso8601.resolution.DateResolution.Day
|
||||
True
|
||||
>>> aniso8601.get_date_resolution('1981-04') == aniso8601.resolution.DateResolution.Month
|
||||
True
|
||||
>>> aniso8601.get_date_resolution('1981') == aniso8601.resolution.DateResolution.Year
|
||||
True
|
||||
|
||||
Development
|
||||
===========
|
||||
|
||||
Setup
|
||||
-----
|
||||
|
||||
It is recommended to develop using a `virtualenv <https://virtualenv.pypa.io/en/stable/>`_.
|
||||
|
||||
The tests require the :code:`relative` feature to be enabled, install the necessary dependencies using pip::
|
||||
|
||||
$ pip install .[relative]
|
||||
|
||||
Tests
|
||||
-----
|
||||
|
||||
To run the unit tests, navigate to the source directory and run the tests for the python version being worked on (python2, python3)::
|
||||
|
||||
$ python2 -m unittest discover aniso8601/tests/
|
||||
|
||||
or::
|
||||
|
||||
$ python3 -m unittest discover aniso8601/tests/
|
||||
|
||||
Contributing
|
||||
============
|
||||
|
||||
aniso8601 is an open source project hosted on `Bitbucket <https://bitbucket.org/nielsenb/aniso8601>`_.
|
||||
|
||||
Any and all bugs are welcome on our `issue tracker <https://bitbucket.org/nielsenb/aniso8601/issues>`_.
|
||||
Of particular interest are valid ISO 8601 strings that don't parse, or invalid ones that do. At a minimum,
|
||||
bug reports should include an example of the misbehaving string, as well as the expected result. Of course
|
||||
patches containing unit tests (or fixed bugs) are welcome!
|
||||
|
||||
References
|
||||
==========
|
||||
|
||||
* `ISO 8601:2004(E) <http://dotat.at/tmp/ISO_8601-2004_E.pdf>`_ (Caution, PDF link)
|
||||
* `Wikipedia article on ISO 8601 <http://en.wikipedia.org/wiki/Iso8601>`_
|
||||
* `Discussion on alternative ISO 8601 parsers for Python <https://groups.google.com/forum/#!topic/comp.lang.python/Q2w4R89Nq1w>`_
|
||||
|
||||
|
@ -1 +0,0 @@
|
||||
pip
|
@ -1,413 +0,0 @@
|
||||
Metadata-Version: 2.0
|
||||
Name: aniso8601
|
||||
Version: 3.0.2
|
||||
Summary: A library for parsing ISO 8601 strings.
|
||||
Home-page: https://bitbucket.org/nielsenb/aniso8601
|
||||
Author: Brandon Nielsen
|
||||
Author-email: nielsenb@jetfuse.net
|
||||
License: UNKNOWN
|
||||
Project-URL: Source, https://bitbucket.org/nielsenb/aniso8601
|
||||
Project-URL: Documentation, http://aniso8601.readthedocs.io/en/latest/
|
||||
Project-URL: Tracker, https://bitbucket.org/nielsenb/aniso8601/issues
|
||||
Keywords: iso8601 parser
|
||||
Platform: UNKNOWN
|
||||
Classifier: Development Status :: 5 - Production/Stable
|
||||
Classifier: Intended Audience :: Developers
|
||||
Classifier: License :: OSI Approved :: BSD License
|
||||
Classifier: Operating System :: OS Independent
|
||||
Classifier: Programming Language :: Python
|
||||
Classifier: Programming Language :: Python :: 2
|
||||
Classifier: Programming Language :: Python :: 2.7
|
||||
Classifier: Programming Language :: Python :: 3
|
||||
Classifier: Programming Language :: Python :: 3.4
|
||||
Classifier: Programming Language :: Python :: 3.5
|
||||
Classifier: Programming Language :: Python :: 3.6
|
||||
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
||||
Provides-Extra: relative
|
||||
Provides-Extra: relative
|
||||
Requires-Dist: python-dateutil; extra == 'relative'
|
||||
|
||||
===========
|
||||
aniso8601
|
||||
===========
|
||||
|
||||
----------------------------------
|
||||
Another ISO 8601 parser for Python
|
||||
----------------------------------
|
||||
|
||||
Features
|
||||
========
|
||||
* Pure Python implementation
|
||||
* Python 3 support
|
||||
* Logical behavior
|
||||
|
||||
- Parse a time, get a `datetime.time <http://docs.python.org/2/library/datetime.html#datetime.time>`_
|
||||
- Parse a date, get a `datetime.date <http://docs.python.org/2/library/datetime.html#datetime.date>`_
|
||||
- Parse a datetime, get a `datetime.datetime <http://docs.python.org/2/library/datetime.html#datetime.datetime>`_
|
||||
- Parse a duration, get a `datetime.timedelta <http://docs.python.org/2/library/datetime.html#datetime.timedelta>`_
|
||||
- Parse an interval, get a tuple of dates or datetimes
|
||||
- Parse a repeating interval, get a date or datetime `generator <http://www.python.org/dev/peps/pep-0255/>`_
|
||||
|
||||
* UTC offset represented as fixed-offset tzinfo
|
||||
* Optional `dateutil.relativedelta <http://dateutil.readthedocs.io/en/latest/relativedelta.html>`_ support for calendar accuracy
|
||||
* No regular expressions
|
||||
|
||||
Installation
|
||||
============
|
||||
|
||||
The recommended installation method is to use pip::
|
||||
|
||||
$ pip install aniso8601
|
||||
|
||||
Alternatively, you can download the source (git repository hosted at `Bitbucket <https://bitbucket.org/nielsenb/aniso8601>`_) and install directly::
|
||||
|
||||
$ python setup.py install
|
||||
|
||||
Use
|
||||
===
|
||||
|
||||
Parsing datetimes
|
||||
-----------------
|
||||
|
||||
To parse a typical ISO 8601 datetime string::
|
||||
|
||||
>>> import aniso8601
|
||||
>>> aniso8601.parse_datetime('1977-06-10T12:00:00Z')
|
||||
datetime.datetime(1977, 6, 10, 12, 0, tzinfo=+0:00:00 UTC)
|
||||
|
||||
Alternative delimiters can be specified, for example, a space::
|
||||
|
||||
>>> aniso8601.parse_datetime('1977-06-10 12:00:00Z', delimiter=' ')
|
||||
datetime.datetime(1977, 6, 10, 12, 0, tzinfo=+0:00:00 UTC)
|
||||
|
||||
UTC offsets are supported::
|
||||
|
||||
>>> aniso8601.parse_datetime('1979-06-05T08:00:00-08:00')
|
||||
datetime.datetime(1979, 6, 5, 8, 0, tzinfo=-8:00:00 UTC)
|
||||
|
||||
If a UTC offset is not specified, the returned datetime will be naive::
|
||||
|
||||
>>> aniso8601.parse_datetime('1983-01-22T08:00:00')
|
||||
datetime.datetime(1983, 1, 22, 8, 0)
|
||||
|
||||
Leap seconds are currently not supported and attempting to parse one raises a :code:`LeapSecondError`::
|
||||
|
||||
>>> aniso8601.parse_datetime('2018-03-06T23:59:60')
|
||||
Traceback (most recent call last):
|
||||
File "<stdin>", line 1, in <module>
|
||||
File "aniso8601/time.py", line 127, in parse_datetime
|
||||
timepart = parse_time(isotimestr)
|
||||
File "aniso8601/time.py", line 110, in parse_time
|
||||
return _parse_time_naive(timestr)
|
||||
File "aniso8601/time.py", line 140, in _parse_time_naive
|
||||
return _RESOLUTION_MAP[get_time_resolution(timestr)](timestr)
|
||||
File "aniso8601/time.py", line 214, in _parse_second_time
|
||||
raise LeapSecondError('Leap seconds are not supported.')
|
||||
aniso8601.exceptions.LeapSecondError: Leap seconds are not supported.
|
||||
|
||||
Parsing dates
|
||||
-------------
|
||||
|
||||
To parse a date represented in an ISO 8601 string::
|
||||
|
||||
>>> import aniso8601
|
||||
>>> aniso8601.parse_date('1984-04-23')
|
||||
datetime.date(1984, 4, 23)
|
||||
|
||||
Basic format is supported as well::
|
||||
|
||||
>>> aniso8601.parse_date('19840423')
|
||||
datetime.date(1984, 4, 23)
|
||||
|
||||
To parse a date using the ISO 8601 week date format::
|
||||
|
||||
>>> aniso8601.parse_date('1986-W38-1')
|
||||
datetime.date(1986, 9, 15)
|
||||
|
||||
To parse an ISO 8601 ordinal date::
|
||||
|
||||
>>> aniso8601.parse_date('1988-132')
|
||||
datetime.date(1988, 5, 11)
|
||||
|
||||
Parsing times
|
||||
-------------
|
||||
|
||||
To parse a time formatted as an ISO 8601 string::
|
||||
|
||||
>>> import aniso8601
|
||||
>>> aniso8601.parse_time('11:31:14')
|
||||
datetime.time(11, 31, 14)
|
||||
|
||||
As with all of the above, basic format is supported::
|
||||
|
||||
>>> aniso8601.parse_time('113114')
|
||||
datetime.time(11, 31, 14)
|
||||
|
||||
A UTC offset can be specified for times::
|
||||
|
||||
>>> aniso8601.parse_time('17:18:19-02:30')
|
||||
datetime.time(17, 18, 19, tzinfo=-2:30:00 UTC)
|
||||
>>> aniso8601.parse_time('171819Z')
|
||||
datetime.time(17, 18, 19, tzinfo=+0:00:00 UTC)
|
||||
|
||||
Reduced accuracy is supported::
|
||||
|
||||
>>> aniso8601.parse_time('21:42')
|
||||
datetime.time(21, 42)
|
||||
>>> aniso8601.parse_time('22')
|
||||
datetime.time(22, 0)
|
||||
|
||||
A decimal fraction is always allowed on the lowest order element of an ISO 8601 formatted time::
|
||||
|
||||
>>> aniso8601.parse_time('22:33.5')
|
||||
datetime.time(22, 33, 30)
|
||||
>>> aniso8601.parse_time('23.75')
|
||||
datetime.time(23, 45)
|
||||
|
||||
Leap seconds are currently not supported and attempting to parse one raises a :code:`LeapSecondError`::
|
||||
|
||||
>>> aniso8601.parse_time('23:59:60')
|
||||
Traceback (most recent call last):
|
||||
File "<stdin>", line 1, in <module>
|
||||
File "aniso8601/time.py", line 110, in parse_time
|
||||
return _parse_time_naive(timestr)
|
||||
File "aniso8601/time.py", line 140, in _parse_time_naive
|
||||
return _RESOLUTION_MAP[get_time_resolution(timestr)](timestr)
|
||||
File "aniso8601/time.py", line 214, in _parse_second_time
|
||||
raise LeapSecondError('Leap seconds are not supported.')
|
||||
aniso8601.exceptions.LeapSecondError: Leap seconds are not supported.
|
||||
|
||||
Parsing durations
|
||||
-----------------
|
||||
|
||||
To parse a duration formatted as an ISO 8601 string::
|
||||
|
||||
>>> import aniso8601
|
||||
>>> aniso8601.parse_duration('P1Y2M3DT4H54M6S')
|
||||
datetime.timedelta(428, 17646)
|
||||
|
||||
Reduced accuracy is supported::
|
||||
|
||||
>>> aniso8601.parse_duration('P1Y')
|
||||
datetime.timedelta(365)
|
||||
|
||||
A decimal fraction is allowed on the lowest order element::
|
||||
|
||||
>>> aniso8601.parse_duration('P1YT3.5M')
|
||||
datetime.timedelta(365, 210)
|
||||
|
||||
The decimal fraction can be specified with a comma instead of a full-stop::
|
||||
|
||||
>>> aniso8601.parse_duration('P1YT3,5M')
|
||||
datetime.timedelta(365, 210)
|
||||
|
||||
Parsing a duration from a combined date and time is supported as well::
|
||||
|
||||
>>> aniso8601.parse_duration('P0001-01-02T01:30:5')
|
||||
datetime.timedelta(397, 5405)
|
||||
|
||||
The above treat years as 365 days and months as 30 days. If calendar level accuracy is required, the relative keyword argument can be used if `python-dateutil <https://pypi.python.org/pypi/python-dateutil>`_ is installed::
|
||||
|
||||
>>> import aniso8601
|
||||
>>> from datetime import date
|
||||
>>> one_month = aniso8601.parse_duration('P1M', relative=True)
|
||||
>>> print one_month
|
||||
relativedelta(months=+1)
|
||||
>>> date(2003,1,27) + one_month
|
||||
datetime.date(2003, 2, 27)
|
||||
>>> date(2003,1,31) + one_month
|
||||
datetime.date(2003, 2, 28)
|
||||
>>> date(2003,1,31) + two_months
|
||||
datetime.date(2003, 3, 31)
|
||||
|
||||
Since a relative fractional month or year is not logical, a :code:`RelativeValueError` is raised when attempting to parse a duration with :code:`relative=True` and fractional month or year::
|
||||
|
||||
>>> aniso8601.parse_duration('P2.1Y', relative=True)
|
||||
Traceback (most recent call last):
|
||||
File "<stdin>", line 1, in <module>
|
||||
File "aniso8601/duration.py", line 29, in parse_duration
|
||||
return _parse_duration_prescribed(isodurationstr, relative)
|
||||
File "aniso8601/duration.py", line 73, in _parse_duration_prescribed
|
||||
raise RelativeValueError('Fractional months and years are not defined for relative intervals.')
|
||||
aniso8601.exceptions.RelativeValueError: Fractional months and years are not defined for relative intervals.
|
||||
|
||||
If :code:`relative=True` is set without python-dateutil available, a :code:`RuntimeError` is raised::
|
||||
|
||||
>>> aniso8601.parse_duration('P1M', relative=True)
|
||||
Traceback (most recent call last):
|
||||
File "<stdin>", line 1, in <module>
|
||||
File "aniso8601/duration.py", line 29, in parse_duration
|
||||
return _parse_duration_prescribed(isodurationstr, relative)
|
||||
File "aniso8601/duration.py", line 77, in _parse_duration_prescribed
|
||||
raise RuntimeError('dateutil must be installed for relative duration support.')
|
||||
RuntimeError: dateutil must be installed for relative duration support
|
||||
|
||||
Parsing intervals
|
||||
-----------------
|
||||
|
||||
To parse an interval specified by a start and end::
|
||||
|
||||
>>> import aniso8601
|
||||
>>> aniso8601.parse_interval('2007-03-01T13:00:00/2008-05-11T15:30:00')
|
||||
(datetime.datetime(2007, 3, 1, 13, 0), datetime.datetime(2008, 5, 11, 15, 30))
|
||||
|
||||
Intervals specified by a start time and a duration are supported::
|
||||
|
||||
>>> aniso8601.parse_interval('2007-03-01T13:00:00Z/P1Y2M10DT2H30M')
|
||||
(datetime.datetime(2007, 3, 1, 13, 0, tzinfo=+0:00:00 UTC), datetime.datetime(2008, 5, 9, 15, 30, tzinfo=+0:00:00 UTC))
|
||||
|
||||
A duration can also be specified by a duration and end time::
|
||||
|
||||
>>> aniso8601.parse_interval('P1M/1981-04-05')
|
||||
(datetime.date(1981, 4, 5), datetime.date(1981, 3, 6))
|
||||
|
||||
Notice that the result of the above parse is not in order from earliest to latest. If sorted intervals are required, simply use the :code:`sorted` keyword as shown below::
|
||||
|
||||
>>> sorted(aniso8601.parse_interval('P1M/1981-04-05'))
|
||||
[datetime.date(1981, 3, 6), datetime.date(1981, 4, 5)]
|
||||
|
||||
The end of an interval is given as a datetime when required to maintain the resolution specified by a duration, even if the duration start is given as a date::
|
||||
|
||||
>>> aniso8601.parse_interval('2014-11-12/PT4H54M6.5S')
|
||||
(datetime.date(2014, 11, 12), datetime.datetime(2014, 11, 12, 4, 54, 6, 500000))
|
||||
|
||||
Repeating intervals are supported as well, and return a generator::
|
||||
|
||||
>>> aniso8601.parse_repeating_interval('R3/1981-04-05/P1D')
|
||||
<generator object date_generator at 0x7f698cdefc80>
|
||||
>>> list(aniso8601.parse_repeating_interval('R3/1981-04-05/P1D'))
|
||||
[datetime.date(1981, 4, 5), datetime.date(1981, 4, 6), datetime.date(1981, 4, 7)]
|
||||
|
||||
Repeating intervals are allowed to go in the reverse direction::
|
||||
|
||||
>>> list(aniso8601.parse_repeating_interval('R2/PT1H2M/1980-03-05T01:01:00'))
|
||||
[datetime.datetime(1980, 3, 5, 1, 1), datetime.datetime(1980, 3, 4, 23, 59)]
|
||||
|
||||
Unbounded intervals are also allowed (Python 2)::
|
||||
|
||||
>>> result = aniso8601.parse_repeating_interval('R/PT1H2M/1980-03-05T01:01:00')
|
||||
>>> result.next()
|
||||
datetime.datetime(1980, 3, 5, 1, 1)
|
||||
>>> result.next()
|
||||
datetime.datetime(1980, 3, 4, 23, 59)
|
||||
|
||||
or for Python 3::
|
||||
|
||||
>>> result = aniso8601.parse_repeating_interval('R/PT1H2M/1980-03-05T01:01:00')
|
||||
>>> next(result)
|
||||
datetime.datetime(1980, 3, 5, 1, 1)
|
||||
>>> next(result)
|
||||
datetime.datetime(1980, 3, 4, 23, 59)
|
||||
|
||||
Note that you should never try to convert a generator produced by an unbounded interval to a list::
|
||||
|
||||
>>> list(aniso8601.parse_repeating_interval('R/PT1H2M/1980-03-05T01:01:00'))
|
||||
Traceback (most recent call last):
|
||||
File "<stdin>", line 1, in <module>
|
||||
File "aniso8601/interval.py", line 161, in _date_generator_unbounded
|
||||
currentdate += timedelta
|
||||
OverflowError: date value out of range
|
||||
|
||||
The above treat years as 365 days and months as 30 days. If calendar level accuracy is required, the relative keyword argument can be used if `python-dateutil <https://pypi.python.org/pypi/python-dateutil>`_ is installed::
|
||||
|
||||
>>> aniso8601.parse_interval('2003-01-27/P1M', relative=True)
|
||||
(datetime.date(2003, 1, 27), datetime.date(2003, 2, 27))
|
||||
>>> aniso8601.parse_interval('2003-01-31/P1M', relative=True)
|
||||
(datetime.date(2003, 1, 31), datetime.date(2003, 2, 28))
|
||||
>>> aniso8601.parse_interval('P1Y/2001-02-28', relative=True)
|
||||
(datetime.date(2001, 2, 28), datetime.date(2000, 2, 28)
|
||||
|
||||
Fractional years and months do not make sense for relative intervals. A :code:`RelativeValueError` is raised when attempting to parse an interval with :code:`relative=True` and a fractional month or year::
|
||||
|
||||
>>> aniso8601.parse_interval('P1.1Y/2001-02-28', relative=True)
|
||||
Traceback (most recent call last):
|
||||
File "<stdin>", line 1, in <module>
|
||||
File "aniso8601/interval.py", line 37, in parse_interval
|
||||
interval_parts = _parse_interval_parts(isointervalstr, intervaldelimiter, datetimedelimiter, relative)
|
||||
File "aniso8601/interval.py", line 89, in _parse_interval_parts
|
||||
duration = parse_duration(firstpart, relative=relative)
|
||||
File "aniso8601/duration.py", line 29, in parse_duration
|
||||
return _parse_duration_prescribed(isodurationstr, relative)
|
||||
File "aniso8601/duration.py", line 73, in _parse_duration_prescribed
|
||||
raise RelativeValueError('Fractional months and years are not defined for relative intervals.')
|
||||
aniso8601.exceptions.RelativeValueError: Fractional months and years are not defined for relative intervals.
|
||||
|
||||
If :code:`relative=True` is set without python-dateutil available, a :code:`RuntimeError` is raised::
|
||||
|
||||
>>> aniso8601.parse_interval('2003-01-27/P1M', relative=True)
|
||||
Traceback (most recent call last):
|
||||
File "<stdin>", line 1, in <module>
|
||||
File "aniso8601/interval.py", line 37, in parse_interval
|
||||
interval_parts = _parse_interval_parts(isointervalstr, intervaldelimiter, datetimedelimiter, relative)
|
||||
File "aniso8601/interval.py", line 108, in _parse_interval_parts
|
||||
duration = parse_duration(secondpart, relative=relative)
|
||||
File "aniso8601/duration.py", line 29, in parse_duration
|
||||
return _parse_duration_prescribed(isodurationstr, relative)
|
||||
File "aniso8601/duration.py", line 77, in _parse_duration_prescribed
|
||||
raise RuntimeError('dateutil must be installed for relative duration support.')
|
||||
RuntimeError: dateutil must be installed for relative duration support.
|
||||
|
||||
Date and time resolution
|
||||
------------------------
|
||||
|
||||
In some situations, it may be useful to figure out the resolution provided by an ISO 8601 date or time string. Two functions are provided for this purpose.
|
||||
|
||||
To get the resolution of a ISO 8601 time string::
|
||||
|
||||
>>> aniso8601.get_time_resolution('11:31:14') == aniso8601.resolution.TimeResolution.Seconds
|
||||
True
|
||||
>>> aniso8601.get_time_resolution('11:31') == aniso8601.resolution.TimeResolution.Minutes
|
||||
True
|
||||
>>> aniso8601.get_time_resolution('11') == aniso8601.resolution.TimeResolution.Hours
|
||||
True
|
||||
|
||||
Similarly, for an ISO 8601 date string::
|
||||
|
||||
>>> aniso8601.get_date_resolution('1981-04-05') == aniso8601.resolution.DateResolution.Day
|
||||
True
|
||||
>>> aniso8601.get_date_resolution('1981-04') == aniso8601.resolution.DateResolution.Month
|
||||
True
|
||||
>>> aniso8601.get_date_resolution('1981') == aniso8601.resolution.DateResolution.Year
|
||||
True
|
||||
|
||||
Development
|
||||
===========
|
||||
|
||||
Setup
|
||||
-----
|
||||
|
||||
It is recommended to develop using a `virtualenv <https://virtualenv.pypa.io/en/stable/>`_.
|
||||
|
||||
The tests require the :code:`relative` feature to be enabled, install the necessary dependencies using pip::
|
||||
|
||||
$ pip install .[relative]
|
||||
|
||||
Tests
|
||||
-----
|
||||
|
||||
To run the unit tests, navigate to the source directory and run the tests for the python version being worked on (python2, python3)::
|
||||
|
||||
$ python2 -m unittest discover aniso8601/tests/
|
||||
|
||||
or::
|
||||
|
||||
$ python3 -m unittest discover aniso8601/tests/
|
||||
|
||||
Contributing
|
||||
============
|
||||
|
||||
aniso8601 is an open source project hosted on `Bitbucket <https://bitbucket.org/nielsenb/aniso8601>`_.
|
||||
|
||||
Any and all bugs are welcome on our `issue tracker <https://bitbucket.org/nielsenb/aniso8601/issues>`_.
|
||||
Of particular interest are valid ISO 8601 strings that don't parse, or invalid ones that do. At a minimum,
|
||||
bug reports should include an example of the misbehaving string, as well as the expected result. Of course
|
||||
patches containing unit tests (or fixed bugs) are welcome!
|
||||
|
||||
References
|
||||
==========
|
||||
|
||||
* `ISO 8601:2004(E) <http://dotat.at/tmp/ISO_8601-2004_E.pdf>`_ (Caution, PDF link)
|
||||
* `Wikipedia article on ISO 8601 <http://en.wikipedia.org/wiki/Iso8601>`_
|
||||
* `Discussion on alternative ISO 8601 parsers for Python <https://groups.google.com/forum/#!topic/comp.lang.python/Q2w4R89Nq1w>`_
|
||||
|
||||
|
@ -1,25 +0,0 @@
|
||||
aniso8601/__init__.py,sha256=r15awjHgn8H6loHfYB4mJgGeiSTiJpIvIpVK4gXCzzk,527
|
||||
aniso8601/compat.py,sha256=TEGb7_3qpbKtAfu7FfSkf0tcc9lquiDZ-Yt4uQCw45o,305
|
||||
aniso8601/date.py,sha256=fmY0t6ESI6dooFOI2g57RFgIPrmxnyEI8DznuW7QrmE,8405
|
||||
aniso8601/duration.py,sha256=qwrK0nS-Zo9hVGZzmhL0aQXOuTv_mBNmO74u8PbC5RE,10726
|
||||
aniso8601/exceptions.py,sha256=N94MSQSnyctkjFXRlg4T91qQpYYSSrXuGCc1V1-knUI,1210
|
||||
aniso8601/interval.py,sha256=VPgu4po6j88CtRy-0RNPBX_BffyiN-mAFvpdyesCtT4,6447
|
||||
aniso8601/resolution.py,sha256=JklhrwxP807fpZZhLvQrDy40lzIz5OctsMK7m1yNSk8,422
|
||||
aniso8601/time.py,sha256=MG61Oc4cyCBYklNPIkO14PCod4F8FyMe9N0X8rme2Rs,8128
|
||||
aniso8601/timezone.py,sha256=9A-ah5xFapOkxOJFEPvmF-_WYnn2zfS5ZrHqQdC5K9M,3376
|
||||
aniso8601-3.0.2.dist-info/DESCRIPTION.rst,sha256=IRO_7zo9wM-GXLQbkggGyZoIVr3ddwLG627qf5-Wl90,15070
|
||||
aniso8601-3.0.2.dist-info/METADATA,sha256=DaU0fNs-ghRC1q6nKwF1cXiQieIyHNh-oZhNv0vRGrc,16259
|
||||
aniso8601-3.0.2.dist-info/RECORD,,
|
||||
aniso8601-3.0.2.dist-info/WHEEL,sha256=5wvfB7GvgZAbKBSE9uX9Zbi6LCL-_KgezgHblXhCRnM,113
|
||||
aniso8601-3.0.2.dist-info/metadata.json,sha256=loCVI9Gvc7WILza0eIH-iQITWlL8rDxjWSM2Z9K6ZLU,1135
|
||||
aniso8601-3.0.2.dist-info/top_level.txt,sha256=MVQomyeED8nGIH7PUQdMzxgLppIB48oYHtcmL17ETB0,10
|
||||
aniso8601-3.0.2.dist-info/INSTALLER,sha256=zuuue4knoyJ-UwPPXg8fezS7VCrXJQrAP7zeNuwvFQg,4
|
||||
aniso8601/__pycache__/compat.cpython-36.pyc,,
|
||||
aniso8601/__pycache__/date.cpython-36.pyc,,
|
||||
aniso8601/__pycache__/duration.cpython-36.pyc,,
|
||||
aniso8601/__pycache__/exceptions.cpython-36.pyc,,
|
||||
aniso8601/__pycache__/interval.cpython-36.pyc,,
|
||||
aniso8601/__pycache__/resolution.cpython-36.pyc,,
|
||||
aniso8601/__pycache__/time.cpython-36.pyc,,
|
||||
aniso8601/__pycache__/timezone.cpython-36.pyc,,
|
||||
aniso8601/__pycache__/__init__.cpython-36.pyc,,
|
@ -1,6 +0,0 @@
|
||||
Wheel-Version: 1.0
|
||||
Generator: bdist_wheel (0.30.0.a0)
|
||||
Root-Is-Purelib: true
|
||||
Tag: py2-none-any
|
||||
Tag: py3-none-any
|
||||
|
@ -1 +0,0 @@
|
||||
{"classifiers": ["Development Status :: 5 - Production/Stable", "Intended Audience :: Developers", "License :: OSI Approved :: BSD License", "Operating System :: OS Independent", "Programming Language :: Python", "Programming Language :: Python :: 2", "Programming Language :: Python :: 2.7", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3.4", "Programming Language :: Python :: 3.5", "Programming Language :: Python :: 3.6", "Topic :: Software Development :: Libraries :: Python Modules"], "extensions": {"python.details": {"contacts": [{"email": "nielsenb@jetfuse.net", "name": "Brandon Nielsen", "role": "author"}], "document_names": {"description": "DESCRIPTION.rst"}, "project_urls": {"Home": "https://bitbucket.org/nielsenb/aniso8601"}}}, "extras": ["relative"], "generator": "bdist_wheel (0.30.0.a0)", "keywords": ["iso8601", "parser"], "metadata_version": "2.0", "name": "aniso8601", "project_url": "Source, https://bitbucket.org/nielsenb/aniso8601", "run_requires": [{"extra": "relative", "requires": ["python-dateutil"]}], "summary": "A library for parsing ISO 8601 strings.", "version": "3.0.2"}
|
@ -1 +0,0 @@
|
||||
aniso8601
|
@ -1,13 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# Copyright (c) 2018, Brandon Nielsen
|
||||
# All rights reserved.
|
||||
#
|
||||
# This software may be modified and distributed under the terms
|
||||
# of the BSD license. See the LICENSE file for details.
|
||||
|
||||
#Import the main parsing functions so they are readily available
|
||||
from aniso8601.time import parse_datetime, parse_time, get_time_resolution
|
||||
from aniso8601.date import parse_date, get_date_resolution
|
||||
from aniso8601.duration import parse_duration
|
||||
from aniso8601.interval import parse_interval, parse_repeating_interval
|
@ -1,16 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# Copyright (c) 2018, Brandon Nielsen
|
||||
# All rights reserved.
|
||||
#
|
||||
# This software may be modified and distributed under the terms
|
||||
# of the BSD license. See the LICENSE file for details.
|
||||
|
||||
import sys
|
||||
|
||||
PY2 = sys.version_info[0] == 2
|
||||
|
||||
if PY2:
|
||||
range = xrange
|
||||
else:
|
||||
range = range
|
@ -1,250 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# Copyright (c) 2018, Brandon Nielsen
|
||||
# All rights reserved.
|
||||
#
|
||||
# This software may be modified and distributed under the terms
|
||||
# of the BSD license. See the LICENSE file for details.
|
||||
|
||||
import datetime
|
||||
|
||||
from aniso8601.exceptions import DayOutOfBoundsError, ISOFormatError, \
|
||||
WeekOutOfBoundsError, YearOutOfBoundsError
|
||||
from aniso8601.resolution import DateResolution
|
||||
|
||||
def get_date_resolution(isodatestr):
|
||||
#Valid string formats are:
|
||||
#
|
||||
#Y[YYY]
|
||||
#YYYY-MM-DD
|
||||
#YYYYMMDD
|
||||
#YYYY-MM
|
||||
#YYYY-Www
|
||||
#YYYYWww
|
||||
#YYYY-Www-D
|
||||
#YYYYWwwD
|
||||
#YYYY-DDD
|
||||
#YYYYDDD
|
||||
if isodatestr.startswith('+') or isodatestr.startswith('-'):
|
||||
raise NotImplementedError('ISO 8601 extended year representation not supported.')
|
||||
|
||||
if isodatestr.find('W') != -1:
|
||||
#Handle ISO 8601 week date format
|
||||
hyphens_present = 1 if isodatestr.find('-') != -1 else 0
|
||||
week_date_len = 7 + hyphens_present
|
||||
weekday_date_len = 8 + 2 * hyphens_present
|
||||
|
||||
if len(isodatestr) == week_date_len:
|
||||
#YYYY-Www
|
||||
#YYYYWww
|
||||
return DateResolution.Week
|
||||
elif len(isodatestr) == weekday_date_len:
|
||||
#YYYY-Www-D
|
||||
#YYYYWwwD
|
||||
return DateResolution.Weekday
|
||||
else:
|
||||
raise ISOFormatError('"{0}" is not a valid ISO 8601 week date.'.format(isodatestr))
|
||||
|
||||
#If the size of the string of 4 or less, assume its a truncated year representation
|
||||
if len(isodatestr) <= 4:
|
||||
return DateResolution.Year
|
||||
|
||||
#An ISO string may be a calendar represntation if:
|
||||
# 1) When split on a hyphen, the sizes of the parts are 4, 2, 2 or 4, 2
|
||||
# 2) There are no hyphens, and the length is 8
|
||||
datestrsplit = isodatestr.split('-')
|
||||
|
||||
#Check case 1
|
||||
if len(datestrsplit) == 2:
|
||||
if len(datestrsplit[0]) == 4 and len(datestrsplit[1]) == 2:
|
||||
return DateResolution.Month
|
||||
|
||||
if len(datestrsplit) == 3:
|
||||
if len(datestrsplit[0]) == 4 and len(datestrsplit[1]) == 2 and len(datestrsplit[2]) == 2:
|
||||
return DateResolution.Day
|
||||
|
||||
#Check case 2
|
||||
if len(isodatestr) == 8 and isodatestr.find('-') == -1:
|
||||
return DateResolution.Day
|
||||
|
||||
#An ISO string may be a ordinal date representation if:
|
||||
# 1) When split on a hyphen, the sizes of the parts are 4, 3
|
||||
# 2) There are no hyphens, and the length is 7
|
||||
|
||||
#Check case 1
|
||||
if len(datestrsplit) == 2:
|
||||
if len(datestrsplit[0]) == 4 and len(datestrsplit[1]) == 3:
|
||||
return DateResolution.Ordinal
|
||||
|
||||
#Check case 2
|
||||
if len(isodatestr) == 7 and isodatestr.find('-') == -1:
|
||||
return DateResolution.Ordinal
|
||||
|
||||
#None of the date representations match
|
||||
raise ISOFormatError('"{0}" is not an ISO 8601 date, perhaps it represents a time or datetime.'.format(isodatestr))
|
||||
|
||||
def parse_date(isodatestr):
|
||||
#Given a string in any ISO 8601 date format, return a datetime.date
|
||||
#object that corresponds to the given date. Valid string formats are:
|
||||
#
|
||||
#Y[YYY]
|
||||
#YYYY-MM-DD
|
||||
#YYYYMMDD
|
||||
#YYYY-MM
|
||||
#YYYY-Www
|
||||
#YYYYWww
|
||||
#YYYY-Www-D
|
||||
#YYYYWwwD
|
||||
#YYYY-DDD
|
||||
#YYYYDDD
|
||||
#
|
||||
#Note that the ISO 8601 date format of ±YYYYY is expressly not supported
|
||||
return _RESOLUTION_MAP[get_date_resolution(isodatestr)](isodatestr)
|
||||
|
||||
def _parse_year(yearstr):
|
||||
#yearstr is of the format Y[YYY]
|
||||
#
|
||||
#0000 (1 BC) is not representible as a Python date so a ValueError is
|
||||
#raised
|
||||
#
|
||||
#Truncated dates, like '19', refer to 1900-1999 inclusive, we simply parse
|
||||
#to 1900-01-01
|
||||
#
|
||||
#Since no additional resolution is provided, the month is set to 1, and
|
||||
#day is set to 1
|
||||
|
||||
if len(yearstr) == 4:
|
||||
isoyear = int(yearstr)
|
||||
else:
|
||||
#Shift 0s in from the left to form complete year
|
||||
isoyear = int(yearstr.ljust(4, '0'))
|
||||
|
||||
if isoyear == 0:
|
||||
raise YearOutOfBoundsError('Year must be between 1..9999.')
|
||||
|
||||
return datetime.date(isoyear, 1, 1)
|
||||
|
||||
def _parse_calendar_day(datestr):
|
||||
#datestr is of the format YYYY-MM-DD or YYYYMMDD
|
||||
if len(datestr) == 10:
|
||||
#YYYY-MM-DD
|
||||
strformat = '%Y-%m-%d'
|
||||
elif len(datestr) == 8:
|
||||
#YYYYMMDD
|
||||
strformat = '%Y%m%d'
|
||||
else:
|
||||
raise ISOFormatError('"{0}" is not a valid ISO 8601 calendar day.'.format(datestr))
|
||||
|
||||
parseddatetime = datetime.datetime.strptime(datestr, strformat)
|
||||
|
||||
#Since no 'time' is given, cast to a date
|
||||
return parseddatetime.date()
|
||||
|
||||
def _parse_calendar_month(datestr):
|
||||
#datestr is of the format YYYY-MM
|
||||
if len(datestr) != 7:
|
||||
raise ISOFormatError('"{0}" is not a valid ISO 8601 calendar month.'.format(datestr))
|
||||
|
||||
parseddatetime = datetime.datetime.strptime(datestr, '%Y-%m')
|
||||
|
||||
#Since no 'time' is given, cast to a date
|
||||
return parseddatetime.date()
|
||||
|
||||
def _parse_week_day(datestr):
|
||||
#datestr is of the format YYYY-Www-D, YYYYWwwD
|
||||
#
|
||||
#W is the week number prefix, ww is the week number, between 1 and 53
|
||||
#0 is not a valid week number, which differs from the Python implementation
|
||||
#
|
||||
#D is the weekday number, between 1 and 7, which differs from the Python
|
||||
#implementation which is between 0 and 6
|
||||
|
||||
isoyear = int(datestr[0:4])
|
||||
gregorianyearstart = _iso_year_start(isoyear)
|
||||
|
||||
#Week number will be the two characters after the W
|
||||
windex = datestr.find('W')
|
||||
isoweeknumber = int(datestr[windex + 1:windex + 3])
|
||||
|
||||
if isoweeknumber == 0 or isoweeknumber > 53:
|
||||
raise WeekOutOfBoundsError('Week number must be between 1..53.')
|
||||
|
||||
if datestr.find('-') != -1 and len(datestr) == 10:
|
||||
#YYYY-Www-D
|
||||
isoday = int(datestr[9:10])
|
||||
elif len(datestr) == 8:
|
||||
#YYYYWwwD
|
||||
isoday = int(datestr[7:8])
|
||||
else:
|
||||
raise ISOFormatError('"{0}" is not a valid ISO 8601 week date.'.format(datestr))
|
||||
|
||||
if isoday == 0 or isoday > 7:
|
||||
raise DayOutOfBoundsError('Weekday number must be between 1..7.')
|
||||
|
||||
return gregorianyearstart + datetime.timedelta(weeks=isoweeknumber - 1, days=isoday - 1)
|
||||
|
||||
def _parse_week(datestr):
|
||||
#datestr is of the format YYYY-Www, YYYYWww
|
||||
#
|
||||
#W is the week number prefix, ww is the week number, between 1 and 53
|
||||
#0 is not a valid week number, which differs from the Python implementation
|
||||
|
||||
isoyear = int(datestr[0:4])
|
||||
gregorianyearstart = _iso_year_start(isoyear)
|
||||
|
||||
#Week number will be the two characters after the W
|
||||
windex = datestr.find('W')
|
||||
isoweeknumber = int(datestr[windex + 1:windex + 3])
|
||||
|
||||
if isoweeknumber == 0 or isoweeknumber > 53:
|
||||
raise WeekOutOfBoundsError('Week number must be between 1..53.')
|
||||
|
||||
return gregorianyearstart + datetime.timedelta(weeks=isoweeknumber - 1, days=0)
|
||||
|
||||
def _parse_ordinal_date(datestr):
|
||||
#datestr is of the format YYYY-DDD or YYYYDDD
|
||||
#DDD can be from 1 - 36[5,6], this matches Python's definition
|
||||
|
||||
isoyear = int(datestr[0:4])
|
||||
|
||||
if datestr.find('-') != -1:
|
||||
#YYYY-DDD
|
||||
isoday = int(datestr[(datestr.find('-') + 1):])
|
||||
else:
|
||||
#YYYYDDD
|
||||
isoday = int(datestr[4:])
|
||||
|
||||
parseddate = datetime.date(isoyear, 1, 1) + datetime.timedelta(days=isoday - 1)
|
||||
|
||||
#Enforce ordinal day limitation
|
||||
#https://bitbucket.org/nielsenb/aniso8601/issues/14/parsing-ordinal-dates-should-only-allow
|
||||
if isoday == 0 or parseddate.year != isoyear:
|
||||
raise DayOutOfBoundsError('Day of year must be from 1..365, 1..366 for leap year.')
|
||||
|
||||
return parseddate
|
||||
|
||||
def _iso_year_start(isoyear):
|
||||
#Given an ISO year, returns the equivalent of the start of the year
|
||||
#on the Gregorian calendar (which is used by Python)
|
||||
#Stolen from:
|
||||
#http://stackoverflow.com/questions/304256/whats-the-best-way-to-find-the-inverse-of-datetime-isocalendar
|
||||
|
||||
#Determine the location of the 4th of January, the first week of
|
||||
#the ISO year is the week containing the 4th of January
|
||||
#http://en.wikipedia.org/wiki/ISO_week_date
|
||||
fourth_jan = datetime.date(isoyear, 1, 4)
|
||||
|
||||
#Note the conversion from ISO day (1 - 7) and Python day (0 - 6)
|
||||
delta = datetime.timedelta(fourth_jan.isoweekday() - 1)
|
||||
|
||||
#Return the start of the year
|
||||
return fourth_jan - delta
|
||||
|
||||
_RESOLUTION_MAP = {
|
||||
DateResolution.Day: _parse_calendar_day,
|
||||
DateResolution.Ordinal: _parse_ordinal_date,
|
||||
DateResolution.Month: _parse_calendar_month,
|
||||
DateResolution.Week: _parse_week,
|
||||
DateResolution.Weekday: _parse_week_day,
|
||||
DateResolution.Year: _parse_year
|
||||
}
|
@ -1,292 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# Copyright (c) 2018, Brandon Nielsen
|
||||
# All rights reserved.
|
||||
#
|
||||
# This software may be modified and distributed under the terms
|
||||
# of the BSD license. See the LICENSE file for details.
|
||||
|
||||
import datetime
|
||||
|
||||
from aniso8601.date import parse_date
|
||||
from aniso8601.exceptions import ISOFormatError, RelativeValueError
|
||||
from aniso8601.time import parse_time
|
||||
from aniso8601 import compat
|
||||
|
||||
def parse_duration(isodurationstr, relative=False):
|
||||
#Given a string representing an ISO 8601 duration, return a
|
||||
#datetime.timedelta (or dateutil.relativedelta.relativedelta
|
||||
#if relative=True) that matches the given duration. Valid formats are:
|
||||
#
|
||||
#PnYnMnDTnHnMnS (or any reduced precision equivalent)
|
||||
#P<date>T<time>
|
||||
|
||||
if isodurationstr[0] != 'P':
|
||||
raise ISOFormatError('ISO 8601 duration must start with a P.')
|
||||
|
||||
#If Y, M, D, H, S, or W are in the string, assume it is a specified duration
|
||||
if _has_any_component(isodurationstr, ['Y', 'M', 'D', 'H', 'S', 'W']) is True:
|
||||
return _parse_duration_prescribed(isodurationstr, relative)
|
||||
|
||||
return _parse_duration_combined(isodurationstr, relative)
|
||||
|
||||
def _parse_duration_prescribed(durationstr, relative):
|
||||
#durationstr can be of the form PnYnMnDTnHnMnS or PnW
|
||||
|
||||
#Make sure the end character is valid
|
||||
#https://bitbucket.org/nielsenb/aniso8601/issues/9/durations-with-trailing-garbage-are-parsed
|
||||
if durationstr[-1] not in ['Y', 'M', 'D', 'H', 'S', 'W']:
|
||||
raise ISOFormatError('ISO 8601 duration must end with a valid character.')
|
||||
|
||||
#Make sure only the lowest order element has decimal precision
|
||||
if durationstr.count('.') > 1:
|
||||
raise ISOFormatError('ISO 8601 allows only lowest order element to have a decimal fraction.')
|
||||
elif durationstr.count('.') == 1:
|
||||
#There should only ever be 1 letter after a decimal if there is more
|
||||
#then one, the string is invalid
|
||||
lettercount = 0
|
||||
|
||||
for character in durationstr.split('.')[1]:
|
||||
if character.isalpha() is True:
|
||||
lettercount += 1
|
||||
|
||||
if lettercount > 1:
|
||||
raise ISOFormatError('ISO 8601 duration must end with a single valid character.')
|
||||
|
||||
#Do not allow W in combination with other designators
|
||||
#https://bitbucket.org/nielsenb/aniso8601/issues/2/week-designators-should-not-be-combinable
|
||||
if durationstr.find('W') != -1 and _has_any_component(durationstr, ['Y', 'M', 'D', 'H', 'S']) is True:
|
||||
raise ISOFormatError('ISO 8601 week designators may not be combined with other time designators.')
|
||||
|
||||
#Parse the elements of the duration
|
||||
if durationstr.find('T') == -1:
|
||||
years, months, weeks, days, hours, minutes, seconds = _parse_duration_prescribed_notime(durationstr)
|
||||
else:
|
||||
years, months, weeks, days, hours, minutes, seconds = _parse_duration_prescribed_time(durationstr)
|
||||
|
||||
if relative is True:
|
||||
try:
|
||||
import dateutil.relativedelta
|
||||
|
||||
if int(years) != years or int(months) != months:
|
||||
#https://github.com/dateutil/dateutil/issues/40
|
||||
raise RelativeValueError('Fractional months and years are not defined for relative intervals.')
|
||||
|
||||
return dateutil.relativedelta.relativedelta(years=int(years), months=int(months), weeks=weeks, days=days, hours=hours, minutes=minutes, seconds=seconds)
|
||||
except ImportError:
|
||||
raise RuntimeError('dateutil must be installed for relative duration support.')
|
||||
|
||||
#Note that weeks can be handled without conversion to days
|
||||
totaldays = years * 365 + months * 30 + days
|
||||
|
||||
return datetime.timedelta(weeks=weeks, days=totaldays, hours=hours, minutes=minutes, seconds=seconds)
|
||||
|
||||
def _parse_duration_prescribed_notime(durationstr):
|
||||
#durationstr can be of the form PnYnMnD or PnW
|
||||
|
||||
#Make sure no time portion is included
|
||||
#https://bitbucket.org/nielsenb/aniso8601/issues/7/durations-with-time-components-before-t
|
||||
if _has_any_component(durationstr, ['H', 'S']):
|
||||
raise ISOFormatError('ISO 8601 time components not allowed in duration without prescribed time.')
|
||||
|
||||
if _component_order_correct(durationstr, ['P', 'Y', 'M', 'D', 'W']) is False:
|
||||
raise ISOFormatError('ISO 8601 duration components must be in the correct order.')
|
||||
|
||||
if durationstr.find('Y') != -1:
|
||||
years = _parse_duration_element(durationstr, 'Y')
|
||||
else:
|
||||
years = 0
|
||||
|
||||
if durationstr.find('M') != -1:
|
||||
months = _parse_duration_element(durationstr, 'M')
|
||||
else:
|
||||
months = 0
|
||||
|
||||
if durationstr.find('W') != -1:
|
||||
weeks = _parse_duration_element(durationstr, 'W')
|
||||
else:
|
||||
weeks = 0
|
||||
|
||||
if durationstr.find('D') != -1:
|
||||
days = _parse_duration_element(durationstr, 'D')
|
||||
else:
|
||||
days = 0
|
||||
|
||||
#No hours, minutes or seconds
|
||||
hours = 0
|
||||
minutes = 0
|
||||
seconds = 0
|
||||
|
||||
return (years, months, weeks, days, hours, minutes, seconds)
|
||||
|
||||
def _parse_duration_prescribed_time(durationstr):
|
||||
#durationstr can be of the form PnYnMnDTnHnMnS
|
||||
|
||||
firsthalf = durationstr[:durationstr.find('T')]
|
||||
secondhalf = durationstr[durationstr.find('T'):]
|
||||
|
||||
#Make sure no time portion is included in the date half
|
||||
#https://bitbucket.org/nielsenb/aniso8601/issues/7/durations-with-time-components-before-t
|
||||
if _has_any_component(firsthalf, ['H', 'S']):
|
||||
raise ISOFormatError('ISO 8601 time components not allowed in date portion of duration.')
|
||||
|
||||
if _component_order_correct(firsthalf, ['P', 'Y', 'M', 'D', 'W']) is False:
|
||||
raise ISOFormatError('ISO 8601 duration components must be in the correct order.')
|
||||
|
||||
#Make sure no date component is included in the time half
|
||||
if _has_any_component(secondhalf, ['Y', 'D']):
|
||||
raise ISOFormatError('ISO 8601 time components not allowed in date portion of duration.')
|
||||
|
||||
if _component_order_correct(secondhalf, ['T', 'H', 'M', 'S']) is False:
|
||||
raise ISOFormatError('ISO 8601 time components in duration must be in the correct order.')
|
||||
|
||||
if firsthalf.find('Y') != -1:
|
||||
years = _parse_duration_element(firsthalf, 'Y')
|
||||
else:
|
||||
years = 0
|
||||
|
||||
if firsthalf.find('M') != -1:
|
||||
months = _parse_duration_element(firsthalf, 'M')
|
||||
else:
|
||||
months = 0
|
||||
|
||||
if firsthalf.find('D') != -1:
|
||||
days = _parse_duration_element(firsthalf, 'D')
|
||||
else:
|
||||
days = 0
|
||||
|
||||
if secondhalf.find('H') != -1:
|
||||
hours = _parse_duration_element(secondhalf, 'H')
|
||||
else:
|
||||
hours = 0
|
||||
|
||||
if secondhalf.find('M') != -1:
|
||||
minutes = _parse_duration_element(secondhalf, 'M')
|
||||
else:
|
||||
minutes = 0
|
||||
|
||||
if secondhalf.find('S') != -1:
|
||||
seconds = _parse_duration_element(secondhalf, 'S')
|
||||
else:
|
||||
seconds = 0
|
||||
|
||||
#Weeks can't be included
|
||||
weeks = 0
|
||||
|
||||
return (years, months, weeks, days, hours, minutes, seconds)
|
||||
|
||||
def _parse_duration_combined(durationstr, relative):
|
||||
#Period of the form P<date>T<time>
|
||||
|
||||
#Split the string in to its component parts
|
||||
datepart, timepart = durationstr[1:].split('T') #We skip the 'P'
|
||||
|
||||
datevalue = parse_date(datepart)
|
||||
timevalue = parse_time(timepart)
|
||||
|
||||
if relative is True:
|
||||
try:
|
||||
import dateutil.relativedelta
|
||||
|
||||
return dateutil.relativedelta.relativedelta(years=datevalue.year, months=datevalue.month, days=datevalue.day, hours=timevalue.hour, minutes=timevalue.minute, seconds=timevalue.second, microseconds=timevalue.microsecond)
|
||||
except ImportError:
|
||||
raise RuntimeError('dateutil must be installed for relative duration support.')
|
||||
else:
|
||||
totaldays = datevalue.year * 365 + datevalue.month * 30 + datevalue.day
|
||||
|
||||
return datetime.timedelta(days=totaldays, hours=timevalue.hour, minutes=timevalue.minute, seconds=timevalue.second, microseconds=timevalue.microsecond)
|
||||
|
||||
def _parse_duration_element(durationstr, elementstr):
|
||||
#Extracts the specified portion of a duration, for instance, given:
|
||||
#durationstr = 'T4H5M6.1234S'
|
||||
#elementstr = 'H'
|
||||
#
|
||||
#returns 4
|
||||
#
|
||||
#Note that the string must start with a character, so its assumed the
|
||||
#full duration string would be split at the 'T'
|
||||
|
||||
durationstartindex = 0
|
||||
durationendindex = durationstr.find(elementstr)
|
||||
|
||||
for characterindex in compat.range(durationendindex - 1, 0, -1):
|
||||
if durationstr[characterindex].isalpha() is True:
|
||||
durationstartindex = characterindex
|
||||
break
|
||||
|
||||
durationstartindex += 1
|
||||
|
||||
if ',' in durationstr:
|
||||
#Replace the comma with a 'full-stop'
|
||||
durationstr = durationstr.replace(',', '.')
|
||||
|
||||
if elementstr == 'S':
|
||||
#We truncate seconds to avoid precision issues with microseconds
|
||||
#https://bitbucket.org/nielsenb/aniso8601/issues/10/sub-microsecond-precision-in-durations-is
|
||||
if '.' in durationstr[durationstartindex:durationendindex]:
|
||||
stopindex = durationstr.index('.')
|
||||
|
||||
if durationendindex - stopindex > 7:
|
||||
durationendindex = stopindex + 7
|
||||
|
||||
return float(durationstr[durationstartindex:durationendindex])
|
||||
|
||||
def _has_any_component(durationstr, components):
|
||||
#Given a duration string, and a list of components, returns True
|
||||
#if any of the listed components are present, False otherwise.
|
||||
#
|
||||
#For instance:
|
||||
#durationstr = 'P1Y'
|
||||
#components = ['Y', 'M']
|
||||
#
|
||||
#returns True
|
||||
#
|
||||
#durationstr = 'P1Y'
|
||||
#components = ['M', 'D']
|
||||
#
|
||||
#returns False
|
||||
|
||||
for component in components:
|
||||
if durationstr.find(component) != -1:
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
def _component_order_correct(durationstr, componentorder):
|
||||
#Given a duration string, and a list of components, returns
|
||||
#True if the components are in the same order as the
|
||||
#component order list, False otherwise. Characters that
|
||||
#are present in the component order list but not in the
|
||||
#duration string are ignored.
|
||||
#
|
||||
#https://bitbucket.org/nielsenb/aniso8601/issues/8/durations-with-components-in-wrong-order
|
||||
#
|
||||
#durationstr = 'P1Y1M1D'
|
||||
#components = ['P', 'Y', 'M', 'D']
|
||||
#
|
||||
#returns True
|
||||
#
|
||||
#durationstr = 'P1Y1M'
|
||||
#components = ['P', 'Y', 'M', 'D']
|
||||
#
|
||||
#returns True
|
||||
#
|
||||
#durationstr = 'P1D1Y1M'
|
||||
#components = ['P', 'Y', 'M', 'D']
|
||||
#
|
||||
#returns False
|
||||
|
||||
componentindex = 0
|
||||
|
||||
for characterindex in compat.range(len(durationstr)):
|
||||
character = durationstr[characterindex]
|
||||
|
||||
if character in componentorder:
|
||||
#This is a character we need to check the order of
|
||||
if character in componentorder[componentindex:]:
|
||||
componentindex = componentorder.index(character)
|
||||
else:
|
||||
#A character is out of order
|
||||
return False
|
||||
|
||||
return True
|
@ -1,37 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# Copyright (c) 2018, Brandon Nielsen
|
||||
# All rights reserved.
|
||||
#
|
||||
# This software may be modified and distributed under the terms
|
||||
# of the BSD license. See the LICENSE file for details.
|
||||
|
||||
class ISOFormatError(ValueError):
|
||||
"""Raised when ISO 8601 string fails a format check."""
|
||||
|
||||
class RelativeValueError(ValueError):
|
||||
"""Raised when an invalid value is given for calendar level accuracy."""
|
||||
|
||||
class YearOutOfBoundsError(ValueError):
|
||||
"""Raised when year exceeds limits."""
|
||||
|
||||
class WeekOutOfBoundsError(ValueError):
|
||||
"""Raised when week exceeds a year."""
|
||||
|
||||
class DayOutOfBoundsError(ValueError):
|
||||
"""Raised when day is outside of 1..365, 1..366 for leap year."""
|
||||
|
||||
class HoursOutOfBoundsError(ValueError):
|
||||
"""Raise when parsed hours are greater than 24."""
|
||||
|
||||
class MinutesOutOfBoundsError(ValueError):
|
||||
"""Raise when parsed seconds are greater than 60."""
|
||||
|
||||
class SecondsOutOfBoundsError(ValueError):
|
||||
"""Raise when parsed seconds are greater than 60."""
|
||||
|
||||
class MidnightBoundsError(ValueError):
|
||||
"""Raise when parsed time has an hour of 24 but is not midnight."""
|
||||
|
||||
class LeapSecondError(NotImplementedError):
|
||||
"""Raised when attempting to parse a leap second"""
|
@ -1,161 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# Copyright (c) 2018, Brandon Nielsen
|
||||
# All rights reserved.
|
||||
#
|
||||
# This software may be modified and distributed under the terms
|
||||
# of the BSD license. See the LICENSE file for details.
|
||||
|
||||
from datetime import datetime
|
||||
from aniso8601.duration import parse_duration
|
||||
from aniso8601.exceptions import ISOFormatError
|
||||
from aniso8601.time import parse_datetime
|
||||
from aniso8601.date import parse_date
|
||||
|
||||
def parse_interval(isointervalstr, intervaldelimiter='/', datetimedelimiter='T', relative=False):
|
||||
#Given a string representing an ISO 8601 interval, return a
|
||||
#tuple of datetime.date or date.datetime objects representing the beginning
|
||||
#and end of the specified interval. Valid formats are:
|
||||
#
|
||||
#<start>/<end>
|
||||
#<start>/<duration>
|
||||
#<duration>/<end>
|
||||
#
|
||||
#The <start> and <end> values can represent dates, or datetimes,
|
||||
#not times.
|
||||
#
|
||||
#The format:
|
||||
#
|
||||
#<duration>
|
||||
#
|
||||
#Is expressly not supported as there is no way to provide the addtional
|
||||
#required context.
|
||||
|
||||
if isointervalstr[0] == 'R':
|
||||
raise ISOFormatError('ISO 8601 repeating intervals must be parsed with parse_repeating_interval.')
|
||||
|
||||
interval_parts = _parse_interval_parts(isointervalstr, intervaldelimiter, datetimedelimiter, relative)
|
||||
|
||||
return (interval_parts[0], interval_parts[1])
|
||||
|
||||
def parse_repeating_interval(isointervalstr, intervaldelimiter='/', datetimedelimiter='T', relative=False):
|
||||
#Given a string representing an ISO 8601 interval repating, return a
|
||||
#generator of datetime.date or date.datetime objects representing the
|
||||
#dates specified by the repeating interval. Valid formats are:
|
||||
#
|
||||
#Rnn/<interval>
|
||||
#R/<interval>
|
||||
|
||||
if isointervalstr[0] != 'R':
|
||||
raise ISOFormatError('ISO 8601 repeating interval must start with an R.')
|
||||
|
||||
#Parse the number of iterations
|
||||
iterationpart, intervalpart = isointervalstr.split(intervaldelimiter, 1)
|
||||
|
||||
if len(iterationpart) > 1:
|
||||
iterations = int(iterationpart[1:])
|
||||
else:
|
||||
iterations = None
|
||||
|
||||
interval_parts = _parse_interval_parts(intervalpart, intervaldelimiter, datetimedelimiter, relative=relative)
|
||||
|
||||
#Now, build and return the generator
|
||||
if iterations is not None:
|
||||
return _date_generator(interval_parts[0], interval_parts[2], iterations)
|
||||
|
||||
return _date_generator_unbounded(interval_parts[0], interval_parts[2])
|
||||
|
||||
def _parse_interval_parts(isointervalstr, intervaldelimiter='/', datetimedelimiter='T', relative=False):
|
||||
#Returns a tuple containing the start of the interval, the end of the interval, and the interval timedelta
|
||||
|
||||
firstpart, secondpart = isointervalstr.split(intervaldelimiter)
|
||||
|
||||
if firstpart[0] == 'P':
|
||||
#<duration>/<end>
|
||||
#Notice that these are not returned 'in order' (earlier to later), this
|
||||
#is to maintain consistency with parsing <start>/<end> durations, as
|
||||
#well as making repeating interval code cleaner. Users who desire
|
||||
#durations to be in order can use the 'sorted' operator.
|
||||
|
||||
#We need to figure out if <end> is a date, or a datetime
|
||||
if secondpart.find(datetimedelimiter) != -1:
|
||||
#<end> is a datetime
|
||||
duration = parse_duration(firstpart, relative=relative)
|
||||
enddatetime = parse_datetime(secondpart, delimiter=datetimedelimiter)
|
||||
|
||||
return (enddatetime, enddatetime - duration, -duration)
|
||||
|
||||
#<end> must just be a date
|
||||
duration = parse_duration(firstpart, relative=relative)
|
||||
enddate = parse_date(secondpart)
|
||||
|
||||
#See if we need to upconvert to datetime to preserve resolution
|
||||
if firstpart.find(datetimedelimiter) != -1:
|
||||
return (enddate, datetime.combine(enddate, datetime.min.time()) - duration, -duration)
|
||||
|
||||
return (enddate, enddate - duration, -duration)
|
||||
elif secondpart[0] == 'P':
|
||||
#<start>/<duration>
|
||||
#We need to figure out if <start> is a date, or a datetime
|
||||
if firstpart.find(datetimedelimiter) != -1:
|
||||
#<start> is a datetime
|
||||
duration = parse_duration(secondpart, relative=relative)
|
||||
startdatetime = parse_datetime(firstpart, delimiter=datetimedelimiter)
|
||||
|
||||
return (startdatetime, startdatetime + duration, duration)
|
||||
|
||||
#<start> must just be a date
|
||||
duration = parse_duration(secondpart, relative=relative)
|
||||
startdate = parse_date(firstpart)
|
||||
|
||||
#See if we need to upconvert to datetime to preserve resolution
|
||||
if secondpart.find(datetimedelimiter) != -1:
|
||||
return (startdate, datetime.combine(startdate, datetime.min.time()) + duration, duration)
|
||||
|
||||
return (startdate, startdate + duration, duration)
|
||||
|
||||
#<start>/<end>
|
||||
if firstpart.find(datetimedelimiter) != -1 and secondpart.find(datetimedelimiter) != -1:
|
||||
#Both parts are datetimes
|
||||
start_datetime = parse_datetime(firstpart, delimiter=datetimedelimiter)
|
||||
end_datetime = parse_datetime(secondpart, delimiter=datetimedelimiter)
|
||||
|
||||
return (start_datetime, end_datetime, end_datetime - start_datetime)
|
||||
elif firstpart.find(datetimedelimiter) != -1 and secondpart.find(datetimedelimiter) == -1:
|
||||
#First part is a datetime, second part is a date
|
||||
start_datetime = parse_datetime(firstpart, delimiter=datetimedelimiter)
|
||||
end_date = parse_date(secondpart)
|
||||
|
||||
return (start_datetime, end_date, datetime.combine(end_date, datetime.min.time()) - start_datetime)
|
||||
elif firstpart.find(datetimedelimiter) == -1 and secondpart.find(datetimedelimiter) != -1:
|
||||
#First part is a date, second part is a datetime
|
||||
start_date = parse_date(firstpart)
|
||||
end_datetime = parse_datetime(secondpart, delimiter=datetimedelimiter)
|
||||
|
||||
return (start_date, end_datetime, end_datetime - datetime.combine(start_date, datetime.min.time()))
|
||||
|
||||
#Both parts are dates
|
||||
start_date = parse_date(firstpart)
|
||||
end_date = parse_date(secondpart)
|
||||
|
||||
return (start_date, end_date, end_date - start_date)
|
||||
|
||||
def _date_generator(startdate, timedelta, iterations):
|
||||
currentdate = startdate
|
||||
currentiteration = 0
|
||||
|
||||
while currentiteration < iterations:
|
||||
yield currentdate
|
||||
|
||||
#Update the values
|
||||
currentdate += timedelta
|
||||
currentiteration += 1
|
||||
|
||||
def _date_generator_unbounded(startdate, timedelta):
|
||||
currentdate = startdate
|
||||
|
||||
while True:
|
||||
yield currentdate
|
||||
|
||||
#Update the value
|
||||
currentdate += timedelta
|
@ -1,15 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# Copyright (c) 2018, Brandon Nielsen
|
||||
# All rights reserved.
|
||||
#
|
||||
# This software may be modified and distributed under the terms
|
||||
# of the BSD license. See the LICENSE file for details.
|
||||
|
||||
from aniso8601 import compat
|
||||
|
||||
class DateResolution(object):
|
||||
Year, Month, Week, Weekday, Day, Ordinal = list(compat.range(6))
|
||||
|
||||
class TimeResolution(object):
|
||||
Seconds, Minutes, Hours = list(compat.range(3))
|
@ -1,257 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# Copyright (c) 2018, Brandon Nielsen
|
||||
# All rights reserved.
|
||||
#
|
||||
# This software may be modified and distributed under the terms
|
||||
# of the BSD license. See the LICENSE file for details.
|
||||
|
||||
import datetime
|
||||
|
||||
from aniso8601.date import parse_date
|
||||
from aniso8601.exceptions import HoursOutOfBoundsError, ISOFormatError, \
|
||||
LeapSecondError, MidnightBoundsError, MinutesOutOfBoundsError, \
|
||||
SecondsOutOfBoundsError
|
||||
from aniso8601.resolution import TimeResolution
|
||||
from aniso8601.timezone import parse_timezone
|
||||
|
||||
def get_time_resolution(isotimestr):
|
||||
#Valid time formats are:
|
||||
#
|
||||
#hh:mm:ss
|
||||
#hhmmss
|
||||
#hh:mm
|
||||
#hhmm
|
||||
#hh
|
||||
#hh:mm:ssZ
|
||||
#hhmmssZ
|
||||
#hh:mmZ
|
||||
#hhmmZ
|
||||
#hhZ
|
||||
#hh:mm:ss±hh:mm
|
||||
#hhmmss±hh:mm
|
||||
#hh:mm±hh:mm
|
||||
#hhmm±hh:mm
|
||||
#hh±hh:mm
|
||||
#hh:mm:ss±hhmm
|
||||
#hhmmss±hhmm
|
||||
#hh:mm±hhmm
|
||||
#hhmm±hhmm
|
||||
#hh±hhmm
|
||||
#hh:mm:ss±hh
|
||||
#hhmmss±hh
|
||||
#hh:mm±hh
|
||||
#hhmm±hh
|
||||
#hh±hh
|
||||
|
||||
timestr = _split_tz(isotimestr)[0]
|
||||
|
||||
if timestr.count(':') == 2:
|
||||
#hh:mm:ss
|
||||
return TimeResolution.Seconds
|
||||
elif timestr.count(':') == 1:
|
||||
#hh:mm
|
||||
return TimeResolution.Minutes
|
||||
|
||||
#Format must be hhmmss, hhmm, or hh
|
||||
if timestr.find('.') == -1:
|
||||
#No time fractions
|
||||
timestrlen = len(timestr)
|
||||
else:
|
||||
#The lowest order element is a fraction
|
||||
timestrlen = len(timestr.split('.')[0])
|
||||
|
||||
if timestrlen == 6:
|
||||
#hhmmss
|
||||
return TimeResolution.Seconds
|
||||
elif timestrlen == 4:
|
||||
#hhmm
|
||||
return TimeResolution.Minutes
|
||||
elif timestrlen == 2:
|
||||
#hh
|
||||
return TimeResolution.Hours
|
||||
|
||||
raise ISOFormatError('"{0}" is not a valid ISO 8601 time.'.format(isotimestr))
|
||||
|
||||
def parse_time(isotimestr):
|
||||
#Given a string in any ISO 8601 time format, return a datetime.time object
|
||||
#that corresponds to the given time. Fixed offset tzdata will be included
|
||||
#if UTC offset is given in the input string. Valid time formats are:
|
||||
#
|
||||
#hh:mm:ss
|
||||
#hhmmss
|
||||
#hh:mm
|
||||
#hhmm
|
||||
#hh
|
||||
#hh:mm:ssZ
|
||||
#hhmmssZ
|
||||
#hh:mmZ
|
||||
#hhmmZ
|
||||
#hhZ
|
||||
#hh:mm:ss±hh:mm
|
||||
#hhmmss±hh:mm
|
||||
#hh:mm±hh:mm
|
||||
#hhmm±hh:mm
|
||||
#hh±hh:mm
|
||||
#hh:mm:ss±hhmm
|
||||
#hhmmss±hhmm
|
||||
#hh:mm±hhmm
|
||||
#hhmm±hhmm
|
||||
#hh±hhmm
|
||||
#hh:mm:ss±hh
|
||||
#hhmmss±hh
|
||||
#hh:mm±hh
|
||||
#hhmm±hh
|
||||
#hh±hh
|
||||
|
||||
(timestr, tzstr) = _split_tz(isotimestr)
|
||||
|
||||
if tzstr is None:
|
||||
return _parse_time_naive(timestr)
|
||||
else:
|
||||
tzinfo = parse_timezone(tzstr)
|
||||
|
||||
return _parse_time_naive(timestr).replace(tzinfo=tzinfo)
|
||||
|
||||
def parse_datetime(isodatetimestr, delimiter='T'):
|
||||
#Given a string in ISO 8601 date time format, return a datetime.datetime
|
||||
#object that corresponds to the given date time.
|
||||
#By default, the ISO 8601 specified T delimiter is used to split the
|
||||
#date and time (<date>T<time>). Fixed offset tzdata will be included
|
||||
#if UTC offset is given in the input string.
|
||||
|
||||
isodatestr, isotimestr = isodatetimestr.split(delimiter)
|
||||
|
||||
datepart = parse_date(isodatestr)
|
||||
|
||||
timepart = parse_time(isotimestr)
|
||||
|
||||
return datetime.datetime.combine(datepart, timepart)
|
||||
|
||||
def _parse_time_naive(timestr):
|
||||
#timestr is of the format hh:mm:ss, hh:mm, hhmmss, hhmm, hh
|
||||
#
|
||||
#hh is between 0 and 24, 24 is not allowed in the Python time format, since
|
||||
#it represents midnight, a time of 00:00:00 is returned
|
||||
#
|
||||
#mm is between 0 and 60, with 60 used to denote a leap second
|
||||
#
|
||||
#No tzinfo will be included
|
||||
return _RESOLUTION_MAP[get_time_resolution(timestr)](timestr)
|
||||
|
||||
def _parse_hour(timestr):
|
||||
#Format must be hh or hh.
|
||||
isohour = float(timestr)
|
||||
|
||||
if isohour == 24:
|
||||
return datetime.time(hour=0, minute=0)
|
||||
elif isohour > 24:
|
||||
raise HoursOutOfBoundsError('Hour must be between 0..24 with 24 representing midnight.')
|
||||
|
||||
#Since the time constructor doesn't handle fractional hours, we put
|
||||
#the hours in to a timedelta, and add it to the time before returning
|
||||
hoursdelta = datetime.timedelta(hours=isohour)
|
||||
|
||||
return _build_time(datetime.time(hour=0), hoursdelta)
|
||||
|
||||
def _parse_minute_time(timestr):
|
||||
#Format must be hhmm, hhmm., hh:mm or hh:mm.
|
||||
if timestr.count(':') == 1:
|
||||
#hh:mm or hh:mm.
|
||||
timestrarray = timestr.split(':')
|
||||
|
||||
isohour = int(timestrarray[0])
|
||||
isominute = float(timestrarray[1]) #Minute may now be a fraction
|
||||
else:
|
||||
#hhmm or hhmm.
|
||||
isohour = int(timestr[0:2])
|
||||
isominute = float(timestr[2:])
|
||||
|
||||
if isominute >= 60:
|
||||
raise MinutesOutOfBoundsError('Minutes must be less than 60.')
|
||||
|
||||
if isohour == 24:
|
||||
if isominute != 0:
|
||||
raise MidnightBoundsError('Hour 24 may only represent midnight.')
|
||||
|
||||
return datetime.time(hour=0, minute=0)
|
||||
|
||||
#Since the time constructor doesn't handle fractional minutes, we put
|
||||
#the minutes in to a timedelta, and add it to the time before returning
|
||||
minutesdelta = datetime.timedelta(minutes=isominute)
|
||||
|
||||
return _build_time(datetime.time(hour=isohour), minutesdelta)
|
||||
|
||||
def _parse_second_time(timestr):
|
||||
#Format must be hhmmss, hhmmss., hh:mm:ss or hh:mm:ss.
|
||||
if timestr.count(':') == 2:
|
||||
#hh:mm:ss or hh:mm:ss.
|
||||
timestrarray = timestr.split(':')
|
||||
|
||||
isohour = int(timestrarray[0])
|
||||
isominute = int(timestrarray[1])
|
||||
|
||||
#Since the time constructor doesn't handle fractional seconds, we put
|
||||
#the seconds in to a timedelta, and add it to the time before returning
|
||||
#The seconds value is truncated to microsecond resolution before
|
||||
#conversion:
|
||||
#https://bitbucket.org/nielsenb/aniso8601/issues/10/sub-microsecond-precision-in-durations-is
|
||||
secondsdelta = datetime.timedelta(seconds=float(timestrarray[2][:9]))
|
||||
else:
|
||||
#hhmmss or hhmmss.
|
||||
isohour = int(timestr[0:2])
|
||||
isominute = int(timestr[2:4])
|
||||
|
||||
#Since the time constructor doesn't handle fractional seconds, we put
|
||||
#the seconds in to a timedelta, and add it to the time before returning
|
||||
#The seconds value is truncated to microsecond resolution before
|
||||
#conversion:
|
||||
#https://bitbucket.org/nielsenb/aniso8601/issues/10/sub-microsecond-precision-in-durations-is
|
||||
secondsdelta = datetime.timedelta(seconds=float(timestr[4:13]))
|
||||
|
||||
if isohour == 23 and isominute == 59 and secondsdelta.seconds == 60:
|
||||
#https://bitbucket.org/nielsenb/aniso8601/issues/10/sub-microsecond-precision-in-durations-is
|
||||
raise LeapSecondError('Leap seconds are not supported.')
|
||||
elif secondsdelta.seconds >= 60:
|
||||
#https://bitbucket.org/nielsenb/aniso8601/issues/13/parsing-of-leap-second-gives-wildly
|
||||
raise SecondsOutOfBoundsError('Seconds must be less than 60.')
|
||||
|
||||
if isominute >= 60:
|
||||
raise MinutesOutOfBoundsError('Minutes must be less than 60.')
|
||||
|
||||
if isohour == 24:
|
||||
#Midnight, see 4.2.1, 4.2.3
|
||||
if isominute != 0 or secondsdelta.total_seconds() != 0:
|
||||
raise MidnightBoundsError('Hour 24 may only represent midnight.')
|
||||
|
||||
return datetime.time(hour=0, minute=0)
|
||||
|
||||
return _build_time(datetime.time(hour=isohour, minute=isominute),
|
||||
secondsdelta)
|
||||
|
||||
def _build_time(time, delta):
|
||||
#Combine today's date (just so we have a date object), the time, the
|
||||
#delta, and return the time component
|
||||
base_datetime = datetime.datetime.combine(datetime.date.today(), time)
|
||||
return (base_datetime + delta).time()
|
||||
|
||||
def _split_tz(isotimestr):
|
||||
if isotimestr.find('+') != -1:
|
||||
timestr = isotimestr[0:isotimestr.find('+')]
|
||||
tzstr = isotimestr[isotimestr.find('+'):]
|
||||
elif isotimestr.find('-') != -1:
|
||||
timestr = isotimestr[0:isotimestr.find('-')]
|
||||
tzstr = isotimestr[isotimestr.find('-'):]
|
||||
elif isotimestr.endswith('Z'):
|
||||
timestr = isotimestr[:-1]
|
||||
tzstr = 'Z'
|
||||
else:
|
||||
timestr = isotimestr
|
||||
tzstr = None
|
||||
return (timestr, tzstr)
|
||||
|
||||
_RESOLUTION_MAP = {
|
||||
TimeResolution.Hours: _parse_hour,
|
||||
TimeResolution.Minutes: _parse_minute_time,
|
||||
TimeResolution.Seconds: _parse_second_time
|
||||
}
|
@ -1,93 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# Copyright (c) 2018, Brandon Nielsen
|
||||
# All rights reserved.
|
||||
#
|
||||
# This software may be modified and distributed under the terms
|
||||
# of the BSD license. See the LICENSE file for details.
|
||||
|
||||
import datetime
|
||||
|
||||
from aniso8601.exceptions import ISOFormatError
|
||||
|
||||
def parse_timezone(tzstr):
|
||||
#tzstr can be Z, ±hh:mm, ±hhmm, ±hh
|
||||
if 'Z' in tzstr:
|
||||
if len(tzstr) != 1:
|
||||
raise ISOFormatError('"{0}" is not a valid ISO 8601 time offset.'.format(tzstr))
|
||||
|
||||
#Z -> UTC
|
||||
return UTCOffset(name='UTC', minutes=0)
|
||||
elif len(tzstr) == 6:
|
||||
#±hh:mm
|
||||
tzhour = int(tzstr[1:3])
|
||||
tzminute = int(tzstr[4:6])
|
||||
elif len(tzstr) == 5:
|
||||
#±hhmm
|
||||
tzhour = int(tzstr[1:3])
|
||||
tzminute = int(tzstr[3:5])
|
||||
elif len(tzstr) == 3:
|
||||
#±hh
|
||||
tzhour = int(tzstr[1:3])
|
||||
tzminute = 0
|
||||
else:
|
||||
raise ISOFormatError('"{0}" is not a valid ISO 8601 time offset.'.format(tzstr))
|
||||
|
||||
if tzstr[0] == '+':
|
||||
return UTCOffset(name=tzstr, minutes=(tzhour * 60 + tzminute))
|
||||
elif tzhour == 0 and tzminute == 0:
|
||||
raise ISOFormatError('Negative ISO 8601 time offset must not be 0.')
|
||||
|
||||
return UTCOffset(name=tzstr, minutes=-(tzhour * 60 + tzminute))
|
||||
|
||||
class UTCOffset(datetime.tzinfo):
|
||||
def __init__(self, name=None, minutes=None):
|
||||
#We build an offset in this manner since the
|
||||
#tzinfo class must have an init that can
|
||||
#"method that can be called with no arguments"
|
||||
self._name = name
|
||||
|
||||
if minutes is not None:
|
||||
self._utcdelta = datetime.timedelta(minutes=minutes)
|
||||
else:
|
||||
self._utcdelta = None
|
||||
|
||||
def __repr__(self):
|
||||
if self._utcdelta >= datetime.timedelta(hours=0):
|
||||
return '+{0} UTC'.format(self._utcdelta)
|
||||
|
||||
#From the docs:
|
||||
#String representations of timedelta objects are normalized
|
||||
#similarly to their internal representation. This leads to
|
||||
#somewhat unusual results for negative timedeltas.
|
||||
|
||||
#Clean this up for printing purposes
|
||||
correcteddays = abs(self._utcdelta.days + 1) #Negative deltas start at -1 day
|
||||
|
||||
deltaseconds = (24 * 60 * 60) - self._utcdelta.seconds #Negative deltas have a positive seconds
|
||||
|
||||
days, remainder = divmod(deltaseconds, 24 * 60 * 60) #(24 hours / day) * (60 minutes / hour) * (60 seconds / hour)
|
||||
hours, remainder = divmod(remainder, 1 * 60 * 60) #(1 hour) * (60 minutes / hour) * (60 seconds / hour)
|
||||
minutes, seconds = divmod(remainder, 1 * 60) #(1 minute) * (60 seconds / minute)
|
||||
|
||||
#Add any remaining days to the correctedDays count
|
||||
correcteddays += days
|
||||
|
||||
if correcteddays == 0:
|
||||
return '-{0}:{1:02}:{2:02} UTC'.format(hours, minutes, seconds)
|
||||
elif correcteddays == 1:
|
||||
return '-1 day, {0}:{1:02}:{2:02} UTC'.format(hours, minutes, seconds)
|
||||
|
||||
return '-{0} days, {1}:{2:02}:{3:02} UTC'.format(correcteddays, hours, minutes, seconds)
|
||||
|
||||
def utcoffset(self, dt):
|
||||
return self._utcdelta
|
||||
|
||||
def tzname(self, dt):
|
||||
return self._name
|
||||
|
||||
def dst(self, dt):
|
||||
#ISO 8601 specifies offsets should be different if DST is required,
|
||||
#instead of allowing for a DST to be specified
|
||||
# https://docs.python.org/2/library/datetime.html#datetime.tzinfo.dst
|
||||
return datetime.timedelta(0)
|
@ -1,10 +0,0 @@
|
||||
from pkg_resources import get_distribution, DistributionNotFound
|
||||
|
||||
try:
|
||||
release = get_distribution('APScheduler').version.split('-')[0]
|
||||
except DistributionNotFound:
|
||||
release = '3.5.0'
|
||||
|
||||
version_info = tuple(int(x) if x.isdigit() else x for x in release.split('.'))
|
||||
version = __version__ = '.'.join(str(x) for x in version_info[:3])
|
||||
del get_distribution, DistributionNotFound
|
@ -1,94 +0,0 @@
|
||||
__all__ = ('EVENT_SCHEDULER_STARTED', 'EVENT_SCHEDULER_SHUTDOWN', 'EVENT_SCHEDULER_PAUSED',
|
||||
'EVENT_SCHEDULER_RESUMED', 'EVENT_EXECUTOR_ADDED', 'EVENT_EXECUTOR_REMOVED',
|
||||
'EVENT_JOBSTORE_ADDED', 'EVENT_JOBSTORE_REMOVED', 'EVENT_ALL_JOBS_REMOVED',
|
||||
'EVENT_JOB_ADDED', 'EVENT_JOB_REMOVED', 'EVENT_JOB_MODIFIED', 'EVENT_JOB_EXECUTED',
|
||||
'EVENT_JOB_ERROR', 'EVENT_JOB_MISSED', 'EVENT_JOB_SUBMITTED', 'EVENT_JOB_MAX_INSTANCES',
|
||||
'SchedulerEvent', 'JobEvent', 'JobExecutionEvent')
|
||||
|
||||
|
||||
EVENT_SCHEDULER_STARTED = EVENT_SCHEDULER_START = 2 ** 0
|
||||
EVENT_SCHEDULER_SHUTDOWN = 2 ** 1
|
||||
EVENT_SCHEDULER_PAUSED = 2 ** 2
|
||||
EVENT_SCHEDULER_RESUMED = 2 ** 3
|
||||
EVENT_EXECUTOR_ADDED = 2 ** 4
|
||||
EVENT_EXECUTOR_REMOVED = 2 ** 5
|
||||
EVENT_JOBSTORE_ADDED = 2 ** 6
|
||||
EVENT_JOBSTORE_REMOVED = 2 ** 7
|
||||
EVENT_ALL_JOBS_REMOVED = 2 ** 8
|
||||
EVENT_JOB_ADDED = 2 ** 9
|
||||
EVENT_JOB_REMOVED = 2 ** 10
|
||||
EVENT_JOB_MODIFIED = 2 ** 11
|
||||
EVENT_JOB_EXECUTED = 2 ** 12
|
||||
EVENT_JOB_ERROR = 2 ** 13
|
||||
EVENT_JOB_MISSED = 2 ** 14
|
||||
EVENT_JOB_SUBMITTED = 2 ** 15
|
||||
EVENT_JOB_MAX_INSTANCES = 2 ** 16
|
||||
EVENT_ALL = (EVENT_SCHEDULER_STARTED | EVENT_SCHEDULER_SHUTDOWN | EVENT_SCHEDULER_PAUSED |
|
||||
EVENT_SCHEDULER_RESUMED | EVENT_EXECUTOR_ADDED | EVENT_EXECUTOR_REMOVED |
|
||||
EVENT_JOBSTORE_ADDED | EVENT_JOBSTORE_REMOVED | EVENT_ALL_JOBS_REMOVED |
|
||||
EVENT_JOB_ADDED | EVENT_JOB_REMOVED | EVENT_JOB_MODIFIED | EVENT_JOB_EXECUTED |
|
||||
EVENT_JOB_ERROR | EVENT_JOB_MISSED | EVENT_JOB_SUBMITTED | EVENT_JOB_MAX_INSTANCES)
|
||||
|
||||
|
||||
class SchedulerEvent(object):
|
||||
"""
|
||||
An event that concerns the scheduler itself.
|
||||
|
||||
:ivar code: the type code of this event
|
||||
:ivar alias: alias of the job store or executor that was added or removed (if applicable)
|
||||
"""
|
||||
|
||||
def __init__(self, code, alias=None):
|
||||
super(SchedulerEvent, self).__init__()
|
||||
self.code = code
|
||||
self.alias = alias
|
||||
|
||||
def __repr__(self):
|
||||
return '<%s (code=%d)>' % (self.__class__.__name__, self.code)
|
||||
|
||||
|
||||
class JobEvent(SchedulerEvent):
|
||||
"""
|
||||
An event that concerns a job.
|
||||
|
||||
:ivar code: the type code of this event
|
||||
:ivar job_id: identifier of the job in question
|
||||
:ivar jobstore: alias of the job store containing the job in question
|
||||
"""
|
||||
|
||||
def __init__(self, code, job_id, jobstore):
|
||||
super(JobEvent, self).__init__(code)
|
||||
self.code = code
|
||||
self.job_id = job_id
|
||||
self.jobstore = jobstore
|
||||
|
||||
|
||||
class JobSubmissionEvent(JobEvent):
|
||||
"""
|
||||
An event that concerns the submission of a job to its executor.
|
||||
|
||||
:ivar scheduled_run_times: a list of datetimes when the job was intended to run
|
||||
"""
|
||||
|
||||
def __init__(self, code, job_id, jobstore, scheduled_run_times):
|
||||
super(JobSubmissionEvent, self).__init__(code, job_id, jobstore)
|
||||
self.scheduled_run_times = scheduled_run_times
|
||||
|
||||
|
||||
class JobExecutionEvent(JobEvent):
|
||||
"""
|
||||
An event that concerns the running of a job within its executor.
|
||||
|
||||
:ivar scheduled_run_time: the time when the job was scheduled to be run
|
||||
:ivar retval: the return value of the successfully executed job
|
||||
:ivar exception: the exception raised by the job
|
||||
:ivar traceback: a formatted traceback for the exception
|
||||
"""
|
||||
|
||||
def __init__(self, code, job_id, jobstore, scheduled_run_time, retval=None, exception=None,
|
||||
traceback=None):
|
||||
super(JobExecutionEvent, self).__init__(code, job_id, jobstore)
|
||||
self.scheduled_run_time = scheduled_run_time
|
||||
self.retval = retval
|
||||
self.exception = exception
|
||||
self.traceback = traceback
|
@ -1,60 +0,0 @@
|
||||
from __future__ import absolute_import
|
||||
|
||||
import sys
|
||||
|
||||
from apscheduler.executors.base import BaseExecutor, run_job
|
||||
|
||||
try:
|
||||
from asyncio import iscoroutinefunction
|
||||
from apscheduler.executors.base_py3 import run_coroutine_job
|
||||
except ImportError:
|
||||
from trollius import iscoroutinefunction
|
||||
run_coroutine_job = None
|
||||
|
||||
|
||||
class AsyncIOExecutor(BaseExecutor):
|
||||
"""
|
||||
Runs jobs in the default executor of the event loop.
|
||||
|
||||
If the job function is a native coroutine function, it is scheduled to be run directly in the
|
||||
event loop as soon as possible. All other functions are run in the event loop's default
|
||||
executor which is usually a thread pool.
|
||||
|
||||
Plugin alias: ``asyncio``
|
||||
"""
|
||||
|
||||
def start(self, scheduler, alias):
|
||||
super(AsyncIOExecutor, self).start(scheduler, alias)
|
||||
self._eventloop = scheduler._eventloop
|
||||
self._pending_futures = set()
|
||||
|
||||
def shutdown(self, wait=True):
|
||||
# There is no way to honor wait=True without converting this method into a coroutine method
|
||||
for f in self._pending_futures:
|
||||
if not f.done():
|
||||
f.cancel()
|
||||
|
||||
self._pending_futures.clear()
|
||||
|
||||
def _do_submit_job(self, job, run_times):
|
||||
def callback(f):
|
||||
self._pending_futures.discard(f)
|
||||
try:
|
||||
events = f.result()
|
||||
except BaseException:
|
||||
self._run_job_error(job.id, *sys.exc_info()[1:])
|
||||
else:
|
||||
self._run_job_success(job.id, events)
|
||||
|
||||
if iscoroutinefunction(job.func):
|
||||
if run_coroutine_job is not None:
|
||||
coro = run_coroutine_job(job, job._jobstore_alias, run_times, self._logger.name)
|
||||
f = self._eventloop.create_task(coro)
|
||||
else:
|
||||
raise Exception('Executing coroutine based jobs is not supported with Trollius')
|
||||
else:
|
||||
f = self._eventloop.run_in_executor(None, run_job, job, job._jobstore_alias, run_times,
|
||||
self._logger.name)
|
||||
|
||||
f.add_done_callback(callback)
|
||||
self._pending_futures.add(f)
|
@ -1,146 +0,0 @@
|
||||
from abc import ABCMeta, abstractmethod
|
||||
from collections import defaultdict
|
||||
from datetime import datetime, timedelta
|
||||
from traceback import format_tb
|
||||
import logging
|
||||
import sys
|
||||
|
||||
from pytz import utc
|
||||
import six
|
||||
|
||||
from apscheduler.events import (
|
||||
JobExecutionEvent, EVENT_JOB_MISSED, EVENT_JOB_ERROR, EVENT_JOB_EXECUTED)
|
||||
|
||||
|
||||
class MaxInstancesReachedError(Exception):
|
||||
def __init__(self, job):
|
||||
super(MaxInstancesReachedError, self).__init__(
|
||||
'Job "%s" has already reached its maximum number of instances (%d)' %
|
||||
(job.id, job.max_instances))
|
||||
|
||||
|
||||
class BaseExecutor(six.with_metaclass(ABCMeta, object)):
|
||||
"""Abstract base class that defines the interface that every executor must implement."""
|
||||
|
||||
_scheduler = None
|
||||
_lock = None
|
||||
_logger = logging.getLogger('apscheduler.executors')
|
||||
|
||||
def __init__(self):
|
||||
super(BaseExecutor, self).__init__()
|
||||
self._instances = defaultdict(lambda: 0)
|
||||
|
||||
def start(self, scheduler, alias):
|
||||
"""
|
||||
Called by the scheduler when the scheduler is being started or when the executor is being
|
||||
added to an already running scheduler.
|
||||
|
||||
:param apscheduler.schedulers.base.BaseScheduler scheduler: the scheduler that is starting
|
||||
this executor
|
||||
:param str|unicode alias: alias of this executor as it was assigned to the scheduler
|
||||
|
||||
"""
|
||||
self._scheduler = scheduler
|
||||
self._lock = scheduler._create_lock()
|
||||
self._logger = logging.getLogger('apscheduler.executors.%s' % alias)
|
||||
|
||||
def shutdown(self, wait=True):
|
||||
"""
|
||||
Shuts down this executor.
|
||||
|
||||
:param bool wait: ``True`` to wait until all submitted jobs
|
||||
have been executed
|
||||
"""
|
||||
|
||||
def submit_job(self, job, run_times):
|
||||
"""
|
||||
Submits job for execution.
|
||||
|
||||
:param Job job: job to execute
|
||||
:param list[datetime] run_times: list of datetimes specifying
|
||||
when the job should have been run
|
||||
:raises MaxInstancesReachedError: if the maximum number of
|
||||
allowed instances for this job has been reached
|
||||
|
||||
"""
|
||||
assert self._lock is not None, 'This executor has not been started yet'
|
||||
with self._lock:
|
||||
if self._instances[job.id] >= job.max_instances:
|
||||
raise MaxInstancesReachedError(job)
|
||||
|
||||
self._do_submit_job(job, run_times)
|
||||
self._instances[job.id] += 1
|
||||
|
||||
@abstractmethod
|
||||
def _do_submit_job(self, job, run_times):
|
||||
"""Performs the actual task of scheduling `run_job` to be called."""
|
||||
|
||||
def _run_job_success(self, job_id, events):
|
||||
"""
|
||||
Called by the executor with the list of generated events when :func:`run_job` has been
|
||||
successfully called.
|
||||
|
||||
"""
|
||||
with self._lock:
|
||||
self._instances[job_id] -= 1
|
||||
if self._instances[job_id] == 0:
|
||||
del self._instances[job_id]
|
||||
|
||||
for event in events:
|
||||
self._scheduler._dispatch_event(event)
|
||||
|
||||
def _run_job_error(self, job_id, exc, traceback=None):
|
||||
"""Called by the executor with the exception if there is an error calling `run_job`."""
|
||||
with self._lock:
|
||||
self._instances[job_id] -= 1
|
||||
if self._instances[job_id] == 0:
|
||||
del self._instances[job_id]
|
||||
|
||||
exc_info = (exc.__class__, exc, traceback)
|
||||
self._logger.error('Error running job %s', job_id, exc_info=exc_info)
|
||||
|
||||
|
||||
def run_job(job, jobstore_alias, run_times, logger_name):
|
||||
"""
|
||||
Called by executors to run the job. Returns a list of scheduler events to be dispatched by the
|
||||
scheduler.
|
||||
|
||||
"""
|
||||
events = []
|
||||
logger = logging.getLogger(logger_name)
|
||||
for run_time in run_times:
|
||||
# See if the job missed its run time window, and handle
|
||||
# possible misfires accordingly
|
||||
if job.misfire_grace_time is not None:
|
||||
difference = datetime.now(utc) - run_time
|
||||
grace_time = timedelta(seconds=job.misfire_grace_time)
|
||||
if difference > grace_time:
|
||||
events.append(JobExecutionEvent(EVENT_JOB_MISSED, job.id, jobstore_alias,
|
||||
run_time))
|
||||
logger.warning('Run time of job "%s" was missed by %s', job, difference)
|
||||
continue
|
||||
|
||||
logger.info('Running job "%s" (scheduled at %s)', job, run_time)
|
||||
try:
|
||||
retval = job.func(*job.args, **job.kwargs)
|
||||
except BaseException:
|
||||
exc, tb = sys.exc_info()[1:]
|
||||
formatted_tb = ''.join(format_tb(tb))
|
||||
events.append(JobExecutionEvent(EVENT_JOB_ERROR, job.id, jobstore_alias, run_time,
|
||||
exception=exc, traceback=formatted_tb))
|
||||
logger.exception('Job "%s" raised an exception', job)
|
||||
|
||||
# This is to prevent cyclic references that would lead to memory leaks
|
||||
if six.PY2:
|
||||
sys.exc_clear()
|
||||
del tb
|
||||
else:
|
||||
import traceback
|
||||
traceback.clear_frames(tb)
|
||||
del tb
|
||||
else:
|
||||
events.append(JobExecutionEvent(EVENT_JOB_EXECUTED, job.id, jobstore_alias, run_time,
|
||||
retval=retval))
|
||||
logger.info('Job "%s" executed successfully', job)
|
||||
|
||||
return events
|
@ -1,41 +0,0 @@
|
||||
import logging
|
||||
import sys
|
||||
from datetime import datetime, timedelta
|
||||
from traceback import format_tb
|
||||
|
||||
from pytz import utc
|
||||
|
||||
from apscheduler.events import (
|
||||
JobExecutionEvent, EVENT_JOB_MISSED, EVENT_JOB_ERROR, EVENT_JOB_EXECUTED)
|
||||
|
||||
|
||||
async def run_coroutine_job(job, jobstore_alias, run_times, logger_name):
|
||||
"""Coroutine version of run_job()."""
|
||||
events = []
|
||||
logger = logging.getLogger(logger_name)
|
||||
for run_time in run_times:
|
||||
# See if the job missed its run time window, and handle possible misfires accordingly
|
||||
if job.misfire_grace_time is not None:
|
||||
difference = datetime.now(utc) - run_time
|
||||
grace_time = timedelta(seconds=job.misfire_grace_time)
|
||||
if difference > grace_time:
|
||||
events.append(JobExecutionEvent(EVENT_JOB_MISSED, job.id, jobstore_alias,
|
||||
run_time))
|
||||
logger.warning('Run time of job "%s" was missed by %s', job, difference)
|
||||
continue
|
||||
|
||||
logger.info('Running job "%s" (scheduled at %s)', job, run_time)
|
||||
try:
|
||||
retval = await job.func(*job.args, **job.kwargs)
|
||||
except BaseException:
|
||||
exc, tb = sys.exc_info()[1:]
|
||||
formatted_tb = ''.join(format_tb(tb))
|
||||
events.append(JobExecutionEvent(EVENT_JOB_ERROR, job.id, jobstore_alias, run_time,
|
||||
exception=exc, traceback=formatted_tb))
|
||||
logger.exception('Job "%s" raised an exception', job)
|
||||
else:
|
||||
events.append(JobExecutionEvent(EVENT_JOB_EXECUTED, job.id, jobstore_alias, run_time,
|
||||
retval=retval))
|
||||
logger.info('Job "%s" executed successfully', job)
|
||||
|
||||
return events
|
@ -1,20 +0,0 @@
|
||||
import sys
|
||||
|
||||
from apscheduler.executors.base import BaseExecutor, run_job
|
||||
|
||||
|
||||
class DebugExecutor(BaseExecutor):
|
||||
"""
|
||||
A special executor that executes the target callable directly instead of deferring it to a
|
||||
thread or process.
|
||||
|
||||
Plugin alias: ``debug``
|
||||
"""
|
||||
|
||||
def _do_submit_job(self, job, run_times):
|
||||
try:
|
||||
events = run_job(job, job._jobstore_alias, run_times, self._logger.name)
|
||||
except BaseException:
|
||||
self._run_job_error(job.id, *sys.exc_info()[1:])
|
||||
else:
|
||||
self._run_job_success(job.id, events)
|
@ -1,30 +0,0 @@
|
||||
from __future__ import absolute_import
|
||||
import sys
|
||||
|
||||
from apscheduler.executors.base import BaseExecutor, run_job
|
||||
|
||||
|
||||
try:
|
||||
import gevent
|
||||
except ImportError: # pragma: nocover
|
||||
raise ImportError('GeventExecutor requires gevent installed')
|
||||
|
||||
|
||||
class GeventExecutor(BaseExecutor):
|
||||
"""
|
||||
Runs jobs as greenlets.
|
||||
|
||||
Plugin alias: ``gevent``
|
||||
"""
|
||||
|
||||
def _do_submit_job(self, job, run_times):
|
||||
def callback(greenlet):
|
||||
try:
|
||||
events = greenlet.get()
|
||||
except BaseException:
|
||||
self._run_job_error(job.id, *sys.exc_info()[1:])
|
||||
else:
|
||||
self._run_job_success(job.id, events)
|
||||
|
||||
gevent.spawn(run_job, job, job._jobstore_alias, run_times, self._logger.name).\
|
||||
link(callback)
|
@ -1,54 +0,0 @@
|
||||
from abc import abstractmethod
|
||||
import concurrent.futures
|
||||
|
||||
from apscheduler.executors.base import BaseExecutor, run_job
|
||||
|
||||
|
||||
class BasePoolExecutor(BaseExecutor):
|
||||
@abstractmethod
|
||||
def __init__(self, pool):
|
||||
super(BasePoolExecutor, self).__init__()
|
||||
self._pool = pool
|
||||
|
||||
def _do_submit_job(self, job, run_times):
|
||||
def callback(f):
|
||||
exc, tb = (f.exception_info() if hasattr(f, 'exception_info') else
|
||||
(f.exception(), getattr(f.exception(), '__traceback__', None)))
|
||||
if exc:
|
||||
self._run_job_error(job.id, exc, tb)
|
||||
else:
|
||||
self._run_job_success(job.id, f.result())
|
||||
|
||||
f = self._pool.submit(run_job, job, job._jobstore_alias, run_times, self._logger.name)
|
||||
f.add_done_callback(callback)
|
||||
|
||||
def shutdown(self, wait=True):
|
||||
self._pool.shutdown(wait)
|
||||
|
||||
|
||||
class ThreadPoolExecutor(BasePoolExecutor):
|
||||
"""
|
||||
An executor that runs jobs in a concurrent.futures thread pool.
|
||||
|
||||
Plugin alias: ``threadpool``
|
||||
|
||||
:param max_workers: the maximum number of spawned threads.
|
||||
"""
|
||||
|
||||
def __init__(self, max_workers=10):
|
||||
pool = concurrent.futures.ThreadPoolExecutor(int(max_workers))
|
||||
super(ThreadPoolExecutor, self).__init__(pool)
|
||||
|
||||
|
||||
class ProcessPoolExecutor(BasePoolExecutor):
|
||||
"""
|
||||
An executor that runs jobs in a concurrent.futures process pool.
|
||||
|
||||
Plugin alias: ``processpool``
|
||||
|
||||
:param max_workers: the maximum number of spawned processes.
|
||||
"""
|
||||
|
||||
def __init__(self, max_workers=10):
|
||||
pool = concurrent.futures.ProcessPoolExecutor(int(max_workers))
|
||||
super(ProcessPoolExecutor, self).__init__(pool)
|
@ -1,54 +0,0 @@
|
||||
from __future__ import absolute_import
|
||||
|
||||
import sys
|
||||
from concurrent.futures import ThreadPoolExecutor
|
||||
|
||||
from tornado.gen import convert_yielded
|
||||
|
||||
from apscheduler.executors.base import BaseExecutor, run_job
|
||||
|
||||
try:
|
||||
from inspect import iscoroutinefunction
|
||||
from apscheduler.executors.base_py3 import run_coroutine_job
|
||||
except ImportError:
|
||||
def iscoroutinefunction(func):
|
||||
return False
|
||||
|
||||
|
||||
class TornadoExecutor(BaseExecutor):
|
||||
"""
|
||||
Runs jobs either in a thread pool or directly on the I/O loop.
|
||||
|
||||
If the job function is a native coroutine function, it is scheduled to be run directly in the
|
||||
I/O loop as soon as possible. All other functions are run in a thread pool.
|
||||
|
||||
Plugin alias: ``tornado``
|
||||
|
||||
:param int max_workers: maximum number of worker threads in the thread pool
|
||||
"""
|
||||
|
||||
def __init__(self, max_workers=10):
|
||||
super(TornadoExecutor, self).__init__()
|
||||
self.executor = ThreadPoolExecutor(max_workers)
|
||||
|
||||
def start(self, scheduler, alias):
|
||||
super(TornadoExecutor, self).start(scheduler, alias)
|
||||
self._ioloop = scheduler._ioloop
|
||||
|
||||
def _do_submit_job(self, job, run_times):
|
||||
def callback(f):
|
||||
try:
|
||||
events = f.result()
|
||||
except BaseException:
|
||||
self._run_job_error(job.id, *sys.exc_info()[1:])
|
||||
else:
|
||||
self._run_job_success(job.id, events)
|
||||
|
||||
if iscoroutinefunction(job.func):
|
||||
f = run_coroutine_job(job, job._jobstore_alias, run_times, self._logger.name)
|
||||
else:
|
||||
f = self.executor.submit(run_job, job, job._jobstore_alias, run_times,
|
||||
self._logger.name)
|
||||
|
||||
f = convert_yielded(f)
|
||||
f.add_done_callback(callback)
|
@ -1,25 +0,0 @@
|
||||
from __future__ import absolute_import
|
||||
|
||||
from apscheduler.executors.base import BaseExecutor, run_job
|
||||
|
||||
|
||||
class TwistedExecutor(BaseExecutor):
|
||||
"""
|
||||
Runs jobs in the reactor's thread pool.
|
||||
|
||||
Plugin alias: ``twisted``
|
||||
"""
|
||||
|
||||
def start(self, scheduler, alias):
|
||||
super(TwistedExecutor, self).start(scheduler, alias)
|
||||
self._reactor = scheduler._reactor
|
||||
|
||||
def _do_submit_job(self, job, run_times):
|
||||
def callback(success, result):
|
||||
if success:
|
||||
self._run_job_success(job.id, result)
|
||||
else:
|
||||
self._run_job_error(job.id, result.value, result.tb)
|
||||
|
||||
self._reactor.getThreadPool().callInThreadWithCallback(
|
||||
callback, run_job, job, job._jobstore_alias, run_times, self._logger.name)
|
@ -1,297 +0,0 @@
|
||||
from collections import Iterable, Mapping
|
||||
from inspect import ismethod, isclass
|
||||
from uuid import uuid4
|
||||
|
||||
import six
|
||||
|
||||
from apscheduler.triggers.base import BaseTrigger
|
||||
from apscheduler.util import (
|
||||
ref_to_obj, obj_to_ref, datetime_repr, repr_escape, get_callable_name, check_callable_args,
|
||||
convert_to_datetime)
|
||||
|
||||
|
||||
class Job(object):
|
||||
"""
|
||||
Contains the options given when scheduling callables and its current schedule and other state.
|
||||
This class should never be instantiated by the user.
|
||||
|
||||
:var str id: the unique identifier of this job
|
||||
:var str name: the description of this job
|
||||
:var func: the callable to execute
|
||||
:var tuple|list args: positional arguments to the callable
|
||||
:var dict kwargs: keyword arguments to the callable
|
||||
:var bool coalesce: whether to only run the job once when several run times are due
|
||||
:var trigger: the trigger object that controls the schedule of this job
|
||||
:var str executor: the name of the executor that will run this job
|
||||
:var int misfire_grace_time: the time (in seconds) how much this job's execution is allowed to
|
||||
be late
|
||||
:var int max_instances: the maximum number of concurrently executing instances allowed for this
|
||||
job
|
||||
:var datetime.datetime next_run_time: the next scheduled run time of this job
|
||||
|
||||
.. note::
|
||||
The ``misfire_grace_time`` has some non-obvious effects on job execution. See the
|
||||
:ref:`missed-job-executions` section in the documentation for an in-depth explanation.
|
||||
"""
|
||||
|
||||
__slots__ = ('_scheduler', '_jobstore_alias', 'id', 'trigger', 'executor', 'func', 'func_ref',
|
||||
'args', 'kwargs', 'name', 'misfire_grace_time', 'coalesce', 'max_instances',
|
||||
'next_run_time')
|
||||
|
||||
def __init__(self, scheduler, id=None, **kwargs):
|
||||
super(Job, self).__init__()
|
||||
self._scheduler = scheduler
|
||||
self._jobstore_alias = None
|
||||
self._modify(id=id or uuid4().hex, **kwargs)
|
||||
|
||||
def modify(self, **changes):
|
||||
"""
|
||||
Makes the given changes to this job and saves it in the associated job store.
|
||||
|
||||
Accepted keyword arguments are the same as the variables on this class.
|
||||
|
||||
.. seealso:: :meth:`~apscheduler.schedulers.base.BaseScheduler.modify_job`
|
||||
|
||||
:return Job: this job instance
|
||||
|
||||
"""
|
||||
self._scheduler.modify_job(self.id, self._jobstore_alias, **changes)
|
||||
return self
|
||||
|
||||
def reschedule(self, trigger, **trigger_args):
|
||||
"""
|
||||
Shortcut for switching the trigger on this job.
|
||||
|
||||
.. seealso:: :meth:`~apscheduler.schedulers.base.BaseScheduler.reschedule_job`
|
||||
|
||||
:return Job: this job instance
|
||||
|
||||
"""
|
||||
self._scheduler.reschedule_job(self.id, self._jobstore_alias, trigger, **trigger_args)
|
||||
return self
|
||||
|
||||
def pause(self):
|
||||
"""
|
||||
Temporarily suspend the execution of this job.
|
||||
|
||||
.. seealso:: :meth:`~apscheduler.schedulers.base.BaseScheduler.pause_job`
|
||||
|
||||
:return Job: this job instance
|
||||
|
||||
"""
|
||||
self._scheduler.pause_job(self.id, self._jobstore_alias)
|
||||
return self
|
||||
|
||||
def resume(self):
|
||||
"""
|
||||
Resume the schedule of this job if previously paused.
|
||||
|
||||
.. seealso:: :meth:`~apscheduler.schedulers.base.BaseScheduler.resume_job`
|
||||
|
||||
:return Job: this job instance
|
||||
|
||||
"""
|
||||
self._scheduler.resume_job(self.id, self._jobstore_alias)
|
||||
return self
|
||||
|
||||
def remove(self):
|
||||
"""
|
||||
Unschedules this job and removes it from its associated job store.
|
||||
|
||||
.. seealso:: :meth:`~apscheduler.schedulers.base.BaseScheduler.remove_job`
|
||||
|
||||
"""
|
||||
self._scheduler.remove_job(self.id, self._jobstore_alias)
|
||||
|
||||
@property
|
||||
def pending(self):
|
||||
"""
|
||||
Returns ``True`` if the referenced job is still waiting to be added to its designated job
|
||||
store.
|
||||
|
||||
"""
|
||||
return self._jobstore_alias is None
|
||||
|
||||
#
|
||||
# Private API
|
||||
#
|
||||
|
||||
def _get_run_times(self, now):
|
||||
"""
|
||||
Computes the scheduled run times between ``next_run_time`` and ``now`` (inclusive).
|
||||
|
||||
:type now: datetime.datetime
|
||||
:rtype: list[datetime.datetime]
|
||||
|
||||
"""
|
||||
run_times = []
|
||||
next_run_time = self.next_run_time
|
||||
while next_run_time and next_run_time <= now:
|
||||
run_times.append(next_run_time)
|
||||
next_run_time = self.trigger.get_next_fire_time(next_run_time, now)
|
||||
|
||||
return run_times
|
||||
|
||||
def _modify(self, **changes):
|
||||
"""
|
||||
Validates the changes to the Job and makes the modifications if and only if all of them
|
||||
validate.
|
||||
|
||||
"""
|
||||
approved = {}
|
||||
|
||||
if 'id' in changes:
|
||||
value = changes.pop('id')
|
||||
if not isinstance(value, six.string_types):
|
||||
raise TypeError("id must be a nonempty string")
|
||||
if hasattr(self, 'id'):
|
||||
raise ValueError('The job ID may not be changed')
|
||||
approved['id'] = value
|
||||
|
||||
if 'func' in changes or 'args' in changes or 'kwargs' in changes:
|
||||
func = changes.pop('func') if 'func' in changes else self.func
|
||||
args = changes.pop('args') if 'args' in changes else self.args
|
||||
kwargs = changes.pop('kwargs') if 'kwargs' in changes else self.kwargs
|
||||
|
||||
if isinstance(func, six.string_types):
|
||||
func_ref = func
|
||||
func = ref_to_obj(func)
|
||||
elif callable(func):
|
||||
try:
|
||||
func_ref = obj_to_ref(func)
|
||||
except ValueError:
|
||||
# If this happens, this Job won't be serializable
|
||||
func_ref = None
|
||||
else:
|
||||
raise TypeError('func must be a callable or a textual reference to one')
|
||||
|
||||
if not hasattr(self, 'name') and changes.get('name', None) is None:
|
||||
changes['name'] = get_callable_name(func)
|
||||
|
||||
if isinstance(args, six.string_types) or not isinstance(args, Iterable):
|
||||
raise TypeError('args must be a non-string iterable')
|
||||
if isinstance(kwargs, six.string_types) or not isinstance(kwargs, Mapping):
|
||||
raise TypeError('kwargs must be a dict-like object')
|
||||
|
||||
check_callable_args(func, args, kwargs)
|
||||
|
||||
approved['func'] = func
|
||||
approved['func_ref'] = func_ref
|
||||
approved['args'] = args
|
||||
approved['kwargs'] = kwargs
|
||||
|
||||
if 'name' in changes:
|
||||
value = changes.pop('name')
|
||||
if not value or not isinstance(value, six.string_types):
|
||||
raise TypeError("name must be a nonempty string")
|
||||
approved['name'] = value
|
||||
|
||||
if 'misfire_grace_time' in changes:
|
||||
value = changes.pop('misfire_grace_time')
|
||||
if value is not None and (not isinstance(value, six.integer_types) or value <= 0):
|
||||
raise TypeError('misfire_grace_time must be either None or a positive integer')
|
||||
approved['misfire_grace_time'] = value
|
||||
|
||||
if 'coalesce' in changes:
|
||||
value = bool(changes.pop('coalesce'))
|
||||
approved['coalesce'] = value
|
||||
|
||||
if 'max_instances' in changes:
|
||||
value = changes.pop('max_instances')
|
||||
if not isinstance(value, six.integer_types) or value <= 0:
|
||||
raise TypeError('max_instances must be a positive integer')
|
||||
approved['max_instances'] = value
|
||||
|
||||
if 'trigger' in changes:
|
||||
trigger = changes.pop('trigger')
|
||||
if not isinstance(trigger, BaseTrigger):
|
||||
raise TypeError('Expected a trigger instance, got %s instead' %
|
||||
trigger.__class__.__name__)
|
||||
|
||||
approved['trigger'] = trigger
|
||||
|
||||
if 'executor' in changes:
|
||||
value = changes.pop('executor')
|
||||
if not isinstance(value, six.string_types):
|
||||
raise TypeError('executor must be a string')
|
||||
approved['executor'] = value
|
||||
|
||||
if 'next_run_time' in changes:
|
||||
value = changes.pop('next_run_time')
|
||||
approved['next_run_time'] = convert_to_datetime(value, self._scheduler.timezone,
|
||||
'next_run_time')
|
||||
|
||||
if changes:
|
||||
raise AttributeError('The following are not modifiable attributes of Job: %s' %
|
||||
', '.join(changes))
|
||||
|
||||
for key, value in six.iteritems(approved):
|
||||
setattr(self, key, value)
|
||||
|
||||
def __getstate__(self):
|
||||
# Don't allow this Job to be serialized if the function reference could not be determined
|
||||
if not self.func_ref:
|
||||
raise ValueError(
|
||||
'This Job cannot be serialized since the reference to its callable (%r) could not '
|
||||
'be determined. Consider giving a textual reference (module:function name) '
|
||||
'instead.' % (self.func,))
|
||||
|
||||
# Instance methods cannot survive serialization as-is, so store the "self" argument
|
||||
# explicitly
|
||||
if ismethod(self.func) and not isclass(self.func.__self__):
|
||||
args = (self.func.__self__,) + tuple(self.args)
|
||||
else:
|
||||
args = self.args
|
||||
|
||||
return {
|
||||
'version': 1,
|
||||
'id': self.id,
|
||||
'func': self.func_ref,
|
||||
'trigger': self.trigger,
|
||||
'executor': self.executor,
|
||||
'args': args,
|
||||
'kwargs': self.kwargs,
|
||||
'name': self.name,
|
||||
'misfire_grace_time': self.misfire_grace_time,
|
||||
'coalesce': self.coalesce,
|
||||
'max_instances': self.max_instances,
|
||||
'next_run_time': self.next_run_time
|
||||
}
|
||||
|
||||
def __setstate__(self, state):
|
||||
if state.get('version', 1) > 1:
|
||||
raise ValueError('Job has version %s, but only version 1 can be handled' %
|
||||
state['version'])
|
||||
|
||||
self.id = state['id']
|
||||
self.func_ref = state['func']
|
||||
self.func = ref_to_obj(self.func_ref)
|
||||
self.trigger = state['trigger']
|
||||
self.executor = state['executor']
|
||||
self.args = state['args']
|
||||
self.kwargs = state['kwargs']
|
||||
self.name = state['name']
|
||||
self.misfire_grace_time = state['misfire_grace_time']
|
||||
self.coalesce = state['coalesce']
|
||||
self.max_instances = state['max_instances']
|
||||
self.next_run_time = state['next_run_time']
|
||||
|
||||
def __eq__(self, other):
|
||||
if isinstance(other, Job):
|
||||
return self.id == other.id
|
||||
return NotImplemented
|
||||
|
||||
def __repr__(self):
|
||||
return '<Job (id=%s name=%s)>' % (repr_escape(self.id), repr_escape(self.name))
|
||||
|
||||
def __str__(self):
|
||||
return repr_escape(self.__unicode__())
|
||||
|
||||
def __unicode__(self):
|
||||
if hasattr(self, 'next_run_time'):
|
||||
status = ('next run at: ' + datetime_repr(self.next_run_time) if
|
||||
self.next_run_time else 'paused')
|
||||
else:
|
||||
status = 'pending'
|
||||
|
||||
return u'%s (trigger: %s, %s)' % (self.name, self.trigger, status)
|
@ -1,143 +0,0 @@
|
||||
from abc import ABCMeta, abstractmethod
|
||||
import logging
|
||||
|
||||
import six
|
||||
|
||||
|
||||
class JobLookupError(KeyError):
|
||||
"""Raised when the job store cannot find a job for update or removal."""
|
||||
|
||||
def __init__(self, job_id):
|
||||
super(JobLookupError, self).__init__(u'No job by the id of %s was found' % job_id)
|
||||
|
||||
|
||||
class ConflictingIdError(KeyError):
|
||||
"""Raised when the uniqueness of job IDs is being violated."""
|
||||
|
||||
def __init__(self, job_id):
|
||||
super(ConflictingIdError, self).__init__(
|
||||
u'Job identifier (%s) conflicts with an existing job' % job_id)
|
||||
|
||||
|
||||
class TransientJobError(ValueError):
|
||||
"""
|
||||
Raised when an attempt to add transient (with no func_ref) job to a persistent job store is
|
||||
detected.
|
||||
"""
|
||||
|
||||
def __init__(self, job_id):
|
||||
super(TransientJobError, self).__init__(
|
||||
u'Job (%s) cannot be added to this job store because a reference to the callable '
|
||||
u'could not be determined.' % job_id)
|
||||
|
||||
|
||||
class BaseJobStore(six.with_metaclass(ABCMeta)):
|
||||
"""Abstract base class that defines the interface that every job store must implement."""
|
||||
|
||||
_scheduler = None
|
||||
_alias = None
|
||||
_logger = logging.getLogger('apscheduler.jobstores')
|
||||
|
||||
def start(self, scheduler, alias):
|
||||
"""
|
||||
Called by the scheduler when the scheduler is being started or when the job store is being
|
||||
added to an already running scheduler.
|
||||
|
||||
:param apscheduler.schedulers.base.BaseScheduler scheduler: the scheduler that is starting
|
||||
this job store
|
||||
:param str|unicode alias: alias of this job store as it was assigned to the scheduler
|
||||
"""
|
||||
|
||||
self._scheduler = scheduler
|
||||
self._alias = alias
|
||||
self._logger = logging.getLogger('apscheduler.jobstores.%s' % alias)
|
||||
|
||||
def shutdown(self):
|
||||
"""Frees any resources still bound to this job store."""
|
||||
|
||||
def _fix_paused_jobs_sorting(self, jobs):
|
||||
for i, job in enumerate(jobs):
|
||||
if job.next_run_time is not None:
|
||||
if i > 0:
|
||||
paused_jobs = jobs[:i]
|
||||
del jobs[:i]
|
||||
jobs.extend(paused_jobs)
|
||||
break
|
||||
|
||||
@abstractmethod
|
||||
def lookup_job(self, job_id):
|
||||
"""
|
||||
Returns a specific job, or ``None`` if it isn't found..
|
||||
|
||||
The job store is responsible for setting the ``scheduler`` and ``jobstore`` attributes of
|
||||
the returned job to point to the scheduler and itself, respectively.
|
||||
|
||||
:param str|unicode job_id: identifier of the job
|
||||
:rtype: Job
|
||||
"""
|
||||
|
||||
@abstractmethod
|
||||
def get_due_jobs(self, now):
|
||||
"""
|
||||
Returns the list of jobs that have ``next_run_time`` earlier or equal to ``now``.
|
||||
The returned jobs must be sorted by next run time (ascending).
|
||||
|
||||
:param datetime.datetime now: the current (timezone aware) datetime
|
||||
:rtype: list[Job]
|
||||
"""
|
||||
|
||||
@abstractmethod
|
||||
def get_next_run_time(self):
|
||||
"""
|
||||
Returns the earliest run time of all the jobs stored in this job store, or ``None`` if
|
||||
there are no active jobs.
|
||||
|
||||
:rtype: datetime.datetime
|
||||
"""
|
||||
|
||||
@abstractmethod
|
||||
def get_all_jobs(self):
|
||||
"""
|
||||
Returns a list of all jobs in this job store.
|
||||
The returned jobs should be sorted by next run time (ascending).
|
||||
Paused jobs (next_run_time == None) should be sorted last.
|
||||
|
||||
The job store is responsible for setting the ``scheduler`` and ``jobstore`` attributes of
|
||||
the returned jobs to point to the scheduler and itself, respectively.
|
||||
|
||||
:rtype: list[Job]
|
||||
"""
|
||||
|
||||
@abstractmethod
|
||||
def add_job(self, job):
|
||||
"""
|
||||
Adds the given job to this store.
|
||||
|
||||
:param Job job: the job to add
|
||||
:raises ConflictingIdError: if there is another job in this store with the same ID
|
||||
"""
|
||||
|
||||
@abstractmethod
|
||||
def update_job(self, job):
|
||||
"""
|
||||
Replaces the job in the store with the given newer version.
|
||||
|
||||
:param Job job: the job to update
|
||||
:raises JobLookupError: if the job does not exist
|
||||
"""
|
||||
|
||||
@abstractmethod
|
||||
def remove_job(self, job_id):
|
||||
"""
|
||||
Removes the given job from this store.
|
||||
|
||||
:param str|unicode job_id: identifier of the job
|
||||
:raises JobLookupError: if the job does not exist
|
||||
"""
|
||||
|
||||
@abstractmethod
|
||||
def remove_all_jobs(self):
|
||||
"""Removes all jobs from this store."""
|
||||
|
||||
def __repr__(self):
|
||||
return '<%s>' % self.__class__.__name__
|
@ -1,108 +0,0 @@
|
||||
from __future__ import absolute_import
|
||||
|
||||
from apscheduler.jobstores.base import BaseJobStore, JobLookupError, ConflictingIdError
|
||||
from apscheduler.util import datetime_to_utc_timestamp
|
||||
|
||||
|
||||
class MemoryJobStore(BaseJobStore):
|
||||
"""
|
||||
Stores jobs in an array in RAM. Provides no persistence support.
|
||||
|
||||
Plugin alias: ``memory``
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
super(MemoryJobStore, self).__init__()
|
||||
# list of (job, timestamp), sorted by next_run_time and job id (ascending)
|
||||
self._jobs = []
|
||||
self._jobs_index = {} # id -> (job, timestamp) lookup table
|
||||
|
||||
def lookup_job(self, job_id):
|
||||
return self._jobs_index.get(job_id, (None, None))[0]
|
||||
|
||||
def get_due_jobs(self, now):
|
||||
now_timestamp = datetime_to_utc_timestamp(now)
|
||||
pending = []
|
||||
for job, timestamp in self._jobs:
|
||||
if timestamp is None or timestamp > now_timestamp:
|
||||
break
|
||||
pending.append(job)
|
||||
|
||||
return pending
|
||||
|
||||
def get_next_run_time(self):
|
||||
return self._jobs[0][0].next_run_time if self._jobs else None
|
||||
|
||||
def get_all_jobs(self):
|
||||
return [j[0] for j in self._jobs]
|
||||
|
||||
def add_job(self, job):
|
||||
if job.id in self._jobs_index:
|
||||
raise ConflictingIdError(job.id)
|
||||
|
||||
timestamp = datetime_to_utc_timestamp(job.next_run_time)
|
||||
index = self._get_job_index(timestamp, job.id)
|
||||
self._jobs.insert(index, (job, timestamp))
|
||||
self._jobs_index[job.id] = (job, timestamp)
|
||||
|
||||
def update_job(self, job):
|
||||
old_job, old_timestamp = self._jobs_index.get(job.id, (None, None))
|
||||
if old_job is None:
|
||||
raise JobLookupError(job.id)
|
||||
|
||||
# If the next run time has not changed, simply replace the job in its present index.
|
||||
# Otherwise, reinsert the job to the list to preserve the ordering.
|
||||
old_index = self._get_job_index(old_timestamp, old_job.id)
|
||||
new_timestamp = datetime_to_utc_timestamp(job.next_run_time)
|
||||
if old_timestamp == new_timestamp:
|
||||
self._jobs[old_index] = (job, new_timestamp)
|
||||
else:
|
||||
del self._jobs[old_index]
|
||||
new_index = self._get_job_index(new_timestamp, job.id)
|
||||
self._jobs.insert(new_index, (job, new_timestamp))
|
||||
|
||||
self._jobs_index[old_job.id] = (job, new_timestamp)
|
||||
|
||||
def remove_job(self, job_id):
|
||||
job, timestamp = self._jobs_index.get(job_id, (None, None))
|
||||
if job is None:
|
||||
raise JobLookupError(job_id)
|
||||
|
||||
index = self._get_job_index(timestamp, job_id)
|
||||
del self._jobs[index]
|
||||
del self._jobs_index[job.id]
|
||||
|
||||
def remove_all_jobs(self):
|
||||
self._jobs = []
|
||||
self._jobs_index = {}
|
||||
|
||||
def shutdown(self):
|
||||
self.remove_all_jobs()
|
||||
|
||||
def _get_job_index(self, timestamp, job_id):
|
||||
"""
|
||||
Returns the index of the given job, or if it's not found, the index where the job should be
|
||||
inserted based on the given timestamp.
|
||||
|
||||
:type timestamp: int
|
||||
:type job_id: str
|
||||
|
||||
"""
|
||||
lo, hi = 0, len(self._jobs)
|
||||
timestamp = float('inf') if timestamp is None else timestamp
|
||||
while lo < hi:
|
||||
mid = (lo + hi) // 2
|
||||
mid_job, mid_timestamp = self._jobs[mid]
|
||||
mid_timestamp = float('inf') if mid_timestamp is None else mid_timestamp
|
||||
if mid_timestamp > timestamp:
|
||||
hi = mid
|
||||
elif mid_timestamp < timestamp:
|
||||
lo = mid + 1
|
||||
elif mid_job.id > job_id:
|
||||
hi = mid
|
||||
elif mid_job.id < job_id:
|
||||
lo = mid + 1
|
||||
else:
|
||||
return mid
|
||||
|
||||
return lo
|
@ -1,141 +0,0 @@
|
||||
from __future__ import absolute_import
|
||||
import warnings
|
||||
|
||||
from apscheduler.jobstores.base import BaseJobStore, JobLookupError, ConflictingIdError
|
||||
from apscheduler.util import maybe_ref, datetime_to_utc_timestamp, utc_timestamp_to_datetime
|
||||
from apscheduler.job import Job
|
||||
|
||||
try:
|
||||
import cPickle as pickle
|
||||
except ImportError: # pragma: nocover
|
||||
import pickle
|
||||
|
||||
try:
|
||||
from bson.binary import Binary
|
||||
from pymongo.errors import DuplicateKeyError
|
||||
from pymongo import MongoClient, ASCENDING
|
||||
except ImportError: # pragma: nocover
|
||||
raise ImportError('MongoDBJobStore requires PyMongo installed')
|
||||
|
||||
|
||||
class MongoDBJobStore(BaseJobStore):
|
||||
"""
|
||||
Stores jobs in a MongoDB database. Any leftover keyword arguments are directly passed to
|
||||
pymongo's `MongoClient
|
||||
<http://api.mongodb.org/python/current/api/pymongo/mongo_client.html#pymongo.mongo_client.MongoClient>`_.
|
||||
|
||||
Plugin alias: ``mongodb``
|
||||
|
||||
:param str database: database to store jobs in
|
||||
:param str collection: collection to store jobs in
|
||||
:param client: a :class:`~pymongo.mongo_client.MongoClient` instance to use instead of
|
||||
providing connection arguments
|
||||
:param int pickle_protocol: pickle protocol level to use (for serialization), defaults to the
|
||||
highest available
|
||||
"""
|
||||
|
||||
def __init__(self, database='apscheduler', collection='jobs', client=None,
|
||||
pickle_protocol=pickle.HIGHEST_PROTOCOL, **connect_args):
|
||||
super(MongoDBJobStore, self).__init__()
|
||||
self.pickle_protocol = pickle_protocol
|
||||
|
||||
if not database:
|
||||
raise ValueError('The "database" parameter must not be empty')
|
||||
if not collection:
|
||||
raise ValueError('The "collection" parameter must not be empty')
|
||||
|
||||
if client:
|
||||
self.client = maybe_ref(client)
|
||||
else:
|
||||
connect_args.setdefault('w', 1)
|
||||
self.client = MongoClient(**connect_args)
|
||||
|
||||
self.collection = self.client[database][collection]
|
||||
|
||||
def start(self, scheduler, alias):
|
||||
super(MongoDBJobStore, self).start(scheduler, alias)
|
||||
self.collection.ensure_index('next_run_time', sparse=True)
|
||||
|
||||
@property
|
||||
def connection(self):
|
||||
warnings.warn('The "connection" member is deprecated -- use "client" instead',
|
||||
DeprecationWarning)
|
||||
return self.client
|
||||
|
||||
def lookup_job(self, job_id):
|
||||
document = self.collection.find_one(job_id, ['job_state'])
|
||||
return self._reconstitute_job(document['job_state']) if document else None
|
||||
|
||||
def get_due_jobs(self, now):
|
||||
timestamp = datetime_to_utc_timestamp(now)
|
||||
return self._get_jobs({'next_run_time': {'$lte': timestamp}})
|
||||
|
||||
def get_next_run_time(self):
|
||||
document = self.collection.find_one({'next_run_time': {'$ne': None}},
|
||||
projection=['next_run_time'],
|
||||
sort=[('next_run_time', ASCENDING)])
|
||||
return utc_timestamp_to_datetime(document['next_run_time']) if document else None
|
||||
|
||||
def get_all_jobs(self):
|
||||
jobs = self._get_jobs({})
|
||||
self._fix_paused_jobs_sorting(jobs)
|
||||
return jobs
|
||||
|
||||
def add_job(self, job):
|
||||
try:
|
||||
self.collection.insert({
|
||||
'_id': job.id,
|
||||
'next_run_time': datetime_to_utc_timestamp(job.next_run_time),
|
||||
'job_state': Binary(pickle.dumps(job.__getstate__(), self.pickle_protocol))
|
||||
})
|
||||
except DuplicateKeyError:
|
||||
raise ConflictingIdError(job.id)
|
||||
|
||||
def update_job(self, job):
|
||||
changes = {
|
||||
'next_run_time': datetime_to_utc_timestamp(job.next_run_time),
|
||||
'job_state': Binary(pickle.dumps(job.__getstate__(), self.pickle_protocol))
|
||||
}
|
||||
result = self.collection.update({'_id': job.id}, {'$set': changes})
|
||||
if result and result['n'] == 0:
|
||||
raise JobLookupError(job.id)
|
||||
|
||||
def remove_job(self, job_id):
|
||||
result = self.collection.remove(job_id)
|
||||
if result and result['n'] == 0:
|
||||
raise JobLookupError(job_id)
|
||||
|
||||
def remove_all_jobs(self):
|
||||
self.collection.remove()
|
||||
|
||||
def shutdown(self):
|
||||
self.client.close()
|
||||
|
||||
def _reconstitute_job(self, job_state):
|
||||
job_state = pickle.loads(job_state)
|
||||
job = Job.__new__(Job)
|
||||
job.__setstate__(job_state)
|
||||
job._scheduler = self._scheduler
|
||||
job._jobstore_alias = self._alias
|
||||
return job
|
||||
|
||||
def _get_jobs(self, conditions):
|
||||
jobs = []
|
||||
failed_job_ids = []
|
||||
for document in self.collection.find(conditions, ['_id', 'job_state'],
|
||||
sort=[('next_run_time', ASCENDING)]):
|
||||
try:
|
||||
jobs.append(self._reconstitute_job(document['job_state']))
|
||||
except BaseException:
|
||||
self._logger.exception('Unable to restore job "%s" -- removing it',
|
||||
document['_id'])
|
||||
failed_job_ids.append(document['_id'])
|
||||
|
||||
# Remove all the jobs we failed to restore
|
||||
if failed_job_ids:
|
||||
self.collection.remove({'_id': {'$in': failed_job_ids}})
|
||||
|
||||
return jobs
|
||||
|
||||
def __repr__(self):
|
||||
return '<%s (client=%s)>' % (self.__class__.__name__, self.client)
|
@ -1,146 +0,0 @@
|
||||
from __future__ import absolute_import
|
||||
from datetime import datetime
|
||||
|
||||
from pytz import utc
|
||||
import six
|
||||
|
||||
from apscheduler.jobstores.base import BaseJobStore, JobLookupError, ConflictingIdError
|
||||
from apscheduler.util import datetime_to_utc_timestamp, utc_timestamp_to_datetime
|
||||
from apscheduler.job import Job
|
||||
|
||||
try:
|
||||
import cPickle as pickle
|
||||
except ImportError: # pragma: nocover
|
||||
import pickle
|
||||
|
||||
try:
|
||||
from redis import StrictRedis
|
||||
except ImportError: # pragma: nocover
|
||||
raise ImportError('RedisJobStore requires redis installed')
|
||||
|
||||
|
||||
class RedisJobStore(BaseJobStore):
|
||||
"""
|
||||
Stores jobs in a Redis database. Any leftover keyword arguments are directly passed to redis's
|
||||
:class:`~redis.StrictRedis`.
|
||||
|
||||
Plugin alias: ``redis``
|
||||
|
||||
:param int db: the database number to store jobs in
|
||||
:param str jobs_key: key to store jobs in
|
||||
:param str run_times_key: key to store the jobs' run times in
|
||||
:param int pickle_protocol: pickle protocol level to use (for serialization), defaults to the
|
||||
highest available
|
||||
"""
|
||||
|
||||
def __init__(self, db=0, jobs_key='apscheduler.jobs', run_times_key='apscheduler.run_times',
|
||||
pickle_protocol=pickle.HIGHEST_PROTOCOL, **connect_args):
|
||||
super(RedisJobStore, self).__init__()
|
||||
|
||||
if db is None:
|
||||
raise ValueError('The "db" parameter must not be empty')
|
||||
if not jobs_key:
|
||||
raise ValueError('The "jobs_key" parameter must not be empty')
|
||||
if not run_times_key:
|
||||
raise ValueError('The "run_times_key" parameter must not be empty')
|
||||
|
||||
self.pickle_protocol = pickle_protocol
|
||||
self.jobs_key = jobs_key
|
||||
self.run_times_key = run_times_key
|
||||
self.redis = StrictRedis(db=int(db), **connect_args)
|
||||
|
||||
def lookup_job(self, job_id):
|
||||
job_state = self.redis.hget(self.jobs_key, job_id)
|
||||
return self._reconstitute_job(job_state) if job_state else None
|
||||
|
||||
def get_due_jobs(self, now):
|
||||
timestamp = datetime_to_utc_timestamp(now)
|
||||
job_ids = self.redis.zrangebyscore(self.run_times_key, 0, timestamp)
|
||||
if job_ids:
|
||||
job_states = self.redis.hmget(self.jobs_key, *job_ids)
|
||||
return self._reconstitute_jobs(six.moves.zip(job_ids, job_states))
|
||||
return []
|
||||
|
||||
def get_next_run_time(self):
|
||||
next_run_time = self.redis.zrange(self.run_times_key, 0, 0, withscores=True)
|
||||
if next_run_time:
|
||||
return utc_timestamp_to_datetime(next_run_time[0][1])
|
||||
|
||||
def get_all_jobs(self):
|
||||
job_states = self.redis.hgetall(self.jobs_key)
|
||||
jobs = self._reconstitute_jobs(six.iteritems(job_states))
|
||||
paused_sort_key = datetime(9999, 12, 31, tzinfo=utc)
|
||||
return sorted(jobs, key=lambda job: job.next_run_time or paused_sort_key)
|
||||
|
||||
def add_job(self, job):
|
||||
if self.redis.hexists(self.jobs_key, job.id):
|
||||
raise ConflictingIdError(job.id)
|
||||
|
||||
with self.redis.pipeline() as pipe:
|
||||
pipe.multi()
|
||||
pipe.hset(self.jobs_key, job.id, pickle.dumps(job.__getstate__(),
|
||||
self.pickle_protocol))
|
||||
if job.next_run_time:
|
||||
pipe.zadd(self.run_times_key, datetime_to_utc_timestamp(job.next_run_time), job.id)
|
||||
pipe.execute()
|
||||
|
||||
def update_job(self, job):
|
||||
if not self.redis.hexists(self.jobs_key, job.id):
|
||||
raise JobLookupError(job.id)
|
||||
|
||||
with self.redis.pipeline() as pipe:
|
||||
pipe.hset(self.jobs_key, job.id, pickle.dumps(job.__getstate__(),
|
||||
self.pickle_protocol))
|
||||
if job.next_run_time:
|
||||
pipe.zadd(self.run_times_key, datetime_to_utc_timestamp(job.next_run_time), job.id)
|
||||
else:
|
||||
pipe.zrem(self.run_times_key, job.id)
|
||||
pipe.execute()
|
||||
|
||||
def remove_job(self, job_id):
|
||||
if not self.redis.hexists(self.jobs_key, job_id):
|
||||
raise JobLookupError(job_id)
|
||||
|
||||
with self.redis.pipeline() as pipe:
|
||||
pipe.hdel(self.jobs_key, job_id)
|
||||
pipe.zrem(self.run_times_key, job_id)
|
||||
pipe.execute()
|
||||
|
||||
def remove_all_jobs(self):
|
||||
with self.redis.pipeline() as pipe:
|
||||
pipe.delete(self.jobs_key)
|
||||
pipe.delete(self.run_times_key)
|
||||
pipe.execute()
|
||||
|
||||
def shutdown(self):
|
||||
self.redis.connection_pool.disconnect()
|
||||
|
||||
def _reconstitute_job(self, job_state):
|
||||
job_state = pickle.loads(job_state)
|
||||
job = Job.__new__(Job)
|
||||
job.__setstate__(job_state)
|
||||
job._scheduler = self._scheduler
|
||||
job._jobstore_alias = self._alias
|
||||
return job
|
||||
|
||||
def _reconstitute_jobs(self, job_states):
|
||||
jobs = []
|
||||
failed_job_ids = []
|
||||
for job_id, job_state in job_states:
|
||||
try:
|
||||
jobs.append(self._reconstitute_job(job_state))
|
||||
except BaseException:
|
||||
self._logger.exception('Unable to restore job "%s" -- removing it', job_id)
|
||||
failed_job_ids.append(job_id)
|
||||
|
||||
# Remove all the jobs we failed to restore
|
||||
if failed_job_ids:
|
||||
with self.redis.pipeline() as pipe:
|
||||
pipe.hdel(self.jobs_key, *failed_job_ids)
|
||||
pipe.zrem(self.run_times_key, *failed_job_ids)
|
||||
pipe.execute()
|
||||
|
||||
return jobs
|
||||
|
||||
def __repr__(self):
|
||||
return '<%s>' % self.__class__.__name__
|
@ -1,153 +0,0 @@
|
||||
from __future__ import absolute_import
|
||||
|
||||
from apscheduler.jobstores.base import BaseJobStore, JobLookupError, ConflictingIdError
|
||||
from apscheduler.util import maybe_ref, datetime_to_utc_timestamp, utc_timestamp_to_datetime
|
||||
from apscheduler.job import Job
|
||||
|
||||
try:
|
||||
import cPickle as pickle
|
||||
except ImportError: # pragma: nocover
|
||||
import pickle
|
||||
|
||||
try:
|
||||
import rethinkdb as r
|
||||
except ImportError: # pragma: nocover
|
||||
raise ImportError('RethinkDBJobStore requires rethinkdb installed')
|
||||
|
||||
|
||||
class RethinkDBJobStore(BaseJobStore):
|
||||
"""
|
||||
Stores jobs in a RethinkDB database. Any leftover keyword arguments are directly passed to
|
||||
rethinkdb's `RethinkdbClient <http://www.rethinkdb.com/api/#connect>`_.
|
||||
|
||||
Plugin alias: ``rethinkdb``
|
||||
|
||||
:param str database: database to store jobs in
|
||||
:param str collection: collection to store jobs in
|
||||
:param client: a :class:`rethinkdb.net.Connection` instance to use instead of providing
|
||||
connection arguments
|
||||
:param int pickle_protocol: pickle protocol level to use (for serialization), defaults to the
|
||||
highest available
|
||||
"""
|
||||
|
||||
def __init__(self, database='apscheduler', table='jobs', client=None,
|
||||
pickle_protocol=pickle.HIGHEST_PROTOCOL, **connect_args):
|
||||
super(RethinkDBJobStore, self).__init__()
|
||||
|
||||
if not database:
|
||||
raise ValueError('The "database" parameter must not be empty')
|
||||
if not table:
|
||||
raise ValueError('The "table" parameter must not be empty')
|
||||
|
||||
self.database = database
|
||||
self.table = table
|
||||
self.client = client
|
||||
self.pickle_protocol = pickle_protocol
|
||||
self.connect_args = connect_args
|
||||
self.conn = None
|
||||
|
||||
def start(self, scheduler, alias):
|
||||
super(RethinkDBJobStore, self).start(scheduler, alias)
|
||||
|
||||
if self.client:
|
||||
self.conn = maybe_ref(self.client)
|
||||
else:
|
||||
self.conn = r.connect(db=self.database, **self.connect_args)
|
||||
|
||||
if self.database not in r.db_list().run(self.conn):
|
||||
r.db_create(self.database).run(self.conn)
|
||||
|
||||
if self.table not in r.table_list().run(self.conn):
|
||||
r.table_create(self.table).run(self.conn)
|
||||
|
||||
if 'next_run_time' not in r.table(self.table).index_list().run(self.conn):
|
||||
r.table(self.table).index_create('next_run_time').run(self.conn)
|
||||
|
||||
self.table = r.db(self.database).table(self.table)
|
||||
|
||||
def lookup_job(self, job_id):
|
||||
results = list(self.table.get_all(job_id).pluck('job_state').run(self.conn))
|
||||
return self._reconstitute_job(results[0]['job_state']) if results else None
|
||||
|
||||
def get_due_jobs(self, now):
|
||||
return self._get_jobs(r.row['next_run_time'] <= datetime_to_utc_timestamp(now))
|
||||
|
||||
def get_next_run_time(self):
|
||||
results = list(
|
||||
self.table
|
||||
.filter(r.row['next_run_time'] != None) # flake8: noqa
|
||||
.order_by(r.asc('next_run_time'))
|
||||
.map(lambda x: x['next_run_time'])
|
||||
.limit(1)
|
||||
.run(self.conn)
|
||||
)
|
||||
return utc_timestamp_to_datetime(results[0]) if results else None
|
||||
|
||||
def get_all_jobs(self):
|
||||
jobs = self._get_jobs()
|
||||
self._fix_paused_jobs_sorting(jobs)
|
||||
return jobs
|
||||
|
||||
def add_job(self, job):
|
||||
job_dict = {
|
||||
'id': job.id,
|
||||
'next_run_time': datetime_to_utc_timestamp(job.next_run_time),
|
||||
'job_state': r.binary(pickle.dumps(job.__getstate__(), self.pickle_protocol))
|
||||
}
|
||||
results = self.table.insert(job_dict).run(self.conn)
|
||||
if results['errors'] > 0:
|
||||
raise ConflictingIdError(job.id)
|
||||
|
||||
def update_job(self, job):
|
||||
changes = {
|
||||
'next_run_time': datetime_to_utc_timestamp(job.next_run_time),
|
||||
'job_state': r.binary(pickle.dumps(job.__getstate__(), self.pickle_protocol))
|
||||
}
|
||||
results = self.table.get_all(job.id).update(changes).run(self.conn)
|
||||
skipped = False in map(lambda x: results[x] == 0, results.keys())
|
||||
if results['skipped'] > 0 or results['errors'] > 0 or not skipped:
|
||||
raise JobLookupError(job.id)
|
||||
|
||||
def remove_job(self, job_id):
|
||||
results = self.table.get_all(job_id).delete().run(self.conn)
|
||||
if results['deleted'] + results['skipped'] != 1:
|
||||
raise JobLookupError(job_id)
|
||||
|
||||
def remove_all_jobs(self):
|
||||
self.table.delete().run(self.conn)
|
||||
|
||||
def shutdown(self):
|
||||
self.conn.close()
|
||||
|
||||
def _reconstitute_job(self, job_state):
|
||||
job_state = pickle.loads(job_state)
|
||||
job = Job.__new__(Job)
|
||||
job.__setstate__(job_state)
|
||||
job._scheduler = self._scheduler
|
||||
job._jobstore_alias = self._alias
|
||||
return job
|
||||
|
||||
def _get_jobs(self, predicate=None):
|
||||
jobs = []
|
||||
failed_job_ids = []
|
||||
query = (self.table.filter(r.row['next_run_time'] != None).filter(predicate) if
|
||||
predicate else self.table)
|
||||
query = query.order_by('next_run_time', 'id').pluck('id', 'job_state')
|
||||
|
||||
for document in query.run(self.conn):
|
||||
try:
|
||||
jobs.append(self._reconstitute_job(document['job_state']))
|
||||
except:
|
||||
self._logger.exception('Unable to restore job "%s" -- removing it', document['id'])
|
||||
failed_job_ids.append(document['id'])
|
||||
|
||||
# Remove all the jobs we failed to restore
|
||||
if failed_job_ids:
|
||||
r.expr(failed_job_ids).for_each(
|
||||
lambda job_id: self.table.get_all(job_id).delete()).run(self.conn)
|
||||
|
||||
return jobs
|
||||
|
||||
def __repr__(self):
|
||||
connection = self.conn
|
||||
return '<%s (connection=%s)>' % (self.__class__.__name__, connection)
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user