├── .gitignore ├── CHANGELOG ├── COPYING.MIT ├── MANIFEST.in ├── README.md ├── docs ├── .nojekyll ├── Makefile ├── _build │ ├── doctrees │ │ ├── authors.doctree │ │ ├── examples.doctree │ │ ├── index.doctree │ │ ├── intro.doctree │ │ ├── license.doctree │ │ ├── task.doctree │ │ ├── thread.doctree │ │ └── threadtask.doctree │ └── html │ │ ├── .buildinfo │ │ ├── _downloads │ │ ├── a18b081f7d6dcca4c52c91e2f6deb6c3 │ │ │ └── hello_world.py │ │ ├── a5acc46a2c9241a97e295f04f5174070 │ │ │ └── client.py │ │ ├── c1dd4f4f2f4cf8025c3a2ba25bdb6544 │ │ │ └── parent_child.py │ │ ├── e3e1e5e874df68c81d982fddd1e522aa │ │ │ └── example_server2.py │ │ ├── ef5c78e045abd894d5d4d0c53efb047e │ │ │ └── example_server.py │ │ └── f924f640fe637e7f9e73bdbeaa5d3c81 │ │ │ └── client2.py │ │ ├── _sources │ │ ├── authors.rst.txt │ │ ├── examples.rst.txt │ │ ├── index.rst.txt │ │ ├── intro.rst.txt │ │ ├── license.rst.txt │ │ ├── task.rst.txt │ │ ├── thread.rst.txt │ │ └── threadtask.rst.txt │ │ ├── _static │ │ ├── _sphinx_javascript_frameworks_compat.js │ │ ├── basic.css │ │ ├── css │ │ │ ├── badge_only.css │ │ │ ├── fonts │ │ │ │ ├── Roboto-Slab-Bold.woff │ │ │ │ ├── Roboto-Slab-Bold.woff2 │ │ │ │ ├── Roboto-Slab-Regular.woff │ │ │ │ ├── Roboto-Slab-Regular.woff2 │ │ │ │ ├── fontawesome-webfont.eot │ │ │ │ ├── fontawesome-webfont.svg │ │ │ │ ├── fontawesome-webfont.ttf │ │ │ │ ├── fontawesome-webfont.woff │ │ │ │ ├── fontawesome-webfont.woff2 │ │ │ │ ├── lato-bold-italic.woff │ │ │ │ ├── lato-bold-italic.woff2 │ │ │ │ ├── lato-bold.woff │ │ │ │ ├── lato-bold.woff2 │ │ │ │ ├── lato-normal-italic.woff │ │ │ │ ├── lato-normal-italic.woff2 │ │ │ │ ├── lato-normal.woff │ │ │ │ └── lato-normal.woff2 │ │ │ └── theme.css │ │ ├── doctools.js │ │ ├── documentation_options.js │ │ ├── file.png │ │ ├── jquery-3.5.1.js │ │ ├── jquery.js │ │ ├── js │ │ │ ├── badge_only.js │ │ │ ├── html5shiv-printshiv.min.js │ │ │ ├── html5shiv.min.js │ │ │ └── theme.js │ │ ├── language_data.js │ │ ├── logo.png │ │ ├── minus.png │ │ ├── plus.png │ │ ├── pygments.css │ │ ├── rtd_override.css │ │ ├── searchtools.js │ │ ├── snakes.png │ │ ├── sphinx_highlight.js │ │ ├── tmp.png │ │ ├── underscore-1.12.0.js │ │ └── underscore.js │ │ ├── authors.html │ │ ├── examples.html │ │ ├── index.html │ │ ├── intro.html │ │ ├── license.html │ │ ├── objects.inv │ │ ├── search.html │ │ ├── searchindex.js │ │ ├── task.html │ │ ├── thread.html │ │ └── threadtask.html ├── _static │ ├── logo.png │ ├── rtd_override.css │ ├── snakes.png │ └── tmp.png ├── _templates │ ├── alabaster │ │ ├── sidebar-bottom.html │ │ └── sidebar-intro.html │ ├── rtd │ │ └── layout.html │ ├── sidebar-bottom.html │ ├── sidebar-intro.html │ └── tmp │ │ └── sidebar-intro.html ├── authors.rst ├── clean.bash ├── compile.bash ├── conf.py ├── examples.rst ├── fix_rst.py ├── home.md ├── images │ ├── skull.png │ └── tmp.png ├── index.html ├── index.rst ├── intro.rst ├── license.rst ├── snippets │ ├── client.py │ ├── client2.py │ ├── compile.bash │ ├── example_server.py │ ├── example_server.py_ │ ├── example_server2.py │ ├── example_server2.py_ │ ├── form_snippets.bash │ ├── hello_world.py │ ├── hello_world.py_ │ ├── parent_child.py │ ├── parent_child.py_ │ ├── paska.txt │ ├── pyeval.py │ ├── requirements.txt │ └── requirements.txt_ ├── task.rst ├── thread.rst └── threadtask.rst ├── setup.py └── task_thread ├── __init__.py ├── base ├── __init__.py ├── data │ └── some_auxiliary_file ├── signals.py └── task_thread.py ├── decorator.py ├── ipc.py ├── manage.py ├── template.py ├── tools.py └── version.py /.gitignore: -------------------------------------------------------------------------------- 1 | __pycache__/ 2 | /build/ 3 | /dist/ 4 | /venv/ 5 | *.egg-info/ 6 | .ipynb_checkpoints/ 7 | Untitled*.ipynb 8 | *.pickle 9 | bash/ 10 | tmp/ 11 | -------------------------------------------------------------------------------- /CHANGELOG: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /COPYING.MIT: -------------------------------------------------------------------------------- 1 | MIT LICENSE 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 4 | 5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 6 | 7 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 8 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | graft docs 2 | include task_thread/data/some_auxiliary_file 3 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # TaskThread 2 | 3 | ## Synopsis 4 | 5 | Organize your complex asyncio python projects into hierarchical, intercommunicating "threads". 6 | 7 | For documentation, click [here](https://elsampsa.github.io/task_thread) 8 | 9 | ## Installing 10 | 11 | Development mode install: 12 | ``` 13 | git clone https://github.com/elsampsa/task_thread 14 | cd task_thread 15 | pip3 install --user --upgrade -e . 16 | ``` 17 | 18 | Or directly from the PyPi repo: 19 | ``` 20 | pip3 install --user task-virtualthread 21 | ``` 22 | 23 | ## Authors 24 | Sampsa Riikonen 25 | 26 | ## Copyright 27 | (C) 2021-2022 Sampsa Riikonen 28 | 29 | ## License 30 | MIT 31 | -------------------------------------------------------------------------------- /docs/.nojekyll: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /docs/Makefile: -------------------------------------------------------------------------------- 1 | # Makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line. 5 | SPHINXOPTS = 6 | SPHINXBUILD = sphinx-build 7 | PAPER = 8 | BUILDDIR = _build 9 | 10 | # User-friendly check for sphinx-build 11 | ifeq ($(shell which $(SPHINXBUILD) >/dev/null 2>&1; echo $$?), 1) 12 | $(error The '$(SPHINXBUILD)' command was not found. Make sure you have Sphinx installed, then set the SPHINXBUILD environment variable to point to the full path of the '$(SPHINXBUILD)' executable. Alternatively you can add the directory with the executable to your PATH. If you don't have Sphinx installed, grab it from http://sphinx-doc.org/) 13 | endif 14 | 15 | # Internal variables. 16 | PAPEROPT_a4 = -D latex_paper_size=a4 17 | PAPEROPT_letter = -D latex_paper_size=letter 18 | ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . 19 | # the i18n builder cannot share the environment and doctrees with the others 20 | I18NSPHINXOPTS = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . 21 | 22 | .PHONY: help 23 | help: 24 | @echo "Please use \`make ' where is one of" 25 | @echo " html to make standalone HTML files" 26 | @echo " dirhtml to make HTML files named index.html in directories" 27 | @echo " singlehtml to make a single large HTML file" 28 | @echo " pickle to make pickle files" 29 | @echo " json to make JSON files" 30 | @echo " htmlhelp to make HTML files and a HTML help project" 31 | @echo " qthelp to make HTML files and a qthelp project" 32 | @echo " applehelp to make an Apple Help Book" 33 | @echo " devhelp to make HTML files and a Devhelp project" 34 | @echo " epub to make an epub" 35 | @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter" 36 | @echo " latexpdf to make LaTeX files and run them through pdflatex" 37 | @echo " latexpdfja to make LaTeX files and run them through platex/dvipdfmx" 38 | @echo " text to make text files" 39 | @echo " man to make manual pages" 40 | @echo " texinfo to make Texinfo files" 41 | @echo " info to make Texinfo files and run them through makeinfo" 42 | @echo " gettext to make PO message catalogs" 43 | @echo " changes to make an overview of all changed/added/deprecated items" 44 | @echo " xml to make Docutils-native XML files" 45 | @echo " pseudoxml to make pseudoxml-XML files for display purposes" 46 | @echo " linkcheck to check all external links for integrity" 47 | @echo " doctest to run all doctests embedded in the documentation (if enabled)" 48 | @echo " coverage to run coverage check of the documentation (if enabled)" 49 | 50 | .PHONY: clean 51 | clean: 52 | rm -rf $(BUILDDIR)/* 53 | 54 | .PHONY: html 55 | html: 56 | $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html 57 | @echo 58 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/html." 59 | 60 | .PHONY: dirhtml 61 | dirhtml: 62 | $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml 63 | @echo 64 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml." 65 | 66 | .PHONY: singlehtml 67 | singlehtml: 68 | $(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml 69 | @echo 70 | @echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml." 71 | 72 | .PHONY: pickle 73 | pickle: 74 | $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle 75 | @echo 76 | @echo "Build finished; now you can process the pickle files." 77 | 78 | .PHONY: json 79 | json: 80 | $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json 81 | @echo 82 | @echo "Build finished; now you can process the JSON files." 83 | 84 | .PHONY: htmlhelp 85 | htmlhelp: 86 | $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp 87 | @echo 88 | @echo "Build finished; now you can run HTML Help Workshop with the" \ 89 | ".hhp project file in $(BUILDDIR)/htmlhelp." 90 | 91 | .PHONY: qthelp 92 | qthelp: 93 | $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp 94 | @echo 95 | @echo "Build finished; now you can run "qcollectiongenerator" with the" \ 96 | ".qhcp project file in $(BUILDDIR)/qthelp, like this:" 97 | @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/Qt-Mongo-Forms.qhcp" 98 | @echo "To view the help file:" 99 | @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/Qt-Mongo-Forms.qhc" 100 | 101 | .PHONY: applehelp 102 | applehelp: 103 | $(SPHINXBUILD) -b applehelp $(ALLSPHINXOPTS) $(BUILDDIR)/applehelp 104 | @echo 105 | @echo "Build finished. The help book is in $(BUILDDIR)/applehelp." 106 | @echo "N.B. You won't be able to view it unless you put it in" \ 107 | "~/Library/Documentation/Help or install it in your application" \ 108 | "bundle." 109 | 110 | .PHONY: devhelp 111 | devhelp: 112 | $(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp 113 | @echo 114 | @echo "Build finished." 115 | @echo "To view the help file:" 116 | @echo "# mkdir -p $$HOME/.local/share/devhelp/Qt-Mongo-Forms" 117 | @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/Qt-Mongo-Forms" 118 | @echo "# devhelp" 119 | 120 | .PHONY: epub 121 | epub: 122 | $(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub 123 | @echo 124 | @echo "Build finished. The epub file is in $(BUILDDIR)/epub." 125 | 126 | .PHONY: latex 127 | latex: 128 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 129 | @echo 130 | @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex." 131 | @echo "Run \`make' in that directory to run these through (pdf)latex" \ 132 | "(use \`make latexpdf' here to do that automatically)." 133 | 134 | .PHONY: latexpdf 135 | latexpdf: 136 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 137 | @echo "Running LaTeX files through pdflatex..." 138 | $(MAKE) -C $(BUILDDIR)/latex all-pdf 139 | @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." 140 | 141 | .PHONY: latexpdfja 142 | latexpdfja: 143 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 144 | @echo "Running LaTeX files through platex and dvipdfmx..." 145 | $(MAKE) -C $(BUILDDIR)/latex all-pdf-ja 146 | @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." 147 | 148 | .PHONY: text 149 | text: 150 | $(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text 151 | @echo 152 | @echo "Build finished. The text files are in $(BUILDDIR)/text." 153 | 154 | .PHONY: man 155 | man: 156 | $(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man 157 | @echo 158 | @echo "Build finished. The manual pages are in $(BUILDDIR)/man." 159 | 160 | .PHONY: texinfo 161 | texinfo: 162 | $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo 163 | @echo 164 | @echo "Build finished. The Texinfo files are in $(BUILDDIR)/texinfo." 165 | @echo "Run \`make' in that directory to run these through makeinfo" \ 166 | "(use \`make info' here to do that automatically)." 167 | 168 | .PHONY: info 169 | info: 170 | $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo 171 | @echo "Running Texinfo files through makeinfo..." 172 | make -C $(BUILDDIR)/texinfo info 173 | @echo "makeinfo finished; the Info files are in $(BUILDDIR)/texinfo." 174 | 175 | .PHONY: gettext 176 | gettext: 177 | $(SPHINXBUILD) -b gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale 178 | @echo 179 | @echo "Build finished. The message catalogs are in $(BUILDDIR)/locale." 180 | 181 | .PHONY: changes 182 | changes: 183 | $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes 184 | @echo 185 | @echo "The overview file is in $(BUILDDIR)/changes." 186 | 187 | .PHONY: linkcheck 188 | linkcheck: 189 | $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck 190 | @echo 191 | @echo "Link check complete; look for any errors in the above output " \ 192 | "or in $(BUILDDIR)/linkcheck/output.txt." 193 | 194 | .PHONY: doctest 195 | doctest: 196 | $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest 197 | @echo "Testing of doctests in the sources finished, look at the " \ 198 | "results in $(BUILDDIR)/doctest/output.txt." 199 | 200 | .PHONY: coverage 201 | coverage: 202 | $(SPHINXBUILD) -b coverage $(ALLSPHINXOPTS) $(BUILDDIR)/coverage 203 | @echo "Testing of coverage in the sources finished, look at the " \ 204 | "results in $(BUILDDIR)/coverage/python.txt." 205 | 206 | .PHONY: xml 207 | xml: 208 | $(SPHINXBUILD) -b xml $(ALLSPHINXOPTS) $(BUILDDIR)/xml 209 | @echo 210 | @echo "Build finished. The XML files are in $(BUILDDIR)/xml." 211 | 212 | .PHONY: pseudoxml 213 | pseudoxml: 214 | $(SPHINXBUILD) -b pseudoxml $(ALLSPHINXOPTS) $(BUILDDIR)/pseudoxml 215 | @echo 216 | @echo "Build finished. The pseudo-XML files are in $(BUILDDIR)/pseudoxml." 217 | -------------------------------------------------------------------------------- /docs/_build/doctrees/authors.doctree: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elsampsa/task_thread/14905bfb9cfaf82c6bc905443ed56e95a7913f14/docs/_build/doctrees/authors.doctree -------------------------------------------------------------------------------- /docs/_build/doctrees/examples.doctree: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elsampsa/task_thread/14905bfb9cfaf82c6bc905443ed56e95a7913f14/docs/_build/doctrees/examples.doctree -------------------------------------------------------------------------------- /docs/_build/doctrees/index.doctree: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elsampsa/task_thread/14905bfb9cfaf82c6bc905443ed56e95a7913f14/docs/_build/doctrees/index.doctree -------------------------------------------------------------------------------- /docs/_build/doctrees/intro.doctree: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elsampsa/task_thread/14905bfb9cfaf82c6bc905443ed56e95a7913f14/docs/_build/doctrees/intro.doctree -------------------------------------------------------------------------------- /docs/_build/doctrees/license.doctree: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elsampsa/task_thread/14905bfb9cfaf82c6bc905443ed56e95a7913f14/docs/_build/doctrees/license.doctree -------------------------------------------------------------------------------- /docs/_build/doctrees/task.doctree: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elsampsa/task_thread/14905bfb9cfaf82c6bc905443ed56e95a7913f14/docs/_build/doctrees/task.doctree -------------------------------------------------------------------------------- /docs/_build/doctrees/thread.doctree: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elsampsa/task_thread/14905bfb9cfaf82c6bc905443ed56e95a7913f14/docs/_build/doctrees/thread.doctree -------------------------------------------------------------------------------- /docs/_build/doctrees/threadtask.doctree: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elsampsa/task_thread/14905bfb9cfaf82c6bc905443ed56e95a7913f14/docs/_build/doctrees/threadtask.doctree -------------------------------------------------------------------------------- /docs/_build/html/.buildinfo: -------------------------------------------------------------------------------- 1 | # Sphinx build info version 1 2 | # This file hashes the configuration used when building these files. When it is not found, a full rebuild will be done. 3 | config: 35eb426cbb6c31db8559fa74c0b587e7 4 | tags: 645f666f9bcd5a90fca523b33c5a78b7 5 | -------------------------------------------------------------------------------- /docs/_build/html/_downloads/a18b081f7d6dcca4c52c91e2f6deb6c3/hello_world.py: -------------------------------------------------------------------------------- 1 | """ 2 | The same "hello world" example we already discussed :ref:`here `. 3 | """ 4 | import asyncio, logging, traceback 5 | from task_thread import TaskThread, reCreate, reSchedule,\ 6 | delete, verbose, signals 7 | """ 8 | Define TaskThread by subclassing 9 | """ 10 | class MyThread(TaskThread): 11 | """ 12 | Let's define a rescheduling task that sleeps for a one seconds & then reschedules itself 13 | """ 14 | async def helloTask__(self): 15 | try: 16 | await asyncio.sleep(1) 17 | self.logger.info("Hello from helloTask__ at %s", self.getId()) 18 | self.tasks.hello_task = await reSchedule(self.helloTask__) 19 | return 20 | 21 | except asyncio.CancelledError: 22 | self.logger.critical("helloTask__: cancelling") 23 | 24 | except Exception as e: 25 | self.logger.info("helloTask__: failed with '%s', traceback will follow", e) 26 | traceback.print_exc() 27 | 28 | """ 29 | Remember to call the superclass constructor 30 | """ 31 | def __init__(self, my_id = 0, parent = None): 32 | super().__init__(parent = parent) 33 | self.my_id = my_id 34 | 35 | """ 36 | Create the tasks under the self.tasks namespace & initialize them to ``None`` 37 | """ 38 | def initVars__(self): 39 | # tasks 40 | self.tasks.hello = None # self.helloTask__ 41 | 42 | """ 43 | The ``helloTask__`` is started for the first time when the thread is started: 44 | """ 45 | @verbose 46 | async def enter__(self): 47 | self.logger.info("enter__ :") 48 | self.tasks.hello = await reCreate(self.tasks.hello, self.helloTask__) 49 | 50 | """ 51 | Define what happens at thread exit: delete any running tasks 52 | """ 53 | @verbose 54 | async def exit__(self): 55 | """Close sockets, databases, etc. Overwrite in child class. 56 | """ 57 | self.tasks.hello = await delete(self.tasks.hello) 58 | self.logger.info("exit__ : finished") 59 | 60 | 61 | """ 62 | Here we would define how signals from a parent are handled. In this example case nothing. 63 | """ 64 | @verbose 65 | async def signalHandler__(self, signal): 66 | self.logger.info("signalHandler__ : got signal %s", signal) 67 | 68 | def getId(self): 69 | return self.my_id 70 | 71 | def getInfo(self): 72 | return "" 73 | 74 | 75 | """ 76 | Let's run MyThread! Terminate by pressing CTRL-C. 77 | """ 78 | if __name__ == "__main__": 79 | loglev = logging.DEBUG 80 | 81 | logger = logging.getLogger("MyThread") 82 | logger.setLevel(loglev) 83 | 84 | thread = MyThread(my_id = "main_thread", parent = None) # no parent, this is the main thread 85 | loop = asyncio.get_event_loop() 86 | loop.run_until_complete(thread.run()) 87 | 88 | -------------------------------------------------------------------------------- /docs/_build/html/_downloads/a5acc46a2c9241a97e295f04f5174070/client.py: -------------------------------------------------------------------------------- 1 | import socket, time 2 | 3 | sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) 4 | sock.connect(("localhost", 5002)) 5 | time.sleep(1) 6 | print("sending msg") 7 | sock.send("kokkelis".encode("ascii")) 8 | time.sleep(1) 9 | print("sending msg2") 10 | sock.send("kokkelis2".encode("ascii")) 11 | time.sleep(1) 12 | print("closing socket") 13 | # sock.shutdown() 14 | sock.close() 15 | -------------------------------------------------------------------------------- /docs/_build/html/_downloads/c1dd4f4f2f4cf8025c3a2ba25bdb6544/parent_child.py: -------------------------------------------------------------------------------- 1 | import asyncio, logging, traceback 2 | from task_thread import TaskThread, reCreate, reSchedule,\ 3 | delete, verbose, signals 4 | 5 | """ 6 | Let's define a signal for parent/child communication 7 | """ 8 | class MessageSignal(signals.Signal): 9 | def __init__(self, origin, message): 10 | self.origin = origin 11 | self.message = message 12 | 13 | def __str__(self): 14 | return "" % (str(self.origin.getId())) 15 | 16 | def getMessage(self): 17 | return self.message 18 | 19 | """ 20 | We use the same ``MyThread`` as in the previous example. 21 | We will use it as a basis for further subclassing 22 | """ 23 | class MyThread(TaskThread): 24 | # as in previous example 25 | # ... 26 | #HIDE> 27 | async def helloTask__(self): 28 | try: 29 | await asyncio.sleep(1) 30 | self.logger.info("Hello from helloTask__ at %s", self.getId()) 31 | self.tasks.hello_task = await reSchedule(self.helloTask__); return 32 | 33 | except asyncio.CancelledError: 34 | self.logger.critical("helloTask__: cancelling") 35 | 36 | except Exception as e: 37 | self.logger.info("helloTask__: failed with '%s', traceback will follow", e) 38 | traceback.print_exc() 39 | 40 | 41 | def __init__(self, my_id = 0, parent = None): 42 | super().__init__(parent = parent) 43 | self.my_id = my_id 44 | 45 | 46 | def initVars__(self): 47 | """Create & initialize here your tasks with none & create your locks 48 | """ 49 | # tasks 50 | self.tasks.hello = None # self.helloTask__ 51 | 52 | 53 | @verbose 54 | async def enter__(self): 55 | """Everything starts from here. This cofunction is awaited (i.e. not scheduled as a task) 56 | 57 | - Await for something critical 58 | - Shedule the re-scheduling tasks 59 | """ 60 | self.logger.info("entry point") 61 | self.tasks.hello = await reCreate(self.tasks.hello, self.helloTask__) 62 | 63 | @verbose 64 | async def exit__(self): 65 | """Close sockets, databases, etc. Overwrite in child class. 66 | """ 67 | self.tasks.hello = await delete(self.tasks.hello) 68 | self.logger.info("exit__ : finished") 69 | 70 | @verbose 71 | async def signalHandler__(self, signal): 72 | """Handles a signal that was sent to this thread using insertSignal 73 | 74 | - Return True if thread should keep on running 75 | - Return False if thread should exit (say, in the case of some fatal error) 76 | 77 | Do subclass this. 78 | """ 79 | self.logger.info("signalHandler__ : got signal %s", signal) 80 | 81 | 82 | def getId(self): 83 | """Do subclass this. 84 | 85 | Return a unique id for this VirtualThread. 86 | """ 87 | return self.my_id 88 | 89 | 90 | def getInfo(self): 91 | """Do subclass this 92 | 93 | Returns information string about this thread 94 | """ 95 | return "" 96 | 97 | # 100 | Let's create a child thread. This thread sends a message to it's parent every 5 seconds: 101 | """ 102 | class ChildThread(MyThread): 103 | """ 104 | Define a rescheduling task that sends a message to it's parent every 5 seconds 105 | """ 106 | async def messageTask__(self): 107 | try: 108 | await asyncio.sleep(5) 109 | self.logger.info("messageTask__: sending a message to parent") 110 | 111 | await self.sigFromChildToParent__( 112 | MessageSignal(origin = self, 113 | message = "hello from child " + str(self.getId()))) 114 | self.tasks.message_task = await reSchedule(self.messageTask__); return 115 | 116 | except asyncio.CancelledError: 117 | self.logger.critical("messageTask__: cancelling") 118 | 119 | except Exception as e: 120 | self.logger.info("messageTask__: failed with '%s', traceback will follow", e) 121 | traceback.print_exc() 122 | 123 | """ 124 | Add the new task to ``self.tasks`` namespace 125 | """ 126 | def initVars__(self): 127 | # tasks 128 | self.tasks.hello = None # self.helloTask__ 129 | self.tasks.message = None # self.messageTask__ 130 | # locks 131 | self.locks.message_lock = asyncio.Lock() 132 | 133 | """ 134 | Start both tasks at thread start 135 | """ 136 | @verbose 137 | async def enter__(self): 138 | self.logger.info("enter__") 139 | self.tasks.hello = await reCreate(self.tasks.hello, self.helloTask__) 140 | self.tasks.message = await reCreate(self.tasks.message, self.messageTask__) 141 | 142 | """ 143 | Kill both tasks at thread exit 144 | """ 145 | @verbose 146 | async def exit__(self): 147 | self.tasks.hello = await delete(self.tasks.hello) 148 | self.tasks.message = await delete(self.tasks.message) 149 | self.logger.info("exit__ : finished") 150 | 151 | 152 | """ 153 | Now the parent thread that receives signals from the child: 154 | """ 155 | class ParentThread(MyThread): 156 | """ 157 | Start ``self.helloTask__`` just like in the child thread. 158 | 159 | The new bit here is, that we create and start a child thread at parent thread start. 160 | 161 | After starting, the child thread is added to the parent's registry using ``addChild``. 162 | 163 | You can experiment by adding more and more child threads. 164 | """ 165 | @verbose 166 | async def enter__(self): 167 | self.logger.info("enter__") 168 | self.tasks.hello = await reCreate(self.tasks.hello, self.helloTask__) 169 | child_thread = ChildThread(my_id = "subthread", parent = self) 170 | await child_thread.run() 171 | await self.addChild(child_thread) 172 | 173 | """ 174 | Define how we handle those signals coming from the running child thread: 175 | """ 176 | @verbose 177 | async def childsignalHandler__(self, signal, child): 178 | self.logger.debug("childsignalHandler__ : got signal %s from child %s", signal, child.getId()) 179 | if isinstance(signal, MessageSignal): 180 | self.logger.info("Got message %s from child with id %s", 181 | signal.getMessage(), child.getId()) 182 | else: 183 | pass 184 | 185 | """ 186 | Please note that you can also start child threads "dynamically", i.e. anywhere in your classes internal methods/recheduling tasks, not just at ``enter__``. 187 | 188 | That's it! Nothing else is needed: when the parent thread terminates, it automagically terminates all of it's child threads. 189 | 190 | Run the program with: 191 | """ 192 | if __name__ == "__main__": 193 | loglev = logging.DEBUG 194 | 195 | logger = logging.getLogger("ParentThread") 196 | logger.setLevel(loglev) 197 | 198 | logger = logging.getLogger("ChildThread") 199 | logger.setLevel(loglev) 200 | 201 | thread = ParentThread(my_id = "parent_thread", parent = None) # no parent, this is the main thread 202 | loop = asyncio.get_event_loop() 203 | loop.run_until_complete(thread.run()) 204 | """ 205 | Terminate by pressing CTRL-C. 206 | """ 207 | -------------------------------------------------------------------------------- /docs/_build/html/_downloads/ef5c78e045abd894d5d4d0c53efb047e/example_server.py: -------------------------------------------------------------------------------- 1 | import asyncio, logging, traceback 2 | from task_thread import TaskThread, reCreate, reSchedule,\ 3 | delete, MessageSignal, verbose, signals 4 | """ 5 | Let's create a TCP server application. The hierarchy looks like this: 6 | 7 | :: 8 | 9 | MasterTCPServerThread 10 | TCPConnectionThread 11 | TCPConnectionThread 12 | ... 13 | 14 | As always, start by subclassing TaskThread. First we create the child ``TCPConnectionThread``. 15 | """ 16 | class TCPConnectionThread(TaskThread): 17 | """ 18 | Constructor takes as an argument stream reader and writer objects: 19 | """ 20 | def __init__(self, parent = None, reader: asyncio.StreamReader = None, writer: asyncio.StreamReader = None): 21 | super().__init__(parent = parent) 22 | assert(reader is not None) 23 | assert(writer is not None) 24 | self.reader = reader 25 | self.writer = writer 26 | self.peername, self.peerport = self.writer.get_extra_info("peername") 27 | 28 | def getId(self): 29 | return str(id(self)) 30 | 31 | def getInfo(self): 32 | return "" % (self.getId(), self.peername) 33 | 34 | """ 35 | Re-scheduling tasks for reading and writing the client socket: 36 | """ 37 | def initVars__(self): 38 | self.tasks.read_socket = None 39 | self.tasks.write_socket = None 40 | 41 | """ 42 | In this version we just read the socket, so start the corresponding re-scheduling task: 43 | """ 44 | @verbose 45 | async def enter__(self): 46 | self.logger.info("enter__ : %s", self.getInfo()) 47 | self.tasks.read_socket = await reCreate(self.tasks.read_socket, self.readSocket__) 48 | 49 | @verbose 50 | async def exit__(self): 51 | self.tasks.read_socket = await delete(self.tasks.read_socket) 52 | self.tasks.write_socket = await delete(self.tasks.write_socket) 53 | self.logger.debug("exit__: bye!") 54 | 55 | """ 56 | Reads packet from the socket & re-schedules itself. 57 | 58 | At failure, kill the whole ``TCPConnectionThread``. This will inform the parent automagically & remove 59 | this child thread from the parent's registry. 60 | """ 61 | async def readSocket__(self): 62 | try: 63 | try: 64 | packet = await self.reader.read() 65 | except Exception as e: 66 | self.logger.warning("readSocket__ : reading failed : '%s'", e) 67 | self.logger.info("readSocket__: tcp connection %s terminated", self.getInfo()) 68 | await self.stop() 69 | else: 70 | if len(packet) > 0: 71 | # all good! keep on reading = reschedule this 72 | self.logger.info("readSocket__: got packet %s", packet) 73 | self.tasks.read_socket = await reSchedule(self.readSocket__); return 74 | else: 75 | self.logger.info("readSocket__: client at %s closed connection", self.peername) 76 | await self.stop() 77 | 78 | except asyncio.CancelledError: 79 | self.logger.info("readSocket__ : cancelling %s", self.getInfo()) 80 | 81 | except Exception as e: 82 | self.logger.warning("readSocket__: failed with '%s'", e) 83 | await self.stop() 84 | 85 | """ 86 | Writing socket continuously (however, not used in this example): 87 | """ 88 | async def writeSocket__(self, packet): 89 | try: 90 | self.writer.write(packet) 91 | # await self.writer.drain() # this would flush 92 | except Exception as e: 93 | self.logger.warning("writeSocket__ : writing failed : '%s'", e) 94 | await self.stop() 95 | 96 | 97 | """ 98 | Next, create the parent ``MasterTCPServerThread``: 99 | """ 100 | class MasterTCPServerThread(TaskThread): 101 | 102 | def __init__(self, parent = None, name = "thread", pause = 10, port = 5002, max_clients = 10): 103 | super().__init__(parent = parent) 104 | self.pause = pause 105 | self.port = port 106 | self.max_clients = max_clients 107 | self.name = name 108 | 109 | 110 | def initVars__(self): 111 | self.server = None 112 | self.tasks.tcp_server = None 113 | 114 | 115 | def getId(self): 116 | return str(id(self)) 117 | 118 | 119 | def getInfo(self): 120 | return "" % (self.name) 121 | 122 | 123 | @verbose 124 | async def enter__(self): 125 | self.logger.info("entry point") 126 | self.tasks.tcp_server = await reCreate(self.tasks.tcp_server, self.tcpServer__) 127 | 128 | 129 | @verbose 130 | async def exit__(self): 131 | self.tasks.tcp_server = await delete(self.tasks.tcp_server) 132 | self.logger.debug("exit__: bye!") 133 | 134 | """ 135 | Each and every child / tcp client sends it's packets to the parent: 136 | """ 137 | @verbose 138 | async def childsignalHandler__(self, signal, child): 139 | self.logger.debug("childsignalHandler__ : got signal %s from child %s", signal, child) 140 | if isinstance(signal, MessageSignal): 141 | self.logger.info("Got message %s from child with id %s", signal.getMessage(), child.getId()) 142 | else: 143 | pass 144 | 145 | """ 146 | Keep on (re)creating the tcp server: 147 | """ 148 | async def tcpServer__(self): 149 | try: 150 | self.server = await asyncio.start_server(self.handler__, "", self.port) 151 | 152 | except asyncio.CancelledError: 153 | self.logger.info("tcpServer__ %i: cancel") 154 | self.server = None 155 | 156 | except Exception as e: 157 | self.logger.warning("tcpServer__: failed with '%s'", str(e)) 158 | self.logger.warning("tcpServer__: will try again in %s secs", self.pause) 159 | await asyncio.sleep(self.pause) 160 | self.tasks.tcp_server = await reSchedule(self.tcpServer__) 161 | 162 | else: 163 | self.logger.debug("tcpServer__ : new server waiting") 164 | 165 | """ 166 | At a client connection, start a new ``TCPConnectionThread``: 167 | """ 168 | @verbose 169 | async def handler__(self, reader, writer): 170 | self.logger.debug("handler__ : new connection for %s", self.getInfo()) 171 | 172 | if len(self.children)>self.max_clients: 173 | self.logger.warning("handler__ : max number of connections is %s", self.max_clients) 174 | return 175 | 176 | child_connection = TCPConnectionThread(reader = reader, writer = writer, parent = self) 177 | await child_connection.run() 178 | await self.addChild(child_connection) 179 | 180 | """ 181 | Run it! 182 | """ 183 | if __name__ == "__main__": 184 | loglev = logging.DEBUG 185 | 186 | logger = logging.getLogger("MasterTCPServerThread") 187 | logger.setLevel(loglev) 188 | logger = logging.getLogger("TCPConnectionThread") 189 | logger.setLevel(loglev) 190 | 191 | thread = MasterTCPServerThread( 192 | parent = None, 193 | name = "TCPServer" 194 | ) 195 | loop = asyncio.get_event_loop() 196 | loop.run_until_complete(thread.run()) 197 | """ 198 | While it's running, start one or several client connections using the provided client code. 199 | """ 200 | -------------------------------------------------------------------------------- /docs/_build/html/_downloads/f924f640fe637e7f9e73bdbeaa5d3c81/client2.py: -------------------------------------------------------------------------------- 1 | import socket, time, json 2 | 3 | INT_NBYTES = 4 4 | 5 | def autoReadSock(sock): 6 | """Read first message length, then the message 7 | """ 8 | len_bytes = bytes() 9 | left = INT_NBYTES 10 | while left > 0: 11 | len_bytes += sock.recv(left) 12 | if len(len_bytes) < 1: 13 | return None 14 | left -= len(len_bytes) 15 | 16 | left = int.from_bytes(len_bytes, "big") 17 | msg_bytes = bytes() 18 | while left > 0: 19 | msg_bytes_ = sock.recv(4096) 20 | if len(msg_bytes_) < 1: 21 | return None 22 | else: 23 | msg_bytes += msg_bytes_ 24 | left -= len(msg_bytes_) 25 | return msg_bytes 26 | 27 | 28 | def bytes2Payload(bytes_): 29 | bytes_ = len(bytes_).to_bytes(INT_NBYTES, "big") + bytes_ 30 | return bytes_ 31 | 32 | 33 | def sendAll(sock = None, bytes_ = None, mbps = 1): 34 | kbps = mbps * 1024 35 | bytes_per_second = 1024*kbps/8 36 | seconds_per_byte = 1/bytes_per_second 37 | block_size = 1500 # standard mtu size 38 | f=block_size*8/1000 # one block in kb 39 | cc = len(bytes_) // block_size 40 | to = 0 41 | 42 | for i in range(cc): 43 | fr = i*block_size 44 | to = (i+1)*block_size 45 | blob_ = bytes_[fr:to] 46 | # print("sending:", fr, to, np.frombuffer(blob_, dtype=np.uint8)[0:10]) 47 | t=time.time() 48 | sock.sendall( 49 | blob_ 50 | ) 51 | dt=time.time()-t 52 | kbps_ = f/dt 53 | delay=max(seconds_per_byte*block_size-dt,0) 54 | time.sleep(delay) # delay to reach target kbps 55 | 56 | if to < len(bytes_): 57 | blob_ = bytes_[to:] 58 | # print("sending rest:",to, np.frombuffer(blob_, dtype=np.uint8)[0:10]) 59 | t=time.time() 60 | sock.sendall( 61 | blob_ 62 | ) 63 | dt=time.time()-t 64 | delay=max(seconds_per_byte*block_size-dt,0) 65 | time.sleep(delay) # delay to reach target kbps 66 | 67 | 68 | def sendAllBytes(sock = None, bytes_ = None, mbps = 1): 69 | bytes_ = bytes2Payload(bytes_) 70 | sendAll(sock = sock, bytes_ = bytes_, mbps = mbps) 71 | 72 | 73 | sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) 74 | sock.connect(("localhost", 5002)) 75 | b = autoReadSock(sock) 76 | dic = json.loads(b.decode("utf-8")) 77 | print("got from server:", dic) 78 | mbps = 1000 79 | n=22; print("sending payload"); sendAllBytes(sock=sock, bytes_=bytes(n), mbps=mbps) 80 | n=68; print("sending payload"); sendAllBytes(sock=sock, bytes_=bytes(n), mbps=mbps) 81 | n=1024; print("sending payload"); sendAllBytes(sock=sock, bytes_=bytes(n), mbps=mbps) 82 | for i in range(50): 83 | n=1024*1024; print("sending payload", i); sendAllBytes(sock=sock, bytes_=bytes(n), mbps=mbps) 84 | sock.close() 85 | -------------------------------------------------------------------------------- /docs/_build/html/_sources/authors.rst.txt: -------------------------------------------------------------------------------- 1 | Authors 2 | ------- 3 | 4 | Sampsa Riikonen 5 | -------------------------------------------------------------------------------- /docs/_build/html/_sources/examples.rst.txt: -------------------------------------------------------------------------------- 1 | 2 | .. _examples: 3 | 4 | Examples 5 | ======== 6 | 7 | 1. Hello World 8 | -------------- 9 | 10 | Download example code from :download:`[here]` 11 | 12 | .. include:: snippets/hello_world.py_ 13 | 14 | 2. Parent/Child Structure 15 | ------------------------- 16 | 17 | Download example code from :download:`[here]` 18 | 19 | .. include:: snippets/parent_child.py_ 20 | 21 | 3. TCP Server 22 | ------------- 23 | 24 | - Download server example code from :download:`[here]` 25 | - Download client side code from :download:`[here]` 26 | 27 | .. include:: snippets/example_server.py_ 28 | 29 | 4. Data Streaming TCP Server 30 | ---------------------------- 31 | 32 | - Download example code from :download:`[here]` 33 | - Download client side code from :download:`[here]` 34 | 35 | .. include:: snippets/example_server2.py_ 36 | 37 | -------------------------------------------------------------------------------- /docs/_build/html/_sources/index.rst.txt: -------------------------------------------------------------------------------- 1 | .. task_thread documentation master file, created by 2 | sphinx-quickstart on Mon Mar 20 16:31:00 2017. 3 | You can adapt this file completely to your liking, but it should at least 4 | contain the root `toctree` directive. 5 | 6 | TaskThread 7 | ========== 8 | 9 | .. meta:: 10 | :description: TaskThread: Asynchronous Programming for Humans 11 | :keywords: python, asyncio 12 | 13 | Contents: 14 | 15 | .. toctree:: 16 | :maxdepth: 2 17 | 18 | intro 19 | thread 20 | task 21 | threadtask 22 | examples 23 | license 24 | authors 25 | -------------------------------------------------------------------------------- /docs/_build/html/_sources/intro.rst.txt: -------------------------------------------------------------------------------- 1 | .. _intro: 2 | 3 | .. A link to :ref:`intro ` 4 | 5 | About TaskThread 6 | ================ 7 | 8 | *Never use asyncio.wait or asyncio.gather again* 9 | 10 | TaskThread is a collection of tools, conventions and a base class that organizes 11 | your asyncio python programs seamlessly into hierarchical, well-organized structures. 12 | 13 | These structures are analogical to threads and have a thread-like API. 14 | 15 | Let's illustrate what all this is about by an example. 16 | 17 | A Sample Problem 18 | ---------------- 19 | 20 | Let's consider a rather complex application which: 21 | 22 | - Exposes a TCP server 23 | - The server accepts various client connections from LAN 24 | - Server processes the incoming data and reconstructs data frames from packets received from individual TCP client connections 25 | - Processed data from each TCP client is forwarded through websockets to a final destination (say, to cloud) 26 | - The processed data is also written into a database 27 | 28 | We can see there is a lot of i/o waiting and "multiplexing" going on that can get messy. 29 | 30 | When using TaskThread, you start unravelling a problem by identifying hierarchies. 31 | 32 | In the present case, a solution, described as a hierarchical list, could look like this: 33 | 34 | :: 35 | 36 | MasterThread 37 | TCPServerThread 38 | TCPConnectionThread 39 | DataProcessorThread 40 | TCPConnectionThread 41 | DataProcessorThread 42 | ... 43 | WebsocketMasterThread 44 | WebSocketSubThread 45 | WebSocketSubThread 46 | ... 47 | DatabaseThread 48 | 49 | 50 | (another kind of hierarchy to solve the problem is, of course, also possible) 51 | 52 | ``MasterThread`` starts the ``TCPServerThread`` which then starts ``TCPConnectionThread`` s on demand. 53 | 54 | Each ``TCPConnectionThread`` starts a ``DataProcessorThread`` which reconstructs the packets from indicidual ``TCPConnectionThread`` s. 55 | 56 | All data flows to upwards in the tree into ``MasterThread`` which then passes it onto to ``WebsocketMasterThread`` and from there to individual 57 | ``WebSocketSubThread`` s. ``MasterThread`` passes the data also to ``DatabaseThread``. 58 | 59 | Let's add this data flow to the hierarchical list: 60 | 61 | :: 62 | 63 | MasterThread 64 | TCPServerThread: UP: data 65 | TCPConnectionThread: UP: data 66 | DataProcessorThread: IN: packets, UP: data 67 | TCPConnectionThread 68 | DataProcessorThread 69 | ... 70 | WebsocketMasterThread: IN: data 71 | WebSocketSubThread: IN data 72 | WebSocketSubThread 73 | ... 74 | DatabaseThread: IN: data 75 | 76 | Here ``UP`` designates data going upwards in the tree, while ``IN`` shows incoming data at deeper level threads. 77 | 78 | Notice that there is not any intercommunication within this tree that is not strictly between a parent and a child. 79 | 80 | Threads, how? 81 | ------------- 82 | 83 | So, how to implement such "threads" ? After all this is asyncio, not multithreading! 84 | 85 | Well, the entities named here ``MasterThread``, ``TCPServerThread``, etc. are not really "threads", but collection of asyncio **tasks** grouped together in a smart way - thus the name TaskThread. 86 | The intercom between the "threads" is done using asyncio **queues**. 87 | 88 | Let's take a deeper look on the **tasks** and **queues**: 89 | 90 | ``DataProcessorThread`` receives packets (via a listening **task**) through a queue from ``TCPConnectionThread``. After reconstructing 91 | some data from the packets using a **task**, ``DataProcessorThread`` has a **task** that sends the dataframes to ``TCPConnectionThread`` through another asyncio **queue** which 92 | ``TCPConnectionThread`` is listening with a **task**, etc. 93 | 94 | These **queues** and listener **tasks** work seamlessly and are hidden from the TaskThread API user. 95 | 96 | At the core of TaskThread philosophy lies **rescheduling tasks**, i.e. asyncio tasks that reschedule themselves, giving the appearance of "threads" and a thread-like API. 97 | 98 | The only thing the API user needs to worry about, is how to initiate, re-schedule and terminate these tasks within their TaskThread implementation. 99 | 100 | Advantages 101 | ---------- 102 | 103 | Have you ever run into a situation where you have a complex asyncio program running tons of simultaneous tasks? 104 | 105 | For example, you need to run ``asyncio.wait`` to "poll" several tasks to see if the tasks have finished or not and then your program's logic is altered based on that result, 106 | creating an asynchronous mess, maybe even runaway tasks. 107 | 108 | Well, you don't need to touch ``asyncio.wait`` or ``asyncio.gather`` ever again, after starting to use TaskThread. 109 | 110 | Your programs will also become naturally well-organized into threads that have 111 | separation of concerns and restricted communication - in accordance with the `HIMO principle `_. 112 | 113 | I hope you got all warmed up by now. Exciting, right!? 114 | 115 | Next we will take a look at :ref:`an anatomy of a TaskThread `. 116 | 117 | Installing 118 | ---------- 119 | 120 | From PyPi simply with: 121 | 122 | :: 123 | 124 | pip3 install --user task-virtualthread 125 | 126 | -------------------------------------------------------------------------------- /docs/_build/html/_sources/license.rst.txt: -------------------------------------------------------------------------------- 1 | Copyright & License 2 | ------------------- 3 | 4 | Copyright 2021-2023 Sampsa Riikonen 5 | 6 | This library is licensed under the `MIT `_ license. 7 | -------------------------------------------------------------------------------- /docs/_build/html/_sources/task.rst.txt: -------------------------------------------------------------------------------- 1 | .. _task: 2 | 3 | Anatomy of a Task 4 | ================= 5 | 6 | How do these "rescheduling tasks" that constitute a "TaskThread" look like? 7 | 8 | Let's consider a silly task that sleeps for 1 sec and then re-schedules itself 9 | 10 | First the imports: 11 | 12 | :: 13 | 14 | import asyncio, logging, traceback 15 | from task_thread import TaskThread, reCreate, reSchedule,\ 16 | delete, verbose, signals 17 | 18 | The rescheduling task itself: 19 | 20 | :: 21 | 22 | async def helloTask__(self): 23 | try: 24 | await asyncio.sleep(1) 25 | self.logger.info("Hello from helloTask__ at %s", self.getId()) 26 | self.tasks.hello_task = await reSchedule(self.helloTask__) 27 | return 28 | 29 | except asyncio.CancelledError: 30 | self.logger.critical("helloTask__: cancelling") 31 | 32 | except Exception as e: 33 | self.logger.info("helloTask__: failed with '%s', traceback will follow", e) 34 | traceback.print_exc() 35 | 36 | with emphasis on this structure: 37 | 38 | :: 39 | 40 | try: 41 | ... 42 | 43 | except asyncio.CancelledError: 44 | ... 45 | 46 | except Exception as e: 47 | ... 48 | 49 | 50 | i.e., if everything is ok - the task has done it's thing, say, reading payload from i/o, 51 | writing i/o, or whatever - it then **reschedules itself**. 52 | 53 | Rescheduling is done using the ``reSchedule`` convenience function: 54 | 55 | :: 56 | 57 | self.tasks.hello_task = await reSchedule(self.helloTask__) 58 | 59 | A large group of these auto-rescheduling tasks that toil around and do their stuff, behave effectively like a "classical" running thread. 60 | 61 | A small warning about task re-scheduling is necessary: 62 | 63 | A function can re-schedule itself with quite a high frequency. However, for each re-scheduling task, you should be at least, aware of the frequency: 64 | if a task re-schedules itself, say, a million times per second, you have created yourself a problem. 65 | 66 | Try to keep your task's re-scheduling frequency in 100 times per second or less. High re-scheduling frequency and its mitigation might become an issue in streaming applications, 67 | while in most other cases you really don't need to think about it. 68 | 69 | Finally, remember to use the correct convenience function for each case: 70 | 71 | - When creating the task for the first time, use ``reCreate`` 72 | - When rescheduling the task, use ``reSchedule`` 73 | - When removing the task, use ``delete`` 74 | 75 | Next, let's :ref:`bring TaskThreads and tasks together `. 76 | 77 | -------------------------------------------------------------------------------- /docs/_build/html/_sources/thread.rst.txt: -------------------------------------------------------------------------------- 1 | 2 | .. _thread: 3 | 4 | Anatomy of a TaskThread 5 | ======================= 6 | 7 | Imports 8 | ------- 9 | 10 | The necessary imports for writing your TaskThread are: 11 | 12 | :: 13 | 14 | import asyncio, logging, traceback 15 | from task_thread import TaskThread, reCreate, reSchedule,\ 16 | delete, verbose, signals 17 | 18 | 19 | Subclassing 20 | ----------- 21 | 22 | You create a custom thread by subclassing from the ``TaskThread`` base class. 23 | 24 | The methods in ``TaskThread`` that you must override are: 25 | 26 | :: 27 | 28 | 29 | def __init__(self, parent = None) 30 | 31 | def initVars__(self) 32 | 33 | async def enter__(self) 34 | 35 | async def exit__(self) 36 | 37 | async def signalHandler__(self, signal) 38 | 39 | async def childsignalHandler__(self, signal, child) 40 | 41 | def getId(self) 42 | 43 | def getInfo(self) 44 | 45 | 46 | Let's take a closer look at each one of these methods. 47 | 48 | In the ``__init__`` you *must* call the superclass method (similar to ``threading`` and ``multiprocess`` modules): 49 | 50 | :: 51 | 52 | def __init__(self, parent = None): 53 | super().__init__(parent = parent) 54 | # whatever extra stuff 55 | 56 | In ``initVars`` you define the rescheduling tasks that define the functionality of your 57 | TaskThread: 58 | 59 | :: 60 | 61 | def initVars__(self): 62 | """Create & initialize here your tasks with none & create your locks 63 | """ 64 | self.tasks.writer = None 65 | self.tasks.reader = None 66 | # etc. 67 | self.locks.writer = asyncio.Lock() 68 | # etc. 69 | 70 | Notice how the tasks are organized under their own namespace ``self.tasks``. 71 | All tasks are initialized as ``None``. There is a separate convenience 72 | namespace ``self.locks`` for asyncio locks. 73 | 74 | The starting point for your thread is defined in ``enter__``: 75 | 76 | :: 77 | 78 | @verbose 79 | async def enter__(self): 80 | self.logger.info("enter__ : %s", self.getInfo()) 81 | self.tasks.writer = await reCreate(self.tasks.writer, self.readerMethod__) 82 | self.tasks.reader = await reCreate(self.tasks.reader, self.writerMethod__) 83 | 84 | Here we use a special decorator ``@verbose`` that makes life with asyncio a bit easier - it catches 85 | some exceptions explicitly for you. 86 | 87 | We start the rescheduled tasks using the convenience function ``reCreate``. The target 88 | of the task is ``self.readerMethod__`` where the task is defined (more on this in the 89 | section :ref:`about tasks `). 90 | 91 | Next, you still remember the hierarchical way the threads are organized and how they 92 | communicate? ``signalHandler__`` defines what the TaskThread should do when it gets 93 | a message/data **from a parent**: 94 | 95 | :: 96 | 97 | @verbose 98 | async def signalHandler__(self, signal): 99 | self.logger.info("signalHandler__ : got signal %s from parent", signal) 100 | 101 | The implementation of this method depends, of course, completely on your TaskThread's 102 | functionality. 103 | 104 | A thread must also know what to do when it gets a signal from a child. This is defined 105 | in ``childsignalHandler__``: 106 | 107 | :: 108 | 109 | @verbose 110 | async def childsignalHandler__(self, signal, child): 111 | self.logger.debug("childsignalHandler__ : got signal %s from child %s", signal, child.getId()) 112 | 113 | Finally, ``getId`` returns some unique string or int corresponding to this TaskThread 114 | (nice for search/organizational purposes), while ``getInfo`` returns a string representation 115 | of the TaskThread (i.e. like ``__str__``). 116 | 117 | You could write: 118 | 119 | :: 120 | 121 | def getId(self): 122 | return str(id(self)) 123 | 124 | def getInfo(self): 125 | return "" 126 | 127 | 128 | API Methods 129 | ----------- 130 | 131 | By using subclassing, we have defined what our TaskThread *does*. 132 | Next we take a look at the API methods, i.e. how to *use* a TaskThread. 133 | 134 | Let's take a quick overview of the available methods in the TaskThread class. 135 | 136 | However, in order to know how to *really* use these methods, you need to go through the :ref:`examples `. 137 | 138 | A TaskThread is created like this: 139 | 140 | :: 141 | 142 | thread = MyThread(parent = parent) 143 | 144 | 145 | Start running it with: 146 | 147 | :: 148 | 149 | await thread.run() 150 | 151 | Stop with: 152 | 153 | :: 154 | 155 | await thread.stop() 156 | 157 | and wait until it has finished: 158 | 159 | :: 160 | 161 | await thread.join() 162 | 163 | A child thread can terminate itself, by calling ``self.stop()``. 164 | 165 | Stopping a child automatically deregisters / removes it from any listening parent. 166 | 167 | You can add a child thread to a parent thread: 168 | 169 | :: 170 | 171 | await thread.addChild(child) 172 | 173 | After that, parent starts listening any signals from the child. 174 | 175 | Finding a child, based on it's id, as returned by it's ``getId()`` method is done with: 176 | 177 | :: 178 | 179 | await thread.findChild(_id = _id) 180 | 181 | Sending a signal from parent to child, i.e. down/deeper in the hierarchical parent/child 182 | structure: 183 | 184 | :: 185 | 186 | await thread.sigFromParentToChild__(signal, child) 187 | 188 | If child is replaced by ``None``, the same signal is sent to all children. 189 | 190 | Sending a signal the other way around: from children to parent, i.e. upwards in the tree: 191 | 192 | :: 193 | 194 | await thread.sigFromChildToParent__(signal) 195 | 196 | 197 | Signals 198 | ------- 199 | 200 | Signals are those things that go to and fro between parent and child threads. 201 | 202 | A typical signal looks like this: 203 | 204 | :: 205 | 206 | class MessageSignal(signals.Signal): 207 | """A generic message message signal, carrying a python object 208 | """ 209 | def __init__(self, message): 210 | self.message = message 211 | 212 | def __str__(self): 213 | return "" % (str(self.message)) 214 | 215 | def getMessage(self): 216 | return self.message 217 | 218 | def __call__(self): 219 | """syntactic sugar""" 220 | return self.getMessage() 221 | 222 | Signals can carry messages, byte payload, whatever. 223 | 224 | Next, let's take a closer look at :ref:`tasks `. 225 | -------------------------------------------------------------------------------- /docs/_build/html/_sources/threadtask.rst.txt: -------------------------------------------------------------------------------- 1 | 2 | .. _threadtask: 3 | 4 | Threads and Tasks 5 | ================= 6 | 7 | So, now you have been initiated on how to create a TaskThread and rescheduling tasks and signals. 8 | 9 | Let's bring it all together. 10 | 11 | :: 12 | 13 | import asyncio, logging, traceback 14 | from task_thread import TaskThread, reCreate, reSchedule,\ 15 | delete, verbose, signals 16 | 17 | Subclass the necessary methods to specify a TaskThread: 18 | 19 | :: 20 | 21 | class MyThread(TaskThread): 22 | 23 | def __init__(self, my_id = 0, parent = None): 24 | super().__init__(parent = parent) 25 | self.my_id = my_id 26 | 27 | List your tasks: 28 | 29 | :: 30 | 31 | def initVars__(self): 32 | self.tasks.hello = None # implementation in "self.helloTask__" 33 | 34 | ``enter__`` starts the task for the first time: 35 | 36 | :: 37 | 38 | @verbose 39 | async def enter__(self): 40 | self.logger.info("enter__") 41 | self.tasks.hello = await reCreate(self.tasks.hello, self.helloTask__) 42 | 43 | In ``exit__``, kill the re-scheduling tasks: 44 | 45 | :: 46 | 47 | @verbose 48 | async def exit__(self): 49 | self.tasks.hello = await delete(self.tasks.hello) 50 | self.logger.info("exit__ : finished") 51 | 52 | For the moment, no signal handling: 53 | 54 | :: 55 | 56 | @verbose 57 | async def signalHandler__(self, signal): 58 | self.logger.info("signalHandler__ : got signal %s", signal) 59 | 60 | Rest of the methods: 61 | 62 | :: 63 | 64 | def getId(self): 65 | return self.my_id 66 | 67 | def getInfo(self): 68 | return "" 69 | 70 | 71 | Finally, define the (only) task that is running in this TaskThread: 72 | 73 | :: 74 | 75 | async def helloTask__(self): 76 | """A task that prints hello & re-schedules itself 77 | """ 78 | try: 79 | await asyncio.sleep(1) 80 | self.logger.info("Hello from helloTask__ at %s", self.getId()) 81 | self.tasks.hello_task = await reSchedule(self.helloTask__) 82 | return 83 | 84 | except asyncio.CancelledError: 85 | self.logger.critical("helloTask__: cancelling") 86 | 87 | except Exception as e: 88 | self.logger.info("helloTask__: failed with '%s', traceback will follow", e) 89 | traceback.print_exc() 90 | 91 | Finaly, the program runs like this: 92 | 93 | :: 94 | 95 | loglev = logging.DEBUG 96 | 97 | logger = logging.getLogger("MyThread") 98 | logger.setLevel(loglev) 99 | 100 | thread = MyThread(my_id = "main_thread", parent = None) 101 | loop = asyncio.get_event_loop() 102 | loop.run_until_complete(thread.run()) 103 | 104 | What next? 105 | 106 | To create a real program that does anything worthwhile, you still need to study 107 | and understand the :ref:`examples `. 108 | -------------------------------------------------------------------------------- /docs/_build/html/_static/_sphinx_javascript_frameworks_compat.js: -------------------------------------------------------------------------------- 1 | /* Compatability shim for jQuery and underscores.js. 2 | * 3 | * Copyright Sphinx contributors 4 | * Released under the two clause BSD licence 5 | */ 6 | 7 | /** 8 | * small helper function to urldecode strings 9 | * 10 | * See https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/decodeURIComponent#Decoding_query_parameters_from_a_URL 11 | */ 12 | jQuery.urldecode = function(x) { 13 | if (!x) { 14 | return x 15 | } 16 | return decodeURIComponent(x.replace(/\+/g, ' ')); 17 | }; 18 | 19 | /** 20 | * small helper function to urlencode strings 21 | */ 22 | jQuery.urlencode = encodeURIComponent; 23 | 24 | /** 25 | * This function returns the parsed url parameters of the 26 | * current request. Multiple values per key are supported, 27 | * it will always return arrays of strings for the value parts. 28 | */ 29 | jQuery.getQueryParameters = function(s) { 30 | if (typeof s === 'undefined') 31 | s = document.location.search; 32 | var parts = s.substr(s.indexOf('?') + 1).split('&'); 33 | var result = {}; 34 | for (var i = 0; i < parts.length; i++) { 35 | var tmp = parts[i].split('=', 2); 36 | var key = jQuery.urldecode(tmp[0]); 37 | var value = jQuery.urldecode(tmp[1]); 38 | if (key in result) 39 | result[key].push(value); 40 | else 41 | result[key] = [value]; 42 | } 43 | return result; 44 | }; 45 | 46 | /** 47 | * highlight a given string on a jquery object by wrapping it in 48 | * span elements with the given class name. 49 | */ 50 | jQuery.fn.highlightText = function(text, className) { 51 | function highlight(node, addItems) { 52 | if (node.nodeType === 3) { 53 | var val = node.nodeValue; 54 | var pos = val.toLowerCase().indexOf(text); 55 | if (pos >= 0 && 56 | !jQuery(node.parentNode).hasClass(className) && 57 | !jQuery(node.parentNode).hasClass("nohighlight")) { 58 | var span; 59 | var isInSVG = jQuery(node).closest("body, svg, foreignObject").is("svg"); 60 | if (isInSVG) { 61 | span = document.createElementNS("http://www.w3.org/2000/svg", "tspan"); 62 | } else { 63 | span = document.createElement("span"); 64 | span.className = className; 65 | } 66 | span.appendChild(document.createTextNode(val.substr(pos, text.length))); 67 | node.parentNode.insertBefore(span, node.parentNode.insertBefore( 68 | document.createTextNode(val.substr(pos + text.length)), 69 | node.nextSibling)); 70 | node.nodeValue = val.substr(0, pos); 71 | if (isInSVG) { 72 | var rect = document.createElementNS("http://www.w3.org/2000/svg", "rect"); 73 | var bbox = node.parentElement.getBBox(); 74 | rect.x.baseVal.value = bbox.x; 75 | rect.y.baseVal.value = bbox.y; 76 | rect.width.baseVal.value = bbox.width; 77 | rect.height.baseVal.value = bbox.height; 78 | rect.setAttribute('class', className); 79 | addItems.push({ 80 | "parent": node.parentNode, 81 | "target": rect}); 82 | } 83 | } 84 | } 85 | else if (!jQuery(node).is("button, select, textarea")) { 86 | jQuery.each(node.childNodes, function() { 87 | highlight(this, addItems); 88 | }); 89 | } 90 | } 91 | var addItems = []; 92 | var result = this.each(function() { 93 | highlight(this, addItems); 94 | }); 95 | for (var i = 0; i < addItems.length; ++i) { 96 | jQuery(addItems[i].parent).before(addItems[i].target); 97 | } 98 | return result; 99 | }; 100 | 101 | /* 102 | * backward compatibility for jQuery.browser 103 | * This will be supported until firefox bug is fixed. 104 | */ 105 | if (!jQuery.browser) { 106 | jQuery.uaMatch = function(ua) { 107 | ua = ua.toLowerCase(); 108 | 109 | var match = /(chrome)[ \/]([\w.]+)/.exec(ua) || 110 | /(webkit)[ \/]([\w.]+)/.exec(ua) || 111 | /(opera)(?:.*version|)[ \/]([\w.]+)/.exec(ua) || 112 | /(msie) ([\w.]+)/.exec(ua) || 113 | ua.indexOf("compatible") < 0 && /(mozilla)(?:.*? rv:([\w.]+)|)/.exec(ua) || 114 | []; 115 | 116 | return { 117 | browser: match[ 1 ] || "", 118 | version: match[ 2 ] || "0" 119 | }; 120 | }; 121 | jQuery.browser = {}; 122 | jQuery.browser[jQuery.uaMatch(navigator.userAgent).browser] = true; 123 | } 124 | -------------------------------------------------------------------------------- /docs/_build/html/_static/css/badge_only.css: -------------------------------------------------------------------------------- 1 | .clearfix{*zoom:1}.clearfix:after,.clearfix:before{display:table;content:""}.clearfix:after{clear:both}@font-face{font-family:FontAwesome;font-style:normal;font-weight:400;src:url(fonts/fontawesome-webfont.eot?674f50d287a8c48dc19ba404d20fe713?#iefix) format("embedded-opentype"),url(fonts/fontawesome-webfont.woff2?af7ae505a9eed503f8b8e6982036873e) format("woff2"),url(fonts/fontawesome-webfont.woff?fee66e712a8a08eef5805a46892932ad) format("woff"),url(fonts/fontawesome-webfont.ttf?b06871f281fee6b241d60582ae9369b9) format("truetype"),url(fonts/fontawesome-webfont.svg?912ec66d7572ff821749319396470bde#FontAwesome) format("svg")}.fa:before{font-family:FontAwesome;font-style:normal;font-weight:400;line-height:1}.fa:before,a .fa{text-decoration:inherit}.fa:before,a .fa,li .fa{display:inline-block}li .fa-large:before{width:1.875em}ul.fas{list-style-type:none;margin-left:2em;text-indent:-.8em}ul.fas li .fa{width:.8em}ul.fas li .fa-large:before{vertical-align:baseline}.fa-book:before,.icon-book:before{content:"\f02d"}.fa-caret-down:before,.icon-caret-down:before{content:"\f0d7"}.fa-caret-up:before,.icon-caret-up:before{content:"\f0d8"}.fa-caret-left:before,.icon-caret-left:before{content:"\f0d9"}.fa-caret-right:before,.icon-caret-right:before{content:"\f0da"}.rst-versions{position:fixed;bottom:0;left:0;width:300px;color:#fcfcfc;background:#1f1d1d;font-family:Lato,proxima-nova,Helvetica Neue,Arial,sans-serif;z-index:400}.rst-versions a{color:#2980b9;text-decoration:none}.rst-versions .rst-badge-small{display:none}.rst-versions .rst-current-version{padding:12px;background-color:#272525;display:block;text-align:right;font-size:90%;cursor:pointer;color:#27ae60}.rst-versions .rst-current-version:after{clear:both;content:"";display:block}.rst-versions .rst-current-version .fa{color:#fcfcfc}.rst-versions .rst-current-version .fa-book,.rst-versions .rst-current-version .icon-book{float:left}.rst-versions .rst-current-version.rst-out-of-date{background-color:#e74c3c;color:#fff}.rst-versions .rst-current-version.rst-active-old-version{background-color:#f1c40f;color:#000}.rst-versions.shift-up{height:auto;max-height:100%;overflow-y:scroll}.rst-versions.shift-up .rst-other-versions{display:block}.rst-versions .rst-other-versions{font-size:90%;padding:12px;color:grey;display:none}.rst-versions .rst-other-versions hr{display:block;height:1px;border:0;margin:20px 0;padding:0;border-top:1px solid #413d3d}.rst-versions .rst-other-versions dd{display:inline-block;margin:0}.rst-versions .rst-other-versions dd a{display:inline-block;padding:6px;color:#fcfcfc}.rst-versions.rst-badge{width:auto;bottom:20px;right:20px;left:auto;border:none;max-width:300px;max-height:90%}.rst-versions.rst-badge .fa-book,.rst-versions.rst-badge .icon-book{float:none;line-height:30px}.rst-versions.rst-badge.shift-up .rst-current-version{text-align:right}.rst-versions.rst-badge.shift-up .rst-current-version .fa-book,.rst-versions.rst-badge.shift-up .rst-current-version .icon-book{float:left}.rst-versions.rst-badge>.rst-current-version{width:auto;height:30px;line-height:30px;padding:0 6px;display:block;text-align:center}@media screen and (max-width:768px){.rst-versions{width:85%;display:none}.rst-versions.shift{display:block}} -------------------------------------------------------------------------------- /docs/_build/html/_static/css/fonts/Roboto-Slab-Bold.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elsampsa/task_thread/14905bfb9cfaf82c6bc905443ed56e95a7913f14/docs/_build/html/_static/css/fonts/Roboto-Slab-Bold.woff -------------------------------------------------------------------------------- /docs/_build/html/_static/css/fonts/Roboto-Slab-Bold.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elsampsa/task_thread/14905bfb9cfaf82c6bc905443ed56e95a7913f14/docs/_build/html/_static/css/fonts/Roboto-Slab-Bold.woff2 -------------------------------------------------------------------------------- /docs/_build/html/_static/css/fonts/Roboto-Slab-Regular.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elsampsa/task_thread/14905bfb9cfaf82c6bc905443ed56e95a7913f14/docs/_build/html/_static/css/fonts/Roboto-Slab-Regular.woff -------------------------------------------------------------------------------- /docs/_build/html/_static/css/fonts/Roboto-Slab-Regular.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elsampsa/task_thread/14905bfb9cfaf82c6bc905443ed56e95a7913f14/docs/_build/html/_static/css/fonts/Roboto-Slab-Regular.woff2 -------------------------------------------------------------------------------- /docs/_build/html/_static/css/fonts/fontawesome-webfont.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elsampsa/task_thread/14905bfb9cfaf82c6bc905443ed56e95a7913f14/docs/_build/html/_static/css/fonts/fontawesome-webfont.eot -------------------------------------------------------------------------------- /docs/_build/html/_static/css/fonts/fontawesome-webfont.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elsampsa/task_thread/14905bfb9cfaf82c6bc905443ed56e95a7913f14/docs/_build/html/_static/css/fonts/fontawesome-webfont.ttf -------------------------------------------------------------------------------- /docs/_build/html/_static/css/fonts/fontawesome-webfont.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elsampsa/task_thread/14905bfb9cfaf82c6bc905443ed56e95a7913f14/docs/_build/html/_static/css/fonts/fontawesome-webfont.woff -------------------------------------------------------------------------------- /docs/_build/html/_static/css/fonts/fontawesome-webfont.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elsampsa/task_thread/14905bfb9cfaf82c6bc905443ed56e95a7913f14/docs/_build/html/_static/css/fonts/fontawesome-webfont.woff2 -------------------------------------------------------------------------------- /docs/_build/html/_static/css/fonts/lato-bold-italic.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elsampsa/task_thread/14905bfb9cfaf82c6bc905443ed56e95a7913f14/docs/_build/html/_static/css/fonts/lato-bold-italic.woff -------------------------------------------------------------------------------- /docs/_build/html/_static/css/fonts/lato-bold-italic.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elsampsa/task_thread/14905bfb9cfaf82c6bc905443ed56e95a7913f14/docs/_build/html/_static/css/fonts/lato-bold-italic.woff2 -------------------------------------------------------------------------------- /docs/_build/html/_static/css/fonts/lato-bold.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elsampsa/task_thread/14905bfb9cfaf82c6bc905443ed56e95a7913f14/docs/_build/html/_static/css/fonts/lato-bold.woff -------------------------------------------------------------------------------- /docs/_build/html/_static/css/fonts/lato-bold.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elsampsa/task_thread/14905bfb9cfaf82c6bc905443ed56e95a7913f14/docs/_build/html/_static/css/fonts/lato-bold.woff2 -------------------------------------------------------------------------------- /docs/_build/html/_static/css/fonts/lato-normal-italic.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elsampsa/task_thread/14905bfb9cfaf82c6bc905443ed56e95a7913f14/docs/_build/html/_static/css/fonts/lato-normal-italic.woff -------------------------------------------------------------------------------- /docs/_build/html/_static/css/fonts/lato-normal-italic.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elsampsa/task_thread/14905bfb9cfaf82c6bc905443ed56e95a7913f14/docs/_build/html/_static/css/fonts/lato-normal-italic.woff2 -------------------------------------------------------------------------------- /docs/_build/html/_static/css/fonts/lato-normal.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elsampsa/task_thread/14905bfb9cfaf82c6bc905443ed56e95a7913f14/docs/_build/html/_static/css/fonts/lato-normal.woff -------------------------------------------------------------------------------- /docs/_build/html/_static/css/fonts/lato-normal.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elsampsa/task_thread/14905bfb9cfaf82c6bc905443ed56e95a7913f14/docs/_build/html/_static/css/fonts/lato-normal.woff2 -------------------------------------------------------------------------------- /docs/_build/html/_static/doctools.js: -------------------------------------------------------------------------------- 1 | /* 2 | * doctools.js 3 | * ~~~~~~~~~~~ 4 | * 5 | * Base JavaScript utilities for all Sphinx HTML documentation. 6 | * 7 | * :copyright: Copyright 2007-2023 by the Sphinx team, see AUTHORS. 8 | * :license: BSD, see LICENSE for details. 9 | * 10 | */ 11 | "use strict"; 12 | 13 | const BLACKLISTED_KEY_CONTROL_ELEMENTS = new Set([ 14 | "TEXTAREA", 15 | "INPUT", 16 | "SELECT", 17 | "BUTTON", 18 | ]); 19 | 20 | const _ready = (callback) => { 21 | if (document.readyState !== "loading") { 22 | callback(); 23 | } else { 24 | document.addEventListener("DOMContentLoaded", callback); 25 | } 26 | }; 27 | 28 | /** 29 | * Small JavaScript module for the documentation. 30 | */ 31 | const Documentation = { 32 | init: () => { 33 | Documentation.initDomainIndexTable(); 34 | Documentation.initOnKeyListeners(); 35 | }, 36 | 37 | /** 38 | * i18n support 39 | */ 40 | TRANSLATIONS: {}, 41 | PLURAL_EXPR: (n) => (n === 1 ? 0 : 1), 42 | LOCALE: "unknown", 43 | 44 | // gettext and ngettext don't access this so that the functions 45 | // can safely bound to a different name (_ = Documentation.gettext) 46 | gettext: (string) => { 47 | const translated = Documentation.TRANSLATIONS[string]; 48 | switch (typeof translated) { 49 | case "undefined": 50 | return string; // no translation 51 | case "string": 52 | return translated; // translation exists 53 | default: 54 | return translated[0]; // (singular, plural) translation tuple exists 55 | } 56 | }, 57 | 58 | ngettext: (singular, plural, n) => { 59 | const translated = Documentation.TRANSLATIONS[singular]; 60 | if (typeof translated !== "undefined") 61 | return translated[Documentation.PLURAL_EXPR(n)]; 62 | return n === 1 ? singular : plural; 63 | }, 64 | 65 | addTranslations: (catalog) => { 66 | Object.assign(Documentation.TRANSLATIONS, catalog.messages); 67 | Documentation.PLURAL_EXPR = new Function( 68 | "n", 69 | `return (${catalog.plural_expr})` 70 | ); 71 | Documentation.LOCALE = catalog.locale; 72 | }, 73 | 74 | /** 75 | * helper function to focus on search bar 76 | */ 77 | focusSearchBar: () => { 78 | document.querySelectorAll("input[name=q]")[0]?.focus(); 79 | }, 80 | 81 | /** 82 | * Initialise the domain index toggle buttons 83 | */ 84 | initDomainIndexTable: () => { 85 | const toggler = (el) => { 86 | const idNumber = el.id.substr(7); 87 | const toggledRows = document.querySelectorAll(`tr.cg-${idNumber}`); 88 | if (el.src.substr(-9) === "minus.png") { 89 | el.src = `${el.src.substr(0, el.src.length - 9)}plus.png`; 90 | toggledRows.forEach((el) => (el.style.display = "none")); 91 | } else { 92 | el.src = `${el.src.substr(0, el.src.length - 8)}minus.png`; 93 | toggledRows.forEach((el) => (el.style.display = "")); 94 | } 95 | }; 96 | 97 | const togglerElements = document.querySelectorAll("img.toggler"); 98 | togglerElements.forEach((el) => 99 | el.addEventListener("click", (event) => toggler(event.currentTarget)) 100 | ); 101 | togglerElements.forEach((el) => (el.style.display = "")); 102 | if (DOCUMENTATION_OPTIONS.COLLAPSE_INDEX) togglerElements.forEach(toggler); 103 | }, 104 | 105 | initOnKeyListeners: () => { 106 | // only install a listener if it is really needed 107 | if ( 108 | !DOCUMENTATION_OPTIONS.NAVIGATION_WITH_KEYS && 109 | !DOCUMENTATION_OPTIONS.ENABLE_SEARCH_SHORTCUTS 110 | ) 111 | return; 112 | 113 | document.addEventListener("keydown", (event) => { 114 | // bail for input elements 115 | if (BLACKLISTED_KEY_CONTROL_ELEMENTS.has(document.activeElement.tagName)) return; 116 | // bail with special keys 117 | if (event.altKey || event.ctrlKey || event.metaKey) return; 118 | 119 | if (!event.shiftKey) { 120 | switch (event.key) { 121 | case "ArrowLeft": 122 | if (!DOCUMENTATION_OPTIONS.NAVIGATION_WITH_KEYS) break; 123 | 124 | const prevLink = document.querySelector('link[rel="prev"]'); 125 | if (prevLink && prevLink.href) { 126 | window.location.href = prevLink.href; 127 | event.preventDefault(); 128 | } 129 | break; 130 | case "ArrowRight": 131 | if (!DOCUMENTATION_OPTIONS.NAVIGATION_WITH_KEYS) break; 132 | 133 | const nextLink = document.querySelector('link[rel="next"]'); 134 | if (nextLink && nextLink.href) { 135 | window.location.href = nextLink.href; 136 | event.preventDefault(); 137 | } 138 | break; 139 | } 140 | } 141 | 142 | // some keyboard layouts may need Shift to get / 143 | switch (event.key) { 144 | case "/": 145 | if (!DOCUMENTATION_OPTIONS.ENABLE_SEARCH_SHORTCUTS) break; 146 | Documentation.focusSearchBar(); 147 | event.preventDefault(); 148 | } 149 | }); 150 | }, 151 | }; 152 | 153 | // quick alias for translations 154 | const _ = Documentation.gettext; 155 | 156 | _ready(Documentation.init); 157 | -------------------------------------------------------------------------------- /docs/_build/html/_static/documentation_options.js: -------------------------------------------------------------------------------- 1 | const DOCUMENTATION_OPTIONS = { 2 | VERSION: '', 3 | LANGUAGE: 'en', 4 | COLLAPSE_INDEX: false, 5 | BUILDER: 'html', 6 | FILE_SUFFIX: '.html', 7 | LINK_SUFFIX: '.html', 8 | HAS_SOURCE: true, 9 | SOURCELINK_SUFFIX: '.txt', 10 | NAVIGATION_WITH_KEYS: false, 11 | SHOW_SEARCH_SUMMARY: true, 12 | ENABLE_SEARCH_SHORTCUTS: true, 13 | }; -------------------------------------------------------------------------------- /docs/_build/html/_static/file.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elsampsa/task_thread/14905bfb9cfaf82c6bc905443ed56e95a7913f14/docs/_build/html/_static/file.png -------------------------------------------------------------------------------- /docs/_build/html/_static/js/badge_only.js: -------------------------------------------------------------------------------- 1 | !function(e){var t={};function r(n){if(t[n])return t[n].exports;var o=t[n]={i:n,l:!1,exports:{}};return e[n].call(o.exports,o,o.exports,r),o.l=!0,o.exports}r.m=e,r.c=t,r.d=function(e,t,n){r.o(e,t)||Object.defineProperty(e,t,{enumerable:!0,get:n})},r.r=function(e){"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},r.t=function(e,t){if(1&t&&(e=r(e)),8&t)return e;if(4&t&&"object"==typeof e&&e&&e.__esModule)return e;var n=Object.create(null);if(r.r(n),Object.defineProperty(n,"default",{enumerable:!0,value:e}),2&t&&"string"!=typeof e)for(var o in e)r.d(n,o,function(t){return e[t]}.bind(null,o));return n},r.n=function(e){var t=e&&e.__esModule?function(){return e.default}:function(){return e};return r.d(t,"a",t),t},r.o=function(e,t){return Object.prototype.hasOwnProperty.call(e,t)},r.p="",r(r.s=4)}({4:function(e,t,r){}}); -------------------------------------------------------------------------------- /docs/_build/html/_static/js/html5shiv-printshiv.min.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @preserve HTML5 Shiv 3.7.3-pre | @afarkas @jdalton @jon_neal @rem | MIT/GPL2 Licensed 3 | */ 4 | !function(a,b){function c(a,b){var c=a.createElement("p"),d=a.getElementsByTagName("head")[0]||a.documentElement;return c.innerHTML="x",d.insertBefore(c.lastChild,d.firstChild)}function d(){var a=y.elements;return"string"==typeof a?a.split(" "):a}function e(a,b){var c=y.elements;"string"!=typeof c&&(c=c.join(" ")),"string"!=typeof a&&(a=a.join(" ")),y.elements=c+" "+a,j(b)}function f(a){var b=x[a[v]];return b||(b={},w++,a[v]=w,x[w]=b),b}function g(a,c,d){if(c||(c=b),q)return c.createElement(a);d||(d=f(c));var e;return e=d.cache[a]?d.cache[a].cloneNode():u.test(a)?(d.cache[a]=d.createElem(a)).cloneNode():d.createElem(a),!e.canHaveChildren||t.test(a)||e.tagUrn?e:d.frag.appendChild(e)}function h(a,c){if(a||(a=b),q)return a.createDocumentFragment();c=c||f(a);for(var e=c.frag.cloneNode(),g=0,h=d(),i=h.length;i>g;g++)e.createElement(h[g]);return e}function i(a,b){b.cache||(b.cache={},b.createElem=a.createElement,b.createFrag=a.createDocumentFragment,b.frag=b.createFrag()),a.createElement=function(c){return y.shivMethods?g(c,a,b):b.createElem(c)},a.createDocumentFragment=Function("h,f","return function(){var n=f.cloneNode(),c=n.createElement;h.shivMethods&&("+d().join().replace(/[\w\-:]+/g,function(a){return b.createElem(a),b.frag.createElement(a),'c("'+a+'")'})+");return n}")(y,b.frag)}function j(a){a||(a=b);var d=f(a);return!y.shivCSS||p||d.hasCSS||(d.hasCSS=!!c(a,"article,aside,dialog,figcaption,figure,footer,header,hgroup,main,nav,section{display:block}mark{background:#FF0;color:#000}template{display:none}")),q||i(a,d),a}function k(a){for(var b,c=a.getElementsByTagName("*"),e=c.length,f=RegExp("^(?:"+d().join("|")+")$","i"),g=[];e--;)b=c[e],f.test(b.nodeName)&&g.push(b.applyElement(l(b)));return g}function l(a){for(var b,c=a.attributes,d=c.length,e=a.ownerDocument.createElement(A+":"+a.nodeName);d--;)b=c[d],b.specified&&e.setAttribute(b.nodeName,b.nodeValue);return e.style.cssText=a.style.cssText,e}function m(a){for(var b,c=a.split("{"),e=c.length,f=RegExp("(^|[\\s,>+~])("+d().join("|")+")(?=[[\\s,>+~#.:]|$)","gi"),g="$1"+A+"\\:$2";e--;)b=c[e]=c[e].split("}"),b[b.length-1]=b[b.length-1].replace(f,g),c[e]=b.join("}");return c.join("{")}function n(a){for(var b=a.length;b--;)a[b].removeNode()}function o(a){function b(){clearTimeout(g._removeSheetTimer),d&&d.removeNode(!0),d=null}var d,e,g=f(a),h=a.namespaces,i=a.parentWindow;return!B||a.printShived?a:("undefined"==typeof h[A]&&h.add(A),i.attachEvent("onbeforeprint",function(){b();for(var f,g,h,i=a.styleSheets,j=[],l=i.length,n=Array(l);l--;)n[l]=i[l];for(;h=n.pop();)if(!h.disabled&&z.test(h.media)){try{f=h.imports,g=f.length}catch(o){g=0}for(l=0;g>l;l++)n.push(f[l]);try{j.push(h.cssText)}catch(o){}}j=m(j.reverse().join("")),e=k(a),d=c(a,j)}),i.attachEvent("onafterprint",function(){n(e),clearTimeout(g._removeSheetTimer),g._removeSheetTimer=setTimeout(b,500)}),a.printShived=!0,a)}var p,q,r="3.7.3",s=a.html5||{},t=/^<|^(?:button|map|select|textarea|object|iframe|option|optgroup)$/i,u=/^(?:a|b|code|div|fieldset|h1|h2|h3|h4|h5|h6|i|label|li|ol|p|q|span|strong|style|table|tbody|td|th|tr|ul)$/i,v="_html5shiv",w=0,x={};!function(){try{var a=b.createElement("a");a.innerHTML="",p="hidden"in a,q=1==a.childNodes.length||function(){b.createElement("a");var a=b.createDocumentFragment();return"undefined"==typeof a.cloneNode||"undefined"==typeof a.createDocumentFragment||"undefined"==typeof a.createElement}()}catch(c){p=!0,q=!0}}();var y={elements:s.elements||"abbr article aside audio bdi canvas data datalist details dialog figcaption figure footer header hgroup main mark meter nav output picture progress section summary template time video",version:r,shivCSS:s.shivCSS!==!1,supportsUnknownElements:q,shivMethods:s.shivMethods!==!1,type:"default",shivDocument:j,createElement:g,createDocumentFragment:h,addElements:e};a.html5=y,j(b);var z=/^$|\b(?:all|print)\b/,A="html5shiv",B=!q&&function(){var c=b.documentElement;return!("undefined"==typeof b.namespaces||"undefined"==typeof b.parentWindow||"undefined"==typeof c.applyElement||"undefined"==typeof c.removeNode||"undefined"==typeof a.attachEvent)}();y.type+=" print",y.shivPrint=o,o(b),"object"==typeof module&&module.exports&&(module.exports=y)}("undefined"!=typeof window?window:this,document); -------------------------------------------------------------------------------- /docs/_build/html/_static/js/html5shiv.min.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @preserve HTML5 Shiv 3.7.3 | @afarkas @jdalton @jon_neal @rem | MIT/GPL2 Licensed 3 | */ 4 | !function(a,b){function c(a,b){var c=a.createElement("p"),d=a.getElementsByTagName("head")[0]||a.documentElement;return c.innerHTML="x",d.insertBefore(c.lastChild,d.firstChild)}function d(){var a=t.elements;return"string"==typeof a?a.split(" "):a}function e(a,b){var c=t.elements;"string"!=typeof c&&(c=c.join(" ")),"string"!=typeof a&&(a=a.join(" ")),t.elements=c+" "+a,j(b)}function f(a){var b=s[a[q]];return b||(b={},r++,a[q]=r,s[r]=b),b}function g(a,c,d){if(c||(c=b),l)return c.createElement(a);d||(d=f(c));var e;return e=d.cache[a]?d.cache[a].cloneNode():p.test(a)?(d.cache[a]=d.createElem(a)).cloneNode():d.createElem(a),!e.canHaveChildren||o.test(a)||e.tagUrn?e:d.frag.appendChild(e)}function h(a,c){if(a||(a=b),l)return a.createDocumentFragment();c=c||f(a);for(var e=c.frag.cloneNode(),g=0,h=d(),i=h.length;i>g;g++)e.createElement(h[g]);return e}function i(a,b){b.cache||(b.cache={},b.createElem=a.createElement,b.createFrag=a.createDocumentFragment,b.frag=b.createFrag()),a.createElement=function(c){return t.shivMethods?g(c,a,b):b.createElem(c)},a.createDocumentFragment=Function("h,f","return function(){var n=f.cloneNode(),c=n.createElement;h.shivMethods&&("+d().join().replace(/[\w\-:]+/g,function(a){return b.createElem(a),b.frag.createElement(a),'c("'+a+'")'})+");return n}")(t,b.frag)}function j(a){a||(a=b);var d=f(a);return!t.shivCSS||k||d.hasCSS||(d.hasCSS=!!c(a,"article,aside,dialog,figcaption,figure,footer,header,hgroup,main,nav,section{display:block}mark{background:#FF0;color:#000}template{display:none}")),l||i(a,d),a}var k,l,m="3.7.3-pre",n=a.html5||{},o=/^<|^(?:button|map|select|textarea|object|iframe|option|optgroup)$/i,p=/^(?:a|b|code|div|fieldset|h1|h2|h3|h4|h5|h6|i|label|li|ol|p|q|span|strong|style|table|tbody|td|th|tr|ul)$/i,q="_html5shiv",r=0,s={};!function(){try{var a=b.createElement("a");a.innerHTML="",k="hidden"in a,l=1==a.childNodes.length||function(){b.createElement("a");var a=b.createDocumentFragment();return"undefined"==typeof a.cloneNode||"undefined"==typeof a.createDocumentFragment||"undefined"==typeof a.createElement}()}catch(c){k=!0,l=!0}}();var t={elements:n.elements||"abbr article aside audio bdi canvas data datalist details dialog figcaption figure footer header hgroup main mark meter nav output picture progress section summary template time video",version:m,shivCSS:n.shivCSS!==!1,supportsUnknownElements:l,shivMethods:n.shivMethods!==!1,type:"default",shivDocument:j,createElement:g,createDocumentFragment:h,addElements:e};a.html5=t,j(b),"object"==typeof module&&module.exports&&(module.exports=t)}("undefined"!=typeof window?window:this,document); -------------------------------------------------------------------------------- /docs/_build/html/_static/js/theme.js: -------------------------------------------------------------------------------- 1 | !function(n){var e={};function t(i){if(e[i])return e[i].exports;var o=e[i]={i:i,l:!1,exports:{}};return n[i].call(o.exports,o,o.exports,t),o.l=!0,o.exports}t.m=n,t.c=e,t.d=function(n,e,i){t.o(n,e)||Object.defineProperty(n,e,{enumerable:!0,get:i})},t.r=function(n){"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(n,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(n,"__esModule",{value:!0})},t.t=function(n,e){if(1&e&&(n=t(n)),8&e)return n;if(4&e&&"object"==typeof n&&n&&n.__esModule)return n;var i=Object.create(null);if(t.r(i),Object.defineProperty(i,"default",{enumerable:!0,value:n}),2&e&&"string"!=typeof n)for(var o in n)t.d(i,o,function(e){return n[e]}.bind(null,o));return i},t.n=function(n){var e=n&&n.__esModule?function(){return n.default}:function(){return n};return t.d(e,"a",e),e},t.o=function(n,e){return Object.prototype.hasOwnProperty.call(n,e)},t.p="",t(t.s=0)}([function(n,e,t){t(1),n.exports=t(3)},function(n,e,t){(function(){var e="undefined"!=typeof window?window.jQuery:t(2);n.exports.ThemeNav={navBar:null,win:null,winScroll:!1,winResize:!1,linkScroll:!1,winPosition:0,winHeight:null,docHeight:null,isRunning:!1,enable:function(n){var t=this;void 0===n&&(n=!0),t.isRunning||(t.isRunning=!0,e((function(e){t.init(e),t.reset(),t.win.on("hashchange",t.reset),n&&t.win.on("scroll",(function(){t.linkScroll||t.winScroll||(t.winScroll=!0,requestAnimationFrame((function(){t.onScroll()})))})),t.win.on("resize",(function(){t.winResize||(t.winResize=!0,requestAnimationFrame((function(){t.onResize()})))})),t.onResize()})))},enableSticky:function(){this.enable(!0)},init:function(n){n(document);var e=this;this.navBar=n("div.wy-side-scroll:first"),this.win=n(window),n(document).on("click","[data-toggle='wy-nav-top']",(function(){n("[data-toggle='wy-nav-shift']").toggleClass("shift"),n("[data-toggle='rst-versions']").toggleClass("shift")})).on("click",".wy-menu-vertical .current ul li a",(function(){var t=n(this);n("[data-toggle='wy-nav-shift']").removeClass("shift"),n("[data-toggle='rst-versions']").toggleClass("shift"),e.toggleCurrent(t),e.hashChange()})).on("click","[data-toggle='rst-current-version']",(function(){n("[data-toggle='rst-versions']").toggleClass("shift-up")})),n("table.docutils:not(.field-list,.footnote,.citation)").wrap("
"),n("table.docutils.footnote").wrap("
"),n("table.docutils.citation").wrap("
"),n(".wy-menu-vertical ul").not(".simple").siblings("a").each((function(){var t=n(this);expand=n(''),expand.on("click",(function(n){return e.toggleCurrent(t),n.stopPropagation(),!1})),t.prepend(expand)}))},reset:function(){var n=encodeURI(window.location.hash)||"#";try{var e=$(".wy-menu-vertical"),t=e.find('[href="'+n+'"]');if(0===t.length){var i=$('.document [id="'+n.substring(1)+'"]').closest("div.section");0===(t=e.find('[href="#'+i.attr("id")+'"]')).length&&(t=e.find('[href="#"]'))}if(t.length>0){$(".wy-menu-vertical .current").removeClass("current").attr("aria-expanded","false"),t.addClass("current").attr("aria-expanded","true"),t.closest("li.toctree-l1").parent().addClass("current").attr("aria-expanded","true");for(let n=1;n<=10;n++)t.closest("li.toctree-l"+n).addClass("current").attr("aria-expanded","true");t[0].scrollIntoView()}}catch(n){console.log("Error expanding nav for anchor",n)}},onScroll:function(){this.winScroll=!1;var n=this.win.scrollTop(),e=n+this.winHeight,t=this.navBar.scrollTop()+(n-this.winPosition);n<0||e>this.docHeight||(this.navBar.scrollTop(t),this.winPosition=n)},onResize:function(){this.winResize=!1,this.winHeight=this.win.height(),this.docHeight=$(document).height()},hashChange:function(){this.linkScroll=!0,this.win.one("hashchange",(function(){this.linkScroll=!1}))},toggleCurrent:function(n){var e=n.closest("li");e.siblings("li.current").removeClass("current").attr("aria-expanded","false"),e.siblings().find("li.current").removeClass("current").attr("aria-expanded","false");var t=e.find("> ul li");t.length&&(t.removeClass("current").attr("aria-expanded","false"),e.toggleClass("current").attr("aria-expanded",(function(n,e){return"true"==e?"false":"true"})))}},"undefined"!=typeof window&&(window.SphinxRtdTheme={Navigation:n.exports.ThemeNav,StickyNav:n.exports.ThemeNav}),function(){for(var n=0,e=["ms","moz","webkit","o"],t=0;t0 63 | var meq1 = "^(" + C + ")?" + V + C + "(" + V + ")?$"; // [C]VC[V] is m=1 64 | var mgr1 = "^(" + C + ")?" + V + C + V + C; // [C]VCVC... is m>1 65 | var s_v = "^(" + C + ")?" + v; // vowel in stem 66 | 67 | this.stemWord = function (w) { 68 | var stem; 69 | var suffix; 70 | var firstch; 71 | var origword = w; 72 | 73 | if (w.length < 3) 74 | return w; 75 | 76 | var re; 77 | var re2; 78 | var re3; 79 | var re4; 80 | 81 | firstch = w.substr(0,1); 82 | if (firstch == "y") 83 | w = firstch.toUpperCase() + w.substr(1); 84 | 85 | // Step 1a 86 | re = /^(.+?)(ss|i)es$/; 87 | re2 = /^(.+?)([^s])s$/; 88 | 89 | if (re.test(w)) 90 | w = w.replace(re,"$1$2"); 91 | else if (re2.test(w)) 92 | w = w.replace(re2,"$1$2"); 93 | 94 | // Step 1b 95 | re = /^(.+?)eed$/; 96 | re2 = /^(.+?)(ed|ing)$/; 97 | if (re.test(w)) { 98 | var fp = re.exec(w); 99 | re = new RegExp(mgr0); 100 | if (re.test(fp[1])) { 101 | re = /.$/; 102 | w = w.replace(re,""); 103 | } 104 | } 105 | else if (re2.test(w)) { 106 | var fp = re2.exec(w); 107 | stem = fp[1]; 108 | re2 = new RegExp(s_v); 109 | if (re2.test(stem)) { 110 | w = stem; 111 | re2 = /(at|bl|iz)$/; 112 | re3 = new RegExp("([^aeiouylsz])\\1$"); 113 | re4 = new RegExp("^" + C + v + "[^aeiouwxy]$"); 114 | if (re2.test(w)) 115 | w = w + "e"; 116 | else if (re3.test(w)) { 117 | re = /.$/; 118 | w = w.replace(re,""); 119 | } 120 | else if (re4.test(w)) 121 | w = w + "e"; 122 | } 123 | } 124 | 125 | // Step 1c 126 | re = /^(.+?)y$/; 127 | if (re.test(w)) { 128 | var fp = re.exec(w); 129 | stem = fp[1]; 130 | re = new RegExp(s_v); 131 | if (re.test(stem)) 132 | w = stem + "i"; 133 | } 134 | 135 | // Step 2 136 | re = /^(.+?)(ational|tional|enci|anci|izer|bli|alli|entli|eli|ousli|ization|ation|ator|alism|iveness|fulness|ousness|aliti|iviti|biliti|logi)$/; 137 | if (re.test(w)) { 138 | var fp = re.exec(w); 139 | stem = fp[1]; 140 | suffix = fp[2]; 141 | re = new RegExp(mgr0); 142 | if (re.test(stem)) 143 | w = stem + step2list[suffix]; 144 | } 145 | 146 | // Step 3 147 | re = /^(.+?)(icate|ative|alize|iciti|ical|ful|ness)$/; 148 | if (re.test(w)) { 149 | var fp = re.exec(w); 150 | stem = fp[1]; 151 | suffix = fp[2]; 152 | re = new RegExp(mgr0); 153 | if (re.test(stem)) 154 | w = stem + step3list[suffix]; 155 | } 156 | 157 | // Step 4 158 | re = /^(.+?)(al|ance|ence|er|ic|able|ible|ant|ement|ment|ent|ou|ism|ate|iti|ous|ive|ize)$/; 159 | re2 = /^(.+?)(s|t)(ion)$/; 160 | if (re.test(w)) { 161 | var fp = re.exec(w); 162 | stem = fp[1]; 163 | re = new RegExp(mgr1); 164 | if (re.test(stem)) 165 | w = stem; 166 | } 167 | else if (re2.test(w)) { 168 | var fp = re2.exec(w); 169 | stem = fp[1] + fp[2]; 170 | re2 = new RegExp(mgr1); 171 | if (re2.test(stem)) 172 | w = stem; 173 | } 174 | 175 | // Step 5 176 | re = /^(.+?)e$/; 177 | if (re.test(w)) { 178 | var fp = re.exec(w); 179 | stem = fp[1]; 180 | re = new RegExp(mgr1); 181 | re2 = new RegExp(meq1); 182 | re3 = new RegExp("^" + C + v + "[^aeiouwxy]$"); 183 | if (re.test(stem) || (re2.test(stem) && !(re3.test(stem)))) 184 | w = stem; 185 | } 186 | re = /ll$/; 187 | re2 = new RegExp(mgr1); 188 | if (re.test(w) && re2.test(w)) { 189 | re = /.$/; 190 | w = w.replace(re,""); 191 | } 192 | 193 | // and turn initial Y back to y 194 | if (firstch == "y") 195 | w = firstch.toLowerCase() + w.substr(1); 196 | return w; 197 | } 198 | } 199 | 200 | -------------------------------------------------------------------------------- /docs/_build/html/_static/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elsampsa/task_thread/14905bfb9cfaf82c6bc905443ed56e95a7913f14/docs/_build/html/_static/logo.png -------------------------------------------------------------------------------- /docs/_build/html/_static/minus.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elsampsa/task_thread/14905bfb9cfaf82c6bc905443ed56e95a7913f14/docs/_build/html/_static/minus.png -------------------------------------------------------------------------------- /docs/_build/html/_static/plus.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elsampsa/task_thread/14905bfb9cfaf82c6bc905443ed56e95a7913f14/docs/_build/html/_static/plus.png -------------------------------------------------------------------------------- /docs/_build/html/_static/pygments.css: -------------------------------------------------------------------------------- 1 | pre { line-height: 125%; } 2 | td.linenos .normal { color: inherit; background-color: transparent; padding-left: 5px; padding-right: 5px; } 3 | span.linenos { color: inherit; background-color: transparent; padding-left: 5px; padding-right: 5px; } 4 | td.linenos .special { color: #000000; background-color: #ffffc0; padding-left: 5px; padding-right: 5px; } 5 | span.linenos.special { color: #000000; background-color: #ffffc0; padding-left: 5px; padding-right: 5px; } 6 | .highlight .hll { background-color: #ffffcc } 7 | .highlight { background: #eeffcc; } 8 | .highlight .c { color: #408090; font-style: italic } /* Comment */ 9 | .highlight .err { border: 1px solid #FF0000 } /* Error */ 10 | .highlight .k { color: #007020; font-weight: bold } /* Keyword */ 11 | .highlight .o { color: #666666 } /* Operator */ 12 | .highlight .ch { color: #408090; font-style: italic } /* Comment.Hashbang */ 13 | .highlight .cm { color: #408090; font-style: italic } /* Comment.Multiline */ 14 | .highlight .cp { color: #007020 } /* Comment.Preproc */ 15 | .highlight .cpf { color: #408090; font-style: italic } /* Comment.PreprocFile */ 16 | .highlight .c1 { color: #408090; font-style: italic } /* Comment.Single */ 17 | .highlight .cs { color: #408090; background-color: #fff0f0 } /* Comment.Special */ 18 | .highlight .gd { color: #A00000 } /* Generic.Deleted */ 19 | .highlight .ge { font-style: italic } /* Generic.Emph */ 20 | .highlight .ges { font-weight: bold; font-style: italic } /* Generic.EmphStrong */ 21 | .highlight .gr { color: #FF0000 } /* Generic.Error */ 22 | .highlight .gh { color: #000080; font-weight: bold } /* Generic.Heading */ 23 | .highlight .gi { color: #00A000 } /* Generic.Inserted */ 24 | .highlight .go { color: #333333 } /* Generic.Output */ 25 | .highlight .gp { color: #c65d09; font-weight: bold } /* Generic.Prompt */ 26 | .highlight .gs { font-weight: bold } /* Generic.Strong */ 27 | .highlight .gu { color: #800080; font-weight: bold } /* Generic.Subheading */ 28 | .highlight .gt { color: #0044DD } /* Generic.Traceback */ 29 | .highlight .kc { color: #007020; font-weight: bold } /* Keyword.Constant */ 30 | .highlight .kd { color: #007020; font-weight: bold } /* Keyword.Declaration */ 31 | .highlight .kn { color: #007020; font-weight: bold } /* Keyword.Namespace */ 32 | .highlight .kp { color: #007020 } /* Keyword.Pseudo */ 33 | .highlight .kr { color: #007020; font-weight: bold } /* Keyword.Reserved */ 34 | .highlight .kt { color: #902000 } /* Keyword.Type */ 35 | .highlight .m { color: #208050 } /* Literal.Number */ 36 | .highlight .s { color: #4070a0 } /* Literal.String */ 37 | .highlight .na { color: #4070a0 } /* Name.Attribute */ 38 | .highlight .nb { color: #007020 } /* Name.Builtin */ 39 | .highlight .nc { color: #0e84b5; font-weight: bold } /* Name.Class */ 40 | .highlight .no { color: #60add5 } /* Name.Constant */ 41 | .highlight .nd { color: #555555; font-weight: bold } /* Name.Decorator */ 42 | .highlight .ni { color: #d55537; font-weight: bold } /* Name.Entity */ 43 | .highlight .ne { color: #007020 } /* Name.Exception */ 44 | .highlight .nf { color: #06287e } /* Name.Function */ 45 | .highlight .nl { color: #002070; font-weight: bold } /* Name.Label */ 46 | .highlight .nn { color: #0e84b5; font-weight: bold } /* Name.Namespace */ 47 | .highlight .nt { color: #062873; font-weight: bold } /* Name.Tag */ 48 | .highlight .nv { color: #bb60d5 } /* Name.Variable */ 49 | .highlight .ow { color: #007020; font-weight: bold } /* Operator.Word */ 50 | .highlight .w { color: #bbbbbb } /* Text.Whitespace */ 51 | .highlight .mb { color: #208050 } /* Literal.Number.Bin */ 52 | .highlight .mf { color: #208050 } /* Literal.Number.Float */ 53 | .highlight .mh { color: #208050 } /* Literal.Number.Hex */ 54 | .highlight .mi { color: #208050 } /* Literal.Number.Integer */ 55 | .highlight .mo { color: #208050 } /* Literal.Number.Oct */ 56 | .highlight .sa { color: #4070a0 } /* Literal.String.Affix */ 57 | .highlight .sb { color: #4070a0 } /* Literal.String.Backtick */ 58 | .highlight .sc { color: #4070a0 } /* Literal.String.Char */ 59 | .highlight .dl { color: #4070a0 } /* Literal.String.Delimiter */ 60 | .highlight .sd { color: #4070a0; font-style: italic } /* Literal.String.Doc */ 61 | .highlight .s2 { color: #4070a0 } /* Literal.String.Double */ 62 | .highlight .se { color: #4070a0; font-weight: bold } /* Literal.String.Escape */ 63 | .highlight .sh { color: #4070a0 } /* Literal.String.Heredoc */ 64 | .highlight .si { color: #70a0d0; font-style: italic } /* Literal.String.Interpol */ 65 | .highlight .sx { color: #c65d09 } /* Literal.String.Other */ 66 | .highlight .sr { color: #235388 } /* Literal.String.Regex */ 67 | .highlight .s1 { color: #4070a0 } /* Literal.String.Single */ 68 | .highlight .ss { color: #517918 } /* Literal.String.Symbol */ 69 | .highlight .bp { color: #007020 } /* Name.Builtin.Pseudo */ 70 | .highlight .fm { color: #06287e } /* Name.Function.Magic */ 71 | .highlight .vc { color: #bb60d5 } /* Name.Variable.Class */ 72 | .highlight .vg { color: #bb60d5 } /* Name.Variable.Global */ 73 | .highlight .vi { color: #bb60d5 } /* Name.Variable.Instance */ 74 | .highlight .vm { color: #bb60d5 } /* Name.Variable.Magic */ 75 | .highlight .il { color: #208050 } /* Literal.Number.Integer.Long */ -------------------------------------------------------------------------------- /docs/_build/html/_static/rtd_override.css: -------------------------------------------------------------------------------- 1 | .wy-nav-content { 2 | max-width: 1200px !important; 3 | } 4 | -------------------------------------------------------------------------------- /docs/_build/html/_static/snakes.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elsampsa/task_thread/14905bfb9cfaf82c6bc905443ed56e95a7913f14/docs/_build/html/_static/snakes.png -------------------------------------------------------------------------------- /docs/_build/html/_static/sphinx_highlight.js: -------------------------------------------------------------------------------- 1 | /* Highlighting utilities for Sphinx HTML documentation. */ 2 | "use strict"; 3 | 4 | const SPHINX_HIGHLIGHT_ENABLED = true 5 | 6 | /** 7 | * highlight a given string on a node by wrapping it in 8 | * span elements with the given class name. 9 | */ 10 | const _highlight = (node, addItems, text, className) => { 11 | if (node.nodeType === Node.TEXT_NODE) { 12 | const val = node.nodeValue; 13 | const parent = node.parentNode; 14 | const pos = val.toLowerCase().indexOf(text); 15 | if ( 16 | pos >= 0 && 17 | !parent.classList.contains(className) && 18 | !parent.classList.contains("nohighlight") 19 | ) { 20 | let span; 21 | 22 | const closestNode = parent.closest("body, svg, foreignObject"); 23 | const isInSVG = closestNode && closestNode.matches("svg"); 24 | if (isInSVG) { 25 | span = document.createElementNS("http://www.w3.org/2000/svg", "tspan"); 26 | } else { 27 | span = document.createElement("span"); 28 | span.classList.add(className); 29 | } 30 | 31 | span.appendChild(document.createTextNode(val.substr(pos, text.length))); 32 | const rest = document.createTextNode(val.substr(pos + text.length)); 33 | parent.insertBefore( 34 | span, 35 | parent.insertBefore( 36 | rest, 37 | node.nextSibling 38 | ) 39 | ); 40 | node.nodeValue = val.substr(0, pos); 41 | /* There may be more occurrences of search term in this node. So call this 42 | * function recursively on the remaining fragment. 43 | */ 44 | _highlight(rest, addItems, text, className); 45 | 46 | if (isInSVG) { 47 | const rect = document.createElementNS( 48 | "http://www.w3.org/2000/svg", 49 | "rect" 50 | ); 51 | const bbox = parent.getBBox(); 52 | rect.x.baseVal.value = bbox.x; 53 | rect.y.baseVal.value = bbox.y; 54 | rect.width.baseVal.value = bbox.width; 55 | rect.height.baseVal.value = bbox.height; 56 | rect.setAttribute("class", className); 57 | addItems.push({ parent: parent, target: rect }); 58 | } 59 | } 60 | } else if (node.matches && !node.matches("button, select, textarea")) { 61 | node.childNodes.forEach((el) => _highlight(el, addItems, text, className)); 62 | } 63 | }; 64 | const _highlightText = (thisNode, text, className) => { 65 | let addItems = []; 66 | _highlight(thisNode, addItems, text, className); 67 | addItems.forEach((obj) => 68 | obj.parent.insertAdjacentElement("beforebegin", obj.target) 69 | ); 70 | }; 71 | 72 | /** 73 | * Small JavaScript module for the documentation. 74 | */ 75 | const SphinxHighlight = { 76 | 77 | /** 78 | * highlight the search words provided in localstorage in the text 79 | */ 80 | highlightSearchWords: () => { 81 | if (!SPHINX_HIGHLIGHT_ENABLED) return; // bail if no highlight 82 | 83 | // get and clear terms from localstorage 84 | const url = new URL(window.location); 85 | const highlight = 86 | localStorage.getItem("sphinx_highlight_terms") 87 | || url.searchParams.get("highlight") 88 | || ""; 89 | localStorage.removeItem("sphinx_highlight_terms") 90 | url.searchParams.delete("highlight"); 91 | window.history.replaceState({}, "", url); 92 | 93 | // get individual terms from highlight string 94 | const terms = highlight.toLowerCase().split(/\s+/).filter(x => x); 95 | if (terms.length === 0) return; // nothing to do 96 | 97 | // There should never be more than one element matching "div.body" 98 | const divBody = document.querySelectorAll("div.body"); 99 | const body = divBody.length ? divBody[0] : document.querySelector("body"); 100 | window.setTimeout(() => { 101 | terms.forEach((term) => _highlightText(body, term, "highlighted")); 102 | }, 10); 103 | 104 | const searchBox = document.getElementById("searchbox"); 105 | if (searchBox === null) return; 106 | searchBox.appendChild( 107 | document 108 | .createRange() 109 | .createContextualFragment( 110 | '" 114 | ) 115 | ); 116 | }, 117 | 118 | /** 119 | * helper function to hide the search marks again 120 | */ 121 | hideSearchWords: () => { 122 | document 123 | .querySelectorAll("#searchbox .highlight-link") 124 | .forEach((el) => el.remove()); 125 | document 126 | .querySelectorAll("span.highlighted") 127 | .forEach((el) => el.classList.remove("highlighted")); 128 | localStorage.removeItem("sphinx_highlight_terms") 129 | }, 130 | 131 | initEscapeListener: () => { 132 | // only install a listener if it is really needed 133 | if (!DOCUMENTATION_OPTIONS.ENABLE_SEARCH_SHORTCUTS) return; 134 | 135 | document.addEventListener("keydown", (event) => { 136 | // bail for input elements 137 | if (BLACKLISTED_KEY_CONTROL_ELEMENTS.has(document.activeElement.tagName)) return; 138 | // bail with special keys 139 | if (event.shiftKey || event.altKey || event.ctrlKey || event.metaKey) return; 140 | if (DOCUMENTATION_OPTIONS.ENABLE_SEARCH_SHORTCUTS && (event.key === "Escape")) { 141 | SphinxHighlight.hideSearchWords(); 142 | event.preventDefault(); 143 | } 144 | }); 145 | }, 146 | }; 147 | 148 | _ready(() => { 149 | /* Do not call highlightSearchWords() when we are on the search page. 150 | * It will highlight words from the *previous* search query. 151 | */ 152 | if (typeof Search === "undefined") SphinxHighlight.highlightSearchWords(); 153 | SphinxHighlight.initEscapeListener(); 154 | }); 155 | -------------------------------------------------------------------------------- /docs/_build/html/_static/tmp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elsampsa/task_thread/14905bfb9cfaf82c6bc905443ed56e95a7913f14/docs/_build/html/_static/tmp.png -------------------------------------------------------------------------------- /docs/_build/html/authors.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Authors — TaskThread documentation 8 | 9 | 10 | 11 | 12 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 |
33 | 84 | 85 |
89 | 90 |
91 |
92 |
93 | 100 |
101 |
102 |
103 |
104 | 105 |
106 |

Authors

107 |

Sampsa Riikonen

108 |
109 | 110 | 111 |
112 |
113 |
116 | 117 |
118 | 119 |
120 |

© Copyright 2022-2023 Sampsa Riikonen.

121 |
122 | 123 | Built with Sphinx using a 124 | theme 125 | provided by Read the Docs. 126 | 127 | 128 |
129 |
130 |
131 |
132 |
133 | 138 | 139 | 140 | 149 | 150 | 151 | -------------------------------------------------------------------------------- /docs/_build/html/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | TaskThread — TaskThread documentation 10 | 11 | 12 | 13 | 14 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 |
35 | 86 | 87 |
91 | 92 |
93 |
94 |
95 | 102 |
103 |
104 |
105 |
106 | 107 |
108 |

TaskThread

109 |

Contents:

110 |
111 | 138 |
139 |
140 | 141 | 142 |
143 |
144 |
147 | 148 |
149 | 150 |
151 |

© Copyright 2022-2023 Sampsa Riikonen.

152 |
153 | 154 | Built with Sphinx using a 155 | theme 156 | provided by Read the Docs. 157 | 158 | 159 |
160 |
161 |
162 |
163 |
164 | 169 | 170 | 171 | 180 | 181 | 182 | -------------------------------------------------------------------------------- /docs/_build/html/license.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Copyright & License — TaskThread documentation 8 | 9 | 10 | 11 | 12 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 |
34 | 85 | 86 |
90 | 91 |
92 |
93 |
94 | 101 |
102 |
103 |
104 |
105 | 106 | 111 | 112 | 113 |
114 |
115 |
119 | 120 |
121 | 122 |
123 |

© Copyright 2022-2023 Sampsa Riikonen.

124 |
125 | 126 | Built with Sphinx using a 127 | theme 128 | provided by Read the Docs. 129 | 130 | 131 |
132 |
133 |
134 |
135 |
136 | 141 | 142 | 143 | 152 | 153 | 154 | -------------------------------------------------------------------------------- /docs/_build/html/objects.inv: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elsampsa/task_thread/14905bfb9cfaf82c6bc905443ed56e95a7913f14/docs/_build/html/objects.inv -------------------------------------------------------------------------------- /docs/_build/html/search.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Search — TaskThread documentation 7 | 8 | 9 | 10 | 11 | 12 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 |
34 | 85 | 86 |
90 | 91 |
92 |
93 |
94 |
    95 |
  • 96 | 97 |
  • 98 |
  • 99 |
100 |
101 |
102 |
103 |
104 | 105 | 112 | 113 | 114 |
115 | 116 |
117 | 118 |
119 |
120 |
121 | 122 |
123 | 124 |
125 |

© Copyright 2022-2023 Sampsa Riikonen.

126 |
127 | 128 | Built with Sphinx using a 129 | theme 130 | provided by Read the Docs. 131 | 132 | 133 |
134 |
135 |
136 |
137 |
138 | 143 | 144 | 145 | 154 | 157 | 158 | 159 | 160 | 161 | 162 | 163 | -------------------------------------------------------------------------------- /docs/_static/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elsampsa/task_thread/14905bfb9cfaf82c6bc905443ed56e95a7913f14/docs/_static/logo.png -------------------------------------------------------------------------------- /docs/_static/rtd_override.css: -------------------------------------------------------------------------------- 1 | .wy-nav-content { 2 | max-width: 1200px !important; 3 | } 4 | -------------------------------------------------------------------------------- /docs/_static/snakes.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elsampsa/task_thread/14905bfb9cfaf82c6bc905443ed56e95a7913f14/docs/_static/snakes.png -------------------------------------------------------------------------------- /docs/_static/tmp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elsampsa/task_thread/14905bfb9cfaf82c6bc905443ed56e95a7913f14/docs/_static/tmp.png -------------------------------------------------------------------------------- /docs/_templates/alabaster/sidebar-bottom.html: -------------------------------------------------------------------------------- 1 |

Links

2 |
    3 |
  • 4 |
5 | -------------------------------------------------------------------------------- /docs/_templates/alabaster/sidebar-intro.html: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 |

Skeleton - a scaffold project

13 | Star 14 | 15 |

Links

16 | 20 | -------------------------------------------------------------------------------- /docs/_templates/rtd/layout.html: -------------------------------------------------------------------------------- 1 | {% extends "!layout.html" %} 2 | 3 | {%- block extrabody %} 4 | {% endblock %} 5 | 6 | {% block extrahead %} 7 | 8 | 9 | 10 | {% endblock %} 11 | 12 | {%- block sidebartitle %} 13 | 14 | 33 | 34 | 38 | 39 | 40 | 41 | 42 | {%- if theme_display_version %} 43 | {%- set nav_version = version %} 44 | {%- if READTHEDOCS and current_version %} 45 | {%- set nav_version = current_version %} 46 | {%- endif %} 47 | {%- if nav_version %} 48 |
49 | TaskThread {{ nav_version }} 50 |
51 |
52 | Concurrent Asynchronous Python 53 |
54 | {%- endif %} 55 | {%- endif %} 56 | 57 | 58 |
59 | TaskThread @ github 60 | Issue Tracker @ github 61 | Discord 62 | 63 | 64 | {%- include "searchbox.html" %} 65 | 66 | {%- endblock %} 67 | -------------------------------------------------------------------------------- /docs/_templates/sidebar-bottom.html: -------------------------------------------------------------------------------- 1 |

Links

2 |
    3 |
  • 4 |
5 | -------------------------------------------------------------------------------- /docs/_templates/sidebar-intro.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 |

Asynchronous Python for Human Beings

11 | Star 12 | 17 | 18 |

Links

19 | 23 | -------------------------------------------------------------------------------- /docs/_templates/tmp/sidebar-intro.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 |

task_thread - a scaffold project

11 | Star 12 | 13 |

Links

14 | 18 | -------------------------------------------------------------------------------- /docs/authors.rst: -------------------------------------------------------------------------------- 1 | Authors 2 | ------- 3 | 4 | Sampsa Riikonen 5 | -------------------------------------------------------------------------------- /docs/clean.bash: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | rm -r -f generated _build 3 | 4 | -------------------------------------------------------------------------------- /docs/compile.bash: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # sphinx-apidoc -f -e -o . .. 3 | sphinx-autogen *.rst 4 | #./fix_rst.py 5 | #cd generated 6 | #sphinx-autogen *.rst 7 | #cd .. 8 | make html 9 | 10 | # # ** Gitlab users ** 11 | # # Enable these lines if you want to hack gitlab for online docs 12 | # # here we assume that you keep your gitlab wiki repositories in $HOME/gitlab_wikis 13 | # wiki=$HOME/gitlab_wikis/task_thread 14 | # rsync -v -r _build $wiki/ 15 | 16 | # # ** Gitlab users ** 17 | # # When you create your gitlab wiki for the first time, run also this: 18 | # cd $wiki 19 | # git add $(git ls-files -o --exclude-standard) 20 | # git commit -a -m "initial commit" 21 | # git push 22 | -------------------------------------------------------------------------------- /docs/examples.rst: -------------------------------------------------------------------------------- 1 | 2 | .. _examples: 3 | 4 | Examples 5 | ======== 6 | 7 | 1. Hello World 8 | -------------- 9 | 10 | Download example code from :download:`[here]` 11 | 12 | .. include:: snippets/hello_world.py_ 13 | 14 | 2. Parent/Child Structure 15 | ------------------------- 16 | 17 | Download example code from :download:`[here]` 18 | 19 | .. include:: snippets/parent_child.py_ 20 | 21 | 3. TCP Server 22 | ------------- 23 | 24 | - Download server example code from :download:`[here]` 25 | - Download client side code from :download:`[here]` 26 | 27 | .. include:: snippets/example_server.py_ 28 | 29 | 4. Data Streaming TCP Server 30 | ---------------------------- 31 | 32 | - Download example code from :download:`[here]` 33 | - Download client side code from :download:`[here]` 34 | 35 | .. include:: snippets/example_server2.py_ 36 | 37 | -------------------------------------------------------------------------------- /docs/fix_rst.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | import glob 3 | import re 4 | 5 | filenames=glob.glob("generated/*.rst") 6 | 7 | for filename in filenames: 8 | print(filename) 9 | 10 | f=open(filename,"r") 11 | st=f.read() 12 | f.close() 13 | 14 | f=open(filename,"w") 15 | for line in st.split("\n"): 16 | p=re.compile("(\s*).. autosummary") 17 | m=p.findall(line) 18 | f.write(line+"\n") 19 | if (len(m)>0): 20 | f.write(m[0]+" "+":toctree:\n") 21 | 22 | -------------------------------------------------------------------------------- /docs/home.md: -------------------------------------------------------------------------------- 1 | For documentation 2 | [click here](_build/html/index.html) 3 | -------------------------------------------------------------------------------- /docs/images/skull.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elsampsa/task_thread/14905bfb9cfaf82c6bc905443ed56e95a7913f14/docs/images/skull.png -------------------------------------------------------------------------------- /docs/images/tmp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elsampsa/task_thread/14905bfb9cfaf82c6bc905443ed56e95a7913f14/docs/images/tmp.png -------------------------------------------------------------------------------- /docs/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /docs/index.rst: -------------------------------------------------------------------------------- 1 | .. task_thread documentation master file, created by 2 | sphinx-quickstart on Mon Mar 20 16:31:00 2017. 3 | You can adapt this file completely to your liking, but it should at least 4 | contain the root `toctree` directive. 5 | 6 | TaskThread 7 | ========== 8 | 9 | .. meta:: 10 | :description: TaskThread: Asynchronous Programming for Humans 11 | :keywords: python, asyncio 12 | 13 | Contents: 14 | 15 | .. toctree:: 16 | :maxdepth: 2 17 | 18 | intro 19 | thread 20 | task 21 | threadtask 22 | examples 23 | license 24 | authors 25 | -------------------------------------------------------------------------------- /docs/intro.rst: -------------------------------------------------------------------------------- 1 | .. _intro: 2 | 3 | .. A link to :ref:`intro ` 4 | 5 | About TaskThread 6 | ================ 7 | 8 | *Never use asyncio.wait or asyncio.gather again* 9 | 10 | TaskThread is a collection of tools, conventions and a base class that organizes 11 | your asyncio python programs seamlessly into hierarchical, well-organized structures. 12 | 13 | These structures are analogical to threads and have a thread-like API. 14 | 15 | Let's illustrate what all this is about by an example. 16 | 17 | A Sample Problem 18 | ---------------- 19 | 20 | Let's consider a rather complex application which: 21 | 22 | - Exposes a TCP server 23 | - The server accepts various client connections from LAN 24 | - Server processes the incoming data and reconstructs data frames from packets received from individual TCP client connections 25 | - Processed data from each TCP client is forwarded through websockets to a final destination (say, to cloud) 26 | - The processed data is also written into a database 27 | 28 | We can see there is a lot of i/o waiting and "multiplexing" going on that can get messy. 29 | 30 | When using TaskThread, you start unravelling a problem by identifying hierarchies. 31 | 32 | In the present case, a solution, described as a hierarchical list, could look like this: 33 | 34 | :: 35 | 36 | MasterThread 37 | TCPServerThread 38 | TCPConnectionThread 39 | DataProcessorThread 40 | TCPConnectionThread 41 | DataProcessorThread 42 | ... 43 | WebsocketMasterThread 44 | WebSocketSubThread 45 | WebSocketSubThread 46 | ... 47 | DatabaseThread 48 | 49 | 50 | (another kind of hierarchy to solve the problem is, of course, also possible) 51 | 52 | ``MasterThread`` starts the ``TCPServerThread`` which then starts ``TCPConnectionThread`` s on demand. 53 | 54 | Each ``TCPConnectionThread`` starts a ``DataProcessorThread`` which reconstructs the packets from indicidual ``TCPConnectionThread`` s. 55 | 56 | All data flows to upwards in the tree into ``MasterThread`` which then passes it onto to ``WebsocketMasterThread`` and from there to individual 57 | ``WebSocketSubThread`` s. ``MasterThread`` passes the data also to ``DatabaseThread``. 58 | 59 | Let's add this data flow to the hierarchical list: 60 | 61 | :: 62 | 63 | MasterThread 64 | TCPServerThread: UP: data 65 | TCPConnectionThread: UP: data 66 | DataProcessorThread: IN: packets, UP: data 67 | TCPConnectionThread 68 | DataProcessorThread 69 | ... 70 | WebsocketMasterThread: IN: data 71 | WebSocketSubThread: IN data 72 | WebSocketSubThread 73 | ... 74 | DatabaseThread: IN: data 75 | 76 | Here ``UP`` designates data going upwards in the tree, while ``IN`` shows incoming data at deeper level threads. 77 | 78 | Notice that there is not any intercommunication within this tree that is not strictly between a parent and a child. 79 | 80 | Threads, how? 81 | ------------- 82 | 83 | So, how to implement such "threads" ? After all this is asyncio, not multithreading! 84 | 85 | Well, the entities named here ``MasterThread``, ``TCPServerThread``, etc. are not really "threads", but collection of asyncio **tasks** grouped together in a smart way - thus the name TaskThread. 86 | The intercom between the "threads" is done using asyncio **queues**. 87 | 88 | Let's take a deeper look on the **tasks** and **queues**: 89 | 90 | ``DataProcessorThread`` receives packets (via a listening **task**) through a queue from ``TCPConnectionThread``. After reconstructing 91 | some data from the packets using a **task**, ``DataProcessorThread`` has a **task** that sends the dataframes to ``TCPConnectionThread`` through another asyncio **queue** which 92 | ``TCPConnectionThread`` is listening with a **task**, etc. 93 | 94 | These **queues** and listener **tasks** work seamlessly and are hidden from the TaskThread API user. 95 | 96 | At the core of TaskThread philosophy lies **rescheduling tasks**, i.e. asyncio tasks that reschedule themselves, giving the appearance of "threads" and a thread-like API. 97 | 98 | The only thing the API user needs to worry about, is how to initiate, re-schedule and terminate these tasks within their TaskThread implementation. 99 | 100 | Advantages 101 | ---------- 102 | 103 | Have you ever run into a situation where you have a complex asyncio program running tons of simultaneous tasks? 104 | 105 | For example, you need to run ``asyncio.wait`` to "poll" several tasks to see if the tasks have finished or not and then your program's logic is altered based on that result, 106 | creating an asynchronous mess, maybe even runaway tasks. 107 | 108 | Well, you don't need to touch ``asyncio.wait`` or ``asyncio.gather`` ever again, after starting to use TaskThread. 109 | 110 | Your programs will also become naturally well-organized into threads that have 111 | separation of concerns and restricted communication - in accordance with the `HIMO principle `_. 112 | 113 | I hope you got all warmed up by now. Exciting, right!? 114 | 115 | Next we will take a look at :ref:`an anatomy of a TaskThread `. 116 | 117 | Installing 118 | ---------- 119 | 120 | From PyPi simply with: 121 | 122 | :: 123 | 124 | pip3 install --user task-virtualthread 125 | 126 | -------------------------------------------------------------------------------- /docs/license.rst: -------------------------------------------------------------------------------- 1 | Copyright & License 2 | ------------------- 3 | 4 | Copyright 2021-2023 Sampsa Riikonen 5 | 6 | This library is licensed under the `MIT `_ license. 7 | -------------------------------------------------------------------------------- /docs/snippets/client.py: -------------------------------------------------------------------------------- 1 | import socket, time 2 | 3 | sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) 4 | sock.connect(("localhost", 5002)) 5 | time.sleep(1) 6 | print("sending msg") 7 | sock.send("kokkelis".encode("ascii")) 8 | time.sleep(1) 9 | print("sending msg2") 10 | sock.send("kokkelis2".encode("ascii")) 11 | time.sleep(1) 12 | print("closing socket") 13 | # sock.shutdown() 14 | sock.close() 15 | -------------------------------------------------------------------------------- /docs/snippets/client2.py: -------------------------------------------------------------------------------- 1 | import socket, time, json 2 | 3 | INT_NBYTES = 4 4 | 5 | def autoReadSock(sock): 6 | """Read first message length, then the message 7 | """ 8 | len_bytes = bytes() 9 | left = INT_NBYTES 10 | while left > 0: 11 | len_bytes += sock.recv(left) 12 | if len(len_bytes) < 1: 13 | return None 14 | left -= len(len_bytes) 15 | 16 | left = int.from_bytes(len_bytes, "big") 17 | msg_bytes = bytes() 18 | while left > 0: 19 | msg_bytes_ = sock.recv(4096) 20 | if len(msg_bytes_) < 1: 21 | return None 22 | else: 23 | msg_bytes += msg_bytes_ 24 | left -= len(msg_bytes_) 25 | return msg_bytes 26 | 27 | 28 | def bytes2Payload(bytes_): 29 | bytes_ = len(bytes_).to_bytes(INT_NBYTES, "big") + bytes_ 30 | return bytes_ 31 | 32 | 33 | def sendAll(sock = None, bytes_ = None, mbps = 1): 34 | kbps = mbps * 1024 35 | bytes_per_second = 1024*kbps/8 36 | seconds_per_byte = 1/bytes_per_second 37 | block_size = 1500 # standard mtu size 38 | f=block_size*8/1000 # one block in kb 39 | cc = len(bytes_) // block_size 40 | to = 0 41 | 42 | for i in range(cc): 43 | fr = i*block_size 44 | to = (i+1)*block_size 45 | blob_ = bytes_[fr:to] 46 | # print("sending:", fr, to, np.frombuffer(blob_, dtype=np.uint8)[0:10]) 47 | t=time.time() 48 | sock.sendall( 49 | blob_ 50 | ) 51 | dt=time.time()-t 52 | kbps_ = f/dt 53 | delay=max(seconds_per_byte*block_size-dt,0) 54 | time.sleep(delay) # delay to reach target kbps 55 | 56 | if to < len(bytes_): 57 | blob_ = bytes_[to:] 58 | # print("sending rest:",to, np.frombuffer(blob_, dtype=np.uint8)[0:10]) 59 | t=time.time() 60 | sock.sendall( 61 | blob_ 62 | ) 63 | dt=time.time()-t 64 | delay=max(seconds_per_byte*block_size-dt,0) 65 | time.sleep(delay) # delay to reach target kbps 66 | 67 | 68 | def sendAllBytes(sock = None, bytes_ = None, mbps = 1): 69 | bytes_ = bytes2Payload(bytes_) 70 | sendAll(sock = sock, bytes_ = bytes_, mbps = mbps) 71 | 72 | 73 | sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) 74 | sock.connect(("localhost", 5002)) 75 | b = autoReadSock(sock) 76 | dic = json.loads(b.decode("utf-8")) 77 | print("got from server:", dic) 78 | mbps = 1000 79 | n=22; print("sending payload"); sendAllBytes(sock=sock, bytes_=bytes(n), mbps=mbps) 80 | n=68; print("sending payload"); sendAllBytes(sock=sock, bytes_=bytes(n), mbps=mbps) 81 | n=1024; print("sending payload"); sendAllBytes(sock=sock, bytes_=bytes(n), mbps=mbps) 82 | for i in range(50): 83 | n=1024*1024; print("sending payload", i); sendAllBytes(sock=sock, bytes_=bytes(n), mbps=mbps) 84 | sock.close() 85 | -------------------------------------------------------------------------------- /docs/snippets/compile.bash: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | ./form_snippets.bash 3 | cd .. 4 | ./compile.bash 5 | cd snippets 6 | 7 | -------------------------------------------------------------------------------- /docs/snippets/example_server.py: -------------------------------------------------------------------------------- 1 | import asyncio, logging, traceback 2 | from task_thread import TaskThread, reCreate, reSchedule,\ 3 | delete, MessageSignal, verbose, signals 4 | """ 5 | Let's create a TCP server application. The hierarchy looks like this: 6 | 7 | :: 8 | 9 | MasterTCPServerThread 10 | TCPConnectionThread 11 | TCPConnectionThread 12 | ... 13 | 14 | As always, start by subclassing TaskThread. First we create the child ``TCPConnectionThread``. 15 | """ 16 | class TCPConnectionThread(TaskThread): 17 | """ 18 | Constructor takes as an argument stream reader and writer objects: 19 | """ 20 | def __init__(self, parent = None, reader: asyncio.StreamReader = None, writer: asyncio.StreamReader = None): 21 | super().__init__(parent = parent) 22 | assert(reader is not None) 23 | assert(writer is not None) 24 | self.reader = reader 25 | self.writer = writer 26 | self.peername, self.peerport = self.writer.get_extra_info("peername") 27 | 28 | def getId(self): 29 | return str(id(self)) 30 | 31 | def getInfo(self): 32 | return "" % (self.getId(), self.peername) 33 | 34 | """ 35 | Re-scheduling tasks for reading and writing the client socket: 36 | """ 37 | def initVars__(self): 38 | self.tasks.read_socket = None 39 | self.tasks.write_socket = None 40 | 41 | """ 42 | In this version we just read the socket, so start the corresponding re-scheduling task: 43 | """ 44 | @verbose 45 | async def enter__(self): 46 | self.logger.info("enter__ : %s", self.getInfo()) 47 | self.tasks.read_socket = await reCreate(self.tasks.read_socket, self.readSocket__) 48 | 49 | @verbose 50 | async def exit__(self): 51 | self.tasks.read_socket = await delete(self.tasks.read_socket) 52 | self.tasks.write_socket = await delete(self.tasks.write_socket) 53 | self.logger.debug("exit__: bye!") 54 | 55 | """ 56 | Reads packet from the socket & re-schedules itself. 57 | 58 | At failure, kill the whole ``TCPConnectionThread``. This will inform the parent automagically & remove 59 | this child thread from the parent's registry. 60 | """ 61 | async def readSocket__(self): 62 | try: 63 | try: 64 | packet = await self.reader.read() 65 | except Exception as e: 66 | self.logger.warning("readSocket__ : reading failed : '%s'", e) 67 | self.logger.info("readSocket__: tcp connection %s terminated", self.getInfo()) 68 | await self.stop() 69 | else: 70 | if len(packet) > 0: 71 | # all good! keep on reading = reschedule this 72 | self.logger.info("readSocket__: got packet %s", packet) 73 | self.tasks.read_socket = await reSchedule(self.readSocket__); return 74 | else: 75 | self.logger.info("readSocket__: client at %s closed connection", self.peername) 76 | await self.stop() 77 | 78 | except asyncio.CancelledError: 79 | self.logger.info("readSocket__ : cancelling %s", self.getInfo()) 80 | 81 | except Exception as e: 82 | self.logger.warning("readSocket__: failed with '%s'", e) 83 | await self.stop() 84 | 85 | """ 86 | Writing socket continuously (however, not used in this example): 87 | """ 88 | async def writeSocket__(self, packet): 89 | try: 90 | self.writer.write(packet) 91 | # await self.writer.drain() # this would flush 92 | except Exception as e: 93 | self.logger.warning("writeSocket__ : writing failed : '%s'", e) 94 | await self.stop() 95 | 96 | 97 | """ 98 | Next, create the parent ``MasterTCPServerThread``: 99 | """ 100 | class MasterTCPServerThread(TaskThread): 101 | 102 | def __init__(self, parent = None, name = "thread", pause = 10, port = 5002, max_clients = 10): 103 | super().__init__(parent = parent) 104 | self.pause = pause 105 | self.port = port 106 | self.max_clients = max_clients 107 | self.name = name 108 | 109 | 110 | def initVars__(self): 111 | self.server = None 112 | self.tasks.tcp_server = None 113 | 114 | 115 | def getId(self): 116 | return str(id(self)) 117 | 118 | 119 | def getInfo(self): 120 | return "" % (self.name) 121 | 122 | 123 | @verbose 124 | async def enter__(self): 125 | self.logger.info("entry point") 126 | self.tasks.tcp_server = await reCreate(self.tasks.tcp_server, self.tcpServer__) 127 | 128 | 129 | @verbose 130 | async def exit__(self): 131 | self.tasks.tcp_server = await delete(self.tasks.tcp_server) 132 | self.logger.debug("exit__: bye!") 133 | 134 | """ 135 | Each and every child / tcp client sends it's packets to the parent: 136 | """ 137 | @verbose 138 | async def childsignalHandler__(self, signal, child): 139 | self.logger.debug("childsignalHandler__ : got signal %s from child %s", signal, child) 140 | if isinstance(signal, MessageSignal): 141 | self.logger.info("Got message %s from child with id %s", signal.getMessage(), child.getId()) 142 | else: 143 | pass 144 | 145 | """ 146 | Keep on (re)creating the tcp server: 147 | """ 148 | async def tcpServer__(self): 149 | try: 150 | self.server = await asyncio.start_server(self.handler__, "", self.port) 151 | 152 | except asyncio.CancelledError: 153 | self.logger.info("tcpServer__ %i: cancel") 154 | self.server = None 155 | 156 | except Exception as e: 157 | self.logger.warning("tcpServer__: failed with '%s'", str(e)) 158 | self.logger.warning("tcpServer__: will try again in %s secs", self.pause) 159 | await asyncio.sleep(self.pause) 160 | self.tasks.tcp_server = await reSchedule(self.tcpServer__) 161 | 162 | else: 163 | self.logger.debug("tcpServer__ : new server waiting") 164 | 165 | """ 166 | At a client connection, start a new ``TCPConnectionThread``: 167 | """ 168 | @verbose 169 | async def handler__(self, reader, writer): 170 | self.logger.debug("handler__ : new connection for %s", self.getInfo()) 171 | 172 | if len(self.children)>self.max_clients: 173 | self.logger.warning("handler__ : max number of connections is %s", self.max_clients) 174 | return 175 | 176 | child_connection = TCPConnectionThread(reader = reader, writer = writer, parent = self) 177 | await child_connection.run() 178 | await self.addChild(child_connection) 179 | 180 | """ 181 | Run it! 182 | """ 183 | if __name__ == "__main__": 184 | loglev = logging.DEBUG 185 | 186 | logger = logging.getLogger("MasterTCPServerThread") 187 | logger.setLevel(loglev) 188 | logger = logging.getLogger("TCPConnectionThread") 189 | logger.setLevel(loglev) 190 | 191 | thread = MasterTCPServerThread( 192 | parent = None, 193 | name = "TCPServer" 194 | ) 195 | loop = asyncio.get_event_loop() 196 | loop.run_until_complete(thread.run()) 197 | """ 198 | While it's running, start one or several client connections using the provided client code. 199 | """ 200 | -------------------------------------------------------------------------------- /docs/snippets/example_server.py_: -------------------------------------------------------------------------------- 1 | 2 | .. code:: python 3 | 4 | import asyncio, logging, traceback 5 | from task_thread import TaskThread, reCreate, reSchedule,\ 6 | delete, MessageSignal, verbose, signals 7 | 8 | Let's create a TCP server application. The hierarchy looks like this: 9 | 10 | :: 11 | 12 | MasterTCPServerThread 13 | TCPConnectionThread 14 | TCPConnectionThread 15 | ... 16 | 17 | As always, start by subclassing TaskThread. First we create the child ``TCPConnectionThread``. 18 | 19 | .. code:: python 20 | 21 | class TCPConnectionThread(TaskThread): 22 | 23 | Constructor takes as an argument stream reader and writer objects: 24 | 25 | .. code:: python 26 | 27 | def __init__(self, parent = None, reader: asyncio.StreamReader = None, writer: asyncio.StreamReader = None): 28 | super().__init__(parent = parent) 29 | assert(reader is not None) 30 | assert(writer is not None) 31 | self.reader = reader 32 | self.writer = writer 33 | self.peername, self.peerport = self.writer.get_extra_info("peername") 34 | 35 | def getId(self): 36 | return str(id(self)) 37 | 38 | def getInfo(self): 39 | return "" % (self.getId(), self.peername) 40 | 41 | 42 | Re-scheduling tasks for reading and writing the client socket: 43 | 44 | .. code:: python 45 | 46 | def initVars__(self): 47 | self.tasks.read_socket = None 48 | self.tasks.write_socket = None 49 | 50 | 51 | In this version we just read the socket, so start the corresponding re-scheduling task: 52 | 53 | .. code:: python 54 | 55 | @verbose 56 | async def enter__(self): 57 | self.logger.info("enter__ : %s", self.getInfo()) 58 | self.tasks.read_socket = await reCreate(self.tasks.read_socket, self.readSocket__) 59 | 60 | @verbose 61 | async def exit__(self): 62 | self.tasks.read_socket = await delete(self.tasks.read_socket) 63 | self.tasks.write_socket = await delete(self.tasks.write_socket) 64 | self.logger.debug("exit__: bye!") 65 | 66 | 67 | Reads packet from the socket & re-schedules itself. 68 | 69 | At failure, kill the whole ``TCPConnectionThread``. This will inform the parent automagically & remove 70 | this child thread from the parent's registry. 71 | 72 | .. code:: python 73 | 74 | async def readSocket__(self): 75 | try: 76 | try: 77 | packet = await self.reader.read() 78 | except Exception as e: 79 | self.logger.warning("readSocket__ : reading failed : '%s'", e) 80 | self.logger.info("readSocket__: tcp connection %s terminated", self.getInfo()) 81 | await self.stop() 82 | else: 83 | if len(packet) > 0: 84 | # all good! keep on reading = reschedule this 85 | self.logger.info("readSocket__: got packet %s", packet) 86 | self.tasks.read_socket = await reSchedule(self.readSocket__); return 87 | else: 88 | self.logger.info("readSocket__: client at %s closed connection", self.peername) 89 | await self.stop() 90 | 91 | except asyncio.CancelledError: 92 | self.logger.info("readSocket__ : cancelling %s", self.getInfo()) 93 | 94 | except Exception as e: 95 | self.logger.warning("readSocket__: failed with '%s'", e) 96 | await self.stop() 97 | 98 | 99 | Writing socket continuously (however, not used in this example): 100 | 101 | .. code:: python 102 | 103 | async def writeSocket__(self, packet): 104 | try: 105 | self.writer.write(packet) 106 | # await self.writer.drain() # this would flush 107 | except Exception as e: 108 | self.logger.warning("writeSocket__ : writing failed : '%s'", e) 109 | await self.stop() 110 | 111 | 112 | 113 | Next, create the parent ``MasterTCPServerThread``: 114 | 115 | .. code:: python 116 | 117 | class MasterTCPServerThread(TaskThread): 118 | 119 | def __init__(self, parent = None, name = "thread", pause = 10, port = 5002, max_clients = 10): 120 | super().__init__(parent = parent) 121 | self.pause = pause 122 | self.port = port 123 | self.max_clients = max_clients 124 | self.name = name 125 | 126 | 127 | def initVars__(self): 128 | self.server = None 129 | self.tasks.tcp_server = None 130 | 131 | 132 | def getId(self): 133 | return str(id(self)) 134 | 135 | 136 | def getInfo(self): 137 | return "" % (self.name) 138 | 139 | 140 | @verbose 141 | async def enter__(self): 142 | self.logger.info("entry point") 143 | self.tasks.tcp_server = await reCreate(self.tasks.tcp_server, self.tcpServer__) 144 | 145 | 146 | @verbose 147 | async def exit__(self): 148 | self.tasks.tcp_server = await delete(self.tasks.tcp_server) 149 | self.logger.debug("exit__: bye!") 150 | 151 | 152 | Each and every child / tcp client sends it's packets to the parent: 153 | 154 | .. code:: python 155 | 156 | @verbose 157 | async def childsignalHandler__(self, signal, child): 158 | self.logger.debug("childsignalHandler__ : got signal %s from child %s", signal, child) 159 | if isinstance(signal, MessageSignal): 160 | self.logger.info("Got message %s from child with id %s", signal.getMessage(), child.getId()) 161 | else: 162 | pass 163 | 164 | 165 | Keep on (re)creating the tcp server: 166 | 167 | .. code:: python 168 | 169 | async def tcpServer__(self): 170 | try: 171 | self.server = await asyncio.start_server(self.handler__, "", self.port) 172 | 173 | except asyncio.CancelledError: 174 | self.logger.info("tcpServer__ %i: cancel") 175 | self.server = None 176 | 177 | except Exception as e: 178 | self.logger.warning("tcpServer__: failed with '%s'", str(e)) 179 | self.logger.warning("tcpServer__: will try again in %s secs", self.pause) 180 | await asyncio.sleep(self.pause) 181 | self.tasks.tcp_server = await reSchedule(self.tcpServer__) 182 | 183 | else: 184 | self.logger.debug("tcpServer__ : new server waiting") 185 | 186 | 187 | At a client connection, start a new ``TCPConnectionThread``: 188 | 189 | .. code:: python 190 | 191 | @verbose 192 | async def handler__(self, reader, writer): 193 | self.logger.debug("handler__ : new connection for %s", self.getInfo()) 194 | 195 | if len(self.children)>self.max_clients: 196 | self.logger.warning("handler__ : max number of connections is %s", self.max_clients) 197 | return 198 | 199 | child_connection = TCPConnectionThread(reader = reader, writer = writer, parent = self) 200 | await child_connection.run() 201 | await self.addChild(child_connection) 202 | 203 | 204 | Run it! 205 | 206 | .. code:: python 207 | 208 | if __name__ == "__main__": 209 | loglev = logging.DEBUG 210 | 211 | logger = logging.getLogger("MasterTCPServerThread") 212 | logger.setLevel(loglev) 213 | logger = logging.getLogger("TCPConnectionThread") 214 | logger.setLevel(loglev) 215 | 216 | thread = MasterTCPServerThread( 217 | parent = None, 218 | name = "TCPServer" 219 | ) 220 | loop = asyncio.get_event_loop() 221 | loop.run_until_complete(thread.run()) 222 | 223 | While it's running, start one or several client connections using the provided client code. 224 | -------------------------------------------------------------------------------- /docs/snippets/form_snippets.bash: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | exe="python3" 3 | 4 | # # list here your example snippets 5 | codes="hello_world.py parent_child.py example_server.py example_server2.py" 6 | 7 | for i in $codes 8 | do 9 | echo $i 10 | $exe pyeval.py $i > $i"_" 11 | done 12 | -------------------------------------------------------------------------------- /docs/snippets/hello_world.py: -------------------------------------------------------------------------------- 1 | """ 2 | The same "hello world" example we already discussed :ref:`here `. 3 | """ 4 | import asyncio, logging, traceback 5 | from task_thread import TaskThread, reCreate, reSchedule,\ 6 | delete, verbose, signals 7 | """ 8 | Define TaskThread by subclassing 9 | """ 10 | class MyThread(TaskThread): 11 | """ 12 | Let's define a rescheduling task that sleeps for a one seconds & then reschedules itself 13 | """ 14 | async def helloTask__(self): 15 | try: 16 | await asyncio.sleep(1) 17 | self.logger.info("Hello from helloTask__ at %s", self.getId()) 18 | self.tasks.hello_task = await reSchedule(self.helloTask__) 19 | return 20 | 21 | except asyncio.CancelledError: 22 | self.logger.critical("helloTask__: cancelling") 23 | 24 | except Exception as e: 25 | self.logger.info("helloTask__: failed with '%s', traceback will follow", e) 26 | traceback.print_exc() 27 | 28 | """ 29 | Remember to call the superclass constructor 30 | """ 31 | def __init__(self, my_id = 0, parent = None): 32 | super().__init__(parent = parent) 33 | self.my_id = my_id 34 | 35 | """ 36 | Create the tasks under the self.tasks namespace & initialize them to ``None`` 37 | """ 38 | def initVars__(self): 39 | # tasks 40 | self.tasks.hello = None # self.helloTask__ 41 | 42 | """ 43 | The ``helloTask__`` is started for the first time when the thread is started: 44 | """ 45 | @verbose 46 | async def enter__(self): 47 | self.logger.info("enter__ :") 48 | self.tasks.hello = await reCreate(self.tasks.hello, self.helloTask__) 49 | 50 | """ 51 | Define what happens at thread exit: delete any running tasks 52 | """ 53 | @verbose 54 | async def exit__(self): 55 | """Close sockets, databases, etc. Overwrite in child class. 56 | """ 57 | self.tasks.hello = await delete(self.tasks.hello) 58 | self.logger.info("exit__ : finished") 59 | 60 | 61 | """ 62 | Here we would define how signals from a parent are handled. In this example case nothing. 63 | """ 64 | @verbose 65 | async def signalHandler__(self, signal): 66 | self.logger.info("signalHandler__ : got signal %s", signal) 67 | 68 | def getId(self): 69 | return self.my_id 70 | 71 | def getInfo(self): 72 | return "" 73 | 74 | 75 | """ 76 | Let's run MyThread! Terminate by pressing CTRL-C. 77 | """ 78 | if __name__ == "__main__": 79 | loglev = logging.DEBUG 80 | 81 | logger = logging.getLogger("MyThread") 82 | logger.setLevel(loglev) 83 | 84 | thread = MyThread(my_id = "main_thread", parent = None) # no parent, this is the main thread 85 | loop = asyncio.get_event_loop() 86 | loop.run_until_complete(thread.run()) 87 | 88 | -------------------------------------------------------------------------------- /docs/snippets/hello_world.py_: -------------------------------------------------------------------------------- 1 | 2 | The same "hello world" example we already discussed :ref:`here `. 3 | 4 | .. code:: python 5 | 6 | import asyncio, logging, traceback 7 | from task_thread import TaskThread, reCreate, reSchedule,\ 8 | delete, verbose, signals 9 | 10 | Define TaskThread by subclassing 11 | 12 | .. code:: python 13 | 14 | class MyThread(TaskThread): 15 | 16 | Let's define a rescheduling task that sleeps for a one seconds & then reschedules itself 17 | 18 | .. code:: python 19 | 20 | async def helloTask__(self): 21 | try: 22 | await asyncio.sleep(1) 23 | self.logger.info("Hello from helloTask__ at %s", self.getId()) 24 | self.tasks.hello_task = await reSchedule(self.helloTask__) 25 | return 26 | 27 | except asyncio.CancelledError: 28 | self.logger.critical("helloTask__: cancelling") 29 | 30 | except Exception as e: 31 | self.logger.info("helloTask__: failed with '%s', traceback will follow", e) 32 | traceback.print_exc() 33 | 34 | 35 | Remember to call the superclass constructor 36 | 37 | .. code:: python 38 | 39 | def __init__(self, my_id = 0, parent = None): 40 | super().__init__(parent = parent) 41 | self.my_id = my_id 42 | 43 | 44 | Create the tasks under the self.tasks namespace & initialize them to ``None`` 45 | 46 | .. code:: python 47 | 48 | def initVars__(self): 49 | # tasks 50 | self.tasks.hello = None # self.helloTask__ 51 | 52 | 53 | The ``helloTask__`` is started for the first time when the thread is started: 54 | 55 | .. code:: python 56 | 57 | @verbose 58 | async def enter__(self): 59 | self.logger.info("enter__ :") 60 | self.tasks.hello = await reCreate(self.tasks.hello, self.helloTask__) 61 | 62 | 63 | Define what happens at thread exit: delete any running tasks 64 | 65 | .. code:: python 66 | 67 | @verbose 68 | async def exit__(self): 69 | """Close sockets, databases, etc. Overwrite in child class. 70 | """ 71 | self.tasks.hello = await delete(self.tasks.hello) 72 | self.logger.info("exit__ : finished") 73 | 74 | 75 | 76 | Here we would define how signals from a parent are handled. In this example case nothing. 77 | 78 | .. code:: python 79 | 80 | @verbose 81 | async def signalHandler__(self, signal): 82 | self.logger.info("signalHandler__ : got signal %s", signal) 83 | 84 | def getId(self): 85 | return self.my_id 86 | 87 | def getInfo(self): 88 | return "" 89 | 90 | 91 | 92 | Let's run MyThread! Terminate by pressing CTRL-C. 93 | 94 | .. code:: python 95 | 96 | if __name__ == "__main__": 97 | loglev = logging.DEBUG 98 | 99 | logger = logging.getLogger("MyThread") 100 | logger.setLevel(loglev) 101 | 102 | thread = MyThread(my_id = "main_thread", parent = None) # no parent, this is the main thread 103 | loop = asyncio.get_event_loop() 104 | loop.run_until_complete(thread.run()) 105 | 106 | -------------------------------------------------------------------------------- /docs/snippets/parent_child.py: -------------------------------------------------------------------------------- 1 | import asyncio, logging, traceback 2 | from task_thread import TaskThread, reCreate, reSchedule,\ 3 | delete, verbose, signals 4 | 5 | """ 6 | Let's define a signal for parent/child communication 7 | """ 8 | class MessageSignal(signals.Signal): 9 | def __init__(self, origin, message): 10 | self.origin = origin 11 | self.message = message 12 | 13 | def __str__(self): 14 | return "" % (str(self.origin.getId())) 15 | 16 | def getMessage(self): 17 | return self.message 18 | 19 | """ 20 | We use the same ``MyThread`` as in the previous example. 21 | We will use it as a basis for further subclassing 22 | """ 23 | class MyThread(TaskThread): 24 | # as in previous example 25 | # ... 26 | #HIDE> 27 | async def helloTask__(self): 28 | try: 29 | await asyncio.sleep(1) 30 | self.logger.info("Hello from helloTask__ at %s", self.getId()) 31 | self.tasks.hello_task = await reSchedule(self.helloTask__); return 32 | 33 | except asyncio.CancelledError: 34 | self.logger.critical("helloTask__: cancelling") 35 | 36 | except Exception as e: 37 | self.logger.info("helloTask__: failed with '%s', traceback will follow", e) 38 | traceback.print_exc() 39 | 40 | 41 | def __init__(self, my_id = 0, parent = None): 42 | super().__init__(parent = parent) 43 | self.my_id = my_id 44 | 45 | 46 | def initVars__(self): 47 | """Create & initialize here your tasks with none & create your locks 48 | """ 49 | # tasks 50 | self.tasks.hello = None # self.helloTask__ 51 | 52 | 53 | @verbose 54 | async def enter__(self): 55 | """Everything starts from here. This cofunction is awaited (i.e. not scheduled as a task) 56 | 57 | - Await for something critical 58 | - Shedule the re-scheduling tasks 59 | """ 60 | self.logger.info("entry point") 61 | self.tasks.hello = await reCreate(self.tasks.hello, self.helloTask__) 62 | 63 | @verbose 64 | async def exit__(self): 65 | """Close sockets, databases, etc. Overwrite in child class. 66 | """ 67 | self.tasks.hello = await delete(self.tasks.hello) 68 | self.logger.info("exit__ : finished") 69 | 70 | @verbose 71 | async def signalHandler__(self, signal): 72 | """Handles a signal that was sent to this thread using insertSignal 73 | 74 | - Return True if thread should keep on running 75 | - Return False if thread should exit (say, in the case of some fatal error) 76 | 77 | Do subclass this. 78 | """ 79 | self.logger.info("signalHandler__ : got signal %s", signal) 80 | 81 | 82 | def getId(self): 83 | """Do subclass this. 84 | 85 | Return a unique id for this VirtualThread. 86 | """ 87 | return self.my_id 88 | 89 | 90 | def getInfo(self): 91 | """Do subclass this 92 | 93 | Returns information string about this thread 94 | """ 95 | return "" 96 | 97 | # 100 | Let's create a child thread. This thread sends a message to it's parent every 5 seconds: 101 | """ 102 | class ChildThread(MyThread): 103 | """ 104 | Define a rescheduling task that sends a message to it's parent every 5 seconds 105 | """ 106 | async def messageTask__(self): 107 | try: 108 | await asyncio.sleep(5) 109 | self.logger.info("messageTask__: sending a message to parent") 110 | 111 | await self.sigFromChildToParent__( 112 | MessageSignal(origin = self, 113 | message = "hello from child " + str(self.getId()))) 114 | self.tasks.message_task = await reSchedule(self.messageTask__); return 115 | 116 | except asyncio.CancelledError: 117 | self.logger.critical("messageTask__: cancelling") 118 | 119 | except Exception as e: 120 | self.logger.info("messageTask__: failed with '%s', traceback will follow", e) 121 | traceback.print_exc() 122 | 123 | """ 124 | Add the new task to ``self.tasks`` namespace 125 | """ 126 | def initVars__(self): 127 | # tasks 128 | self.tasks.hello = None # self.helloTask__ 129 | self.tasks.message = None # self.messageTask__ 130 | # locks 131 | self.locks.message_lock = asyncio.Lock() 132 | 133 | """ 134 | Start both tasks at thread start 135 | """ 136 | @verbose 137 | async def enter__(self): 138 | self.logger.info("enter__") 139 | self.tasks.hello = await reCreate(self.tasks.hello, self.helloTask__) 140 | self.tasks.message = await reCreate(self.tasks.message, self.messageTask__) 141 | 142 | """ 143 | Kill both tasks at thread exit 144 | """ 145 | @verbose 146 | async def exit__(self): 147 | self.tasks.hello = await delete(self.tasks.hello) 148 | self.tasks.message = await delete(self.tasks.message) 149 | self.logger.info("exit__ : finished") 150 | 151 | 152 | """ 153 | Now the parent thread that receives signals from the child: 154 | """ 155 | class ParentThread(MyThread): 156 | """ 157 | Start ``self.helloTask__`` just like in the child thread. 158 | 159 | The new bit here is, that we create and start a child thread at parent thread start. 160 | 161 | After starting, the child thread is added to the parent's registry using ``addChild``. 162 | 163 | You can experiment by adding more and more child threads. 164 | """ 165 | @verbose 166 | async def enter__(self): 167 | self.logger.info("enter__") 168 | self.tasks.hello = await reCreate(self.tasks.hello, self.helloTask__) 169 | child_thread = ChildThread(my_id = "subthread", parent = self) 170 | await child_thread.run() 171 | await self.addChild(child_thread) 172 | 173 | """ 174 | Define how we handle those signals coming from the running child thread: 175 | """ 176 | @verbose 177 | async def childsignalHandler__(self, signal, child): 178 | self.logger.debug("childsignalHandler__ : got signal %s from child %s", signal, child.getId()) 179 | if isinstance(signal, MessageSignal): 180 | self.logger.info("Got message %s from child with id %s", 181 | signal.getMessage(), child.getId()) 182 | else: 183 | pass 184 | 185 | """ 186 | Please note that you can also start child threads "dynamically", i.e. anywhere in your classes internal methods/recheduling tasks, not just at ``enter__``. 187 | 188 | That's it! Nothing else is needed: when the parent thread terminates, it automagically terminates all of it's child threads. 189 | 190 | Run the program with: 191 | """ 192 | if __name__ == "__main__": 193 | loglev = logging.DEBUG 194 | 195 | logger = logging.getLogger("ParentThread") 196 | logger.setLevel(loglev) 197 | 198 | logger = logging.getLogger("ChildThread") 199 | logger.setLevel(loglev) 200 | 201 | thread = ParentThread(my_id = "parent_thread", parent = None) # no parent, this is the main thread 202 | loop = asyncio.get_event_loop() 203 | loop.run_until_complete(thread.run()) 204 | """ 205 | Terminate by pressing CTRL-C. 206 | """ 207 | -------------------------------------------------------------------------------- /docs/snippets/parent_child.py_: -------------------------------------------------------------------------------- 1 | 2 | .. code:: python 3 | 4 | import asyncio, logging, traceback 5 | from task_thread import TaskThread, reCreate, reSchedule,\ 6 | delete, verbose, signals 7 | 8 | 9 | Let's define a signal for parent/child communication 10 | 11 | .. code:: python 12 | 13 | class MessageSignal(signals.Signal): 14 | def __init__(self, origin, message): 15 | self.origin = origin 16 | self.message = message 17 | 18 | def __str__(self): 19 | return "" % (str(self.origin.getId())) 20 | 21 | def getMessage(self): 22 | return self.message 23 | 24 | 25 | We use the same ``MyThread`` as in the previous example. 26 | We will use it as a basis for further subclassing 27 | 28 | .. code:: python 29 | 30 | class MyThread(TaskThread): 31 | # as in previous example 32 | # ... 33 | 34 | 35 | Let's create a child thread. This thread sends a message to it's parent every 5 seconds: 36 | 37 | .. code:: python 38 | 39 | class ChildThread(MyThread): 40 | 41 | Define a rescheduling task that sends a message to it's parent every 5 seconds 42 | 43 | .. code:: python 44 | 45 | async def messageTask__(self): 46 | try: 47 | await asyncio.sleep(5) 48 | self.logger.info("messageTask__: sending a message to parent") 49 | 50 | await self.sigFromChildToParent__( 51 | MessageSignal(origin = self, 52 | message = "hello from child " + str(self.getId()))) 53 | self.tasks.message_task = await reSchedule(self.messageTask__); return 54 | 55 | except asyncio.CancelledError: 56 | self.logger.critical("messageTask__: cancelling") 57 | 58 | except Exception as e: 59 | self.logger.info("messageTask__: failed with '%s', traceback will follow", e) 60 | traceback.print_exc() 61 | 62 | 63 | Add the new task to ``self.tasks`` namespace 64 | 65 | .. code:: python 66 | 67 | def initVars__(self): 68 | # tasks 69 | self.tasks.hello = None # self.helloTask__ 70 | self.tasks.message = None # self.messageTask__ 71 | # locks 72 | self.locks.message_lock = asyncio.Lock() 73 | 74 | 75 | Start both tasks at thread start 76 | 77 | .. code:: python 78 | 79 | @verbose 80 | async def enter__(self): 81 | self.logger.info("enter__") 82 | self.tasks.hello = await reCreate(self.tasks.hello, self.helloTask__) 83 | self.tasks.message = await reCreate(self.tasks.message, self.messageTask__) 84 | 85 | 86 | Kill both tasks at thread exit 87 | 88 | .. code:: python 89 | 90 | @verbose 91 | async def exit__(self): 92 | self.tasks.hello = await delete(self.tasks.hello) 93 | self.tasks.message = await delete(self.tasks.message) 94 | self.logger.info("exit__ : finished") 95 | 96 | 97 | 98 | Now the parent thread that receives signals from the child: 99 | 100 | .. code:: python 101 | 102 | class ParentThread(MyThread): 103 | 104 | Start ``self.helloTask__`` just like in the child thread. 105 | 106 | The new bit here is, that we create and start a child thread at parent thread start. 107 | 108 | After starting, the child thread is added to the parent's registry using ``addChild``. 109 | 110 | You can experiment by adding more and more child threads. 111 | 112 | .. code:: python 113 | 114 | @verbose 115 | async def enter__(self): 116 | self.logger.info("enter__") 117 | self.tasks.hello = await reCreate(self.tasks.hello, self.helloTask__) 118 | child_thread = ChildThread(my_id = "subthread", parent = self) 119 | await child_thread.run() 120 | await self.addChild(child_thread) 121 | 122 | 123 | Define how we handle those signals coming from the running child thread: 124 | 125 | .. code:: python 126 | 127 | @verbose 128 | async def childsignalHandler__(self, signal, child): 129 | self.logger.debug("childsignalHandler__ : got signal %s from child %s", signal, child.getId()) 130 | if isinstance(signal, MessageSignal): 131 | self.logger.info("Got message %s from child with id %s", 132 | signal.getMessage(), child.getId()) 133 | else: 134 | pass 135 | 136 | 137 | Please note that you can also start child threads "dynamically", i.e. anywhere in your classes internal methods/recheduling tasks, not just at ``enter__``. 138 | 139 | That's it! Nothing else is needed: when the parent thread terminates, it automagically terminates all of it's child threads. 140 | 141 | Run the program with: 142 | 143 | .. code:: python 144 | 145 | if __name__ == "__main__": 146 | loglev = logging.DEBUG 147 | 148 | logger = logging.getLogger("ParentThread") 149 | logger.setLevel(loglev) 150 | 151 | logger = logging.getLogger("ChildThread") 152 | logger.setLevel(loglev) 153 | 154 | thread = ParentThread(my_id = "parent_thread", parent = None) # no parent, this is the main thread 155 | loop = asyncio.get_event_loop() 156 | loop.run_until_complete(thread.run()) 157 | 158 | Terminate by pressing CTRL-C. 159 | -------------------------------------------------------------------------------- /docs/snippets/pyeval.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | """Script's first argument: input file 3 | """ 4 | import sys 5 | 6 | def main(): 7 | hide = False 8 | rst_mode = False 9 | empty = None # checks if found comment or code sections were empty or not 10 | leadspace = 0 11 | f = open(sys.argv[1]) 12 | lines = [] 13 | c = 0 # line counter 14 | last_csec = 0 # last line where a code section started 15 | inst = " " 16 | newlines = [] 17 | for line in f.readlines(): 18 | st = line[:-1] # remove newline 19 | if st.strip().lower() in ["#"]: 20 | hide = False 21 | c+=1 22 | continue 23 | elif st.strip().lower() in ["#hide>", "#"]: 24 | hide = True 25 | if hide: 26 | c+=1 27 | continue 28 | if (c == 0 and st.strip() != '"""'): 29 | # first line and NOT starting with comment section 30 | # so start with code section 31 | """print("") 32 | print(".. code:: python") 33 | print("") 34 | print(inst+st)""" 35 | newlines += [ 36 | "", 37 | ".. code:: python", 38 | "", 39 | inst+st 40 | ] 41 | elif (st.strip() == '"""'): 42 | # comment section starts 43 | # let's see how many empty lines we had in the prepending code section 44 | if empty: 45 | # new comment section starts but the previous code 46 | # section was empty .. so let's remote it 47 | for i in range(0, empty): 48 | newlines.pop(-1) 49 | rst_mode = True 50 | # check how many leading spaces 51 | leadspace = len(st) - len(st.lstrip()) 52 | # print("") 53 | newlines.append("") 54 | empty = 1 # only empty lines in this comment section for the moment 55 | elif (st.strip() == '"""'): 56 | # rtf comment section stops - start code section again 57 | if empty: 58 | # new code section starts but the previous comment 59 | # section was empty .. so let's remote it 60 | for i in range(0, empty): 61 | newlines.pop(-1) 62 | last_csec=len(newlines) 63 | rst_mode = False 64 | """print("") 65 | print(".. code:: python") 66 | print("")""" 67 | newlines += [ 68 | "", 69 | ".. code:: python", 70 | "" 71 | ] 72 | empty = 3 # only empty lines in this code section for the moment 73 | elif (rst_mode == False): 74 | # print into code section 75 | # print(inst+st) 76 | if (empty is not None) and len(st.strip()) == 0: # empty line in this code section 77 | empty += 1 78 | else: 79 | empty = None # ok.. all the lines were not empty 80 | newlines.append(inst+st) 81 | else: 82 | # print into comment section 83 | # print(st[leadspace:]) 84 | if (empty is not None) and len(st.strip()) == 0: # empty line in this comment section 85 | empty += 1 86 | else: 87 | empty = None # ok.. all the lines were not empty 88 | newlines.append(st[leadspace:]) 89 | c+=1 90 | 91 | if empty: 92 | # previous section was empty .. so let's remote it 93 | for i in range(0, empty): 94 | newlines.pop(-1) 95 | 96 | for line in newlines: 97 | print(line) 98 | 99 | if __name__ == "__main__": 100 | main() 101 | 102 | -------------------------------------------------------------------------------- /docs/snippets/requirements.txt: -------------------------------------------------------------------------------- 1 | docutils>=0.3 2 | 3 | -------------------------------------------------------------------------------- /docs/snippets/requirements.txt_: -------------------------------------------------------------------------------- 1 | :: 2 | 3 | docutils>=0.3 4 | 5 | -------------------------------------------------------------------------------- /docs/task.rst: -------------------------------------------------------------------------------- 1 | .. _task: 2 | 3 | Anatomy of a Task 4 | ================= 5 | 6 | How do these "rescheduling tasks" that constitute a "TaskThread" look like? 7 | 8 | Let's consider a silly task that sleeps for 1 sec and then re-schedules itself 9 | 10 | First the imports: 11 | 12 | :: 13 | 14 | import asyncio, logging, traceback 15 | from task_thread import TaskThread, reCreate, reSchedule,\ 16 | delete, verbose, signals 17 | 18 | The rescheduling task itself: 19 | 20 | :: 21 | 22 | async def helloTask__(self): 23 | try: 24 | await asyncio.sleep(1) 25 | self.logger.info("Hello from helloTask__ at %s", self.getId()) 26 | self.tasks.hello_task = await reSchedule(self.helloTask__) 27 | return 28 | 29 | except asyncio.CancelledError: 30 | self.logger.critical("helloTask__: cancelling") 31 | 32 | except Exception as e: 33 | self.logger.info("helloTask__: failed with '%s', traceback will follow", e) 34 | traceback.print_exc() 35 | 36 | with emphasis on this structure: 37 | 38 | :: 39 | 40 | try: 41 | ... 42 | 43 | except asyncio.CancelledError: 44 | ... 45 | 46 | except Exception as e: 47 | ... 48 | 49 | 50 | i.e., if everything is ok - the task has done it's thing, say, reading payload from i/o, 51 | writing i/o, or whatever - it then **reschedules itself**. 52 | 53 | Rescheduling is done using the ``reSchedule`` convenience function: 54 | 55 | :: 56 | 57 | self.tasks.hello_task = await reSchedule(self.helloTask__) 58 | 59 | A large group of these auto-rescheduling tasks that toil around and do their stuff, behave effectively like a "classical" running thread. 60 | 61 | A small warning about task re-scheduling is necessary: 62 | 63 | A function can re-schedule itself with quite a high frequency. However, for each re-scheduling task, you should be at least, aware of the frequency: 64 | if a task re-schedules itself, say, a million times per second, you have created yourself a problem. 65 | 66 | Try to keep your task's re-scheduling frequency in 100 times per second or less. High re-scheduling frequency and its mitigation might become an issue in streaming applications, 67 | while in most other cases you really don't need to think about it. 68 | 69 | Finally, remember to use the correct convenience function for each case: 70 | 71 | - When creating the task for the first time, use ``reCreate`` 72 | - When rescheduling the task, use ``reSchedule`` 73 | - When removing the task, use ``delete`` 74 | 75 | Next, let's :ref:`bring TaskThreads and tasks together `. 76 | 77 | -------------------------------------------------------------------------------- /docs/thread.rst: -------------------------------------------------------------------------------- 1 | 2 | .. _thread: 3 | 4 | Anatomy of a TaskThread 5 | ======================= 6 | 7 | Imports 8 | ------- 9 | 10 | The necessary imports for writing your TaskThread are: 11 | 12 | :: 13 | 14 | import asyncio, logging, traceback 15 | from task_thread import TaskThread, reCreate, reSchedule,\ 16 | delete, verbose, signals 17 | 18 | 19 | Subclassing 20 | ----------- 21 | 22 | You create a custom thread by subclassing from the ``TaskThread`` base class. 23 | 24 | The methods in ``TaskThread`` that you must override are: 25 | 26 | :: 27 | 28 | 29 | def __init__(self, parent = None) 30 | 31 | def initVars__(self) 32 | 33 | async def enter__(self) 34 | 35 | async def exit__(self) 36 | 37 | async def signalHandler__(self, signal) 38 | 39 | async def childsignalHandler__(self, signal, child) 40 | 41 | def getId(self) 42 | 43 | def getInfo(self) 44 | 45 | 46 | Let's take a closer look at each one of these methods. 47 | 48 | In the ``__init__`` you *must* call the superclass method (similar to ``threading`` and ``multiprocess`` modules): 49 | 50 | :: 51 | 52 | def __init__(self, parent = None): 53 | super().__init__(parent = parent) 54 | # whatever extra stuff 55 | 56 | In ``initVars`` you define the rescheduling tasks that define the functionality of your 57 | TaskThread: 58 | 59 | :: 60 | 61 | def initVars__(self): 62 | """Create & initialize here your tasks with none & create your locks 63 | """ 64 | self.tasks.writer = None 65 | self.tasks.reader = None 66 | # etc. 67 | self.locks.writer = asyncio.Lock() 68 | # etc. 69 | 70 | Notice how the tasks are organized under their own namespace ``self.tasks``. 71 | All tasks are initialized as ``None``. There is a separate convenience 72 | namespace ``self.locks`` for asyncio locks. 73 | 74 | The starting point for your thread is defined in ``enter__``: 75 | 76 | :: 77 | 78 | @verbose 79 | async def enter__(self): 80 | self.logger.info("enter__ : %s", self.getInfo()) 81 | self.tasks.writer = await reCreate(self.tasks.writer, self.readerMethod__) 82 | self.tasks.reader = await reCreate(self.tasks.reader, self.writerMethod__) 83 | 84 | Here we use a special decorator ``@verbose`` that makes life with asyncio a bit easier - it catches 85 | some exceptions explicitly for you. 86 | 87 | We start the rescheduled tasks using the convenience function ``reCreate``. The target 88 | of the task is ``self.readerMethod__`` where the task is defined (more on this in the 89 | section :ref:`about tasks `). 90 | 91 | Next, you still remember the hierarchical way the threads are organized and how they 92 | communicate? ``signalHandler__`` defines what the TaskThread should do when it gets 93 | a message/data **from a parent**: 94 | 95 | :: 96 | 97 | @verbose 98 | async def signalHandler__(self, signal): 99 | self.logger.info("signalHandler__ : got signal %s from parent", signal) 100 | 101 | The implementation of this method depends, of course, completely on your TaskThread's 102 | functionality. 103 | 104 | A thread must also know what to do when it gets a signal from a child. This is defined 105 | in ``childsignalHandler__``: 106 | 107 | :: 108 | 109 | @verbose 110 | async def childsignalHandler__(self, signal, child): 111 | self.logger.debug("childsignalHandler__ : got signal %s from child %s", signal, child.getId()) 112 | 113 | Finally, ``getId`` returns some unique string or int corresponding to this TaskThread 114 | (nice for search/organizational purposes), while ``getInfo`` returns a string representation 115 | of the TaskThread (i.e. like ``__str__``). 116 | 117 | You could write: 118 | 119 | :: 120 | 121 | def getId(self): 122 | return str(id(self)) 123 | 124 | def getInfo(self): 125 | return "" 126 | 127 | 128 | API Methods 129 | ----------- 130 | 131 | By using subclassing, we have defined what our TaskThread *does*. 132 | Next we take a look at the API methods, i.e. how to *use* a TaskThread. 133 | 134 | Let's take a quick overview of the available methods in the TaskThread class. 135 | 136 | However, in order to know how to *really* use these methods, you need to go through the :ref:`examples `. 137 | 138 | A TaskThread is created like this: 139 | 140 | :: 141 | 142 | thread = MyThread(parent = parent) 143 | 144 | 145 | Start running it with: 146 | 147 | :: 148 | 149 | await thread.run() 150 | 151 | Stop with: 152 | 153 | :: 154 | 155 | await thread.stop() 156 | 157 | and wait until it has finished: 158 | 159 | :: 160 | 161 | await thread.join() 162 | 163 | A child thread can terminate itself, by calling ``self.stop()``. 164 | 165 | Stopping a child automatically deregisters / removes it from any listening parent. 166 | 167 | You can add a child thread to a parent thread: 168 | 169 | :: 170 | 171 | await thread.addChild(child) 172 | 173 | After that, parent starts listening any signals from the child. 174 | 175 | Finding a child, based on it's id, as returned by it's ``getId()`` method is done with: 176 | 177 | :: 178 | 179 | await thread.findChild(_id = _id) 180 | 181 | Sending a signal from parent to child, i.e. down/deeper in the hierarchical parent/child 182 | structure: 183 | 184 | :: 185 | 186 | await thread.sigFromParentToChild__(signal, child) 187 | 188 | If child is replaced by ``None``, the same signal is sent to all children. 189 | 190 | Sending a signal the other way around: from children to parent, i.e. upwards in the tree: 191 | 192 | :: 193 | 194 | await thread.sigFromChildToParent__(signal) 195 | 196 | 197 | Signals 198 | ------- 199 | 200 | Signals are those things that go to and fro between parent and child threads. 201 | 202 | A typical signal looks like this: 203 | 204 | :: 205 | 206 | class MessageSignal(signals.Signal): 207 | """A generic message message signal, carrying a python object 208 | """ 209 | def __init__(self, message): 210 | self.message = message 211 | 212 | def __str__(self): 213 | return "" % (str(self.message)) 214 | 215 | def getMessage(self): 216 | return self.message 217 | 218 | def __call__(self): 219 | """syntactic sugar""" 220 | return self.getMessage() 221 | 222 | Signals can carry messages, byte payload, whatever. 223 | 224 | Next, let's take a closer look at :ref:`tasks `. 225 | -------------------------------------------------------------------------------- /docs/threadtask.rst: -------------------------------------------------------------------------------- 1 | 2 | .. _threadtask: 3 | 4 | Threads and Tasks 5 | ================= 6 | 7 | So, now you have been initiated on how to create a TaskThread and rescheduling tasks and signals. 8 | 9 | Let's bring it all together. 10 | 11 | :: 12 | 13 | import asyncio, logging, traceback 14 | from task_thread import TaskThread, reCreate, reSchedule,\ 15 | delete, verbose, signals 16 | 17 | Subclass the necessary methods to specify a TaskThread: 18 | 19 | :: 20 | 21 | class MyThread(TaskThread): 22 | 23 | def __init__(self, my_id = 0, parent = None): 24 | super().__init__(parent = parent) 25 | self.my_id = my_id 26 | 27 | List your tasks: 28 | 29 | :: 30 | 31 | def initVars__(self): 32 | self.tasks.hello = None # implementation in "self.helloTask__" 33 | 34 | ``enter__`` starts the task for the first time: 35 | 36 | :: 37 | 38 | @verbose 39 | async def enter__(self): 40 | self.logger.info("enter__") 41 | self.tasks.hello = await reCreate(self.tasks.hello, self.helloTask__) 42 | 43 | In ``exit__``, kill the re-scheduling tasks: 44 | 45 | :: 46 | 47 | @verbose 48 | async def exit__(self): 49 | self.tasks.hello = await delete(self.tasks.hello) 50 | self.logger.info("exit__ : finished") 51 | 52 | For the moment, no signal handling: 53 | 54 | :: 55 | 56 | @verbose 57 | async def signalHandler__(self, signal): 58 | self.logger.info("signalHandler__ : got signal %s", signal) 59 | 60 | Rest of the methods: 61 | 62 | :: 63 | 64 | def getId(self): 65 | return self.my_id 66 | 67 | def getInfo(self): 68 | return "" 69 | 70 | 71 | Finally, define the (only) task that is running in this TaskThread: 72 | 73 | :: 74 | 75 | async def helloTask__(self): 76 | """A task that prints hello & re-schedules itself 77 | """ 78 | try: 79 | await asyncio.sleep(1) 80 | self.logger.info("Hello from helloTask__ at %s", self.getId()) 81 | self.tasks.hello_task = await reSchedule(self.helloTask__) 82 | return 83 | 84 | except asyncio.CancelledError: 85 | self.logger.critical("helloTask__: cancelling") 86 | 87 | except Exception as e: 88 | self.logger.info("helloTask__: failed with '%s', traceback will follow", e) 89 | traceback.print_exc() 90 | 91 | Finaly, the program runs like this: 92 | 93 | :: 94 | 95 | loglev = logging.DEBUG 96 | 97 | logger = logging.getLogger("MyThread") 98 | logger.setLevel(loglev) 99 | 100 | thread = MyThread(my_id = "main_thread", parent = None) 101 | loop = asyncio.get_event_loop() 102 | loop.run_until_complete(thread.run()) 103 | 104 | What next? 105 | 106 | To create a real program that does anything worthwhile, you still need to study 107 | and understand the :ref:`examples `. 108 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import setup, find_packages 2 | # import sys 3 | 4 | # The following line is modified by setver.bash 5 | version = '0.0.3' 6 | 7 | # # https://setuptools.readthedocs.io/en/latest/setuptools.html#basic-use 8 | setup( 9 | # name = "task_thread", # shit.. reserved 10 | name = "task_virtualthread", 11 | version = version, 12 | install_requires = [ 13 | "numpy" 14 | ], 15 | packages = find_packages(), # # includes python code from every directory that has an "__init__.py" file in it. If no "__init__.py" is found, the directory is omitted. Other directories / files to be included, are defined in the MANIFEST.in file 16 | include_package_data=True, # # conclusion: NEVER forget this : files get included but not installed 17 | # # "package_data" keyword is a practical joke: use MANIFEST.in instead 18 | 19 | # # WARNING: If you are using namespace packages, automatic package finding does not work, so use this: 20 | #packages=[ 21 | # 'task_thread' 22 | #], 23 | 24 | #scripts=[ 25 | # "bin/somescript" 26 | #], 27 | 28 | # # "entry points" get installed into $HOME/.local/bin 29 | # # https://unix.stackexchange.com/questions/316765/which-distributions-have-home-local-bin-in-path 30 | #entry_points={ 31 | # 'console_scripts': [ 32 | # 'my-command = task_thread.subpackage1.cli:main' # this would create a command "my-command" that maps to task_thread.subpackage1.cli method "main" 33 | # ] 34 | #}, 35 | 36 | # # enable this if you need to run a post-install script: 37 | #cmdclass={ 38 | # 'install': PostInstallCommand, 39 | # }, 40 | 41 | # metadata for upload to PyPI 42 | author = "Sampsa Riikonen", 43 | author_email = "sampsa.riikonen@iki.fi", 44 | description = "TaskThread: asyncio made easy", 45 | license = "MIT", 46 | keywords = "python sphinx packaging", 47 | url = "https://elsampsa.github.io/task_thread/", # project homepage 48 | 49 | long_description ="""TaskThread: asyncio made easy""", 50 | long_description_content_type='text/plain', 51 | # long_description_content_type='text/x-rst', # this works 52 | # long_description_content_type='text/markdown', # this does not work 53 | 54 | # see: https://autopilot-docs.readthedocs.io/en/latest/license_list.html 55 | classifiers =[ # Optional 56 | # How mature is this project? Common values are 57 | # 3 - Alpha 58 | # 4 - Beta 59 | # 5 - Production/Stable 60 | 'Development Status :: 4 - Beta', 61 | # Indicate who your project is intended for 62 | 'Intended Audience :: Developers', 63 | # 'Operating System :: POSIX :: Linux', 64 | # 'Topic :: Multimedia :: Video', # set a topic 65 | # Pick your license as you wish 66 | # 'License :: OSI Approved :: GNU Affero General Public License v3 or later (AGPLv3+)', 67 | 'License :: OSI Approved :: MIT License', 68 | # Specify the Python versions you support here. In particular, ensure 69 | # that you indicate whether you support Python 2, Python 3 or both. 70 | 'Programming Language :: Python :: 3', 71 | 'Programming Language :: Python :: 3.8', 72 | 'Programming Language :: Python :: 3.9', 73 | 'Programming Language :: Python :: 3.10', 74 | ], 75 | #project_urls={ # some additional urls 76 | # 'Tutorial': 'https://elsampsa.github.io/task_thread/' 77 | #}, 78 | ) 79 | -------------------------------------------------------------------------------- /task_thread/__init__.py: -------------------------------------------------------------------------------- 1 | from task_thread.base import * 2 | from task_thread.decorator import * 3 | from task_thread.manage import * 4 | -------------------------------------------------------------------------------- /task_thread/base/__init__.py: -------------------------------------------------------------------------------- 1 | from task_thread.base.task_thread import * 2 | from task_thread.base.signals import * 3 | -------------------------------------------------------------------------------- /task_thread/base/data/some_auxiliary_file: -------------------------------------------------------------------------------- 1 | Some auxiliary data ("static files") that is used by the python module. Use tools.getDataFile("some_auxiliar_file") to get the complete path to this file. 2 | -------------------------------------------------------------------------------- /task_thread/base/signals.py: -------------------------------------------------------------------------------- 1 | """signals.py : base definition of signals/message sent between TaskThreads 2 | 3 | * Copyright: 2022 Sampsa Riikonen 4 | * Authors : Sampsa Riikonen 5 | * Date : 1/2022 6 | * Version : 0.0.3 7 | 8 | This file is part of the task_thread library 9 | 10 | Licensed according to the MIT License. Please see file COPYING.MIT for more details. 11 | """ 12 | 13 | 14 | class Signal: 15 | """Signal base class""" 16 | 17 | def __init__(self): 18 | pass 19 | 20 | 21 | # Common signals to all threads 22 | 23 | 24 | class TerminateSignal(Signal): 25 | """Parent requests the termination of the child thread""" 26 | 27 | def __str__(self): 28 | return "" 29 | 30 | 31 | class CloseSignal(Signal): 32 | """Child informs parent that it has been closed. Parent can stop listening to child""" 33 | 34 | def __str__(self): 35 | return "" 36 | 37 | 38 | # An example custom signal 39 | 40 | 41 | class MessageSignal(Signal): 42 | """A generic message message signal, carrying a python object""" 43 | 44 | def __init__(self, origin, message): 45 | self.origin = origin 46 | self.message = message 47 | 48 | def __str__(self): 49 | return "" % (str(self.origin)) 50 | 51 | def getMessage(self): 52 | return self.message 53 | -------------------------------------------------------------------------------- /task_thread/decorator.py: -------------------------------------------------------------------------------- 1 | """decorator.py : decorators for coroutines to make development easier 2 | 3 | * Copyright: 2021-2022 Sampsa Riikonen 4 | * Authors : Sampsa Riikonen 5 | * Date : 6/2021 6 | * Version : 0.0.3 7 | 8 | This file is part of the task_thread library 9 | 10 | Licensed according to the MIT License. Please see file COPYING.MIT for more details. 11 | """ 12 | import asyncio 13 | import sys 14 | import traceback 15 | 16 | 17 | def verbose(f): 18 | """Decorator for coroutines 19 | 20 | - Returns None if there is an exception 21 | - If there is an exception, an additional BaseException is raised (https://bugs.python.org/issue35867) 22 | - NameError, AssertionError, etc. are caught, not when the task is finished, but only when the task is garbage collected 23 | """ 24 | 25 | async def wrapper(*args, **kwargs): 26 | try: 27 | return await f(*args, **kwargs) 28 | 29 | except asyncio.CancelledError as e: # propagate task cancel 30 | raise (e) 31 | 32 | except Exception as e: # any other exception should be reported, and BaseException raised so that the program stops 33 | # raise(BaseException) # enable this if you wan't exceptions raised. Good for first-stage debugging # DEBUGGING 34 | traceback.print_exc() 35 | sys.exit(2) 36 | 37 | wrapper.__name__ = f.__name__ 38 | wrapper.__doc__ = f.__doc__ 39 | return wrapper 40 | -------------------------------------------------------------------------------- /task_thread/ipc.py: -------------------------------------------------------------------------------- 1 | """ipc.py : Intercom using system pipes with special interest in intercom between 2 | async and normal processes 3 | 4 | * Copyright: 2021-2022 Sampsa Riikonen 5 | * Authors : Sampsa Riikonen 6 | * Date : 6/2021 7 | * Version : 0.0.3 8 | 9 | This file is part of the task_thread library 10 | 11 | Licensed according to the MIT License. Please see file COPYING.MIT for more details. 12 | """ 13 | import os, pickle, math 14 | 15 | 16 | class Duplex: 17 | def __init__(self, read_fd, write_fd): 18 | # file descriptors, i.e. numbers: 19 | self.read_fd = read_fd 20 | self.write_fd = write_fd 21 | # these are _io.FileIO objects: 22 | self.reader = os.fdopen(read_fd, "br", buffering=0) 23 | self.writer = os.fdopen(write_fd, "bw", buffering=0) 24 | 25 | def getReadIO(self): 26 | """_io.FileIO object""" 27 | return self.reader 28 | 29 | def getReadFd(self): 30 | return self.read_fd 31 | 32 | def getWriteFd(self): 33 | return self.write_fd 34 | 35 | def getWriteIO(self): 36 | """_io.FileIO object""" 37 | return self.writer 38 | 39 | def recv(self): 40 | """Traditional blocking recv""" 41 | msg = b"" 42 | N = None 43 | cc = 0 44 | while True: 45 | # print("waiting stream") 46 | res = self.reader.read(8) 47 | cc += 8 48 | if N is None: 49 | # decode the first 8 bytes into int 50 | N = int.from_bytes(res, byteorder="big") 51 | # print("N>", N) 52 | # print(res, len(res)) 53 | msg += res 54 | if cc >= N: 55 | break 56 | msg = msg[8:N] # remove any padding bytes 57 | obj = pickle.loads(msg) 58 | return obj 59 | 60 | def send(self, obj): 61 | """Tradition blocking send""" 62 | msg = to8ByteMessage(obj) 63 | n = self.writer.write(msg) 64 | # self.writer.flush() # no effect 65 | # print("wrote", n, "bytes") 66 | return n 67 | 68 | def __del__(self): 69 | self.reader.close() 70 | self.writer.close() 71 | 72 | 73 | def getPipes(block_A=False, block_B=False): 74 | """ 75 | 76 | Either A or B can be blocking or non-blocking 77 | 78 | non-blocking pipe-terminal is required for asyncio 79 | 80 | :: 81 | 82 | A B 83 | 84 | w --------> r 85 | r <-------- w 86 | 87 | (A_w, A_r) is returned as single duplex 88 | """ 89 | B_read_fd, A_write_fd = os.pipe() 90 | A_read_fd, B_write_fd = os.pipe() 91 | # print("read, write pair", B_read_fd, A_write_fd) 92 | # print("read, write pair", A_read_fd, B_write_fd) 93 | # these a file descriptors, i.e. numbers 94 | 95 | if block_A: 96 | os.set_blocking(A_read_fd, True) 97 | os.set_blocking(A_write_fd, True) 98 | else: 99 | os.set_blocking(A_read_fd, False) 100 | os.set_blocking(A_write_fd, False) 101 | 102 | if block_B: 103 | os.set_blocking(B_read_fd, True) 104 | os.set_blocking(B_write_fd, True) 105 | else: 106 | os.set_blocking(B_read_fd, False) 107 | os.set_blocking(B_write_fd, False) 108 | 109 | return Duplex(A_read_fd, A_write_fd), Duplex(B_read_fd, B_write_fd) 110 | 111 | 112 | def to8ByteMessage(obj): 113 | b = pickle.dumps(obj) 114 | # r, w, e = safe_select([], [self.write_fd], []) 115 | # print("writing to", self.write_fd) 116 | val = len(b) + 8 # length of the message, including the first 8 bytes 117 | lenbytes = val.to_bytes(8, byteorder="big") 118 | n_pad = math.ceil(val / 8) * 8 - val 119 | pad = bytes(n_pad) 120 | return lenbytes + b + pad 121 | -------------------------------------------------------------------------------- /task_thread/manage.py: -------------------------------------------------------------------------------- 1 | """manage.py : recurrent task creation, rescheduling and cancelling 2 | 3 | * Copyright: 2021-2022 Sampsa Riikonen 4 | * Authors : Sampsa Riikonen 5 | * Date : 6/2021 6 | * Version : 0.0.3 7 | 8 | This file is part of the task_thread library 9 | 10 | Licensed according to the MIT License. Please see file COPYING.MIT for more details. 11 | """ 12 | import asyncio 13 | 14 | 15 | async def reCreate(task_ref, cofunc, *args, **kwargs): 16 | """Task book-keeping and management: create a task 17 | 18 | :param task_ref: a reference to the task. Can be None (implicating this is a new task) 19 | :param cofunc: cofunction representing the tasks functionality 20 | :param *args: passed on to the confunction 21 | :param **kwargs: passed on to the confunction 22 | 23 | :: 24 | 25 | # in your class' constructor: 26 | self.your_task = None 27 | 28 | # later on, in your async code: 29 | self.your_task = await reCreate(self.your_task, self.yourTask__, some_parameter) 30 | 31 | - If reference is not None, cancels the task 32 | - Creates a new task and returns it 33 | """ 34 | if task_ref is not None: 35 | if not asyncio.isfuture(task_ref): 36 | print("reCreate: WARNING:", task_ref, "is not a task") 37 | return None 38 | task_ref.cancel() 39 | try: 40 | await asyncio.wait_for(task_ref, timeout=15) 41 | except Exception as e: 42 | print("WARNING: reCreate failed with", e, "for task", str(task_ref)) 43 | task = asyncio.get_event_loop().create_task(cofunc(*args, **kwargs)) 44 | return task 45 | 46 | 47 | async def reSchedule(cofunc, *args, **kwargs): 48 | """Task book-keeping and management: re-schedule a task 49 | 50 | :: 51 | 52 | # inside the co-function that represent a task: 53 | self.your_task = await reSchedule(self.your_task, some_parameter, task_id = task_id); return 54 | 55 | """ 56 | task = asyncio.get_event_loop().create_task(cofunc(*args, **kwargs)) 57 | return task 58 | 59 | 60 | async def delete(task_ref): 61 | """Task book-keeping and management: delete a task 62 | 63 | :: 64 | 65 | self.your_task = await delete(self.your_task) 66 | 67 | """ 68 | if task_ref is not None: 69 | if task_ref.done(): 70 | pass 71 | else: 72 | task_ref.cancel() 73 | try: 74 | await asyncio.wait_for(task_ref, timeout=10) 75 | except Exception as e: 76 | print("WARNING: delete failed with", e, "for task", str(task_ref)) 77 | return None 78 | -------------------------------------------------------------------------------- /task_thread/template.py: -------------------------------------------------------------------------------- 1 | """NAME.py : Description of the file 2 | 3 | * Copyright: 2021-2022 Sampsa Riikonen 4 | * Authors : Sampsa Riikonen 5 | * Date : 6/2021 6 | * Version : 0.0.3 7 | 8 | This file is part of the task_thread library 9 | 10 | Licensed according to the MIT License. Please see file COPYING.MIT for more details. 11 | """ 12 | 13 | # stdlib 14 | import sys 15 | import logging 16 | import asyncio 17 | 18 | 19 | def test1(): 20 | st = """Empty test 21 | """ 22 | pre = __name__ + "test1 :" 23 | print(pre, st) 24 | 25 | 26 | def test2(): 27 | st = """Empty test 28 | """ 29 | pre = __name__ + "test2 :" 30 | print(pre, st) 31 | 32 | 33 | def main(): 34 | pre = __name__ + "main :" 35 | print(pre, "main: arguments: ", sys.argv) 36 | if len(sys.argv) < 2: 37 | print(pre, "main: needs test number") 38 | else: 39 | st = "test" + str(sys.argv[1]) + "()" 40 | exec(st) 41 | 42 | 43 | if __name__ == "__main__": 44 | main() 45 | -------------------------------------------------------------------------------- /task_thread/version.py: -------------------------------------------------------------------------------- 1 | """This module has the version number. 2 | Automatically changed with the "setver.bash" script. Don't touch! 3 | """ 4 | VERSION_MAJOR = 0 5 | VERSION_MINOR = 0 6 | VERSION_PATCH = 2 7 | --------------------------------------------------------------------------------