├── .github ├── dependabot.yml └── workflows │ ├── build.yml │ └── release.yml ├── .gitignore ├── .pre-commit-config.yaml ├── .pylintrc ├── .readthedocs.yml ├── CODE_OF_CONDUCT.md ├── LICENSE ├── LICENSES ├── CC-BY-4.0.txt ├── MIT.txt └── Unlicense.txt ├── README.rst ├── README.rst.license ├── circuitpython_schedule.py ├── docs ├── _static │ ├── favicon.ico │ └── favicon.ico.license ├── api.rst ├── api.rst.license ├── conf.py ├── examples.rst ├── examples.rst.license ├── index.rst └── index.rst.license ├── examples ├── circuitpython_schedule_rtc.py ├── circuitpython_schedule_simpletest.py └── circuitpython_schedule_util.py ├── pyproject.toml ├── requirements.txt └── setup.py /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | # To get started with Dependabot version updates, you'll need to specify which 2 | # package ecosystems to update and where the package manifests are located. 3 | # Please see the documentation for all configuration options: 4 | # https://docs.github.com/code-security/dependabot/dependabot-version-updates/configuration-options-for-the-dependabot.yml-file 5 | 6 | version: 2 7 | updates: 8 | - package-ecosystem: "pip" # See documentation for possible values 9 | directory: "/" # Location of package manifests 10 | schedule: 11 | interval: "weekly" 12 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2017 Scott Shawcroft, written for Adafruit Industries 2 | # 3 | # SPDX-License-Identifier: MIT 4 | 5 | name: Build CI 6 | 7 | on: [pull_request, push] 8 | 9 | jobs: 10 | test: 11 | runs-on: ubuntu-latest 12 | steps: 13 | - name: Dump GitHub context 14 | env: 15 | GITHUB_CONTEXT: ${{ toJson(github) }} 16 | run: echo "$GITHUB_CONTEXT" 17 | - name: Translate Repo Name For Build Tools filename_prefix 18 | id: repo-name 19 | run: | 20 | echo ::set-output name=repo-name::$( 21 | echo ${{ github.repository }} | 22 | awk -F '\/' '{ print tolower($2) }' | 23 | tr '_' '-' 24 | ) 25 | - name: Set up Python 3.6 26 | uses: actions/setup-python@v2 27 | with: 28 | python-version: 3.6 29 | - name: Versions 30 | run: | 31 | python3 --version 32 | - name: Checkout Current Repo 33 | uses: actions/checkout@v2 34 | with: 35 | submodules: true 36 | - name: Checkout tools repo 37 | uses: actions/checkout@v2 38 | with: 39 | repository: adafruit/actions-ci-circuitpython-libs 40 | path: actions-ci 41 | - name: Install dependencies 42 | # (e.g. - apt-get: gettext, etc; pip: circuitpython-build-tools, requirements.txt; etc.) 43 | run: | 44 | source actions-ci/install.sh 45 | - name: Pip install pylint, Sphinx, pre-commit 46 | run: | 47 | pip install --force-reinstall pylint Sphinx sphinx-rtd-theme pre-commit 48 | - name: Library version 49 | run: git describe --dirty --always --tags 50 | - name: Pre-commit hooks 51 | run: | 52 | pre-commit run --all-files 53 | - name: Build assets 54 | run: circuitpython-build-bundles --filename_prefix ${{ steps.repo-name.outputs.repo-name }} --library_location . 55 | - name: Archive bundles 56 | uses: actions/upload-artifact@v2 57 | with: 58 | name: bundles 59 | path: ${{ github.workspace }}/bundles/ 60 | - name: Check For docs folder 61 | id: need-docs 62 | run: | 63 | echo ::set-output name=docs::$( find . -wholename './docs' ) 64 | - name: Build docs 65 | if: contains(steps.need-docs.outputs.docs, 'docs') 66 | working-directory: docs 67 | run: sphinx-build -E -W -b html . _build/html 68 | - name: Check For setup.py 69 | id: need-pypi 70 | run: | 71 | echo ::set-output name=setup-py::$( find . -wholename './setup.py' ) 72 | - name: Build Python package 73 | if: contains(steps.need-pypi.outputs.setup-py, 'setup.py') 74 | run: | 75 | pip install --upgrade setuptools wheel twine readme_renderer testresources 76 | python setup.py sdist 77 | python setup.py bdist_wheel --universal 78 | twine check dist/* 79 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2017 Scott Shawcroft, written for Adafruit Industries 2 | # 3 | # SPDX-License-Identifier: MIT 4 | 5 | name: Release Actions 6 | 7 | on: 8 | release: 9 | types: [published] 10 | 11 | jobs: 12 | upload-release-assets: 13 | runs-on: ubuntu-latest 14 | steps: 15 | - name: Dump GitHub context 16 | env: 17 | GITHUB_CONTEXT: ${{ toJson(github) }} 18 | run: echo "$GITHUB_CONTEXT" 19 | - name: Translate Repo Name For Build Tools filename_prefix 20 | id: repo-name 21 | run: | 22 | echo ::set-output name=repo-name::$( 23 | echo ${{ github.repository }} | 24 | awk -F '\/' '{ print tolower($2) }' | 25 | tr '_' '-' 26 | ) 27 | - name: Set up Python 3.6 28 | uses: actions/setup-python@v2 29 | with: 30 | python-version: 3.6 31 | - name: Versions 32 | run: | 33 | python3 --version 34 | - name: Checkout Current Repo 35 | uses: actions/checkout@v2 36 | with: 37 | submodules: true 38 | - name: Checkout tools repo 39 | uses: actions/checkout@v2 40 | with: 41 | repository: adafruit/actions-ci-circuitpython-libs 42 | path: actions-ci 43 | - name: Install deps 44 | run: | 45 | source actions-ci/install.sh 46 | - name: Build assets 47 | run: circuitpython-build-bundles --filename_prefix ${{ steps.repo-name.outputs.repo-name }} --library_location . 48 | - name: Upload Release Assets 49 | # the 'official' actions version does not yet support dynamically 50 | # supplying asset names to upload. @csexton's version chosen based on 51 | # discussion in the issue below, as its the simplest to implement and 52 | # allows for selecting files with a pattern. 53 | # https://github.com/actions/upload-release-asset/issues/4 54 | #uses: actions/upload-release-asset@v1.0.1 55 | uses: csexton/release-asset-action@master 56 | with: 57 | pattern: "bundles/*" 58 | github-token: ${{ secrets.GITHUB_TOKEN }} 59 | 60 | upload-pypi: 61 | runs-on: ubuntu-latest 62 | steps: 63 | - uses: actions/checkout@v2 64 | - name: Check For setup.py 65 | id: need-pypi 66 | run: | 67 | echo ::set-output name=setup-py::$( find . -wholename './setup.py' ) 68 | - name: Set up Python 69 | if: contains(steps.need-pypi.outputs.setup-py, 'setup.py') 70 | uses: actions/setup-python@v2 71 | with: 72 | python-version: '3.x' 73 | - name: Install dependencies 74 | if: contains(steps.need-pypi.outputs.setup-py, 'setup.py') 75 | run: | 76 | python -m pip install --upgrade pip 77 | pip install setuptools wheel twine 78 | - name: Build and publish 79 | if: contains(steps.need-pypi.outputs.setup-py, 'setup.py') 80 | env: 81 | TWINE_USERNAME: ${{ secrets.pypi_username }} 82 | TWINE_PASSWORD: ${{ secrets.pypi_password }} 83 | run: | 84 | python setup.py sdist 85 | twine upload dist/* 86 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2017 Scott Shawcroft, written for Adafruit Industries 2 | # 3 | # SPDX-License-Identifier: Unlicense 4 | 5 | *.mpy 6 | .idea 7 | __pycache__ 8 | _build 9 | *.pyc 10 | .env 11 | .python-version 12 | build*/ 13 | bundles 14 | *.DS_Store 15 | .eggs 16 | dist 17 | **/*.egg-info 18 | .vscode 19 | -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2020 Diego Elio Pettenò 2 | # 3 | # SPDX-License-Identifier: Unlicense 4 | 5 | repos: 6 | - repo: https://github.com/python/black 7 | rev: 20.8b1 8 | hooks: 9 | - id: black 10 | - repo: https://github.com/fsfe/reuse-tool 11 | rev: v0.13.0 12 | hooks: 13 | - id: reuse 14 | - repo: https://github.com/pre-commit/pre-commit-hooks 15 | rev: v2.3.0 16 | hooks: 17 | - id: check-yaml 18 | - id: end-of-file-fixer 19 | - id: trailing-whitespace 20 | - repo: https://github.com/pycqa/pylint 21 | rev: pylint-2.7.1 22 | hooks: 23 | - id: pylint 24 | name: pylint (library code) 25 | types: [python] 26 | exclude: "^(docs/|examples/|tests/|setup.py$)" 27 | - repo: local 28 | hooks: 29 | - id: pylint_examples 30 | name: pylint (examples code) 31 | description: Run pylint rules on "examples/*.py" files 32 | entry: /usr/bin/env bash -c 33 | args: ['([[ ! -d "examples" ]] || for example in $(find . -path "./examples/*.py"); do pylint --disable=missing-docstring,invalid-name $example; done)'] 34 | language: system 35 | - id: pylint_tests 36 | name: pylint (tests code) 37 | description: Run pylint rules on "tests/*.py" files 38 | entry: /usr/bin/env bash -c 39 | args: ['([[ ! -d "tests" ]] || for test in $(find . -path "./tests/*.py"); do pylint --disable=missing-docstring $test; done)'] 40 | language: system 41 | -------------------------------------------------------------------------------- /.pylintrc: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2017 Scott Shawcroft, written for Adafruit Industries 2 | # 3 | # SPDX-License-Identifier: Unlicense 4 | 5 | [MASTER] 6 | 7 | # A comma-separated list of package or module names from where C extensions may 8 | # be loaded. Extensions are loading into the active Python interpreter and may 9 | # run arbitrary code 10 | extension-pkg-whitelist= 11 | 12 | # Add files or directories to the ignore-list. They should be base names, not 13 | # paths. 14 | ignore=CVS 15 | 16 | # Add files or directories matching the regex patterns to the ignore-list. The 17 | # regex matches against base names, not paths. 18 | ignore-patterns= 19 | 20 | # Python code to execute, usually for sys.path manipulation such as 21 | # pygtk.require(). 22 | #init-hook= 23 | 24 | # Use multiple processes to speed up Pylint. 25 | # jobs=1 26 | jobs=2 27 | 28 | # List of plugins (as comma separated values of python modules names) to load, 29 | # usually to register additional checkers. 30 | load-plugins= 31 | 32 | # Pickle collected data for later comparisons. 33 | persistent=yes 34 | 35 | # Specify a configuration file. 36 | #rcfile= 37 | 38 | # Allow loading of arbitrary C extensions. Extensions are imported into the 39 | # active Python interpreter and may run arbitrary code. 40 | unsafe-load-any-extension=no 41 | 42 | 43 | [MESSAGES CONTROL] 44 | 45 | # Only show warnings with the listed confidence levels. Leave empty to show 46 | # all. Valid levels: HIGH, INFERENCE, INFERENCE_FAILURE, UNDEFINED 47 | confidence= 48 | 49 | # Disable the message, report, category or checker with the given id(s). You 50 | # can either give multiple identifiers separated by comma (,) or put this 51 | # option multiple times (only on the command line, not in the configuration 52 | # file where it should appear only once).You can also use "--disable=all" to 53 | # disable everything first and then reenable specific checks. For example, if 54 | # you want to run only the similarities checker, you can use "--disable=all 55 | # --enable=similarities". If you want to run only the classes checker, but have 56 | # no Warning level messages displayed, use"--disable=all --enable=classes 57 | # --disable=W" 58 | # disable=import-error,print-statement,parameter-unpacking,unpacking-in-except,old-raise-syntax,backtick,long-suffix,old-ne-operator,old-octal-literal,import-star-module-level,raw-checker-failed,bad-inline-option,locally-disabled,locally-enabled,file-ignored,suppressed-message,useless-suppression,deprecated-pragma,apply-builtin,basestring-builtin,buffer-builtin,cmp-builtin,coerce-builtin,execfile-builtin,file-builtin,long-builtin,raw_input-builtin,reduce-builtin,standarderror-builtin,unicode-builtin,xrange-builtin,coerce-method,delslice-method,getslice-method,setslice-method,no-absolute-import,old-division,dict-iter-method,dict-view-method,next-method-called,metaclass-assignment,indexing-exception,raising-string,reload-builtin,oct-method,hex-method,nonzero-method,cmp-method,input-builtin,round-builtin,intern-builtin,unichr-builtin,map-builtin-not-iterating,zip-builtin-not-iterating,range-builtin-not-iterating,filter-builtin-not-iterating,using-cmp-argument,eq-without-hash,div-method,idiv-method,rdiv-method,exception-message-attribute,invalid-str-codec,sys-max-int,bad-python3-import,deprecated-string-function,deprecated-str-translate-call 59 | disable=print-statement,parameter-unpacking,unpacking-in-except,old-raise-syntax,backtick,long-suffix,old-ne-operator,old-octal-literal,import-star-module-level,raw-checker-failed,bad-inline-option,locally-disabled,locally-enabled,file-ignored,suppressed-message,useless-suppression,deprecated-pragma,apply-builtin,basestring-builtin,buffer-builtin,cmp-builtin,coerce-builtin,execfile-builtin,file-builtin,long-builtin,raw_input-builtin,reduce-builtin,standarderror-builtin,unicode-builtin,xrange-builtin,coerce-method,delslice-method,getslice-method,setslice-method,no-absolute-import,old-division,dict-iter-method,dict-view-method,next-method-called,metaclass-assignment,indexing-exception,raising-string,reload-builtin,oct-method,hex-method,nonzero-method,cmp-method,input-builtin,round-builtin,intern-builtin,unichr-builtin,map-builtin-not-iterating,zip-builtin-not-iterating,range-builtin-not-iterating,filter-builtin-not-iterating,using-cmp-argument,eq-without-hash,div-method,idiv-method,rdiv-method,exception-message-attribute,invalid-str-codec,sys-max-int,bad-python3-import,deprecated-string-function,deprecated-str-translate-call,import-error,bad-continuation,pointless-string-statement 60 | 61 | # Enable the message, report, category or checker with the given id(s). You can 62 | # either give multiple identifier separated by comma (,) or put this option 63 | # multiple time (only on the command line, not in the configuration file where 64 | # it should appear only once). See also the "--disable" option for examples. 65 | enable= 66 | 67 | 68 | [REPORTS] 69 | 70 | # Python expression which should return a note less than 10 (10 is the highest 71 | # note). You have access to the variables errors warning, statement which 72 | # respectively contain the number of errors / warnings messages and the total 73 | # number of statements analyzed. This is used by the global evaluation report 74 | # (RP0004). 75 | evaluation=10.0 - ((float(5 * error + warning + refactor + convention) / statement) * 10) 76 | 77 | # Template used to display messages. This is a python new-style format string 78 | # used to format the message information. See doc for all details 79 | #msg-template= 80 | 81 | # Set the output format. Available formats are text, parseable, colorized, json 82 | # and msvs (visual studio).You can also give a reporter class, eg 83 | # mypackage.mymodule.MyReporterClass. 84 | output-format=text 85 | 86 | # Tells whether to display a full report or only the messages 87 | reports=no 88 | 89 | # Activate the evaluation score. 90 | score=yes 91 | 92 | 93 | [REFACTORING] 94 | 95 | # Maximum number of nested blocks for function / method body 96 | max-nested-blocks=5 97 | 98 | 99 | [LOGGING] 100 | 101 | # Logging modules to check that the string format arguments are in logging 102 | # function parameter format 103 | logging-modules=logging 104 | 105 | 106 | [SPELLING] 107 | 108 | # Spelling dictionary name. Available dictionaries: none. To make it working 109 | # install python-enchant package. 110 | spelling-dict= 111 | 112 | # List of comma separated words that should not be checked. 113 | spelling-ignore-words= 114 | 115 | # A path to a file that contains private dictionary; one word per line. 116 | spelling-private-dict-file= 117 | 118 | # Tells whether to store unknown words to indicated private dictionary in 119 | # --spelling-private-dict-file option instead of raising a message. 120 | spelling-store-unknown-words=no 121 | 122 | 123 | [MISCELLANEOUS] 124 | 125 | # List of note tags to take in consideration, separated by a comma. 126 | # notes=FIXME,XXX,TODO 127 | notes=FIXME,XXX 128 | 129 | 130 | [TYPECHECK] 131 | 132 | # List of decorators that produce context managers, such as 133 | # contextlib.contextmanager. Add to this list to register other decorators that 134 | # produce valid context managers. 135 | contextmanager-decorators=contextlib.contextmanager 136 | 137 | # List of members which are set dynamically and missed by pylint inference 138 | # system, and so shouldn't trigger E1101 when accessed. Python regular 139 | # expressions are accepted. 140 | generated-members= 141 | 142 | # Tells whether missing members accessed in mixin class should be ignored. A 143 | # mixin class is detected if its name ends with "mixin" (case insensitive). 144 | ignore-mixin-members=yes 145 | 146 | # This flag controls whether pylint should warn about no-member and similar 147 | # checks whenever an opaque object is returned when inferring. The inference 148 | # can return multiple potential results while evaluating a Python object, but 149 | # some branches might not be evaluated, which results in partial inference. In 150 | # that case, it might be useful to still emit no-member and other checks for 151 | # the rest of the inferred objects. 152 | ignore-on-opaque-inference=yes 153 | 154 | # List of class names for which member attributes should not be checked (useful 155 | # for classes with dynamically set attributes). This supports the use of 156 | # qualified names. 157 | ignored-classes=optparse.Values,thread._local,_thread._local 158 | 159 | # List of module names for which member attributes should not be checked 160 | # (useful for modules/projects where namespaces are manipulated during runtime 161 | # and thus existing member attributes cannot be deduced by static analysis. It 162 | # supports qualified module names, as well as Unix pattern matching. 163 | ignored-modules=board 164 | 165 | # Show a hint with possible names when a member name was not found. The aspect 166 | # of finding the hint is based on edit distance. 167 | missing-member-hint=yes 168 | 169 | # The minimum edit distance a name should have in order to be considered a 170 | # similar match for a missing member name. 171 | missing-member-hint-distance=1 172 | 173 | # The total number of similar names that should be taken in consideration when 174 | # showing a hint for a missing member. 175 | missing-member-max-choices=1 176 | 177 | 178 | [VARIABLES] 179 | 180 | # List of additional names supposed to be defined in builtins. Remember that 181 | # you should avoid to define new builtins when possible. 182 | additional-builtins= 183 | 184 | # Tells whether unused global variables should be treated as a violation. 185 | allow-global-unused-variables=yes 186 | 187 | # List of strings which can identify a callback function by name. A callback 188 | # name must start or end with one of those strings. 189 | callbacks=cb_,_cb 190 | 191 | # A regular expression matching the name of dummy variables (i.e. expectedly 192 | # not used). 193 | dummy-variables-rgx=_+$|(_[a-zA-Z0-9_]*[a-zA-Z0-9]+?$)|dummy|^ignored_|^unused_ 194 | 195 | # Argument names that match this expression will be ignored. Default to name 196 | # with leading underscore 197 | ignored-argument-names=_.*|^ignored_|^unused_ 198 | 199 | # Tells whether we should check for unused import in __init__ files. 200 | init-import=no 201 | 202 | # List of qualified module names which can have objects that can redefine 203 | # builtins. 204 | redefining-builtins-modules=six.moves,future.builtins 205 | 206 | 207 | [FORMAT] 208 | 209 | # Expected format of line ending, e.g. empty (any line ending), LF or CRLF. 210 | # expected-line-ending-format= 211 | expected-line-ending-format=LF 212 | 213 | # Regexp for a line that is allowed to be longer than the limit. 214 | ignore-long-lines=^\s*(# )??$ 215 | 216 | # Number of spaces of indent required inside a hanging or continued line. 217 | indent-after-paren=4 218 | 219 | # String used as indentation unit. This is usually " " (4 spaces) or "\t" (1 220 | # tab). 221 | indent-string=' ' 222 | 223 | # Maximum number of characters on a single line. 224 | max-line-length=100 225 | 226 | # Maximum number of lines in a module 227 | max-module-lines=1000 228 | 229 | # List of optional constructs for which whitespace checking is disabled. `dict- 230 | # separator` is used to allow tabulation in dicts, etc.: {1 : 1,\n222: 2}. 231 | # `trailing-comma` allows a space between comma and closing bracket: (a, ). 232 | # `empty-line` allows space-only lines. 233 | no-space-check=trailing-comma,dict-separator 234 | 235 | # Allow the body of a class to be on the same line as the declaration if body 236 | # contains single statement. 237 | single-line-class-stmt=no 238 | 239 | # Allow the body of an if to be on the same line as the test if there is no 240 | # else. 241 | single-line-if-stmt=no 242 | 243 | 244 | [SIMILARITIES] 245 | 246 | # Ignore comments when computing similarities. 247 | ignore-comments=yes 248 | 249 | # Ignore docstrings when computing similarities. 250 | ignore-docstrings=yes 251 | 252 | # Ignore imports when computing similarities. 253 | ignore-imports=yes 254 | 255 | # Minimum lines number of a similarity. 256 | min-similarity-lines=4 257 | 258 | 259 | [BASIC] 260 | 261 | # Naming hint for argument names 262 | argument-name-hint=(([a-z][a-z0-9_]{2,30})|(_[a-z0-9_]*))$ 263 | 264 | # Regular expression matching correct argument names 265 | argument-rgx=(([a-z][a-z0-9_]{2,30})|(_[a-z0-9_]*))$ 266 | 267 | # Naming hint for attribute names 268 | attr-name-hint=(([a-z][a-z0-9_]{2,30})|(_[a-z0-9_]*))$ 269 | 270 | # Regular expression matching correct attribute names 271 | attr-rgx=(([a-z][a-z0-9_]{2,30})|(_[a-z0-9_]*))$ 272 | 273 | # Bad variable names which should always be refused, separated by a comma 274 | bad-names=foo,bar,baz,toto,tutu,tata 275 | 276 | # Naming hint for class attribute names 277 | class-attribute-name-hint=([A-Za-z_][A-Za-z0-9_]{2,30}|(__.*__))$ 278 | 279 | # Regular expression matching correct class attribute names 280 | class-attribute-rgx=([A-Za-z_][A-Za-z0-9_]{2,30}|(__.*__))$ 281 | 282 | # Naming hint for class names 283 | # class-name-hint=[A-Z_][a-zA-Z0-9]+$ 284 | class-name-hint=[A-Z_][a-zA-Z0-9_]+$ 285 | 286 | # Regular expression matching correct class names 287 | # class-rgx=[A-Z_][a-zA-Z0-9]+$ 288 | class-rgx=[A-Z_][a-zA-Z0-9_]+$ 289 | 290 | # Naming hint for constant names 291 | const-name-hint=(([A-Z_][A-Z0-9_]*)|(__.*__))$ 292 | 293 | # Regular expression matching correct constant names 294 | const-rgx=(([A-Z_][A-Z0-9_]*)|(__.*__))$ 295 | 296 | # Minimum line length for functions/classes that require docstrings, shorter 297 | # ones are exempt. 298 | docstring-min-length=-1 299 | 300 | # Naming hint for function names 301 | function-name-hint=(([a-z][a-z0-9_]{2,30})|(_[a-z0-9_]*))$ 302 | 303 | # Regular expression matching correct function names 304 | function-rgx=(([a-z][a-z0-9_]{2,30})|(_[a-z0-9_]*))$ 305 | 306 | # Good variable names which should always be accepted, separated by a comma 307 | # good-names=i,j,k,ex,Run,_ 308 | good-names=r,g,b,w,i,j,k,n,x,y,z,ex,ok,Run,_ 309 | 310 | # Include a hint for the correct naming format with invalid-name 311 | include-naming-hint=no 312 | 313 | # Naming hint for inline iteration names 314 | inlinevar-name-hint=[A-Za-z_][A-Za-z0-9_]*$ 315 | 316 | # Regular expression matching correct inline iteration names 317 | inlinevar-rgx=[A-Za-z_][A-Za-z0-9_]*$ 318 | 319 | # Naming hint for method names 320 | method-name-hint=(([a-z][a-z0-9_]{2,30})|(_[a-z0-9_]*))$ 321 | 322 | # Regular expression matching correct method names 323 | method-rgx=(([a-z][a-z0-9_]{2,30})|(_[a-z0-9_]*))$ 324 | 325 | # Naming hint for module names 326 | module-name-hint=(([a-z_][a-z0-9_]*)|([A-Z][a-zA-Z0-9]+))$ 327 | 328 | # Regular expression matching correct module names 329 | module-rgx=(([a-z_][a-z0-9_]*)|([A-Z][a-zA-Z0-9]+))$ 330 | 331 | # Colon-delimited sets of names that determine each other's naming style when 332 | # the name regexes allow several styles. 333 | name-group= 334 | 335 | # Regular expression which should only match function or class names that do 336 | # not require a docstring. 337 | no-docstring-rgx=^_ 338 | 339 | # List of decorators that produce properties, such as abc.abstractproperty. Add 340 | # to this list to register other decorators that produce valid properties. 341 | property-classes=abc.abstractproperty 342 | 343 | # Naming hint for variable names 344 | variable-name-hint=(([a-z][a-z0-9_]{2,30})|(_[a-z0-9_]*))$ 345 | 346 | # Regular expression matching correct variable names 347 | variable-rgx=(([a-z][a-z0-9_]{2,30})|(_[a-z0-9_]*))$ 348 | 349 | 350 | [IMPORTS] 351 | 352 | # Allow wildcard imports from modules that define __all__. 353 | allow-wildcard-with-all=no 354 | 355 | # Analyse import fallback blocks. This can be used to support both Python 2 and 356 | # 3 compatible code, which means that the block might have code that exists 357 | # only in one or another interpreter, leading to false positives when analysed. 358 | analyse-fallback-blocks=no 359 | 360 | # Deprecated modules which should not be used, separated by a comma 361 | deprecated-modules=optparse,tkinter.tix 362 | 363 | # Create a graph of external dependencies in the given file (report RP0402 must 364 | # not be disabled) 365 | ext-import-graph= 366 | 367 | # Create a graph of every (i.e. internal and external) dependencies in the 368 | # given file (report RP0402 must not be disabled) 369 | import-graph= 370 | 371 | # Create a graph of internal dependencies in the given file (report RP0402 must 372 | # not be disabled) 373 | int-import-graph= 374 | 375 | # Force import order to recognize a module as part of the standard 376 | # compatibility libraries. 377 | known-standard-library= 378 | 379 | # Force import order to recognize a module as part of a third party library. 380 | known-third-party=enchant 381 | 382 | 383 | [CLASSES] 384 | 385 | # List of method names used to declare (i.e. assign) instance attributes. 386 | defining-attr-methods=__init__,__new__,setUp 387 | 388 | # List of member names, which should be excluded from the protected access 389 | # warning. 390 | exclude-protected=_asdict,_fields,_replace,_source,_make 391 | 392 | # List of valid names for the first argument in a class method. 393 | valid-classmethod-first-arg=cls 394 | 395 | # List of valid names for the first argument in a metaclass class method. 396 | valid-metaclass-classmethod-first-arg=mcs 397 | 398 | 399 | [DESIGN] 400 | 401 | # Maximum number of arguments for function / method 402 | max-args=5 403 | 404 | # Maximum number of attributes for a class (see R0902). 405 | # max-attributes=7 406 | max-attributes=11 407 | 408 | # Maximum number of boolean expressions in a if statement 409 | max-bool-expr=5 410 | 411 | # Maximum number of branch for function / method body 412 | max-branches=12 413 | 414 | # Maximum number of locals for function / method body 415 | max-locals=15 416 | 417 | # Maximum number of parents for a class (see R0901). 418 | max-parents=7 419 | 420 | # Maximum number of public methods for a class (see R0904). 421 | max-public-methods=20 422 | 423 | # Maximum number of return / yield for function / method body 424 | max-returns=6 425 | 426 | # Maximum number of statements in function / method body 427 | max-statements=50 428 | 429 | # Minimum number of public methods for a class (see R0903). 430 | min-public-methods=1 431 | 432 | 433 | [EXCEPTIONS] 434 | 435 | # Exceptions that will emit a warning when being caught. Defaults to 436 | # "Exception" 437 | overgeneral-exceptions=Exception 438 | -------------------------------------------------------------------------------- /.readthedocs.yml: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2017 Scott Shawcroft, written for Adafruit Industries 2 | # 3 | # SPDX-License-Identifier: Unlicense 4 | 5 | python: 6 | version: 3 7 | requirements_file: requirements.txt 8 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | 7 | # Adafruit Community Code of Conduct 8 | 9 | ## Our Pledge 10 | 11 | In the interest of fostering an open and welcoming environment, we as 12 | contributors and leaders pledge to making participation in our project and 13 | our community a harassment-free experience for everyone, regardless of age, body 14 | size, disability, ethnicity, gender identity and expression, level or type of 15 | experience, education, socio-economic status, nationality, personal appearance, 16 | race, religion, or sexual identity and orientation. 17 | 18 | ## Our Standards 19 | 20 | We are committed to providing a friendly, safe and welcoming environment for 21 | all. 22 | 23 | Examples of behavior that contributes to creating a positive environment 24 | include: 25 | 26 | * Be kind and courteous to others 27 | * Using welcoming and inclusive language 28 | * Being respectful of differing viewpoints and experiences 29 | * Collaborating with other community members 30 | * Gracefully accepting constructive criticism 31 | * Focusing on what is best for the community 32 | * Showing empathy towards other community members 33 | 34 | Examples of unacceptable behavior by participants include: 35 | 36 | * The use of sexualized language or imagery and sexual attention or advances 37 | * The use of inappropriate images, including in a community member's avatar 38 | * The use of inappropriate language, including in a community member's nickname 39 | * Any spamming, flaming, baiting or other attention-stealing behavior 40 | * Excessive or unwelcome helping; answering outside the scope of the question 41 | asked 42 | * Trolling, insulting/derogatory comments, and personal or political attacks 43 | * Promoting or spreading disinformation, lies, or conspiracy theories against 44 | a person, group, organisation, project, or community 45 | * Public or private harassment 46 | * Publishing others' private information, such as a physical or electronic 47 | address, without explicit permission 48 | * Other conduct which could reasonably be considered inappropriate 49 | 50 | The goal of the standards and moderation guidelines outlined here is to build 51 | and maintain a respectful community. We ask that you don’t just aim to be 52 | "technically unimpeachable", but rather try to be your best self. 53 | 54 | We value many things beyond technical expertise, including collaboration and 55 | supporting others within our community. Providing a positive experience for 56 | other community members can have a much more significant impact than simply 57 | providing the correct answer. 58 | 59 | ## Our Responsibilities 60 | 61 | Project leaders are responsible for clarifying the standards of acceptable 62 | behavior and are expected to take appropriate and fair corrective action in 63 | response to any instances of unacceptable behavior. 64 | 65 | Project leaders have the right and responsibility to remove, edit, or 66 | reject messages, comments, commits, code, issues, and other contributions 67 | that are not aligned to this Code of Conduct, or to ban temporarily or 68 | permanently any community member for other behaviors that they deem 69 | inappropriate, threatening, offensive, or harmful. 70 | 71 | ## Moderation 72 | 73 | Instances of behaviors that violate the Adafruit Community Code of Conduct 74 | may be reported by any member of the community. Community members are 75 | encouraged to report these situations, including situations they witness 76 | involving other community members. 77 | 78 | You may report in the following ways: 79 | 80 | In any situation, you may send an email to . 81 | 82 | On the Adafruit Discord, you may send an open message from any channel 83 | to all Community Moderators by tagging @community moderators. You may 84 | also send an open message from any channel, or a direct message to 85 | @kattni#1507, @tannewt#4653, @danh#1614, @cater#2442, 86 | @sommersoft#0222, @Mr. Certainly#0472 or @Andon#8175. 87 | 88 | Email and direct message reports will be kept confidential. 89 | 90 | In situations on Discord where the issue is particularly egregious, possibly 91 | illegal, requires immediate action, or violates the Discord terms of service, 92 | you should also report the message directly to Discord. 93 | 94 | These are the steps for upholding our community’s standards of conduct. 95 | 96 | 1. Any member of the community may report any situation that violates the 97 | Adafruit Community Code of Conduct. All reports will be reviewed and 98 | investigated. 99 | 2. If the behavior is an egregious violation, the community member who 100 | committed the violation may be banned immediately, without warning. 101 | 3. Otherwise, moderators will first respond to such behavior with a warning. 102 | 4. Moderators follow a soft "three strikes" policy - the community member may 103 | be given another chance, if they are receptive to the warning and change their 104 | behavior. 105 | 5. If the community member is unreceptive or unreasonable when warned by a 106 | moderator, or the warning goes unheeded, they may be banned for a first or 107 | second offense. Repeated offenses will result in the community member being 108 | banned. 109 | 110 | ## Scope 111 | 112 | This Code of Conduct and the enforcement policies listed above apply to all 113 | Adafruit Community venues. This includes but is not limited to any community 114 | spaces (both public and private), the entire Adafruit Discord server, and 115 | Adafruit GitHub repositories. Examples of Adafruit Community spaces include 116 | but are not limited to meet-ups, audio chats on the Adafruit Discord, or 117 | interaction at a conference. 118 | 119 | This Code of Conduct applies both within project spaces and in public spaces 120 | when an individual is representing the project or its community. As a community 121 | member, you are representing our community, and are expected to behave 122 | accordingly. 123 | 124 | ## Attribution 125 | 126 | This Code of Conduct is adapted from the [Contributor Covenant], 127 | version 1.4, available at 128 | , 129 | and the [Rust Code of Conduct](https://www.rust-lang.org/en-US/conduct.html). 130 | 131 | For other projects adopting the Adafruit Community Code of 132 | Conduct, please contact the maintainers of those projects for enforcement. 133 | If you wish to use this code of conduct for your own project, consider 134 | explicitly mentioning your moderation policy or making a copy with your 135 | own moderation policy so as to avoid confusion. 136 | 137 | [Contributor Covenant]: https://www.contributor-covenant.org 138 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2021 Nathan Byrd 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /LICENSES/CC-BY-4.0.txt: -------------------------------------------------------------------------------- 1 | Creative Commons Attribution 4.0 International Creative Commons Corporation 2 | ("Creative Commons") is not a law firm and does not provide legal services 3 | or legal advice. Distribution of Creative Commons public licenses does not 4 | create a lawyer-client or other relationship. Creative Commons makes its licenses 5 | and related information available on an "as-is" basis. Creative Commons gives 6 | no warranties regarding its licenses, any material licensed under their terms 7 | and conditions, or any related information. Creative Commons disclaims all 8 | liability for damages resulting from their use to the fullest extent possible. 9 | 10 | Using Creative Commons Public Licenses 11 | 12 | Creative Commons public licenses provide a standard set of terms and conditions 13 | that creators and other rights holders may use to share original works of 14 | authorship and other material subject to copyright and certain other rights 15 | specified in the public license below. The following considerations are for 16 | informational purposes only, are not exhaustive, and do not form part of our 17 | licenses. 18 | 19 | Considerations for licensors: Our public licenses are intended for use by 20 | those authorized to give the public permission to use material in ways otherwise 21 | restricted by copyright and certain other rights. Our licenses are irrevocable. 22 | Licensors should read and understand the terms and conditions of the license 23 | they choose before applying it. Licensors should also secure all rights necessary 24 | before applying our licenses so that the public can reuse the material as 25 | expected. Licensors should clearly mark any material not subject to the license. 26 | This includes other CC-licensed material, or material used under an exception 27 | or limitation to copyright. More considerations for licensors : wiki.creativecommons.org/Considerations_for_licensors 28 | 29 | Considerations for the public: By using one of our public licenses, a licensor 30 | grants the public permission to use the licensed material under specified 31 | terms and conditions. If the licensor's permission is not necessary for any 32 | reason–for example, because of any applicable exception or limitation to copyright–then 33 | that use is not regulated by the license. Our licenses grant only permissions 34 | under copyright and certain other rights that a licensor has authority to 35 | grant. Use of the licensed material may still be restricted for other reasons, 36 | including because others have copyright or other rights in the material. A 37 | licensor may make special requests, such as asking that all changes be marked 38 | or described. Although not required by our licenses, you are encouraged to 39 | respect those requests where reasonable. More considerations for the public 40 | : wiki.creativecommons.org/Considerations_for_licensees Creative Commons Attribution 41 | 4.0 International Public License 42 | 43 | By exercising the Licensed Rights (defined below), You accept and agree to 44 | be bound by the terms and conditions of this Creative Commons Attribution 45 | 4.0 International Public License ("Public License"). To the extent this Public 46 | License may be interpreted as a contract, You are granted the Licensed Rights 47 | in consideration of Your acceptance of these terms and conditions, and the 48 | Licensor grants You such rights in consideration of benefits the Licensor 49 | receives from making the Licensed Material available under these terms and 50 | conditions. 51 | 52 | Section 1 – Definitions. 53 | 54 | a. Adapted Material means material subject to Copyright and Similar Rights 55 | that is derived from or based upon the Licensed Material and in which the 56 | Licensed Material is translated, altered, arranged, transformed, or otherwise 57 | modified in a manner requiring permission under the Copyright and Similar 58 | Rights held by the Licensor. For purposes of this Public License, where the 59 | Licensed Material is a musical work, performance, or sound recording, Adapted 60 | Material is always produced where the Licensed Material is synched in timed 61 | relation with a moving image. 62 | 63 | b. Adapter's License means the license You apply to Your Copyright and Similar 64 | Rights in Your contributions to Adapted Material in accordance with the terms 65 | and conditions of this Public License. 66 | 67 | c. Copyright and Similar Rights means copyright and/or similar rights closely 68 | related to copyright including, without limitation, performance, broadcast, 69 | sound recording, and Sui Generis Database Rights, without regard to how the 70 | rights are labeled or categorized. For purposes of this Public License, the 71 | rights specified in Section 2(b)(1)-(2) are not Copyright and Similar Rights. 72 | 73 | d. Effective Technological Measures means those measures that, in the absence 74 | of proper authority, may not be circumvented under laws fulfilling obligations 75 | under Article 11 of the WIPO Copyright Treaty adopted on December 20, 1996, 76 | and/or similar international agreements. 77 | 78 | e. Exceptions and Limitations means fair use, fair dealing, and/or any other 79 | exception or limitation to Copyright and Similar Rights that applies to Your 80 | use of the Licensed Material. 81 | 82 | f. Licensed Material means the artistic or literary work, database, or other 83 | material to which the Licensor applied this Public License. 84 | 85 | g. Licensed Rights means the rights granted to You subject to the terms and 86 | conditions of this Public License, which are limited to all Copyright and 87 | Similar Rights that apply to Your use of the Licensed Material and that the 88 | Licensor has authority to license. 89 | 90 | h. Licensor means the individual(s) or entity(ies) granting rights under this 91 | Public License. 92 | 93 | i. Share means to provide material to the public by any means or process that 94 | requires permission under the Licensed Rights, such as reproduction, public 95 | display, public performance, distribution, dissemination, communication, or 96 | importation, and to make material available to the public including in ways 97 | that members of the public may access the material from a place and at a time 98 | individually chosen by them. 99 | 100 | j. Sui Generis Database Rights means rights other than copyright resulting 101 | from Directive 96/9/EC of the European Parliament and of the Council of 11 102 | March 1996 on the legal protection of databases, as amended and/or succeeded, 103 | as well as other essentially equivalent rights anywhere in the world. 104 | 105 | k. You means the individual or entity exercising the Licensed Rights under 106 | this Public License. Your has a corresponding meaning. 107 | 108 | Section 2 – Scope. 109 | 110 | a. License grant. 111 | 112 | 1. Subject to the terms and conditions of this Public License, the Licensor 113 | hereby grants You a worldwide, royalty-free, non-sublicensable, non-exclusive, 114 | irrevocable license to exercise the Licensed Rights in the Licensed Material 115 | to: 116 | 117 | A. reproduce and Share the Licensed Material, in whole or in part; and 118 | 119 | B. produce, reproduce, and Share Adapted Material. 120 | 121 | 2. Exceptions and Limitations. For the avoidance of doubt, where Exceptions 122 | and Limitations apply to Your use, this Public License does not apply, and 123 | You do not need to comply with its terms and conditions. 124 | 125 | 3. Term. The term of this Public License is specified in Section 6(a). 126 | 127 | 4. Media and formats; technical modifications allowed. The Licensor authorizes 128 | You to exercise the Licensed Rights in all media and formats whether now known 129 | or hereafter created, and to make technical modifications necessary to do 130 | so. The Licensor waives and/or agrees not to assert any right or authority 131 | to forbid You from making technical modifications necessary to exercise the 132 | Licensed Rights, including technical modifications necessary to circumvent 133 | Effective Technological Measures. For purposes of this Public License, simply 134 | making modifications authorized by this Section 2(a)(4) never produces Adapted 135 | Material. 136 | 137 | 5. Downstream recipients. 138 | 139 | A. Offer from the Licensor – Licensed Material. Every recipient of the Licensed 140 | Material automatically receives an offer from the Licensor to exercise the 141 | Licensed Rights under the terms and conditions of this Public License. 142 | 143 | B. No downstream restrictions. You may not offer or impose any additional 144 | or different terms or conditions on, or apply any Effective Technological 145 | Measures to, the Licensed Material if doing so restricts exercise of the Licensed 146 | Rights by any recipient of the Licensed Material. 147 | 148 | 6. No endorsement. Nothing in this Public License constitutes or may be construed 149 | as permission to assert or imply that You are, or that Your use of the Licensed 150 | Material is, connected with, or sponsored, endorsed, or granted official status 151 | by, the Licensor or others designated to receive attribution as provided in 152 | Section 3(a)(1)(A)(i). 153 | 154 | b. Other rights. 155 | 156 | 1. Moral rights, such as the right of integrity, are not licensed under this 157 | Public License, nor are publicity, privacy, and/or other similar personality 158 | rights; however, to the extent possible, the Licensor waives and/or agrees 159 | not to assert any such rights held by the Licensor to the limited extent necessary 160 | to allow You to exercise the Licensed Rights, but not otherwise. 161 | 162 | 2. Patent and trademark rights are not licensed under this Public License. 163 | 164 | 3. To the extent possible, the Licensor waives any right to collect royalties 165 | from You for the exercise of the Licensed Rights, whether directly or through 166 | a collecting society under any voluntary or waivable statutory or compulsory 167 | licensing scheme. In all other cases the Licensor expressly reserves any right 168 | to collect such royalties. 169 | 170 | Section 3 – License Conditions. 171 | 172 | Your exercise of the Licensed Rights is expressly made subject to the following 173 | conditions. 174 | 175 | a. Attribution. 176 | 177 | 1. If You Share the Licensed Material (including in modified form), You must: 178 | 179 | A. retain the following if it is supplied by the Licensor with the Licensed 180 | Material: 181 | 182 | i. identification of the creator(s) of the Licensed Material and any others 183 | designated to receive attribution, in any reasonable manner requested by the 184 | Licensor (including by pseudonym if designated); 185 | 186 | ii. a copyright notice; 187 | 188 | iii. a notice that refers to this Public License; 189 | 190 | iv. a notice that refers to the disclaimer of warranties; 191 | 192 | v. a URI or hyperlink to the Licensed Material to the extent reasonably practicable; 193 | 194 | B. indicate if You modified the Licensed Material and retain an indication 195 | of any previous modifications; and 196 | 197 | C. indicate the Licensed Material is licensed under this Public License, and 198 | include the text of, or the URI or hyperlink to, this Public License. 199 | 200 | 2. You may satisfy the conditions in Section 3(a)(1) in any reasonable manner 201 | based on the medium, means, and context in which You Share the Licensed Material. 202 | For example, it may be reasonable to satisfy the conditions by providing a 203 | URI or hyperlink to a resource that includes the required information. 204 | 205 | 3. If requested by the Licensor, You must remove any of the information required 206 | by Section 3(a)(1)(A) to the extent reasonably practicable. 207 | 208 | 4. If You Share Adapted Material You produce, the Adapter's License You apply 209 | must not prevent recipients of the Adapted Material from complying with this 210 | Public License. 211 | 212 | Section 4 – Sui Generis Database Rights. 213 | 214 | Where the Licensed Rights include Sui Generis Database Rights that apply to 215 | Your use of the Licensed Material: 216 | 217 | a. for the avoidance of doubt, Section 2(a)(1) grants You the right to extract, 218 | reuse, reproduce, and Share all or a substantial portion of the contents of 219 | the database; 220 | 221 | b. if You include all or a substantial portion of the database contents in 222 | a database in which You have Sui Generis Database Rights, then the database 223 | in which You have Sui Generis Database Rights (but not its individual contents) 224 | is Adapted Material; and 225 | 226 | c. You must comply with the conditions in Section 3(a) if You Share all or 227 | a substantial portion of the contents of the database. 228 | 229 | For the avoidance of doubt, this Section 4 supplements and does not replace 230 | Your obligations under this Public License where the Licensed Rights include 231 | other Copyright and Similar Rights. 232 | 233 | Section 5 – Disclaimer of Warranties and Limitation of Liability. 234 | 235 | a. Unless otherwise separately undertaken by the Licensor, to the extent possible, 236 | the Licensor offers the Licensed Material as-is and as-available, and makes 237 | no representations or warranties of any kind concerning the Licensed Material, 238 | whether express, implied, statutory, or other. This includes, without limitation, 239 | warranties of title, merchantability, fitness for a particular purpose, non-infringement, 240 | absence of latent or other defects, accuracy, or the presence or absence of 241 | errors, whether or not known or discoverable. Where disclaimers of warranties 242 | are not allowed in full or in part, this disclaimer may not apply to You. 243 | 244 | b. To the extent possible, in no event will the Licensor be liable to You 245 | on any legal theory (including, without limitation, negligence) or otherwise 246 | for any direct, special, indirect, incidental, consequential, punitive, exemplary, 247 | or other losses, costs, expenses, or damages arising out of this Public License 248 | or use of the Licensed Material, even if the Licensor has been advised of 249 | the possibility of such losses, costs, expenses, or damages. Where a limitation 250 | of liability is not allowed in full or in part, this limitation may not apply 251 | to You. 252 | 253 | c. The disclaimer of warranties and limitation of liability provided above 254 | shall be interpreted in a manner that, to the extent possible, most closely 255 | approximates an absolute disclaimer and waiver of all liability. 256 | 257 | Section 6 – Term and Termination. 258 | 259 | a. This Public License applies for the term of the Copyright and Similar Rights 260 | licensed here. However, if You fail to comply with this Public License, then 261 | Your rights under this Public License terminate automatically. 262 | 263 | b. Where Your right to use the Licensed Material has terminated under Section 264 | 6(a), it reinstates: 265 | 266 | 1. automatically as of the date the violation is cured, provided it is cured 267 | within 30 days of Your discovery of the violation; or 268 | 269 | 2. upon express reinstatement by the Licensor. 270 | 271 | c. For the avoidance of doubt, this Section 6(b) does not affect any right 272 | the Licensor may have to seek remedies for Your violations of this Public 273 | License. 274 | 275 | d. For the avoidance of doubt, the Licensor may also offer the Licensed Material 276 | under separate terms or conditions or stop distributing the Licensed Material 277 | at any time; however, doing so will not terminate this Public License. 278 | 279 | e. Sections 1, 5, 6, 7, and 8 survive termination of this Public License. 280 | 281 | Section 7 – Other Terms and Conditions. 282 | 283 | a. The Licensor shall not be bound by any additional or different terms or 284 | conditions communicated by You unless expressly agreed. 285 | 286 | b. Any arrangements, understandings, or agreements regarding the Licensed 287 | Material not stated herein are separate from and independent of the terms 288 | and conditions of this Public License. 289 | 290 | Section 8 – Interpretation. 291 | 292 | a. For the avoidance of doubt, this Public License does not, and shall not 293 | be interpreted to, reduce, limit, restrict, or impose conditions on any use 294 | of the Licensed Material that could lawfully be made without permission under 295 | this Public License. 296 | 297 | b. To the extent possible, if any provision of this Public License is deemed 298 | unenforceable, it shall be automatically reformed to the minimum extent necessary 299 | to make it enforceable. If the provision cannot be reformed, it shall be severed 300 | from this Public License without affecting the enforceability of the remaining 301 | terms and conditions. 302 | 303 | c. No term or condition of this Public License will be waived and no failure 304 | to comply consented to unless expressly agreed to by the Licensor. 305 | 306 | d. Nothing in this Public License constitutes or may be interpreted as a limitation 307 | upon, or waiver of, any privileges and immunities that apply to the Licensor 308 | or You, including from the legal processes of any jurisdiction or authority. 309 | 310 | Creative Commons is not a party to its public licenses. Notwithstanding, Creative 311 | Commons may elect to apply one of its public licenses to material it publishes 312 | and in those instances will be considered the "Licensor." The text of the 313 | Creative Commons public licenses is dedicated to the public domain under the 314 | CC0 Public Domain Dedication. Except for the limited purpose of indicating 315 | that material is shared under a Creative Commons public license or as otherwise 316 | permitted by the Creative Commons policies published at creativecommons.org/policies, 317 | Creative Commons does not authorize the use of the trademark "Creative Commons" 318 | or any other trademark or logo of Creative Commons without its prior written 319 | consent including, without limitation, in connection with any unauthorized 320 | modifications to any of its public licenses or any other arrangements, understandings, 321 | or agreements concerning use of licensed material. For the avoidance of doubt, 322 | this paragraph does not form part of the public licenses. 323 | 324 | Creative Commons may be contacted at creativecommons.org. 325 | -------------------------------------------------------------------------------- /LICENSES/MIT.txt: -------------------------------------------------------------------------------- 1 | MIT License Copyright (c) 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is furnished 8 | to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice (including the next 11 | paragraph) shall be included in all copies or substantial portions of the 12 | Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 16 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS 17 | OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 18 | WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF 19 | OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 20 | -------------------------------------------------------------------------------- /LICENSES/Unlicense.txt: -------------------------------------------------------------------------------- 1 | This is free and unencumbered software released into the public domain. 2 | 3 | Anyone is free to copy, modify, publish, use, compile, sell, or distribute 4 | this software, either in source code form or as a compiled binary, for any 5 | purpose, commercial or non-commercial, and by any means. 6 | 7 | In jurisdictions that recognize copyright laws, the author or authors of this 8 | software dedicate any and all copyright interest in the software to the public 9 | domain. We make this dedication for the benefit of the public at large and 10 | to the detriment of our heirs and successors. We intend this dedication to 11 | be an overt act of relinquishment in perpetuity of all present and future 12 | rights to this software under copyright law. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 16 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS 17 | BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 18 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH 19 | THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. For more information, 20 | please refer to 21 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | Introduction 2 | ============ 3 | 4 | 5 | .. image:: https://readthedocs.org/projects/circuitpython-schedule/badge/?version=latest 6 | :target: https://circuitpython-schedule.readthedocs.io/ 7 | :alt: Documentation Status 8 | 9 | 10 | .. image:: https://img.shields.io/discord/327254708534116352.svg 11 | :target: https://adafru.it/discord 12 | :alt: Discord 13 | 14 | 15 | .. image:: https://github.com/cognitivegears/CircuitPython_Schedule/workflows/Build%20CI/badge.svg 16 | :target: https://github.com/cognitivegears/CircuitPython_Schedule/actions 17 | :alt: Build Status 18 | 19 | 20 | .. image:: https://img.shields.io/badge/code%20style-black-000000.svg 21 | :target: https://github.com/psf/black 22 | :alt: Code Style: Black 23 | 24 | Reduced version of the schedule library for CircuitPython 25 | 26 | This library is a reduced version of the Python `schedule 27 | library `_. Credit to `Dan Bader 28 | (dbader) `_ for 29 | this excellent library. 30 | 31 | 32 | Dependencies 33 | ============= 34 | This driver depends on: 35 | 36 | * `Adafruit CircuitPython `_ 37 | * `Adafruit CircuitPython Datetime 38 | `_ 39 | 40 | Please ensure all dependencies are available on the CircuitPython filesystem. 41 | This is easily achieved by downloading 42 | `the Adafruit library and driver bundle `_ 43 | or individual libraries can be installed using 44 | `circup `_.Installing from PyPI 45 | ===================== 46 | 47 | 48 | On supported GNU/Linux systems like the Raspberry Pi, you can install the driver locally `from 49 | PyPI `_. 50 | To install for current user: 51 | 52 | .. code-block:: shell 53 | 54 | pip3 install circuitpython-schedule 55 | 56 | To install system-wide (this may be required in some cases): 57 | 58 | .. code-block:: shell 59 | 60 | sudo pip3 install circuitpython-schedule 61 | 62 | To install in a virtual environment in your current project: 63 | 64 | .. code-block:: shell 65 | 66 | mkdir project-name && cd project-name 67 | python3 -m venv .env 68 | source .env/bin/activate 69 | pip3 install circuitpython-schedule 70 | 71 | 72 | 73 | Usage Example 74 | ============= 75 | 76 | .. code-block:: python 77 | 78 | import time 79 | import circuitpython_schedule as schedule 80 | 81 | def greet(): 82 | print("Hello, world!") 83 | 84 | """ Note: pass the function name, like greet, not greet():""" 85 | schedule.every(10).seconds.do(greet) 86 | 87 | while True: 88 | schedule.run_pending() 89 | time.sleep(1) 90 | 91 | 92 | 93 | Contributing 94 | ============ 95 | 96 | Contributions are welcome! Please read our `Code of Conduct 97 | `_ 98 | before contributing to help this project stay welcoming. 99 | 100 | Documentation 101 | ============= 102 | 103 | Read the `module documentation`__. 104 | 105 | .. _moduledocumentation: https://circuitpython-schedule.readthedocs.io/ 106 | 107 | __ moduledocumentation_ 108 | -------------------------------------------------------------------------------- /README.rst.license: -------------------------------------------------------------------------------- 1 | SPDX-FileCopyrightText: 2017 Scott Shawcroft, written for Adafruit Industries 2 | SPDX-FileCopyrightText: Copyright (c) 2021 Nathan Byrd 3 | SPDX-License-Identifier: MIT 4 | -------------------------------------------------------------------------------- /circuitpython_schedule.py: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: Copyright (c) 2013-2021 Daniel Bader 2 | # SPDX-FileCopyrightText: Copyright (c) 2021 Nathan Byrd 3 | # 4 | # SPDX-License-Identifier: MIT 5 | 6 | """ 7 | Reduced version of the schedule library for CircuitPython 8 | 9 | Based on: 10 | 11 | schedule 12 | -------- 13 | Python job scheduling for humans. 14 | 15 | github.com/dbader/schedule 16 | 17 | An in-process scheduler for periodic jobs that uses the builder pattern 18 | for configuration. Schedule lets you run Python functions (or any other 19 | callable) periodically at pre-determined intervals using a simple, 20 | human-friendly syntax. 21 | 22 | Inspired by Addam Wiggins' article "Rethinking Cron" [1] and the 23 | "clockwork" Ruby module [2][3]. 24 | 25 | Features: 26 | - A simple to use API for scheduling jobs. 27 | - Very lightweight and no external dependencies. 28 | - Excellent test coverage. 29 | - Tested on Python 3.6, 3.7, 3.8, 3.9 30 | 31 | Usage: 32 | >>> import circuitpython_schedule as schedule 33 | >>> import time 34 | 35 | >>> def job(): 36 | >>> print("I'm working on stuff.") 37 | 38 | >>> schedule.every(10).minutes.do(job) 39 | >>> schedule.every(5).to(10).days.do(job) 40 | >>> schedule.every().hour.do(job) 41 | >>> schedule.every().day.at("10:30").do(job) 42 | 43 | >>> while True: 44 | >>> schedule.run_pending() 45 | >>> time.sleep(1) 46 | 47 | [1] https://adam.herokuapp.com/past/2010/4/13/rethinking_cron/ 48 | [2] https://github.com/Rykian/clockwork 49 | [3] https://adam.herokuapp.com/past/2010/6/30/replace_cron_with_clockwork/ 50 | """ 51 | import random 52 | import re 53 | import time 54 | import adafruit_datetime as datetime 55 | 56 | # Needed for compatibility with parent module - changing any of these 57 | # would change the module syntax. 58 | # pylint: disable=too-many-branches 59 | # pylint: disable=too-many-instance-attributes 60 | # pylint: disable=too-many-public-methods 61 | # pylint: disable=no-self-use 62 | 63 | 64 | class ScheduleError(Exception): 65 | """Base schedule exception""" 66 | 67 | 68 | class ScheduleValueError(ScheduleError): 69 | """Base schedule value error""" 70 | 71 | 72 | class IntervalError(ScheduleValueError): 73 | """An improper interval was used""" 74 | 75 | 76 | # pylint: disable=too-few-public-methods 77 | class CancelJob: 78 | """ 79 | Can be returned from a job to unschedule itself. 80 | """ 81 | 82 | 83 | class Scheduler: 84 | """ 85 | Objects instantiated by the :class:`Scheduler ` are 86 | factories to create jobs, keep record of scheduled jobs and 87 | handle their execution. 88 | """ 89 | 90 | def __init__(self) -> None: 91 | self.jobs = [] 92 | 93 | def run_pending(self) -> None: 94 | """ 95 | Run all jobs that are scheduled to run. 96 | 97 | Please note that it is *intended behavior that run_pending() 98 | does not run missed jobs*. For example, if you've registered a job 99 | that should run every minute and you only call run_pending() 100 | in one hour increments then your job won't be run 60 times in 101 | between but only once. 102 | """ 103 | runnable_jobs = (job for job in self.jobs if job.should_run) 104 | for job in sorted(runnable_jobs): 105 | self._run_job(job) 106 | 107 | def run_all(self, delay_seconds: int = 0) -> None: 108 | """ 109 | Run all jobs regardless if they are scheduled to run or not. 110 | 111 | A delay of 'delay' seconds is added between each job. This helps 112 | distribute system load generated by the jobs more evenly 113 | over time. 114 | 115 | :param delay_seconds: A delay added between every executed job 116 | """ 117 | for job in self.jobs[:]: 118 | self._run_job(job) 119 | time.sleep(delay_seconds) 120 | 121 | def get_jobs(self, tag=None): 122 | """ 123 | Gets scheduled jobs marked with the given tag, or all jobs 124 | if tag is omitted. 125 | 126 | :param tag: An identifier used to identify a subset of 127 | jobs to retrieve 128 | """ 129 | if tag is None: 130 | return self.jobs[:] 131 | # else 132 | return [job for job in self.jobs if tag in job.tags] 133 | 134 | def clear(self, tag=None) -> None: 135 | """ 136 | Deletes scheduled jobs marked with the given tag, or all jobs 137 | if tag is omitted. 138 | 139 | :param tag: An identifier used to identify a subset of 140 | jobs to delete 141 | """ 142 | if tag is None: 143 | del self.jobs[:] 144 | else: 145 | self.jobs[:] = (job for job in self.jobs if tag not in job.tags) 146 | 147 | def cancel_job(self, job: "Job") -> None: 148 | """ 149 | Delete a scheduled job. 150 | 151 | :param job: The job to be unscheduled 152 | """ 153 | try: 154 | self.jobs.remove(job) 155 | except ValueError: 156 | pass 157 | 158 | def every(self, interval: int = 1) -> "Job": 159 | """ 160 | Schedule a new periodic job. 161 | 162 | :param interval: A quantity of a certain time unit 163 | :return: An unconfigured :class:`Job ` 164 | """ 165 | job = Job(interval, self) 166 | return job 167 | 168 | def _run_job(self, job: "Job") -> None: 169 | ret = job.run() 170 | if isinstance(ret, CancelJob) or ret is CancelJob: 171 | self.cancel_job(job) 172 | 173 | @property 174 | def next_run(self): 175 | """ 176 | Datetime when the next job should run. 177 | 178 | :return: A :class:`~datetime.datetime` object 179 | or None if no jobs scheduled 180 | """ 181 | if not self.jobs: 182 | return None 183 | return min(self.jobs).next_run 184 | 185 | @property 186 | def idle_seconds(self): 187 | """ 188 | :return: Number of seconds until 189 | :meth:`next_run ` 190 | or None if no jobs are scheduled 191 | """ 192 | if not self.next_run: 193 | return None 194 | return (self.next_run - datetime.datetime.now()).total_seconds() 195 | 196 | 197 | class Job: 198 | """ 199 | A periodic job as used by :class:`Scheduler`. 200 | 201 | :param interval: A quantity of a certain time unit 202 | :param scheduler: The :class:`Scheduler ` instance that 203 | this job will register itself with once it has 204 | been fully configured in :meth:`Job.do()`. 205 | 206 | Every job runs at a given fixed time interval that is defined by: 207 | 208 | * a :meth:`time unit ` 209 | * a quantity of time units defined by interval 210 | 211 | A job is usually created and returned by :meth:`Scheduler.every` 212 | method, which also defines its interval. 213 | """ 214 | 215 | def __init__(self, interval: int, scheduler: Scheduler = None): 216 | self.interval = interval # pause interval * unit between runs 217 | self.latest = None # upper limit to the interval 218 | self.job_func = None # the job job_func to run 219 | 220 | # time units, e.g. 'minutes', 'hours', ... 221 | self.unit = None 222 | 223 | # optional time at which this job runs 224 | self.at_time = None 225 | 226 | # datetime of the last runno-self-use 227 | self.last_run = None 228 | 229 | # datetime of the next run 230 | self.next_run = None 231 | 232 | # timedelta between runs, only valid for 233 | self.period = None 234 | 235 | # Specific day of the week to start on 236 | self.start_day = None 237 | 238 | # optional time of final run 239 | self.cancel_after = None 240 | 241 | self.tags = set() # unique set of tags for the job 242 | self.scheduler = scheduler # scheduler to register with 243 | 244 | def __lt__(self, other) -> bool: 245 | """ 246 | PeriodicJobs are sortable based on the scheduled time they 247 | run next. 248 | """ 249 | return self.next_run < other.next_run 250 | 251 | def __str__(self) -> str: 252 | if hasattr(self.job_func, "__name__"): 253 | job_func_name = self.job_func.__name__ # type: ignore 254 | else: 255 | job_func_name = repr(self.job_func) 256 | 257 | return ("Job(interval={}, unit={}, do={}, args={}, kwargs={})").format( 258 | self.interval, 259 | self.unit, 260 | job_func_name, 261 | "()", 262 | "{}", 263 | ) 264 | 265 | def __repr__(self): 266 | def format_time(time_value): 267 | return time_value.isoformat() if time_value else "[never]" 268 | 269 | timestats = "(last run: %s, next run: %s)" % ( 270 | format_time(self.last_run), 271 | format_time(self.next_run), 272 | ) 273 | 274 | if hasattr(self.job_func, "__name__"): 275 | job_func_name = self.job_func.__name__ 276 | else: 277 | job_func_name = repr(self.job_func) 278 | args = [] 279 | kwargs = [] 280 | call_repr = job_func_name + "(" + ", ".join(args + kwargs) + ")" 281 | 282 | if self.at_time is not None: 283 | return "Every %s %s at %s do %s %s" % ( 284 | self.interval, 285 | self.unit[:-1] if self.interval == 1 else self.unit, 286 | self.at_time, 287 | call_repr, 288 | timestats, 289 | ) 290 | # else: 291 | fmt = ( 292 | "Every %(interval)s " 293 | + ("to %(latest)s " if self.latest is not None else "") 294 | + "%(unit)s do %(call_repr)s %(timestats)s" 295 | ) 296 | 297 | return fmt % dict( 298 | interval=self.interval, 299 | latest=self.latest, 300 | unit=(self.unit[:-1] if self.interval == 1 else self.unit), 301 | call_repr=call_repr, 302 | timestats=timestats, 303 | ) 304 | 305 | @property 306 | def second(self): 307 | """Specify the type of an interval as a single second. 308 | Only works if the interval is set to 1 309 | 310 | Raises: 311 | IntervalError: Thrown if the interval is not 1 312 | 313 | Returns: 314 | circuitpython_schedule: Returns self 315 | """ 316 | if self.interval != 1: 317 | raise IntervalError("Use seconds instead of second") 318 | return self.seconds 319 | 320 | @property 321 | def seconds(self): 322 | """Specify the type of an interval as seconds. 323 | 324 | Returns: 325 | circuitpython_schedule: Returns self 326 | """ 327 | self.unit = "seconds" 328 | return self 329 | 330 | @property 331 | def minute(self): 332 | """Specify the type of an interval as minute. 333 | Only works if the interval is set to 1 334 | 335 | Raises: 336 | IntervalError: Thrown if the interval is not 1 337 | 338 | Returns: 339 | circuitpython_schedule: Returns self 340 | """ 341 | if self.interval != 1: 342 | raise IntervalError("Use minutes instead of minute") 343 | return self.minutes 344 | 345 | @property 346 | def minutes(self): 347 | """Specify the type of an interval as minutes. 348 | 349 | Returns: 350 | circuitpython_schedule: Returns self 351 | """ 352 | self.unit = "minutes" 353 | return self 354 | 355 | @property 356 | def hour(self): 357 | """Specify the type of an interval as hour. 358 | Only works if the interval is set to 1 359 | 360 | Raises: 361 | IntervalError: Thrown if the interval is not 1 362 | 363 | Returns: 364 | circuitpython_schedule: Returns self 365 | """ 366 | if self.interval != 1: 367 | raise IntervalError("Use hours instead of hour") 368 | return self.hours 369 | 370 | @property 371 | def hours(self): 372 | """Specify the type of an interval as hours. 373 | 374 | Returns: 375 | circuitpython_schedule: Returns self 376 | """ 377 | self.unit = "hours" 378 | return self 379 | 380 | @property 381 | def day(self): 382 | """Specify the type of an interval as day. 383 | Only works if the interval is set to 1 384 | 385 | Raises: 386 | IntervalError: Thrown if the interval is not 1 387 | 388 | Returns: 389 | circuitpython_schedule: Returns self 390 | """ 391 | if self.interval != 1: 392 | raise IntervalError("Use days instead of day") 393 | return self.days 394 | 395 | @property 396 | def days(self): 397 | """Specify the type of an interval as days. 398 | 399 | Returns: 400 | circuitpython_schedule: Returns self 401 | """ 402 | self.unit = "days" 403 | return self 404 | 405 | @property 406 | def week(self): 407 | """Specify the type of an interval as week 408 | Only works if the interval is set to 1 409 | 410 | Raises: 411 | IntervalError: Thrown if the interval is not 1 412 | 413 | Returns: 414 | circuitpython_schedule: self 415 | """ 416 | if self.interval != 1: 417 | raise IntervalError("Use weeks instead of week") 418 | return self.weeks 419 | 420 | @property 421 | def weeks(self): 422 | """Specify the type of an interval as weeks 423 | 424 | Returns: 425 | circuitpython_schedule: Returns self 426 | """ 427 | self.unit = "weeks" 428 | return self 429 | 430 | @property 431 | def monday(self): 432 | """Set the target day of the week as Monday. 433 | Only works for weekly jobs. 434 | 435 | Raises: 436 | IntervalError: Thrown if interval is not weekly 437 | 438 | Returns: 439 | circuitpython_schedule: Returns self 440 | """ 441 | if self.interval != 1: 442 | raise IntervalError( 443 | "Scheduling .monday() jobs is only allowed for weekly jobs. " 444 | "Using .monday() on a job scheduled to run every 2 or more weeks " 445 | "is not supported." 446 | ) 447 | self.start_day = "monday" 448 | return self.weeks 449 | 450 | @property 451 | def tuesday(self): 452 | """Set the target day of the week as Tuesday. 453 | Only works for weekly jobs. 454 | 455 | Raises: 456 | IntervalError: Thrown if interval is not weekly 457 | 458 | Returns: 459 | circuitpython_schedule: Returns self 460 | """ 461 | if self.interval != 1: 462 | raise IntervalError( 463 | "Scheduling .tuesday() jobs is only allowed for weekly jobs. " 464 | "Using .tuesday() on a job scheduled to run every 2 or more weeks " 465 | "is not supported." 466 | ) 467 | self.start_day = "tuesday" 468 | return self.weeks 469 | 470 | @property 471 | def wednesday(self): 472 | """Set the target day of the week as Wednesday 473 | Only works for weekly jobs. 474 | 475 | Raises: 476 | IntervalError: Thrown if interval is not weekly 477 | 478 | Returns: 479 | circuitpython_schedule: Returns self 480 | """ 481 | if self.interval != 1: 482 | raise IntervalError( 483 | "Scheduling .wednesday() jobs is only allowed for weekly jobs. " 484 | "Using .wednesday() on a job scheduled to run every 2 or more weeks " 485 | "is not supported." 486 | ) 487 | self.start_day = "wednesday" 488 | return self.weeks 489 | 490 | @property 491 | def thursday(self): 492 | """Set the target day of the week as Thursday. 493 | Only works for weekly jobs. 494 | 495 | Raises: 496 | IntervalError: Thrown if interval is not weekly 497 | 498 | Returns: 499 | circuitpython_schedule: Returns self 500 | """ 501 | if self.interval != 1: 502 | raise IntervalError( 503 | "Scheduling .thursday() jobs is only allowed for weekly jobs. " 504 | "Using .thursday() on a job scheduled to run every 2 or more weeks " 505 | "is not supported." 506 | ) 507 | self.start_day = "thursday" 508 | return self.weeks 509 | 510 | @property 511 | def friday(self): 512 | """Set the target day of the week as Friday. 513 | Only works for weekly jobs. 514 | 515 | Raises: 516 | IntervalError: Thrown if interval is not weekly 517 | 518 | Returns: 519 | circuitpython_schedule: Returns self 520 | """ 521 | if self.interval != 1: 522 | raise IntervalError( 523 | "Scheduling .friday() jobs is only allowed for weekly jobs. " 524 | "Using .friday() on a job scheduled to run every 2 or more weeks " 525 | "is not supported." 526 | ) 527 | self.start_day = "friday" 528 | return self.weeks 529 | 530 | @property 531 | def saturday(self): 532 | """Set the target day of the week as Saturday. 533 | Only works for weekly jobs. 534 | 535 | Raises: 536 | IntervalError: Thrown if interval is not weekly 537 | 538 | Returns: 539 | circuitpython_schedule: Returns self 540 | """ 541 | if self.interval != 1: 542 | raise IntervalError( 543 | "Scheduling .saturday() jobs is only allowed for weekly jobs. " 544 | "Using .saturday() on a job scheduled to run every 2 or more weeks " 545 | "is not supported." 546 | ) 547 | self.start_day = "saturday" 548 | return self.weeks 549 | 550 | @property 551 | def sunday(self): 552 | """Set the target day of the week as Sunday. 553 | Only works for weekly jobs. 554 | 555 | Raises: 556 | IntervalError: Thrown if interval is not weekly 557 | 558 | Returns: 559 | circuitpyhon_schedule: Returns self 560 | """ 561 | if self.interval != 1: 562 | raise IntervalError( 563 | "Scheduling .sunday() jobs is only allowed for weekly jobs. " 564 | "Using .sunday() on a job scheduled to run every 2 or more weeks " 565 | "is not supported." 566 | ) 567 | self.start_day = "sunday" 568 | return self.weeks 569 | 570 | def tag(self, *tags): 571 | """ 572 | Tags the job with one or more unique identifiers. 573 | 574 | Tags must be hashable. Duplicate tags are discarded. 575 | 576 | :param tags: A unique list of ``Hashable`` tags. 577 | :return: The invoked job instance 578 | """ 579 | self.tags.update(tags) 580 | return self 581 | 582 | # pylint: disable=invalid-name 583 | def at(self, time_str): 584 | """ 585 | Specify a particular time that the job should be run at. 586 | 587 | :param time_str: A string in one of the following formats: 588 | 589 | - For daily jobs -> 'HH:MM:SS' or 'HH:MM' 590 | - For hourly jobs -> 'MM:SS' or ':MM' 591 | - For minute jobs -> ':SS' 592 | 593 | The format must make sense given how often the job is 594 | repeating; for example, a job that repeats every minute 595 | should not be given a string in the form 'HH:MM:SS'. The 596 | difference between ':MM' and ':SS' is inferred from the 597 | selected time-unit (e.g. every().hour.at(':30') vs. 598 | every().minute.at(':30')). 599 | 600 | :return: The invoked job instance 601 | """ 602 | if self.unit not in ("days", "hours", "minutes") and not self.start_day: 603 | raise ScheduleValueError( 604 | "Invalid unit (valid units are `days`, `hours`, and `minutes`)" 605 | ) 606 | if not isinstance(time_str, str): 607 | raise TypeError("at() should be passed a string") 608 | if self.unit == "days" or self.start_day: 609 | if not re.match(r"^([0-2]\d:)?[0-5]\d:[0-5]\d$", time_str): 610 | raise ScheduleValueError( 611 | "Invalid time format for a daily job (valid format is HH:MM(:SS)?)" 612 | ) 613 | if self.unit == "hours": 614 | if not re.match(r"^([0-5]\d)?:[0-5]\d$", time_str): 615 | raise ScheduleValueError( 616 | "Invalid time format for an hourly job (valid format is (MM)?:SS)" 617 | ) 618 | 619 | if self.unit == "minutes": 620 | if not re.match(r"^:[0-5]\d$", time_str): 621 | raise ScheduleValueError( 622 | "Invalid time format for a minutely job (valid format is :SS)" 623 | ) 624 | time_values = time_str.split(":") 625 | if len(time_values) == 3: 626 | hour, minute, second = time_values 627 | elif len(time_values) == 2 and self.unit == "minutes": 628 | hour = 0 629 | minute = 0 630 | _, second = time_values 631 | elif len(time_values) == 2 and self.unit == "hours" and time_values[0]: 632 | hour = 0 633 | minute, second = time_values 634 | else: 635 | hour, minute = time_values 636 | second = 0 637 | if self.unit == "days" or self.start_day: 638 | hour = int(hour) 639 | if not 0 <= hour <= 23: 640 | raise ScheduleValueError( 641 | "Invalid number of hours ({} is not between 0 and 23)" 642 | ) 643 | elif self.unit == "hours": 644 | hour = 0 645 | elif self.unit == "minutes": 646 | hour = 0 647 | minute = 0 648 | minute = int(minute) 649 | second = int(second) 650 | self.at_time = datetime.time(hour, minute, second) 651 | return self 652 | 653 | def to(self, latest: int): 654 | """ 655 | Schedule the job to run at an irregular (randomized) interval. 656 | 657 | The job's interval will randomly vary from the value given 658 | to 'every' to 'latest'. The range defined is inclusive on 659 | both ends. For example, 'every(A).to(B).seconds' executes 660 | the job function every N seconds such that A <= N <= B. 661 | 662 | :param latest: Maximum interval between randomized job runs 663 | :return: The invoked job instance 664 | """ 665 | self.latest = latest 666 | return self 667 | 668 | def until(self, until_time): 669 | """ 670 | Schedule job to run until the specified moment. 671 | 672 | The job is canceled whenever the next run is calculated and it turns out the 673 | next run is after the until_time. The job is also canceled right before it runs, 674 | if the current time is after until_time. This latter case can happen when the 675 | the job was scheduled to run before until_time, but runs after until_time. 676 | 677 | If until_time is a moment in the past, ScheduleValueError is thrown. 678 | 679 | :param until_time: A moment in the future representing the latest time a job can 680 | be run. If only a time is supplied, the date is set to today. 681 | The following formats are accepted: 682 | 683 | - datetime.datetime 684 | - datetime.timedelta 685 | - datetime.time 686 | - String in one of the following formats: "%Y-%m-%d %H:%M:%S", 687 | "%Y-%m-%d %H:%M", "%Y-%m-%d", "%H:%M:%S", "%H:%M" 688 | as defined by strptime() behaviour. If an invalid string format is passed, 689 | ScheduleValueError is thrown. 690 | 691 | :return: The invoked job instance 692 | """ 693 | 694 | if isinstance(until_time, datetime.datetime): 695 | self.cancel_after = until_time 696 | elif isinstance(until_time, datetime.timedelta): 697 | self.cancel_after = datetime.datetime.now() + until_time 698 | elif isinstance(until_time, datetime.time): 699 | self.cancel_after = datetime.datetime.combine( 700 | datetime.datetime.now(), until_time 701 | ) 702 | elif isinstance(until_time, str): 703 | raise ScheduleValueError("String format not supported") 704 | else: 705 | raise TypeError( 706 | "until() takes a string, datetime.datetime, datetime.timedelta, " 707 | "datetime.time parameter" 708 | ) 709 | if self.cancel_after < datetime.datetime.now(): 710 | raise ScheduleValueError( 711 | "Cannot schedule a job to run until a time in the past" 712 | ) 713 | return self 714 | 715 | # pylint: disable=unused-argument 716 | def do(self, job_func, *args, **kwargs): 717 | """ 718 | Specifies the job_func that should be called every time the 719 | job runs. 720 | 721 | Any additional arguments are passed on to job_func when 722 | the job runs. 723 | 724 | NOTE: This version does not support arguments 725 | 726 | :param job_func: The function to be scheduled. This should be a callable 727 | function name, not a call. i.e. "greet", not "greet()" 728 | :return: The invoked job instance 729 | """ 730 | if not callable(job_func): 731 | raise ScheduleValueError( 732 | "Job function is not callable. " 733 | "Did you pass 'job_func()' instead of 'job_func'?" 734 | ) 735 | 736 | self.job_func = job_func 737 | self._schedule_next_run() 738 | if self.scheduler is None: 739 | raise ScheduleError( 740 | "Unable to a add job to schedule. " 741 | "Job is not associated with an scheduler" 742 | ) 743 | self.scheduler.jobs.append(self) 744 | return self 745 | 746 | @property 747 | def should_run(self) -> bool: 748 | """ 749 | :return: ``True`` if the job should be run now. 750 | """ 751 | assert self.next_run is not None, "must run _schedule_next_run before" 752 | return datetime.datetime.now() >= self.next_run 753 | 754 | def run(self): 755 | """ 756 | Run the job and immediately reschedule it. 757 | If the job's deadline is reached (configured using .until()), the job is not 758 | run and CancelJob is returned immediately. If the next scheduled run exceeds 759 | the job's deadline, CancelJob is returned after the execution. In this latter 760 | case CancelJob takes priority over any other returned value. 761 | 762 | :return: The return value returned by the 'job_func', or CancelJob if the job's 763 | deadline is reached. 764 | 765 | """ 766 | if self._is_overdue(datetime.datetime.now()): 767 | return CancelJob 768 | 769 | ret = self.job_func() 770 | self.last_run = datetime.datetime.now() 771 | self._schedule_next_run() 772 | 773 | if self._is_overdue(self.next_run): 774 | return CancelJob 775 | return ret 776 | 777 | def _schedule_next_run(self) -> None: 778 | """ 779 | Compute the instant when this job should run next. 780 | """ 781 | if self.unit not in ("seconds", "minutes", "hours", "days", "weeks"): 782 | raise ScheduleValueError( 783 | "Invalid unit (valid units are `seconds`, `minutes`, `hours`, " 784 | "`days`, and `weeks`)" 785 | ) 786 | 787 | if self.latest is not None: 788 | if not self.latest >= self.interval: 789 | raise ScheduleError("`latest` is greater than `interval`") 790 | interval = random.randint(self.interval, self.latest) 791 | else: 792 | interval = self.interval 793 | 794 | self.period = datetime.timedelta(**{self.unit: interval}) 795 | self.next_run = datetime.datetime.now() + self.period 796 | if self.start_day is not None: 797 | if self.unit != "weeks": 798 | raise ScheduleValueError("`unit` should be 'weeks'") 799 | weekdays = ( 800 | "monday", 801 | "tuesday", 802 | "wednesday", 803 | "thursday", 804 | "friday", 805 | "saturday", 806 | "sunday", 807 | ) 808 | if self.start_day not in weekdays: 809 | raise ScheduleValueError( 810 | "Invalid start day (valid start days are {})".format(weekdays) 811 | ) 812 | weekday = weekdays.index(self.start_day) 813 | days_ahead = weekday - self.next_run.weekday() 814 | if days_ahead <= 0: # Target day already happened this week 815 | days_ahead += 7 816 | self.next_run += datetime.timedelta(days_ahead) - self.period 817 | if self.at_time is not None: 818 | if self.unit not in ("days", "hours", "minutes") and self.start_day is None: 819 | raise ScheduleValueError("Invalid unit without specifying start day") 820 | kwargs = {"second": self.at_time.second, "microsecond": 0} 821 | if self.unit == "days" or self.start_day is not None: 822 | kwargs["hour"] = self.at_time.hour 823 | if self.unit in ["days", "hours"] or self.start_day is not None: 824 | kwargs["minute"] = self.at_time.minute 825 | self.next_run = self.next_run.replace(**kwargs) # type: ignore 826 | # Make sure we run at the specified time *today* (or *this hour*) 827 | # as well. This accounts for when a job takes so long it finished 828 | # in the next period. 829 | if not self.last_run or (self.next_run - self.last_run) > self.period: 830 | now = datetime.datetime.now() 831 | if ( 832 | self.unit == "days" 833 | and self.at_time > now.time() 834 | and self.interval == 1 835 | ): 836 | self.next_run = self.next_run - datetime.timedelta(days=1) 837 | elif self.unit == "hours" and ( 838 | self.at_time.minute > now.minute 839 | or ( 840 | self.at_time.minute == now.minute 841 | and self.at_time.second > now.second 842 | ) 843 | ): 844 | self.next_run = self.next_run - datetime.timedelta(hours=1) 845 | elif self.unit == "minutes" and self.at_time.second > now.second: 846 | self.next_run = self.next_run - datetime.timedelta(minutes=1) 847 | if self.start_day is not None and self.at_time is not None: 848 | # Let's see if we will still make that time we specified today 849 | if (self.next_run - datetime.datetime.now()).days >= 7: 850 | self.next_run -= self.period 851 | 852 | def _is_overdue(self, when: datetime.datetime): 853 | return self.cancel_after is not None and when > self.cancel_after 854 | 855 | 856 | # The following methods are shortcuts for not having to 857 | # create a Scheduler instance: 858 | 859 | #: Default :class:`Scheduler ` object 860 | default_scheduler = Scheduler() 861 | 862 | #: Default :class:`Jobs ` list 863 | jobs = default_scheduler.jobs # todo: should this be a copy, e.g. jobs()? 864 | 865 | 866 | def every(interval: int = 1) -> Job: 867 | """Calls :meth:`every ` on the 868 | :data:`default scheduler instance `. 869 | """ 870 | return default_scheduler.every(interval) 871 | 872 | 873 | def run_pending() -> None: 874 | """Calls :meth:`run_pending ` on the 875 | :data:`default scheduler instance `. 876 | """ 877 | default_scheduler.run_pending() 878 | 879 | 880 | def run_all(delay_seconds: int = 0) -> None: 881 | """Calls :meth:`run_all ` on the 882 | :data:`default scheduler instance `. 883 | """ 884 | default_scheduler.run_all(delay_seconds=delay_seconds) 885 | 886 | 887 | def get_jobs(tag=None): 888 | """Calls :meth:`get_jobs ` on the 889 | :data:`default scheduler instance `. 890 | """ 891 | return default_scheduler.get_jobs(tag) 892 | 893 | 894 | def clear(tag=None) -> None: 895 | """Calls :meth:`clear ` on the 896 | :data:`default scheduler instance `. 897 | """ 898 | default_scheduler.clear(tag) 899 | 900 | 901 | def cancel_job(job: Job) -> None: 902 | """Calls :meth:`cancel_job ` on the 903 | :data:`default scheduler instance `. 904 | """ 905 | default_scheduler.cancel_job(job) 906 | 907 | 908 | def next_run(): 909 | """Calls :meth:`next_run ` on the 910 | :data:`default scheduler instance `. 911 | """ 912 | return default_scheduler.next_run 913 | 914 | 915 | def idle_seconds(): 916 | """Calls :meth:`idle_seconds ` on the 917 | :data:`default scheduler instance `. 918 | """ 919 | return default_scheduler.idle_seconds 920 | 921 | 922 | def repeat(job, *args, **kwargs): 923 | """ 924 | Decorator to schedule a new periodic job. 925 | 926 | Any additional arguments are passed on to the decorated function 927 | when the job runs. 928 | 929 | :param job: a :class:`Jobs ` 930 | """ 931 | 932 | def _schedule_decorator(decorated_function): 933 | job.do(decorated_function, *args, **kwargs) 934 | return decorated_function 935 | 936 | return _schedule_decorator 937 | -------------------------------------------------------------------------------- /docs/_static/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cognitivegears/CircuitPython_Schedule/9f8b91e10dbd700bf8debf0fde622c97276c3c46/docs/_static/favicon.ico -------------------------------------------------------------------------------- /docs/_static/favicon.ico.license: -------------------------------------------------------------------------------- 1 | SPDX-FileCopyrightText: 2018 Phillip Torrone for Adafruit Industries 2 | 3 | SPDX-License-Identifier: CC-BY-4.0 4 | -------------------------------------------------------------------------------- /docs/api.rst: -------------------------------------------------------------------------------- 1 | 2 | .. If you created a package, create one automodule per module in the package. 3 | 4 | .. If your library file(s) are nested in a directory (e.g. /adafruit_foo/foo.py) 5 | .. use this format as the module name: "adafruit_foo.foo" 6 | 7 | .. automodule:: circuitpython_schedule 8 | :members: 9 | -------------------------------------------------------------------------------- /docs/api.rst.license: -------------------------------------------------------------------------------- 1 | SPDX-FileCopyrightText: 2017 Scott Shawcroft, written for Adafruit Industries 2 | SPDX-FileCopyrightText: Copyright (c) 2021 Nathan Byrd 3 | 4 | SPDX-License-Identifier: MIT 5 | -------------------------------------------------------------------------------- /docs/conf.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | # SPDX-FileCopyrightText: 2017 Scott Shawcroft, written for Adafruit Industries 4 | # 5 | # SPDX-License-Identifier: MIT 6 | 7 | import os 8 | import sys 9 | 10 | sys.path.insert(0, os.path.abspath("..")) 11 | 12 | # -- General configuration ------------------------------------------------ 13 | 14 | # Add any Sphinx extension module names here, as strings. They can be 15 | # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom 16 | # ones. 17 | extensions = [ 18 | "sphinx.ext.autodoc", 19 | "sphinx.ext.intersphinx", 20 | "sphinx.ext.napoleon", 21 | "sphinx.ext.todo", 22 | ] 23 | 24 | # TODO: Please Read! 25 | # Uncomment the below if you use native CircuitPython modules such as 26 | # digitalio, micropython and busio. List the modules you use. Without it, the 27 | # autodoc module docs will fail to generate with a warning. 28 | # autodoc_mock_imports = ["digitalio", "busio"] 29 | 30 | 31 | intersphinx_mapping = { 32 | "python": ("https://docs.python.org/3.4", None), 33 | "CircuitPython": ("https://circuitpython.readthedocs.io/en/latest/", None), 34 | } 35 | 36 | # Show the docstring from both the class and its __init__() method. 37 | autoclass_content = "both" 38 | 39 | # Add any paths that contain templates here, relative to this directory. 40 | templates_path = ["_templates"] 41 | 42 | source_suffix = ".rst" 43 | 44 | # The master toctree document. 45 | master_doc = "index" 46 | 47 | # General information about the project. 48 | project = " CircuitPython schedule Library" 49 | copyright = "2021 Nathan Byrd" 50 | author = "Nathan Byrd" 51 | 52 | # The version info for the project you're documenting, acts as replacement for 53 | # |version| and |release|, also used in various other places throughout the 54 | # built documents. 55 | # 56 | # The short X.Y version. 57 | version = "1.0" 58 | # The full version, including alpha/beta/rc tags. 59 | release = "1.0" 60 | 61 | # The language for content autogenerated by Sphinx. Refer to documentation 62 | # for a list of supported languages. 63 | # 64 | # This is also used if you do content translation via gettext catalogs. 65 | # Usually you set "language" from the command line for these cases. 66 | language = None 67 | 68 | # List of patterns, relative to source directory, that match files and 69 | # directories to ignore when looking for source files. 70 | # This patterns also effect to html_static_path and html_extra_path 71 | exclude_patterns = [ 72 | "_build", 73 | "Thumbs.db", 74 | ".DS_Store", 75 | ".env", 76 | "CODE_OF_CONDUCT.md", 77 | ] 78 | 79 | # The reST default role (used for this markup: `text`) to use for all 80 | # documents. 81 | # 82 | default_role = "any" 83 | 84 | # If true, '()' will be appended to :func: etc. cross-reference text. 85 | # 86 | add_function_parentheses = True 87 | 88 | # The name of the Pygments (syntax highlighting) style to use. 89 | pygments_style = "sphinx" 90 | 91 | # If true, `todo` and `todoList` produce output, else they produce nothing. 92 | todo_include_todos = False 93 | 94 | # If this is True, todo emits a warning for each TODO entries. The default is False. 95 | todo_emit_warnings = True 96 | 97 | napoleon_numpy_docstring = False 98 | 99 | # -- Options for HTML output ---------------------------------------------- 100 | 101 | # The theme to use for HTML and HTML Help pages. See the documentation for 102 | # a list of builtin themes. 103 | # 104 | on_rtd = os.environ.get("READTHEDOCS", None) == "True" 105 | 106 | if not on_rtd: # only import and set the theme if we're building docs locally 107 | try: 108 | import sphinx_rtd_theme 109 | 110 | html_theme = "sphinx_rtd_theme" 111 | html_theme_path = [sphinx_rtd_theme.get_html_theme_path(), "."] 112 | except: 113 | html_theme = "default" 114 | html_theme_path = ["."] 115 | else: 116 | html_theme_path = ["."] 117 | 118 | # Add any paths that contain custom static files (such as style sheets) here, 119 | # relative to this directory. They are copied after the builtin static files, 120 | # so a file named "default.css" will overwrite the builtin "default.css". 121 | html_static_path = ["_static"] 122 | 123 | # The name of an image file (relative to this directory) to use as a favicon of 124 | # the docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 125 | # pixels large. 126 | # 127 | html_favicon = "_static/favicon.ico" 128 | 129 | # Output file base name for HTML help builder. 130 | htmlhelp_basename = "CircuitPython_ScheduleLibrarydoc" 131 | 132 | # -- Options for LaTeX output --------------------------------------------- 133 | 134 | latex_elements = { 135 | # The paper size ('letterpaper' or 'a4paper'). 136 | # 'papersize': 'letterpaper', 137 | # The font size ('10pt', '11pt' or '12pt'). 138 | # 'pointsize': '10pt', 139 | # Additional stuff for the LaTeX preamble. 140 | # 'preamble': '', 141 | # Latex figure (float) alignment 142 | # 'figure_align': 'htbp', 143 | } 144 | 145 | # Grouping the document tree into LaTeX files. List of tuples 146 | # (source start file, target name, title, 147 | # author, documentclass [howto, manual, or own class]). 148 | latex_documents = [ 149 | ( 150 | master_doc, 151 | "CircuitPython_ScheduleLibrary.tex", 152 | "CircuitPython Schedule Library Documentation", 153 | author, 154 | "manual", 155 | ), 156 | ] 157 | 158 | # -- Options for manual page output --------------------------------------- 159 | 160 | # One entry per manual page. List of tuples 161 | # (source start file, name, description, authors, manual section). 162 | man_pages = [ 163 | ( 164 | master_doc, 165 | "CircuitPython_ScheduleLibrary", 166 | "CircuitPython Schedule Library Documentation", 167 | [author], 168 | 1, 169 | ), 170 | ] 171 | 172 | # -- Options for Texinfo output ------------------------------------------- 173 | 174 | # Grouping the document tree into Texinfo files. List of tuples 175 | # (source start file, target name, title, author, 176 | # dir menu entry, description, category) 177 | texinfo_documents = [ 178 | ( 179 | master_doc, 180 | "CircuitPython_ScheduleLibrary", 181 | "CircuitPython Schedule Library Documentation", 182 | author, 183 | "CircuitPython_ScheduleLibrary", 184 | "Schedule library implementation for CircuitPython", 185 | "Miscellaneous", 186 | ), 187 | ] 188 | -------------------------------------------------------------------------------- /docs/examples.rst: -------------------------------------------------------------------------------- 1 | Simple test 2 | ------------ 3 | 4 | Ensure your device works with this simple test. 5 | 6 | .. literalinclude:: ../examples/circuitpython_schedule_simpletest.py 7 | :caption: examples/circuitpython_schedule_simpletest.py 8 | :linenos: 9 | 10 | 11 | Additional utilities 12 | --------------------- 13 | 14 | Additional utilities that are available 15 | 16 | .. literalinclude:: ../examples/circuitpython_schedule_util.py 17 | :caption: examples/circuitpython_schedule_util.py 18 | :linenos: 19 | 20 | 21 | Real Time Clock 22 | ---------------- 23 | 24 | This module works great in combination with a Real Time Clock (RTC), if one is 25 | available on your device. For example: 26 | 27 | .. literalinclude:: ../examples/circuitpython_schedule_rtc.py 28 | :caption: examples/circuitpython_schedule_rtc.py 29 | :linenos: 30 | -------------------------------------------------------------------------------- /docs/examples.rst.license: -------------------------------------------------------------------------------- 1 | SPDX-FileCopyrightText: 2017 Scott Shawcroft, written for Adafruit Industries 2 | SPDX-FileCopyrightText: Copyright (c) 2021 Nathan Byrd 3 | 4 | SPDX-License-Identifier: MIT 5 | -------------------------------------------------------------------------------- /docs/index.rst: -------------------------------------------------------------------------------- 1 | 2 | .. include:: ../README.rst 3 | 4 | Table of Contents 5 | ================= 6 | 7 | .. toctree:: 8 | :maxdepth: 4 9 | :hidden: 10 | 11 | self 12 | 13 | .. toctree:: 14 | :caption: Examples 15 | 16 | examples 17 | 18 | .. toctree:: 19 | :caption: API Reference 20 | :maxdepth: 3 21 | 22 | api 23 | 24 | .. toctree:: 25 | :caption: Tutorials 26 | 27 | .. toctree:: 28 | :caption: Related Products 29 | 30 | .. toctree:: 31 | :caption: Other Links 32 | 33 | Download 34 | CircuitPython Reference Documentation 35 | CircuitPython Support Forum 36 | Discord Chat 37 | Adafruit Learning System 38 | Adafruit Blog 39 | Adafruit Store 40 | 41 | Indices and tables 42 | ================== 43 | 44 | * :ref:`genindex` 45 | * :ref:`modindex` 46 | * :ref:`search` 47 | -------------------------------------------------------------------------------- /docs/index.rst.license: -------------------------------------------------------------------------------- 1 | SPDX-FileCopyrightText: 2017 Scott Shawcroft, written for Adafruit Industries 2 | SPDX-FileCopyrightText: Copyright (c) 2021 Nathan Byrd 3 | 4 | SPDX-License-Identifier: MIT 5 | -------------------------------------------------------------------------------- /examples/circuitpython_schedule_rtc.py: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2017 Scott Shawcroft, written for Adafruit Industries 2 | # SPDX-FileCopyrightText: Copyright (c) 2021 Nathan Byrd 3 | 4 | # SPDX-License-Identifier: MIT 5 | 6 | import time 7 | import rtc 8 | import busio 9 | import board 10 | import adafruit_pcf8523 11 | import circuitpython_schedule as schedule 12 | 13 | 14 | def greet(): 15 | print("Hello, world!") 16 | 17 | 18 | i2c = busio.I2C(board.SCL, board.SDA) 19 | rtc_device = adafruit_pcf8523.PCF8523(i2c) 20 | rtc.RTC().datetime = rtc_device.datetime 21 | 22 | # schedule every 10 seconds 23 | schedule.every(10).seconds.do(greet) 24 | 25 | # schedule every 10 minutes 26 | schedule.every(10).minutes.do(greet) 27 | 28 | # schedule once a day 29 | schedule.every().day.at("10:30").do(greet) 30 | 31 | # schedule from 5 to 10 minutes 32 | schedule.every(5).to(10).minutes.do(greet) 33 | 34 | # schedule on a particular day 35 | schedule.every().monday.do(greet) 36 | 37 | # schedule day and time 38 | schedule.every().wednesday.at("13:15").do(greet) 39 | 40 | # schedule once a minute at seventeen seconds 41 | schedule.every().minute.at(":17").do(greet) 42 | 43 | 44 | while True: 45 | # Run any pending jobs 46 | schedule.run_pending() 47 | time.sleep(1) 48 | -------------------------------------------------------------------------------- /examples/circuitpython_schedule_simpletest.py: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2017 Scott Shawcroft, written for Adafruit Industries 2 | # SPDX-FileCopyrightText: Copyright (c) 2021 Nathan Byrd 3 | 4 | # SPDX-License-Identifier: MIT 5 | 6 | import time 7 | import circuitpython_schedule as schedule 8 | 9 | 10 | def greet(): 11 | print("Hello, world!") 12 | 13 | 14 | # Note: pass functions, not function calls - i.e. "greet", not "greet()" 15 | 16 | # schedule every 10 seconds 17 | schedule.every(10).seconds.do(greet) 18 | 19 | # schedule every 10 minutes 20 | schedule.every(10).minutes.do(greet) 21 | 22 | # schedule once a day 23 | schedule.every().day.at("10:30").do(greet) 24 | 25 | # schedule from 5 to 10 minutes 26 | schedule.every(5).to(10).minutes.do(greet) 27 | 28 | # schedule on a particular day 29 | schedule.every().monday.do(greet) 30 | 31 | # schedule day and time 32 | schedule.every().wednesday.at("13:15").do(greet) 33 | 34 | # schedule once a minute at seventeen seconds 35 | schedule.every().minute.at(":17").do(greet) 36 | 37 | 38 | while True: 39 | # Run any pending jobs 40 | schedule.run_pending() 41 | time.sleep(1) 42 | -------------------------------------------------------------------------------- /examples/circuitpython_schedule_util.py: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2017 Scott Shawcroft, written for Adafruit Industries 2 | # SPDX-FileCopyrightText: Copyright (c) 2021 Nathan Byrd 3 | 4 | # SPDX-License-Identifier: MIT 5 | 6 | import time 7 | import circuitpython_schedule as schedule 8 | 9 | 10 | def greet(): 11 | print("Hello, world!") 12 | 13 | 14 | # schedule every 10 seconds 15 | schedule.every(10).seconds.do(greet) 16 | 17 | # Get the datetime of next run 18 | next_run_datetime = schedule.next_run() 19 | 20 | # Get the number of seconds until the next run 21 | next_run_seconds = schedule.idle_seconds() 22 | 23 | # cancel all jobs 24 | schedule.clear() 25 | 26 | # schedule every 1 second 27 | schedule.every(1).second.do(greet) 28 | 29 | while True: 30 | # Run any pending jobs 31 | schedule.run_pending() 32 | time.sleep(1) 33 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2020 Diego Elio Pettenò 2 | # 3 | # SPDX-License-Identifier: Unlicense 4 | 5 | [tool.black] 6 | target-version = ['py35'] 7 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2017 Scott Shawcroft, written for Adafruit Industries 2 | # SPDX-FileCopyrightText: Copyright (c) 2021 Nathan Byrd 3 | # 4 | # SPDX-License-Identifier: MIT 5 | 6 | Adafruit-Blinka 7 | adafruit-circuitpython-datetime 8 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2017 Scott Shawcroft, written for Adafruit Industries 2 | # SPDX-FileCopyrightText: Copyright (c) 2021 Nathan Byrd 3 | # 4 | # SPDX-License-Identifier: MIT 5 | 6 | """A setuptools based setup module. 7 | 8 | See: 9 | https://packaging.python.org/en/latest/distributing.html 10 | https://github.com/pypa/sampleproject 11 | """ 12 | 13 | from setuptools import setup, find_packages 14 | 15 | # To use a consistent encoding 16 | from codecs import open 17 | from os import path 18 | 19 | here = path.abspath(path.dirname(__file__)) 20 | 21 | # Get the long description from the README file 22 | with open(path.join(here, "README.rst"), encoding="utf-8") as f: 23 | long_description = f.read() 24 | 25 | setup( 26 | # Community Bundle Information 27 | name="circuitpython-schedule", 28 | use_scm_version=True, 29 | setup_requires=["setuptools_scm"], 30 | description="Reduced version of the schedule library for CircuitPython", 31 | long_description=long_description, 32 | long_description_content_type="text/x-rst", 33 | # The project's main homepage. 34 | url="https://github.com/cognitivegears/CircuitPython_Schedule.git", 35 | # Author details 36 | author="Nathan Byrd", 37 | author_email="", 38 | install_requires=[ 39 | "Adafruit-Blinka", 40 | "adafruit-circuitpython-datetime", 41 | ], 42 | # Choose your license 43 | license="MIT", 44 | # See https://pypi.python.org/pypi?%3Aaction=list_classifiers 45 | classifiers=[ 46 | "Development Status :: 3 - Alpha", 47 | "Intended Audience :: Developers", 48 | "Topic :: Software Development :: Libraries", 49 | "Topic :: System :: Hardware", 50 | "License :: OSI Approved :: MIT License", 51 | "Programming Language :: Python :: 3", 52 | "Programming Language :: Python :: 3.4", 53 | "Programming Language :: Python :: 3.5", 54 | ], 55 | # What does your project relate to? 56 | keywords="adafruit blinka circuitpython micropython uschedule schedule time date calendar cron scheduling repeat scheduler datetime", 57 | # You can just specify the packages manually here if your project is 58 | # simple. Or you can use find_packages(). 59 | # TODO: IF LIBRARY FILES ARE A PACKAGE FOLDER, 60 | # CHANGE `py_modules=['...']` TO `packages=['...']` 61 | py_modules=["circuitpython_schedule"], 62 | ) 63 | --------------------------------------------------------------------------------