mirror of https://github.com/nocturn9x/giambio.git
Added some exclude paths to gitignore
This commit is contained in:
parent
e29eaf3862
commit
70646a4767
|
@ -1,131 +1,134 @@
|
|||
# Byte-compiled / optimized / DLL files
|
||||
__pycache__/
|
||||
*.py[cod]
|
||||
*$py.class
|
||||
|
||||
# C extensions
|
||||
*.so
|
||||
|
||||
# Distribution / packaging
|
||||
.Python
|
||||
build/
|
||||
develop-eggs/
|
||||
dist/
|
||||
downloads/
|
||||
eggs/
|
||||
.eggs/
|
||||
lib/
|
||||
lib64/
|
||||
parts/
|
||||
sdist/
|
||||
var/
|
||||
wheels/
|
||||
pip-wheel-metadata/
|
||||
share/python-wheels/
|
||||
*.egg-info/
|
||||
.installed.cfg
|
||||
*.egg
|
||||
MANIFEST
|
||||
|
||||
# PyInstaller
|
||||
# Usually these files are written by a python script from a template
|
||||
# before PyInstaller builds the exe, so as to inject date/other infos into it.
|
||||
*.manifest
|
||||
*.spec
|
||||
|
||||
# Installer logs
|
||||
pip-log.txt
|
||||
pip-delete-this-directory.txt
|
||||
|
||||
# Unit test / coverage reports
|
||||
htmlcov/
|
||||
.tox/
|
||||
.nox/
|
||||
.coverage
|
||||
.coverage.*
|
||||
.cache
|
||||
nosetests.xml
|
||||
coverage.xml
|
||||
*.cover
|
||||
*.py,cover
|
||||
.hypothesis/
|
||||
.pytest_cache/
|
||||
|
||||
# Translations
|
||||
*.mo
|
||||
*.pot
|
||||
|
||||
# Django stuff:
|
||||
*.log
|
||||
local_settings.py
|
||||
db.sqlite3
|
||||
db.sqlite3-journal
|
||||
|
||||
# Flask stuff:
|
||||
instance/
|
||||
.webassets-cache
|
||||
|
||||
# Scrapy stuff:
|
||||
.scrapy
|
||||
|
||||
# Sphinx documentation
|
||||
docs/_build/
|
||||
|
||||
# PyBuilder
|
||||
target/
|
||||
|
||||
# Jupyter Notebook
|
||||
.ipynb_checkpoints
|
||||
|
||||
# IPython
|
||||
profile_default/
|
||||
ipython_config.py
|
||||
|
||||
# pyenv
|
||||
.python-version
|
||||
|
||||
# pipenv
|
||||
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
|
||||
# However, in case of collaboration, if having platform-specific dependencies or dependencies
|
||||
# having no cross-platform support, pipenv may install dependencies that don't work, or not
|
||||
# install all needed dependencies.
|
||||
#Pipfile.lock
|
||||
|
||||
# PEP 582; used by e.g. github.com/David-OConnor/pyflow
|
||||
__pypackages__/
|
||||
|
||||
# Celery stuff
|
||||
celerybeat-schedule
|
||||
celerybeat.pid
|
||||
|
||||
# SageMath parsed files
|
||||
*.sage.py
|
||||
|
||||
# Environments
|
||||
.env
|
||||
.venv
|
||||
env/
|
||||
venv/
|
||||
ENV/
|
||||
env.bak/
|
||||
venv.bak/
|
||||
|
||||
# Spyder project settings
|
||||
.spyderproject
|
||||
.spyproject
|
||||
|
||||
# Rope project settings
|
||||
.ropeproject
|
||||
|
||||
# mkdocs documentation
|
||||
/site
|
||||
|
||||
# mypy
|
||||
.mypy_cache/
|
||||
.dmypy.json
|
||||
dmypy.json
|
||||
|
||||
# Pyre type checker
|
||||
.pyre/
|
||||
|
||||
.*.swp
|
||||
# Byte-compiled / optimized / DLL files
|
||||
__pycache__/
|
||||
*.py[cod]
|
||||
*$py.class
|
||||
|
||||
# C extensions
|
||||
*.so
|
||||
|
||||
# Distribution / packaging
|
||||
.Python
|
||||
build/
|
||||
develop-eggs/
|
||||
dist/
|
||||
downloads/
|
||||
eggs/
|
||||
.eggs/
|
||||
lib/
|
||||
lib64/
|
||||
parts/
|
||||
sdist/
|
||||
var/
|
||||
wheels/
|
||||
pip-wheel-metadata/
|
||||
share/python-wheels/
|
||||
*.egg-info/
|
||||
.installed.cfg
|
||||
*.egg
|
||||
MANIFEST
|
||||
|
||||
# PyInstaller
|
||||
# Usually these files are written by a python script from a template
|
||||
# before PyInstaller builds the exe, so as to inject date/other infos into it.
|
||||
*.manifest
|
||||
*.spec
|
||||
|
||||
# Installer logs
|
||||
pip-log.txt
|
||||
pip-delete-this-directory.txt
|
||||
|
||||
# Unit test / coverage reports
|
||||
htmlcov/
|
||||
.tox/
|
||||
.nox/
|
||||
.coverage
|
||||
.coverage.*
|
||||
.cache
|
||||
nosetests.xml
|
||||
coverage.xml
|
||||
*.cover
|
||||
*.py,cover
|
||||
.hypothesis/
|
||||
.pytest_cache/
|
||||
|
||||
# Translations
|
||||
*.mo
|
||||
*.pot
|
||||
|
||||
# Django stuff:
|
||||
*.log
|
||||
local_settings.py
|
||||
db.sqlite3
|
||||
db.sqlite3-journal
|
||||
|
||||
# Flask stuff:
|
||||
instance/
|
||||
.webassets-cache
|
||||
|
||||
# Scrapy stuff:
|
||||
.scrapy
|
||||
|
||||
# Sphinx documentation
|
||||
docs/_build/
|
||||
|
||||
# PyBuilder
|
||||
target/
|
||||
|
||||
# Jupyter Notebook
|
||||
.ipynb_checkpoints
|
||||
|
||||
# IPython
|
||||
profile_default/
|
||||
ipython_config.py
|
||||
|
||||
# pyenv
|
||||
.python-version
|
||||
pyenv.cfg
|
||||
|
||||
# pipenv
|
||||
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
|
||||
# However, in case of collaboration, if having platform-specific dependencies or dependencies
|
||||
# having no cross-platform support, pipenv may install dependencies that don't work, or not
|
||||
# install all needed dependencies.
|
||||
#Pipfile.lock
|
||||
|
||||
# PEP 582; used by e.g. github.com/David-OConnor/pyflow
|
||||
__pypackages__/
|
||||
|
||||
# Celery stuff
|
||||
celerybeat-schedule
|
||||
celerybeat.pid
|
||||
|
||||
# SageMath parsed files
|
||||
*.sage.py
|
||||
|
||||
# Environments
|
||||
.env
|
||||
bin
|
||||
lib
|
||||
.venv
|
||||
env/
|
||||
venv/
|
||||
ENV/
|
||||
env.bak/
|
||||
venv.bak/
|
||||
|
||||
# Spyder project settings
|
||||
.spyderproject
|
||||
.spyproject
|
||||
|
||||
# Rope project settings
|
||||
.ropeproject
|
||||
|
||||
# mkdocs documentation
|
||||
/site
|
||||
|
||||
# mypy
|
||||
.mypy_cache/
|
||||
.dmypy.json
|
||||
dmypy.json
|
||||
|
||||
# Pyre type checker
|
||||
.pyre/
|
||||
|
||||
.*.swp
|
||||
|
|
374
LICENSE
374
LICENSE
|
@ -1,187 +1,187 @@
|
|||
Apache License
|
||||
Version 2.0, January 2004
|
||||
http://www.apache.org/licenses/
|
||||
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||
|
||||
1. Definitions.
|
||||
|
||||
"License" shall mean the terms and conditions for use, reproduction,
|
||||
and distribution as defined by Sections 1 through 9 of this document.
|
||||
|
||||
"Licensor" shall mean the copyright owner or entity authorized by
|
||||
the copyright owner that is granting the License.
|
||||
|
||||
"Legal Entity" shall mean the union of the acting entity and all
|
||||
other entities that control, are controlled by, or are under common
|
||||
control with that entity. For the purposes of this definition,
|
||||
"control" means (i) the power, direct or indirect, to cause the
|
||||
direction or management of such entity, whether by contract or
|
||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||
|
||||
"You" (or "Your") shall mean an individual or Legal Entity
|
||||
exercising permissions granted by this License.
|
||||
|
||||
"Source" form shall mean the preferred form for making modifications,
|
||||
including but not limited to software source code, documentation
|
||||
source, and configuration files.
|
||||
|
||||
"Object" form shall mean any form resulting from mechanical
|
||||
transformation or translation of a Source form, including but
|
||||
not limited to compiled object code, generated documentation,
|
||||
and conversions to other media types.
|
||||
|
||||
"Work" shall mean the work of authorship, whether in Source or
|
||||
Object form, made available under the License, as indicated by a
|
||||
copyright notice that is included in or attached to the work
|
||||
(an example is provided in the Appendix below).
|
||||
|
||||
"Derivative Works" shall mean any work, whether in Source or Object
|
||||
form, that is based on (or derived from) the Work and for which the
|
||||
editorial revisions, annotations, elaborations, or other modifications
|
||||
represent, as a whole, an original work of authorship. For the purposes
|
||||
of this License, Derivative Works shall not include works that remain
|
||||
separable from, or merely link (or bind by name) to the interfaces of,
|
||||
the Work and Derivative Works thereof.
|
||||
|
||||
"Contribution" shall mean any work of authorship, including
|
||||
the original version of the Work and any modifications or additions
|
||||
to that Work or Derivative Works thereof, that is intentionally
|
||||
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||
or by an individual or Legal Entity authorized to submit on behalf of
|
||||
the copyright owner. For the purposes of this definition, "submitted"
|
||||
means any form of electronic, verbal, or written communication sent
|
||||
to the Licensor or its representatives, including but not limited to
|
||||
communication on electronic mailing lists, source code control systems,
|
||||
and issue tracking systems that are managed by, or on behalf of, the
|
||||
Licensor for the purpose of discussing and improving the Work, but
|
||||
excluding communication that is conspicuously marked or otherwise
|
||||
designated in writing by the copyright owner as "Not a Contribution."
|
||||
|
||||
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||
on behalf of whom a Contribution has been received by Licensor and
|
||||
subsequently incorporated within the Work.
|
||||
|
||||
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
copyright license to reproduce, prepare Derivative Works of,
|
||||
publicly display, publicly perform, sublicense, and distribute the
|
||||
Work and such Derivative Works in Source or Object form.
|
||||
|
||||
3. Grant of Patent License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
(except as stated in this section) patent license to make, have made,
|
||||
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||
where such license applies only to those patent claims licensable
|
||||
by such Contributor that are necessarily infringed by their
|
||||
Contribution(s) alone or by combination of their Contribution(s)
|
||||
with the Work to which such Contribution(s) was submitted. If You
|
||||
institute patent litigation against any entity (including a
|
||||
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||
or a Contribution incorporated within the Work constitutes direct
|
||||
or contributory patent infringement, then any patent licenses
|
||||
granted to You under this License for that Work shall terminate
|
||||
as of the date such litigation is filed.
|
||||
|
||||
4. Redistribution. You may reproduce and distribute copies of the
|
||||
Work or Derivative Works thereof in any medium, with or without
|
||||
modifications, and in Source or Object form, provided that You
|
||||
meet the following conditions:
|
||||
|
||||
(a) You must give any other recipients of the Work or
|
||||
Derivative Works a copy of this License; and
|
||||
|
||||
(b) You must cause any modified files to carry prominent notices
|
||||
stating that You changed the files; and
|
||||
|
||||
(c) You must retain, in the Source form of any Derivative Works
|
||||
that You distribute, all copyright, patent, trademark, and
|
||||
attribution notices from the Source form of the Work,
|
||||
excluding those notices that do not pertain to any part of
|
||||
the Derivative Works; and
|
||||
|
||||
(d) If the Work includes a "NOTICE" text file as part of its
|
||||
distribution, then any Derivative Works that You distribute must
|
||||
include a readable copy of the attribution notices contained
|
||||
within such NOTICE file, excluding those notices that do not
|
||||
pertain to any part of the Derivative Works, in at least one
|
||||
of the following places: within a NOTICE text file distributed
|
||||
as part of the Derivative Works; within the Source form or
|
||||
documentation, if provided along with the Derivative Works; or,
|
||||
within a display generated by the Derivative Works, if and
|
||||
wherever such third-party notices normally appear. The contents
|
||||
of the NOTICE file are for informational purposes only and
|
||||
do not modify the License. You may add Your own attribution
|
||||
notices within Derivative Works that You distribute, alongside
|
||||
or as an addendum to the NOTICE text from the Work, provided
|
||||
that such additional attribution notices cannot be construed
|
||||
as modifying the License.
|
||||
|
||||
You may add Your own copyright statement to Your modifications and
|
||||
may provide additional or different license terms and conditions
|
||||
for use, reproduction, or distribution of Your modifications, or
|
||||
for any such Derivative Works as a whole, provided Your use,
|
||||
reproduction, and distribution of the Work otherwise complies with
|
||||
the conditions stated in this License.
|
||||
|
||||
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||
any Contribution intentionally submitted for inclusion in the Work
|
||||
by You to the Licensor shall be under the terms and conditions of
|
||||
this License, without any additional terms or conditions.
|
||||
Notwithstanding the above, nothing herein shall supersede or modify
|
||||
the terms of any separate license agreement you may have executed
|
||||
with Licensor regarding such Contributions.
|
||||
|
||||
6. Trademarks. This License does not grant permission to use the trade
|
||||
names, trademarks, service marks, or product names of the Licensor,
|
||||
except as required for reasonable and customary use in describing the
|
||||
origin of the Work and reproducing the content of the NOTICE file.
|
||||
|
||||
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||
agreed to in writing, Licensor provides the Work (and each
|
||||
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
implied, including, without limitation, any warranties or conditions
|
||||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||
appropriateness of using or redistributing the Work and assume any
|
||||
risks associated with Your exercise of permissions under this License.
|
||||
|
||||
8. Limitation of Liability. In no event and under no legal theory,
|
||||
whether in tort (including negligence), contract, or otherwise,
|
||||
unless required by applicable law (such as deliberate and grossly
|
||||
negligent acts) or agreed to in writing, shall any Contributor be
|
||||
liable to You for damages, including any direct, indirect, special,
|
||||
incidental, or consequential damages of any character arising as a
|
||||
result of this License or out of the use or inability to use the
|
||||
Work (including but not limited to damages for loss of goodwill,
|
||||
work stoppage, computer failure or malfunction, or any and all
|
||||
other commercial damages or losses), even if such Contributor
|
||||
has been advised of the possibility of such damages.
|
||||
|
||||
9. Accepting Warranty or Additional Liability. While redistributing
|
||||
the Work or Derivative Works thereof, You may choose to offer,
|
||||
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||
or other liability obligations and/or rights consistent with this
|
||||
License. However, in accepting such obligations, You may act only
|
||||
on Your own behalf and on Your sole responsibility, not on behalf
|
||||
of any other Contributor, and only if You agree to indemnify,
|
||||
defend, and hold each Contributor harmless for any liability
|
||||
incurred by, or claims asserted against, such Contributor by reason
|
||||
of your accepting any such warranty or additional liability.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
APPENDIX: How to apply the Apache License to your work.
|
||||
|
||||
To apply the Apache License to your work, attach the following
|
||||
boilerplate notice, with the fields enclosed by brackets "[]"
|
||||
replaced with your own identifying information. (Don't include
|
||||
the brackets!) The text should be enclosed in the appropriate
|
||||
comment syntax for the file format. We also recommend that a
|
||||
file or class name and description of purpose be included on the
|
||||
same "printed page" as the copyright notice for easier
|
||||
identification within third-party archives.
|
||||
Apache License
|
||||
Version 2.0, January 2004
|
||||
http://www.apache.org/licenses/
|
||||
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||
|
||||
1. Definitions.
|
||||
|
||||
"License" shall mean the terms and conditions for use, reproduction,
|
||||
and distribution as defined by Sections 1 through 9 of this document.
|
||||
|
||||
"Licensor" shall mean the copyright owner or entity authorized by
|
||||
the copyright owner that is granting the License.
|
||||
|
||||
"Legal Entity" shall mean the union of the acting entity and all
|
||||
other entities that control, are controlled by, or are under common
|
||||
control with that entity. For the purposes of this definition,
|
||||
"control" means (i) the power, direct or indirect, to cause the
|
||||
direction or management of such entity, whether by contract or
|
||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||
|
||||
"You" (or "Your") shall mean an individual or Legal Entity
|
||||
exercising permissions granted by this License.
|
||||
|
||||
"Source" form shall mean the preferred form for making modifications,
|
||||
including but not limited to software source code, documentation
|
||||
source, and configuration files.
|
||||
|
||||
"Object" form shall mean any form resulting from mechanical
|
||||
transformation or translation of a Source form, including but
|
||||
not limited to compiled object code, generated documentation,
|
||||
and conversions to other media types.
|
||||
|
||||
"Work" shall mean the work of authorship, whether in Source or
|
||||
Object form, made available under the License, as indicated by a
|
||||
copyright notice that is included in or attached to the work
|
||||
(an example is provided in the Appendix below).
|
||||
|
||||
"Derivative Works" shall mean any work, whether in Source or Object
|
||||
form, that is based on (or derived from) the Work and for which the
|
||||
editorial revisions, annotations, elaborations, or other modifications
|
||||
represent, as a whole, an original work of authorship. For the purposes
|
||||
of this License, Derivative Works shall not include works that remain
|
||||
separable from, or merely link (or bind by name) to the interfaces of,
|
||||
the Work and Derivative Works thereof.
|
||||
|
||||
"Contribution" shall mean any work of authorship, including
|
||||
the original version of the Work and any modifications or additions
|
||||
to that Work or Derivative Works thereof, that is intentionally
|
||||
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||
or by an individual or Legal Entity authorized to submit on behalf of
|
||||
the copyright owner. For the purposes of this definition, "submitted"
|
||||
means any form of electronic, verbal, or written communication sent
|
||||
to the Licensor or its representatives, including but not limited to
|
||||
communication on electronic mailing lists, source code control systems,
|
||||
and issue tracking systems that are managed by, or on behalf of, the
|
||||
Licensor for the purpose of discussing and improving the Work, but
|
||||
excluding communication that is conspicuously marked or otherwise
|
||||
designated in writing by the copyright owner as "Not a Contribution."
|
||||
|
||||
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||
on behalf of whom a Contribution has been received by Licensor and
|
||||
subsequently incorporated within the Work.
|
||||
|
||||
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
copyright license to reproduce, prepare Derivative Works of,
|
||||
publicly display, publicly perform, sublicense, and distribute the
|
||||
Work and such Derivative Works in Source or Object form.
|
||||
|
||||
3. Grant of Patent License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
(except as stated in this section) patent license to make, have made,
|
||||
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||
where such license applies only to those patent claims licensable
|
||||
by such Contributor that are necessarily infringed by their
|
||||
Contribution(s) alone or by combination of their Contribution(s)
|
||||
with the Work to which such Contribution(s) was submitted. If You
|
||||
institute patent litigation against any entity (including a
|
||||
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||
or a Contribution incorporated within the Work constitutes direct
|
||||
or contributory patent infringement, then any patent licenses
|
||||
granted to You under this License for that Work shall terminate
|
||||
as of the date such litigation is filed.
|
||||
|
||||
4. Redistribution. You may reproduce and distribute copies of the
|
||||
Work or Derivative Works thereof in any medium, with or without
|
||||
modifications, and in Source or Object form, provided that You
|
||||
meet the following conditions:
|
||||
|
||||
(a) You must give any other recipients of the Work or
|
||||
Derivative Works a copy of this License; and
|
||||
|
||||
(b) You must cause any modified files to carry prominent notices
|
||||
stating that You changed the files; and
|
||||
|
||||
(c) You must retain, in the Source form of any Derivative Works
|
||||
that You distribute, all copyright, patent, trademark, and
|
||||
attribution notices from the Source form of the Work,
|
||||
excluding those notices that do not pertain to any part of
|
||||
the Derivative Works; and
|
||||
|
||||
(d) If the Work includes a "NOTICE" text file as part of its
|
||||
distribution, then any Derivative Works that You distribute must
|
||||
include a readable copy of the attribution notices contained
|
||||
within such NOTICE file, excluding those notices that do not
|
||||
pertain to any part of the Derivative Works, in at least one
|
||||
of the following places: within a NOTICE text file distributed
|
||||
as part of the Derivative Works; within the Source form or
|
||||
documentation, if provided along with the Derivative Works; or,
|
||||
within a display generated by the Derivative Works, if and
|
||||
wherever such third-party notices normally appear. The contents
|
||||
of the NOTICE file are for informational purposes only and
|
||||
do not modify the License. You may add Your own attribution
|
||||
notices within Derivative Works that You distribute, alongside
|
||||
or as an addendum to the NOTICE text from the Work, provided
|
||||
that such additional attribution notices cannot be construed
|
||||
as modifying the License.
|
||||
|
||||
You may add Your own copyright statement to Your modifications and
|
||||
may provide additional or different license terms and conditions
|
||||
for use, reproduction, or distribution of Your modifications, or
|
||||
for any such Derivative Works as a whole, provided Your use,
|
||||
reproduction, and distribution of the Work otherwise complies with
|
||||
the conditions stated in this License.
|
||||
|
||||
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||
any Contribution intentionally submitted for inclusion in the Work
|
||||
by You to the Licensor shall be under the terms and conditions of
|
||||
this License, without any additional terms or conditions.
|
||||
Notwithstanding the above, nothing herein shall supersede or modify
|
||||
the terms of any separate license agreement you may have executed
|
||||
with Licensor regarding such Contributions.
|
||||
|
||||
6. Trademarks. This License does not grant permission to use the trade
|
||||
names, trademarks, service marks, or product names of the Licensor,
|
||||
except as required for reasonable and customary use in describing the
|
||||
origin of the Work and reproducing the content of the NOTICE file.
|
||||
|
||||
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||
agreed to in writing, Licensor provides the Work (and each
|
||||
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
implied, including, without limitation, any warranties or conditions
|
||||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||
appropriateness of using or redistributing the Work and assume any
|
||||
risks associated with Your exercise of permissions under this License.
|
||||
|
||||
8. Limitation of Liability. In no event and under no legal theory,
|
||||
whether in tort (including negligence), contract, or otherwise,
|
||||
unless required by applicable law (such as deliberate and grossly
|
||||
negligent acts) or agreed to in writing, shall any Contributor be
|
||||
liable to You for damages, including any direct, indirect, special,
|
||||
incidental, or consequential damages of any character arising as a
|
||||
result of this License or out of the use or inability to use the
|
||||
Work (including but not limited to damages for loss of goodwill,
|
||||
work stoppage, computer failure or malfunction, or any and all
|
||||
other commercial damages or losses), even if such Contributor
|
||||
has been advised of the possibility of such damages.
|
||||
|
||||
9. Accepting Warranty or Additional Liability. While redistributing
|
||||
the Work or Derivative Works thereof, You may choose to offer,
|
||||
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||
or other liability obligations and/or rights consistent with this
|
||||
License. However, in accepting such obligations, You may act only
|
||||
on Your own behalf and on Your sole responsibility, not on behalf
|
||||
of any other Contributor, and only if You agree to indemnify,
|
||||
defend, and hold each Contributor harmless for any liability
|
||||
incurred by, or claims asserted against, such Contributor by reason
|
||||
of your accepting any such warranty or additional liability.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
APPENDIX: How to apply the Apache License to your work.
|
||||
|
||||
To apply the Apache License to your work, attach the following
|
||||
boilerplate notice, with the fields enclosed by brackets "[]"
|
||||
replaced with your own identifying information. (Don't include
|
||||
the brackets!) The text should be enclosed in the appropriate
|
||||
comment syntax for the file format. We also recommend that a
|
||||
file or class name and description of purpose be included on the
|
||||
same "printed page" as the copyright notice for easier
|
||||
identification within third-party archives.
|
||||
|
|
760
README.md
760
README.md
|
@ -1,380 +1,380 @@
|
|||
# giambio - Asynchronous Python made easy (and friendly)
|
||||
|
||||
giambio is an event-driven concurrency library meant* to perform efficient and high-performant I/O multiplexing.
|
||||
This library implements what is known as a _stackless mode of execution_, or
|
||||
"green threads", though the latter term is misleading as **no multithreading is involved** (at least not by default).
|
||||
|
||||
|
||||
_*_: The library *works* (sometimes), but its still in its very early stages and is nowhere close being
|
||||
production ready, so be aware that it is likely that you'll find bugs and race conditions
|
||||
|
||||
## Disclaimer
|
||||
|
||||
Right now this is nothing more than a toy implementation to help me understand how this whole `async`/`await` thing works
|
||||
and it is pretty much guaranteed to explode spectacularly badly while using it. If you find any bugs, please report them!
|
||||
|
||||
Oh and by the way, this project was hugely inspired by the [curio](https://github.com/dabeaz/curio) and the
|
||||
[trio](https://github.com/python-trio/trio) projects, you might want to have a look at their amazing work if you need a
|
||||
rock-solid and structured concurrency framework (I personally recommend trio and that's definitely not related to the fact
|
||||
that most of the following text is ~~stolen~~ inspired from its documentation)
|
||||
|
||||
|
||||
# What the hell is async anyway?
|
||||
|
||||
Libraries like giambio shine the most when it comes to performing asyncronous I/O (reading a socket, writing to a file, that sort of thing).
|
||||
The most common example of this is a network server that needs to handle multiple connections at the same time.
|
||||
One possible approach to achieve concurrency is to use threads, and despite their bad reputation in Python, they
|
||||
actually might be a good choice when it comes to I/O for reasons that span far beyond the scope of this tutorial.
|
||||
If you choose to use threads, there are a couple things you can do, involving what is known as _thread synchronization
|
||||
primitives_ and _thread pools_, but once again that is beyond the purposes of this quickstart guide.
|
||||
A library like giambio comes into play when you need to perform lots of [blocking operations](https://en.wikipedia.org/wiki/Blocking_(computing)),
|
||||
and network servers happen to be heavily based on I/O: a blocking operation.
|
||||
Starting to see where we're heading?
|
||||
|
||||
|
||||
## A deeper dive
|
||||
|
||||
Giambio has been designed with simplicity in mind, so this document won't explain all the gritty details about _how_ async is
|
||||
implemented in Python (you might want to check out [this article](https://snarky.ca/how-the-heck-does-async-await-work-in-python-3-5/) if you want to learn more about all the implementation details).
|
||||
For the sake of this tutorial, all you need to know is that giambio is all about a feature added in Python 3.5:
|
||||
asynchronous functions, or 'async' for short.
|
||||
|
||||
Async functions are functions defined with `async def` instead of the regular `def`, like so:
|
||||
|
||||
```python
|
||||
async def async_fun(): # An async function
|
||||
print("Hello, world!")
|
||||
|
||||
def sync_fun(): # A regular (sync) function
|
||||
print("Hello, world!")
|
||||
```
|
||||
|
||||
First of all, async functions like to stick together: to call an async function you need to put `await` in front of it, like below:
|
||||
|
||||
```python
|
||||
|
||||
async def async_two():
|
||||
print("Hello from async_two!")
|
||||
|
||||
async def async_one():
|
||||
print("Hello from async_one!")
|
||||
await async_two() # This is an async call
|
||||
```
|
||||
|
||||
It has to be noted that using `await` outside of an async function is a `SyntaxError`, so basically async
|
||||
functions have a unique superpower: they, and no-one else, can call other async functions.
|
||||
|
||||
This already presents a chicken-and-egg problem, because when you fire up Python, it is running plain ol'
|
||||
synchronous code; So how do we enter the async context in the first place?
|
||||
|
||||
That is done via a special _synchronous function_, `giambio.run` in our case, that has the ability to call
|
||||
asynchronous functions and can therefore initiate the async context. For this
|
||||
reason, `giambio.run` **must** be called from a synchronous context, to avoid a horrible _deadlock_.
|
||||
|
||||
Now that you know all of this, you might be wondering why on earth would one use async functions instead of
|
||||
regular functions: after all, their ability to call other async functions seems pretty pointless in itself, doesn't it?
|
||||
Take a look at this example below:
|
||||
|
||||
```python
|
||||
import giambio
|
||||
|
||||
async def foo():
|
||||
print("Hello, world!")
|
||||
|
||||
giambio.run(foo) # Prints 'Hello, world!'
|
||||
```
|
||||
|
||||
This could as well be written the following way and would produce the same output:
|
||||
|
||||
```python
|
||||
def foo():
|
||||
print("Hello, world!")
|
||||
|
||||
foo() # Prints 'Hello, world!'
|
||||
```
|
||||
|
||||
To answer this question, we have to dig a bit deeper about _what_ giambio gives you in exchange for all this `async`/`await` madness.
|
||||
|
||||
We already introduced `giambio.run`, a special runner function that can start the async context from a synchronous one, but giambio
|
||||
provides also a set of tools, mainly for doing I/O. These functions, as you might have guessed, are async functions and they're useful!
|
||||
So if you wanna take advantage of giambio, and hopefully you will after reading this guide, you need to write async code.
|
||||
As an example, take this function using `giambio.sleep` (`giambio.sleep` is like `time.sleep`, but with an async flavor):
|
||||
|
||||
__Side note__: If you have decent knowledge about asynchronous python, you might have noticed that we haven't mentioned coroutines
|
||||
so far. Don't worry, that is intentional: giambio never lets a user deal with coroutines on the surface because the whole async
|
||||
model is much simpler if we take coroutines out of the game, and everything works just the same.
|
||||
|
||||
```python
|
||||
import giambio
|
||||
|
||||
async def sleep_double(n):
|
||||
await giambio.sleep(2 * n)
|
||||
|
||||
giambio.run(sleep_double, 2) # This hangs for 4 seconds and returns
|
||||
```
|
||||
|
||||
As it turns out, this function is one that's actually worth making async: because it calls another async function.
|
||||
Not that there's nothing wrong with our `foo` from before, it surely works, but it doesn't really make sense to
|
||||
make it async in the first place.
|
||||
|
||||
### Don't forget the `await`!
|
||||
|
||||
As we already learned, async functions can only be called with the `await` keyword, and it would be logical to
|
||||
think that forgetting to do so would raise an error, but it's actually a little bit trickier than that.
|
||||
|
||||
Take this example here:
|
||||
|
||||
```python
|
||||
import giambio
|
||||
|
||||
async def sleep_double_broken(n):
|
||||
print("Taking a nap!")
|
||||
start = giambio.clock()
|
||||
giambio.sleep(2 * n) # We forgot the await!
|
||||
end = giambio.clock() - start
|
||||
print(f"Slept for {end:.2f} seconds!")
|
||||
|
||||
giambio.run(sleep_double_broken, 2)
|
||||
```
|
||||
|
||||
Running this code, will produce an output that looks like this:
|
||||
|
||||
```
|
||||
Taking a nap!
|
||||
Slept 0.00 seconds!
|
||||
__main__:7: RuntimeWarning: coroutine 'sleep' was never awaited
|
||||
```
|
||||
|
||||
Wait, what happened here? From this output, it looks like the code worked, but something clearly went wrong:
|
||||
the function didn't sleep. Python gives us a hint that we broke _something_ by raising a warning, complaining
|
||||
that `coroutine 'sleep' was never awaited` (you might not see this warning because it depends on whether a
|
||||
garbage collection cycle occurred or not).
|
||||
I know I said we weren't going to talk about coroutines, but you have to blame Python, not me. Just know that
|
||||
if you see a warning like that, it means that somewhere in your code you forgot an `await` when calling an async
|
||||
function, so try fixing that before trying to figure out what could be the problem if you have a long traceback:
|
||||
most likely that's just collateral damage caused by the missing keyword.
|
||||
|
||||
If you're ok with just remembering to put `await` every time you call an async function, you can safely skip to
|
||||
the next section, but for the curios among y'all I might as well explain exactly what happened there.
|
||||
|
||||
When coroutines are called without the `await`, they don't exactly do nothing: they return this weird 'coroutine'
|
||||
object
|
||||
|
||||
```python
|
||||
|
||||
>>> giambio.sleep(1)
|
||||
<coroutine object sleep at 0x1069520d0>
|
||||
```
|
||||
|
||||
The reason for this is that while giambio tries to separate the async and sync worlds, therefore considering
|
||||
`await giambio.sleep(1)` as a single unit, when you `await` an async function Python does 2 things:
|
||||
- It creates this weird coroutine object
|
||||
- Passes that object to `await`, which runs the function
|
||||
|
||||
So basically that's why you always need to put `await` in front of an async function when calling it.
|
||||
|
||||
|
||||
## Something actually useful
|
||||
|
||||
Ok, so far you've learned that asynchronous functions can call other async functions, and that giambio has a special
|
||||
runner function that can start the whole async context, but we didn't really do anything _useful_.
|
||||
Our previous examples could be written using sync functions (like `time.sleep`) and they would work just fine, that isn't
|
||||
quite useful is it?
|
||||
|
||||
But here comes the reason why you would want to use a library like giambio: it can run multiple async functions __at the same time__.
|
||||
Yep, you read that right.
|
||||
|
||||
To demonstrate this, have a look a this example
|
||||
|
||||
|
||||
```python
|
||||
import giambio
|
||||
|
||||
async def countdown(n: int):
|
||||
print(f"Counting down from {n}!")
|
||||
while n > 0:
|
||||
print(f"Down {n}")
|
||||
n -= 1
|
||||
await giambio.sleep(1)
|
||||
print("Countdown over")
|
||||
return 0
|
||||
|
||||
async def countup(stop: int):
|
||||
print(f"Counting up to {stop}!")
|
||||
x = 0
|
||||
while x < stop:
|
||||
print(f"Up {x}")
|
||||
x += 1
|
||||
await giambio.sleep(2)
|
||||
print("Countup over")
|
||||
return 1
|
||||
|
||||
async def main():
|
||||
start = giambio.clock()
|
||||
async with giambio.create_pool() as pool:
|
||||
pool.spawn(countdown, 10)
|
||||
pool.spawn(countup, 5)
|
||||
print("Children spawned, awaiting completion")
|
||||
print(f"Task execution complete in {giambio.clock() - start:2f} seconds")
|
||||
|
||||
if __name__ == "__main__":
|
||||
giambio.run(main)
|
||||
|
||||
```
|
||||
|
||||
There is a lot going on here, and we'll explain every bit of it step by step:
|
||||
|
||||
- First, we imported giambio and defined two async functions: `countup` and `countdown`
|
||||
- These two functions do exactly what their name suggests, but for the purposes of
|
||||
this tutorial, `countup` will be running twice as slow as `countdown` (see the call
|
||||
to `await giambio.sleep(2)`?)
|
||||
- Here comes the real fun: `async with`? What's going on there?
|
||||
As it turns out, Python 3.5 didn't just add async functions, but also quite a bit
|
||||
of related new syntax. One of the things that was added is asynchronous context managers.
|
||||
You might have already encountered context managers in python, but in case you didn't,
|
||||
a line such as `with foo as sth` tells the Python interpreter to call `foo.__enter__()`
|
||||
at the beginning of the block, and `foo.__exit__()` at the end of the block. The `as`
|
||||
keyword just assigns the return value of `foo.__enter__()` to the variable `sth`. So
|
||||
context managers are a shorthand for calling functions, and since Python 3.5 added
|
||||
async functions, we also needed async context managers. While `with foo as sth` calls
|
||||
`foo.__enter__()`, `async with foo as sth` calls `await foo.__aenter__()`, easy huh?
|
||||
|
||||
__Note__: On a related note, Python 3.5 also added asynchronous for loops! The logic is
|
||||
the same though: while `for item in container` calls `container.__next__()` to fetch the
|
||||
next item, `async for item in container` calls `await container.__anext__()` to do so.
|
||||
It's _that_ simple, mostly just remember to stick `await` everywhere and you'll be good.
|
||||
|
||||
- Ok, so now we grasp `async with`, but what's with that `create_pool()`? In giambio,
|
||||
there are actually 2 ways to call async functions: one we've already seen (`await fn()`),
|
||||
while the other is trough an asynchronous pool. The cool part about `pool.spawn()` is
|
||||
that it will return immediately, without waiting for the async function to finish. So,
|
||||
now our functions are running in the background.
|
||||
After we spawn our tasks, we hit the call to `print` and the end of the block, so Python
|
||||
calls the pool's `__aexit__()` method. What this does is pause the parent task (our `main`
|
||||
async function in this case) until all children task have exited, and as it turns out, that
|
||||
is a good thing.
|
||||
The reason why pools always wait for all children to have finished executing is that it makes
|
||||
easier propagating exceptions in the parent if something goes wrong: unlike many other frameworks,
|
||||
exceptions in giambio always behave as expected
|
||||
|
||||
|
||||
Ok, so, let's try running this snippet and see what we get:
|
||||
|
||||
```
|
||||
Children spawned, awaiting completion
|
||||
Counting down from 10!
|
||||
Down 10
|
||||
Counting up to 5!
|
||||
Up 0
|
||||
Down 9
|
||||
Up 1
|
||||
Down 8
|
||||
Down 7
|
||||
Up 2
|
||||
Down 6
|
||||
Down 5
|
||||
Up 3
|
||||
Down 4
|
||||
Down 3
|
||||
Up 4
|
||||
Down 2
|
||||
Down 1
|
||||
Countup over
|
||||
Countdown over
|
||||
Task execution complete in 10.07 seconds
|
||||
```
|
||||
|
||||
(Your output might have some lines swapped compared to this)
|
||||
|
||||
You see how `countup` and `countdown` both start and finish
|
||||
together? Moreover, even though each function slept for about 10
|
||||
seconds (therefore 20 seconds total), the program just took 10
|
||||
seconds to complete, so our children are really running at the same time.
|
||||
|
||||
If you've ever done thread programming, this will feel like home, and that's good:
|
||||
that's exactly what we want. But beware! No threads are involved here, giambio is
|
||||
running in a single thread. That's why we talked about _tasks_ rather than _threads_
|
||||
so far. The difference between the two is that you can run a lot of tasks in a single
|
||||
thread, and that with threads Python can switch which thread is running at any time.
|
||||
Giambio, on the other hand, can switch tasks only at certain fixed points called
|
||||
_checkpoints_, more on that later.
|
||||
|
||||
### A sneak peak into the async world
|
||||
|
||||
The basic idea behind libraries like giambio is that they can run a lot of tasks
|
||||
at the same time by switching back and forth between them at appropriate places.
|
||||
An example for that could be a web server: while the server is waiting for a response
|
||||
from a client, we can accept another connection. You don't necessarily need all these
|
||||
pesky details to use giambio, but it's good to have at least an high-level understanding
|
||||
of how this all works.
|
||||
|
||||
The peculiarity of asynchronous functions is that they can suspend their execution: that's
|
||||
what `await` does, it yields back the execution control to giambio, which can then decide
|
||||
what to do next.
|
||||
|
||||
To understand this better, take a look at this code:
|
||||
|
||||
```python
|
||||
def countdown(n: int) -> int:
|
||||
while n:
|
||||
yield n
|
||||
n -= 1
|
||||
|
||||
for x in countdown(5):
|
||||
print(x)
|
||||
```
|
||||
|
||||
In the above snippet, `countdown` is a generator function. Generators are really useful because
|
||||
they allow to customize iteration. Running that code produces the following output:
|
||||
|
||||
```
|
||||
5
|
||||
4
|
||||
3
|
||||
2
|
||||
1
|
||||
```
|
||||
|
||||
The trick for this to work is `yield`.
|
||||
What `yield` does is return back to the caller and suspend itself: In our case, `yield`
|
||||
returns to the for loop, which calls `countdown` again. So, the generator resumes right
|
||||
after the `yield`, decrements n, and loops right back to the top for the while loop to
|
||||
execute again. It's that suspension part that allows the async magic to happen: the whole
|
||||
`async`/`await` logic overlaps a lot with generator functions.
|
||||
|
||||
Some libraries, like `asyncio`, take advantage of this yielding mechanism, because they were made
|
||||
way before Python 3.5 added that nice new syntax.
|
||||
|
||||
So, since only async functions can suspend themselves, the only places where giambio will switch
|
||||
tasks is where there is a call to `await something()`. If there is no `await`, then you can be sure
|
||||
that giambio will not switch tasks (because it can't): this makes the asynchronous model much easier
|
||||
to reason about, because you can know if a function will ever switch, and where will it do so, just
|
||||
by looking at its source code. That is very different from what threads do: they can (and will) switch
|
||||
whenever they feel like it.
|
||||
|
||||
Remember when we talked about checkpoints? That's what they are: calls to async functions that allow
|
||||
giambio to switch tasks. The problem with checkpoints is that if you don't have enough of them in your code,
|
||||
then giambio will switch less frequently, hurting concurrency. It turns out that a quick and easy fix
|
||||
for that is calling `await giambio.sleep(0)`; This will implicitly let giambio kick in and do its job,
|
||||
and it will reschedule the caller almost immediately, because the sleep time is 0.
|
||||
|
||||
### Mix and match? No thanks
|
||||
|
||||
You may wonder whether you can mix async libraries: for instance, can we call `trio.sleep` in a
|
||||
giambio application? The answer is no, we can't, and there's a reason for that. Giambio wraps all
|
||||
your asynchronous code in its event loop, which is what actually runs the tasks. When you call
|
||||
`await giambio.something()`, what you're doing is sending "commands" to the event loop asking it
|
||||
to perform a certain thing in a given task, and to communicate your intent to the loop, the
|
||||
primitives (such as `giambio.sleep`) talk a language that only giambio's event loop can understand.
|
||||
Other libraries have other private "languages", so mixing them is not possible: doing so will cause
|
||||
giambio to get very confused and most likely just explode spectacularly badly
|
||||
|
||||
|
||||
TODO: I/O
|
||||
|
||||
## Contributing
|
||||
|
||||
This is a relatively young project and it is looking for collaborators! It's not rocket science,
|
||||
but writing a proper framework like this implies some non-trivial issues that require proper and optimized solutions,
|
||||
so if you feel like you want to challenge yourself don't hesitate to contact me on [Telegram](https://telegram.me/nocturn9x)
|
||||
or by [E-mail](mailto:hackhab@gmail.com)
|
||||
# giambio - Asynchronous Python made easy (and friendly)
|
||||
|
||||
giambio is an event-driven concurrency library meant* to perform efficient and high-performant I/O multiplexing.
|
||||
This library implements what is known as a _stackless mode of execution_, or
|
||||
"green threads", though the latter term is misleading as **no multithreading is involved** (at least not by default).
|
||||
|
||||
|
||||
_*_: The library *works* (sometimes), but its still in its very early stages and is nowhere close being
|
||||
production ready, so be aware that it is likely that you'll find bugs and race conditions
|
||||
|
||||
## Disclaimer
|
||||
|
||||
Right now this is nothing more than a toy implementation to help me understand how this whole `async`/`await` thing works
|
||||
and it is pretty much guaranteed to explode spectacularly badly while using it. If you find any bugs, please report them!
|
||||
|
||||
Oh and by the way, this project was hugely inspired by the [curio](https://github.com/dabeaz/curio) and the
|
||||
[trio](https://github.com/python-trio/trio) projects, you might want to have a look at their amazing work if you need a
|
||||
rock-solid and structured concurrency framework (I personally recommend trio and that's definitely not related to the fact
|
||||
that most of the following text is ~~stolen~~ inspired from its documentation)
|
||||
|
||||
|
||||
# What the hell is async anyway?
|
||||
|
||||
Libraries like giambio shine the most when it comes to performing asyncronous I/O (reading a socket, writing to a file, that sort of thing).
|
||||
The most common example of this is a network server that needs to handle multiple connections at the same time.
|
||||
One possible approach to achieve concurrency is to use threads, and despite their bad reputation in Python, they
|
||||
actually might be a good choice when it comes to I/O for reasons that span far beyond the scope of this tutorial.
|
||||
If you choose to use threads, there are a couple things you can do, involving what is known as _thread synchronization
|
||||
primitives_ and _thread pools_, but once again that is beyond the purposes of this quickstart guide.
|
||||
A library like giambio comes into play when you need to perform lots of [blocking operations](https://en.wikipedia.org/wiki/Blocking_(computing)),
|
||||
and network servers happen to be heavily based on I/O: a blocking operation.
|
||||
Starting to see where we're heading?
|
||||
|
||||
|
||||
## A deeper dive
|
||||
|
||||
Giambio has been designed with simplicity in mind, so this document won't explain all the gritty details about _how_ async is
|
||||
implemented in Python (you might want to check out [this article](https://snarky.ca/how-the-heck-does-async-await-work-in-python-3-5/) if you want to learn more about all the implementation details).
|
||||
For the sake of this tutorial, all you need to know is that giambio is all about a feature added in Python 3.5:
|
||||
asynchronous functions, or 'async' for short.
|
||||
|
||||
Async functions are functions defined with `async def` instead of the regular `def`, like so:
|
||||
|
||||
```python
|
||||
async def async_fun(): # An async function
|
||||
print("Hello, world!")
|
||||
|
||||
def sync_fun(): # A regular (sync) function
|
||||
print("Hello, world!")
|
||||
```
|
||||
|
||||
First of all, async functions like to stick together: to call an async function you need to put `await` in front of it, like below:
|
||||
|
||||
```python
|
||||
|
||||
async def async_two():
|
||||
print("Hello from async_two!")
|
||||
|
||||
async def async_one():
|
||||
print("Hello from async_one!")
|
||||
await async_two() # This is an async call
|
||||
```
|
||||
|
||||
It has to be noted that using `await` outside of an async function is a `SyntaxError`, so basically async
|
||||
functions have a unique superpower: they, and no-one else, can call other async functions.
|
||||
|
||||
This already presents a chicken-and-egg problem, because when you fire up Python, it is running plain ol'
|
||||
synchronous code; So how do we enter the async context in the first place?
|
||||
|
||||
That is done via a special _synchronous function_, `giambio.run` in our case, that has the ability to call
|
||||
asynchronous functions and can therefore initiate the async context. For this
|
||||
reason, `giambio.run` **must** be called from a synchronous context, to avoid a horrible _deadlock_.
|
||||
|
||||
Now that you know all of this, you might be wondering why on earth would one use async functions instead of
|
||||
regular functions: after all, their ability to call other async functions seems pretty pointless in itself, doesn't it?
|
||||
Take a look at this example below:
|
||||
|
||||
```python
|
||||
import giambio
|
||||
|
||||
async def foo():
|
||||
print("Hello, world!")
|
||||
|
||||
giambio.run(foo) # Prints 'Hello, world!'
|
||||
```
|
||||
|
||||
This could as well be written the following way and would produce the same output:
|
||||
|
||||
```python
|
||||
def foo():
|
||||
print("Hello, world!")
|
||||
|
||||
foo() # Prints 'Hello, world!'
|
||||
```
|
||||
|
||||
To answer this question, we have to dig a bit deeper about _what_ giambio gives you in exchange for all this `async`/`await` madness.
|
||||
|
||||
We already introduced `giambio.run`, a special runner function that can start the async context from a synchronous one, but giambio
|
||||
provides also a set of tools, mainly for doing I/O. These functions, as you might have guessed, are async functions and they're useful!
|
||||
So if you wanna take advantage of giambio, and hopefully you will after reading this guide, you need to write async code.
|
||||
As an example, take this function using `giambio.sleep` (`giambio.sleep` is like `time.sleep`, but with an async flavor):
|
||||
|
||||
__Side note__: If you have decent knowledge about asynchronous python, you might have noticed that we haven't mentioned coroutines
|
||||
so far. Don't worry, that is intentional: giambio never lets a user deal with coroutines on the surface because the whole async
|
||||
model is much simpler if we take coroutines out of the game, and everything works just the same.
|
||||
|
||||
```python
|
||||
import giambio
|
||||
|
||||
async def sleep_double(n):
|
||||
await giambio.sleep(2 * n)
|
||||
|
||||
giambio.run(sleep_double, 2) # This hangs for 4 seconds and returns
|
||||
```
|
||||
|
||||
As it turns out, this function is one that's actually worth making async: because it calls another async function.
|
||||
Not that there's nothing wrong with our `foo` from before, it surely works, but it doesn't really make sense to
|
||||
make it async in the first place.
|
||||
|
||||
### Don't forget the `await`!
|
||||
|
||||
As we already learned, async functions can only be called with the `await` keyword, and it would be logical to
|
||||
think that forgetting to do so would raise an error, but it's actually a little bit trickier than that.
|
||||
|
||||
Take this example here:
|
||||
|
||||
```python
|
||||
import giambio
|
||||
|
||||
async def sleep_double_broken(n):
|
||||
print("Taking a nap!")
|
||||
start = giambio.clock()
|
||||
giambio.sleep(2 * n) # We forgot the await!
|
||||
end = giambio.clock() - start
|
||||
print(f"Slept for {end:.2f} seconds!")
|
||||
|
||||
giambio.run(sleep_double_broken, 2)
|
||||
```
|
||||
|
||||
Running this code, will produce an output that looks like this:
|
||||
|
||||
```
|
||||
Taking a nap!
|
||||
Slept 0.00 seconds!
|
||||
__main__:7: RuntimeWarning: coroutine 'sleep' was never awaited
|
||||
```
|
||||
|
||||
Wait, what happened here? From this output, it looks like the code worked, but something clearly went wrong:
|
||||
the function didn't sleep. Python gives us a hint that we broke _something_ by raising a warning, complaining
|
||||
that `coroutine 'sleep' was never awaited` (you might not see this warning because it depends on whether a
|
||||
garbage collection cycle occurred or not).
|
||||
I know I said we weren't going to talk about coroutines, but you have to blame Python, not me. Just know that
|
||||
if you see a warning like that, it means that somewhere in your code you forgot an `await` when calling an async
|
||||
function, so try fixing that before trying to figure out what could be the problem if you have a long traceback:
|
||||
most likely that's just collateral damage caused by the missing keyword.
|
||||
|
||||
If you're ok with just remembering to put `await` every time you call an async function, you can safely skip to
|
||||
the next section, but for the curios among y'all I might as well explain exactly what happened there.
|
||||
|
||||
When coroutines are called without the `await`, they don't exactly do nothing: they return this weird 'coroutine'
|
||||
object
|
||||
|
||||
```python
|
||||
|
||||
>>> giambio.sleep(1)
|
||||
<coroutine object sleep at 0x1069520d0>
|
||||
```
|
||||
|
||||
The reason for this is that while giambio tries to separate the async and sync worlds, therefore considering
|
||||
`await giambio.sleep(1)` as a single unit, when you `await` an async function Python does 2 things:
|
||||
- It creates this weird coroutine object
|
||||
- Passes that object to `await`, which runs the function
|
||||
|
||||
So basically that's why you always need to put `await` in front of an async function when calling it.
|
||||
|
||||
|
||||
## Something actually useful
|
||||
|
||||
Ok, so far you've learned that asynchronous functions can call other async functions, and that giambio has a special
|
||||
runner function that can start the whole async context, but we didn't really do anything _useful_.
|
||||
Our previous examples could be written using sync functions (like `time.sleep`) and they would work just fine, that isn't
|
||||
quite useful is it?
|
||||
|
||||
But here comes the reason why you would want to use a library like giambio: it can run multiple async functions __at the same time__.
|
||||
Yep, you read that right.
|
||||
|
||||
To demonstrate this, have a look a this example
|
||||
|
||||
|
||||
```python
|
||||
import giambio
|
||||
|
||||
async def countdown(n: int):
|
||||
print(f"Counting down from {n}!")
|
||||
while n > 0:
|
||||
print(f"Down {n}")
|
||||
n -= 1
|
||||
await giambio.sleep(1)
|
||||
print("Countdown over")
|
||||
return 0
|
||||
|
||||
async def countup(stop: int):
|
||||
print(f"Counting up to {stop}!")
|
||||
x = 0
|
||||
while x < stop:
|
||||
print(f"Up {x}")
|
||||
x += 1
|
||||
await giambio.sleep(2)
|
||||
print("Countup over")
|
||||
return 1
|
||||
|
||||
async def main():
|
||||
start = giambio.clock()
|
||||
async with giambio.create_pool() as pool:
|
||||
pool.spawn(countdown, 10)
|
||||
pool.spawn(countup, 5)
|
||||
print("Children spawned, awaiting completion")
|
||||
print(f"Task execution complete in {giambio.clock() - start:2f} seconds")
|
||||
|
||||
if __name__ == "__main__":
|
||||
giambio.run(main)
|
||||
|
||||
```
|
||||
|
||||
There is a lot going on here, and we'll explain every bit of it step by step:
|
||||
|
||||
- First, we imported giambio and defined two async functions: `countup` and `countdown`
|
||||
- These two functions do exactly what their name suggests, but for the purposes of
|
||||
this tutorial, `countup` will be running twice as slow as `countdown` (see the call
|
||||
to `await giambio.sleep(2)`?)
|
||||
- Here comes the real fun: `async with`? What's going on there?
|
||||
As it turns out, Python 3.5 didn't just add async functions, but also quite a bit
|
||||
of related new syntax. One of the things that was added is asynchronous context managers.
|
||||
You might have already encountered context managers in python, but in case you didn't,
|
||||
a line such as `with foo as sth` tells the Python interpreter to call `foo.__enter__()`
|
||||
at the beginning of the block, and `foo.__exit__()` at the end of the block. The `as`
|
||||
keyword just assigns the return value of `foo.__enter__()` to the variable `sth`. So
|
||||
context managers are a shorthand for calling functions, and since Python 3.5 added
|
||||
async functions, we also needed async context managers. While `with foo as sth` calls
|
||||
`foo.__enter__()`, `async with foo as sth` calls `await foo.__aenter__()`, easy huh?
|
||||
|
||||
__Note__: On a related note, Python 3.5 also added asynchronous for loops! The logic is
|
||||
the same though: while `for item in container` calls `container.__next__()` to fetch the
|
||||
next item, `async for item in container` calls `await container.__anext__()` to do so.
|
||||
It's _that_ simple, mostly just remember to stick `await` everywhere and you'll be good.
|
||||
|
||||
- Ok, so now we grasp `async with`, but what's with that `create_pool()`? In giambio,
|
||||
there are actually 2 ways to call async functions: one we've already seen (`await fn()`),
|
||||
while the other is trough an asynchronous pool. The cool part about `pool.spawn()` is
|
||||
that it will return immediately, without waiting for the async function to finish. So,
|
||||
now our functions are running in the background.
|
||||
After we spawn our tasks, we hit the call to `print` and the end of the block, so Python
|
||||
calls the pool's `__aexit__()` method. What this does is pause the parent task (our `main`
|
||||
async function in this case) until all children task have exited, and as it turns out, that
|
||||
is a good thing.
|
||||
The reason why pools always wait for all children to have finished executing is that it makes
|
||||
easier propagating exceptions in the parent if something goes wrong: unlike many other frameworks,
|
||||
exceptions in giambio always behave as expected
|
||||
|
||||
|
||||
Ok, so, let's try running this snippet and see what we get:
|
||||
|
||||
```
|
||||
Children spawned, awaiting completion
|
||||
Counting down from 10!
|
||||
Down 10
|
||||
Counting up to 5!
|
||||
Up 0
|
||||
Down 9
|
||||
Up 1
|
||||
Down 8
|
||||
Down 7
|
||||
Up 2
|
||||
Down 6
|
||||
Down 5
|
||||
Up 3
|
||||
Down 4
|
||||
Down 3
|
||||
Up 4
|
||||
Down 2
|
||||
Down 1
|
||||
Countup over
|
||||
Countdown over
|
||||
Task execution complete in 10.07 seconds
|
||||
```
|
||||
|
||||
(Your output might have some lines swapped compared to this)
|
||||
|
||||
You see how `countup` and `countdown` both start and finish
|
||||
together? Moreover, even though each function slept for about 10
|
||||
seconds (therefore 20 seconds total), the program just took 10
|
||||
seconds to complete, so our children are really running at the same time.
|
||||
|
||||
If you've ever done thread programming, this will feel like home, and that's good:
|
||||
that's exactly what we want. But beware! No threads are involved here, giambio is
|
||||
running in a single thread. That's why we talked about _tasks_ rather than _threads_
|
||||
so far. The difference between the two is that you can run a lot of tasks in a single
|
||||
thread, and that with threads Python can switch which thread is running at any time.
|
||||
Giambio, on the other hand, can switch tasks only at certain fixed points called
|
||||
_checkpoints_, more on that later.
|
||||
|
||||
### A sneak peak into the async world
|
||||
|
||||
The basic idea behind libraries like giambio is that they can run a lot of tasks
|
||||
at the same time by switching back and forth between them at appropriate places.
|
||||
An example for that could be a web server: while the server is waiting for a response
|
||||
from a client, we can accept another connection. You don't necessarily need all these
|
||||
pesky details to use giambio, but it's good to have at least an high-level understanding
|
||||
of how this all works.
|
||||
|
||||
The peculiarity of asynchronous functions is that they can suspend their execution: that's
|
||||
what `await` does, it yields back the execution control to giambio, which can then decide
|
||||
what to do next.
|
||||
|
||||
To understand this better, take a look at this code:
|
||||
|
||||
```python
|
||||
def countdown(n: int) -> int:
|
||||
while n:
|
||||
yield n
|
||||
n -= 1
|
||||
|
||||
for x in countdown(5):
|
||||
print(x)
|
||||
```
|
||||
|
||||
In the above snippet, `countdown` is a generator function. Generators are really useful because
|
||||
they allow to customize iteration. Running that code produces the following output:
|
||||
|
||||
```
|
||||
5
|
||||
4
|
||||
3
|
||||
2
|
||||
1
|
||||
```
|
||||
|
||||
The trick for this to work is `yield`.
|
||||
What `yield` does is return back to the caller and suspend itself: In our case, `yield`
|
||||
returns to the for loop, which calls `countdown` again. So, the generator resumes right
|
||||
after the `yield`, decrements n, and loops right back to the top for the while loop to
|
||||
execute again. It's that suspension part that allows the async magic to happen: the whole
|
||||
`async`/`await` logic overlaps a lot with generator functions.
|
||||
|
||||
Some libraries, like `asyncio`, take advantage of this yielding mechanism, because they were made
|
||||
way before Python 3.5 added that nice new syntax.
|
||||
|
||||
So, since only async functions can suspend themselves, the only places where giambio will switch
|
||||
tasks is where there is a call to `await something()`. If there is no `await`, then you can be sure
|
||||
that giambio will not switch tasks (because it can't): this makes the asynchronous model much easier
|
||||
to reason about, because you can know if a function will ever switch, and where will it do so, just
|
||||
by looking at its source code. That is very different from what threads do: they can (and will) switch
|
||||
whenever they feel like it.
|
||||
|
||||
Remember when we talked about checkpoints? That's what they are: calls to async functions that allow
|
||||
giambio to switch tasks. The problem with checkpoints is that if you don't have enough of them in your code,
|
||||
then giambio will switch less frequently, hurting concurrency. It turns out that a quick and easy fix
|
||||
for that is calling `await giambio.sleep(0)`; This will implicitly let giambio kick in and do its job,
|
||||
and it will reschedule the caller almost immediately, because the sleep time is 0.
|
||||
|
||||
### Mix and match? No thanks
|
||||
|
||||
You may wonder whether you can mix async libraries: for instance, can we call `trio.sleep` in a
|
||||
giambio application? The answer is no, we can't, and there's a reason for that. Giambio wraps all
|
||||
your asynchronous code in its event loop, which is what actually runs the tasks. When you call
|
||||
`await giambio.something()`, what you're doing is sending "commands" to the event loop asking it
|
||||
to perform a certain thing in a given task, and to communicate your intent to the loop, the
|
||||
primitives (such as `giambio.sleep`) talk a language that only giambio's event loop can understand.
|
||||
Other libraries have other private "languages", so mixing them is not possible: doing so will cause
|
||||
giambio to get very confused and most likely just explode spectacularly badly
|
||||
|
||||
|
||||
TODO: I/O
|
||||
|
||||
## Contributing
|
||||
|
||||
This is a relatively young project and it is looking for collaborators! It's not rocket science,
|
||||
but writing a proper framework like this implies some non-trivial issues that require proper and optimized solutions,
|
||||
so if you feel like you want to challenge yourself don't hesitate to contact me on [Telegram](https://telegram.me/nocturn9x)
|
||||
or by [E-mail](mailto:hackhab@gmail.com)
|
||||
|
|
|
@ -1,38 +1,38 @@
|
|||
"""
|
||||
Copyright (C) 2020 nocturn9x
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
"""
|
||||
|
||||
__author__ = "Nocturn9x aka Isgiambyy"
|
||||
__version__ = (1, 0, 0)
|
||||
|
||||
|
||||
from . import exceptions
|
||||
from .traps import sleep, current_task
|
||||
from .objects import Event
|
||||
from .run import run, clock, wrap_socket, create_pool, get_event_loop, new_event_loop
|
||||
|
||||
|
||||
__all__ = [
|
||||
"exceptions",
|
||||
"sleep",
|
||||
"Event",
|
||||
"run",
|
||||
"clock",
|
||||
"wrap_socket",
|
||||
"create_pool",
|
||||
"get_event_loop",
|
||||
"current_task",
|
||||
"new_event_loop"
|
||||
]
|
||||
"""
|
||||
Copyright (C) 2020 nocturn9x
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
"""
|
||||
|
||||
__author__ = "Nocturn9x aka Isgiambyy"
|
||||
__version__ = (1, 0, 0)
|
||||
|
||||
|
||||
from . import exceptions
|
||||
from .traps import sleep, current_task
|
||||
from .objects import Event
|
||||
from .run import run, clock, wrap_socket, create_pool, get_event_loop, new_event_loop
|
||||
|
||||
|
||||
__all__ = [
|
||||
"exceptions",
|
||||
"sleep",
|
||||
"Event",
|
||||
"run",
|
||||
"clock",
|
||||
"wrap_socket",
|
||||
"create_pool",
|
||||
"get_event_loop",
|
||||
"current_task",
|
||||
"new_event_loop"
|
||||
]
|
||||
|
|
|
@ -1,69 +1,70 @@
|
|||
"""
|
||||
Higher-level context managers for async pools
|
||||
|
||||
Copyright (C) 2020 nocturn9x
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
"""
|
||||
|
||||
|
||||
import types
|
||||
from .core import AsyncScheduler
|
||||
from .objects import Task
|
||||
|
||||
|
||||
class TaskManager:
|
||||
"""
|
||||
An asynchronous context manager for giambio
|
||||
"""
|
||||
|
||||
def __init__(self, loop: AsyncScheduler) -> None:
|
||||
"""
|
||||
Object constructor
|
||||
"""
|
||||
|
||||
self.loop = loop
|
||||
self.tasks = []
|
||||
|
||||
def spawn(self, func: types.FunctionType, *args):
|
||||
"""
|
||||
Spawns a child task
|
||||
"""
|
||||
|
||||
task = Task(func(*args), func.__name__ or str(func))
|
||||
task.parent = self.loop.current_task
|
||||
self.loop.tasks.append(task)
|
||||
self.tasks.append(task)
|
||||
|
||||
def spawn_after(self, func: types.FunctionType, n: int, *args):
|
||||
"""
|
||||
Schedules a task for execution after n seconds
|
||||
"""
|
||||
|
||||
assert n >= 0, "The time delay can't be negative"
|
||||
task = Task(func(*args), func.__name__ or str(func))
|
||||
task.parent = self.loop.current_task
|
||||
self.loop.paused.put(task, n)
|
||||
self.tasks.append(task)
|
||||
|
||||
async def __aenter__(self):
|
||||
return self
|
||||
|
||||
async def __aexit__(self, exc_type, exc, tb):
|
||||
for task in self.tasks:
|
||||
try:
|
||||
await task.join()
|
||||
except BaseException as e:
|
||||
self.tasks.remove(task)
|
||||
for to_cancel in self.tasks:
|
||||
await to_cancel.cancel()
|
||||
"""
|
||||
Higher-level context managers for async pools
|
||||
|
||||
Copyright (C) 2020 nocturn9x
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
"""
|
||||
|
||||
|
||||
import types
|
||||
from .core import AsyncScheduler
|
||||
from .objects import Task
|
||||
|
||||
|
||||
class TaskManager:
|
||||
"""
|
||||
An asynchronous context manager for giambio
|
||||
"""
|
||||
|
||||
def __init__(self, loop: AsyncScheduler) -> None:
|
||||
"""
|
||||
Object constructor
|
||||
"""
|
||||
|
||||
self.loop = loop
|
||||
self.tasks = []
|
||||
|
||||
def spawn(self, func: types.FunctionType, *args):
|
||||
"""
|
||||
Spawns a child task
|
||||
"""
|
||||
|
||||
task = Task(func(*args), func.__name__ or str(func))
|
||||
task.parent = self.loop.current_task
|
||||
self.loop.tasks.append(task)
|
||||
self.tasks.append(task)
|
||||
|
||||
def spawn_after(self, func: types.FunctionType, n: int, *args):
|
||||
"""
|
||||
Schedules a task for execution after n seconds
|
||||
"""
|
||||
|
||||
assert n >= 0, "The time delay can't be negative"
|
||||
task = Task(func(*args), func.__name__ or str(func))
|
||||
task.parent = self.loop.current_task
|
||||
self.loop.paused.put(task, n)
|
||||
self.tasks.append(task)
|
||||
|
||||
async def __aenter__(self):
|
||||
return self
|
||||
|
||||
async def __aexit__(self, exc_type, exc, tb):
|
||||
for task in self.tasks:
|
||||
try:
|
||||
await task.join()
|
||||
except BaseException:
|
||||
self.tasks.remove(task)
|
||||
for to_cancel in self.tasks:
|
||||
await to_cancel.cancel()
|
||||
print("oof")
|
796
giambio/core.py
796
giambio/core.py
|
@ -1,398 +1,398 @@
|
|||
"""
|
||||
The main runtime environment for giambio
|
||||
|
||||
Copyright (C) 2020 nocturn9x
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
"""
|
||||
|
||||
# Import libraries and internal resources
|
||||
import types
|
||||
import socket
|
||||
from time import sleep as wait
|
||||
from timeit import default_timer
|
||||
from .objects import Task, TimeQueue
|
||||
from socket import SOL_SOCKET, SO_ERROR
|
||||
from .traps import want_read, want_write
|
||||
from collections import deque
|
||||
from .socket import AsyncSocket, WantWrite, WantRead
|
||||
from selectors import DefaultSelector, EVENT_READ, EVENT_WRITE
|
||||
from .exceptions import (InternalError,
|
||||
CancelledError,
|
||||
ResourceBusy,
|
||||
)
|
||||
|
||||
|
||||
|
||||
class AsyncScheduler:
|
||||
"""
|
||||
An asynchronous scheduler implementation. Tries to mimic the threaded
|
||||
model in its simplicity, without using actual threads, but rather alternating
|
||||
across coroutines execution to let more than one thing at a time to proceed
|
||||
with its calculations. An attempt to fix the threaded model has been made
|
||||
without making the API unnecessarily complicated.
|
||||
A few examples are tasks cancellation and exception propagation.
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
"""
|
||||
Object constructor
|
||||
"""
|
||||
|
||||
# Tasks that are ready to run
|
||||
self.tasks = deque()
|
||||
# Selector object to perform I/O multiplexing
|
||||
self.selector = DefaultSelector()
|
||||
# This will always point to the currently running coroutine (Task object)
|
||||
self.current_task = None
|
||||
# Monotonic clock to keep track of elapsed time reliably
|
||||
self.clock = default_timer
|
||||
# Tasks that are asleep
|
||||
self.paused = TimeQueue(self.clock)
|
||||
# All active Event objects
|
||||
self.events = set()
|
||||
# Data to send back to a trap
|
||||
self.to_send = None
|
||||
# Have we ever ran?
|
||||
self.has_ran = False
|
||||
|
||||
def done(self):
|
||||
"""
|
||||
Returns True if there is work to do
|
||||
"""
|
||||
|
||||
if self.selector.get_map() or any([self.paused,
|
||||
self.tasks,
|
||||
self.events
|
||||
]):
|
||||
return False
|
||||
return True
|
||||
|
||||
def shutdown(self):
|
||||
"""
|
||||
Shuts down the event loop
|
||||
"""
|
||||
|
||||
# TODO: See if other teardown is required (massive join()?)
|
||||
self.selector.close()
|
||||
|
||||
def run(self):
|
||||
"""
|
||||
Starts the loop and 'listens' for events until there is work to do,
|
||||
then exits. This behavior kinda reflects a kernel, as coroutines can
|
||||
request the loop's functionality only trough some fixed entry points,
|
||||
which in turn yield and give execution control to the loop itself.
|
||||
"""
|
||||
|
||||
while True:
|
||||
try:
|
||||
if self.done():
|
||||
self.shutdown()
|
||||
break
|
||||
elif not self.tasks:
|
||||
if self.paused:
|
||||
# If there are no actively running tasks
|
||||
# we try to schedule the asleep ones
|
||||
self.awake_sleeping()
|
||||
if self.selector.get_map():
|
||||
# The next step is checking for I/O
|
||||
self.check_io()
|
||||
if self.events:
|
||||
# Try to awake event-waiting tasks
|
||||
self.check_events()
|
||||
# While there are tasks to run
|
||||
while self.tasks:
|
||||
# Sets the currently running task
|
||||
self.current_task = self.tasks.popleft()
|
||||
if self.current_task.cancel_pending:
|
||||
self.do_cancel()
|
||||
if self.to_send and self.current_task.status != "init":
|
||||
data = self.to_send
|
||||
else:
|
||||
data = None
|
||||
# Run a single step with the calculation
|
||||
method, *args = self.current_task.run(data)
|
||||
self.current_task.status = "run"
|
||||
self.current_task.steps += 1
|
||||
# Data has been sent, reset it to None
|
||||
if self.to_send and self.current_task != "init":
|
||||
self.to_send = None
|
||||
# Sneaky method call, thanks to David Beazley for this ;)
|
||||
getattr(self, method)(*args)
|
||||
except AttributeError: # If this happens, that's quite bad!
|
||||
raise InternalError("Uh oh! Something very bad just happened, did"
|
||||
" you try to mix primitives from other async libraries?") from None
|
||||
except CancelledError:
|
||||
self.current_task.status = "cancelled"
|
||||
self.current_task.cancelled = True
|
||||
self.current_task.cancel_pending = False
|
||||
self.join() # TODO: Investigate if a call to join() is needed
|
||||
except StopIteration as ret:
|
||||
# Coroutine ends
|
||||
self.current_task.status = "end"
|
||||
self.current_task.result = ret.value
|
||||
self.current_task.finished = True
|
||||
self.join()
|
||||
except BaseException as err:
|
||||
self.current_task.exc = err
|
||||
self.current_task.status = "crashed"
|
||||
self.join()
|
||||
|
||||
def do_cancel(self):
|
||||
"""
|
||||
Performs task cancellation by throwing CancelledError inside the current
|
||||
task in order to stop it from executing. The loop continues to execute
|
||||
as tasks are independent
|
||||
"""
|
||||
|
||||
# TODO: Do we need anything else?
|
||||
self.current_task.throw(CancelledError)
|
||||
|
||||
def get_running(self):
|
||||
"""
|
||||
Returns the current task
|
||||
"""
|
||||
|
||||
self.tasks.append(self.current_task)
|
||||
self.to_send = self.current_task
|
||||
|
||||
def check_events(self):
|
||||
"""
|
||||
Checks for ready or expired events and triggers them
|
||||
"""
|
||||
|
||||
for event in self.events.copy():
|
||||
if event.set:
|
||||
event.event_caught = True
|
||||
event.waiters
|
||||
self.tasks.extend(event.waiters)
|
||||
self.events.remove(event)
|
||||
|
||||
def awake_sleeping(self):
|
||||
"""
|
||||
Checks for and reschedules sleeping tasks
|
||||
"""
|
||||
|
||||
wait(max(0.0, self.paused[0][0] - self.clock()))
|
||||
# Sleep until the closest deadline in order not to waste CPU cycles
|
||||
while self.paused[0][0] < self.clock():
|
||||
# Reschedules tasks when their deadline has elapsed
|
||||
self.tasks.append(self.paused.get())
|
||||
if not self.paused:
|
||||
break
|
||||
|
||||
def check_io(self):
|
||||
"""
|
||||
Checks and schedules task to perform I/O
|
||||
"""
|
||||
|
||||
if self.tasks or self.events: # If there are tasks or events, never wait
|
||||
timeout = 0.0
|
||||
elif self.paused: # If there are asleep tasks, wait until the closest
|
||||
# deadline
|
||||
timeout = max(0.0, self.paused[0][0] - self.clock())
|
||||
else:
|
||||
timeout = None # If we _only_ have I/O to do, then wait indefinitely
|
||||
for key in dict(self.selector.get_map()).values():
|
||||
# We make sure we don't reschedule finished tasks
|
||||
if key.data.finished:
|
||||
key.data.last_io = ()
|
||||
self.selector.unregister(key.fileobj)
|
||||
if self.selector.get_map(): # If there is indeed tasks waiting on I/O
|
||||
io_ready = self.selector.select(timeout)
|
||||
# Get sockets that are ready and schedule their tasks
|
||||
for key, _ in io_ready:
|
||||
self.tasks.append(key.data) # Resource ready? Schedule its task
|
||||
|
||||
def start(self, func: types.FunctionType, *args):
|
||||
"""
|
||||
Starts the event loop from a sync context
|
||||
"""
|
||||
|
||||
entry = Task(func(*args), func.__name__ or str(func))
|
||||
self.tasks.append(entry)
|
||||
self.run()
|
||||
self.has_ran = True
|
||||
if entry.exc:
|
||||
raise entry.exc from None
|
||||
|
||||
def reschedule_joinee(self):
|
||||
"""
|
||||
Reschedules the joinee(s) task of the
|
||||
currently running task, if any
|
||||
"""
|
||||
|
||||
self.tasks.extend(self.current_task.waiters)
|
||||
|
||||
def join(self):
|
||||
"""
|
||||
Handler for the 'join' event, does some magic to tell the scheduler
|
||||
to wait until the current coroutine ends
|
||||
"""
|
||||
|
||||
child = self.current_task
|
||||
child.joined = True
|
||||
if child.parent:
|
||||
child.waiters.append(child.parent)
|
||||
if child.finished:
|
||||
self.reschedule_joinee()
|
||||
elif child.exc:
|
||||
... # TODO: Handle exceptions
|
||||
|
||||
def sleep(self, seconds: int or float):
|
||||
"""
|
||||
Puts the caller to sleep for a given amount of seconds
|
||||
"""
|
||||
|
||||
if seconds:
|
||||
self.current_task.status = "sleep"
|
||||
self.paused.put(self.current_task, seconds)
|
||||
else:
|
||||
self.tasks.append(self.current_task)
|
||||
|
||||
# TODO: More generic I/O rather than just sockets
|
||||
def want_read(self, sock: socket.socket):
|
||||
"""
|
||||
Handler for the 'want_read' event, registers the socket inside the
|
||||
selector to perform I/0 multiplexing
|
||||
"""
|
||||
|
||||
self.current_task.status = "I/O"
|
||||
if self.current_task.last_io:
|
||||
if self.current_task.last_io == ("READ", sock):
|
||||
# Socket is already scheduled!
|
||||
return
|
||||
else:
|
||||
self.selector.unregister(sock)
|
||||
self.current_task.last_io = "READ", sock
|
||||
try:
|
||||
self.selector.register(sock, EVENT_READ, self.current_task)
|
||||
except KeyError:
|
||||
# The socket is already registered doing something else
|
||||
raise ResourceBusy("The given resource is busy!") from None
|
||||
|
||||
def want_write(self, sock: socket.socket):
|
||||
"""
|
||||
Handler for the 'want_write' event, registers the socket inside the
|
||||
selector to perform I/0 multiplexing
|
||||
"""
|
||||
|
||||
self.current_task.status = "I/O"
|
||||
if self.current_task.last_io:
|
||||
if self.current_task.last_io == ("WRITE", sock):
|
||||
# Socket is already scheduled!
|
||||
return
|
||||
else:
|
||||
# TODO: Inspect why modify() causes issues
|
||||
self.selector.unregister(sock)
|
||||
self.current_task.last_io = "WRITE", sock
|
||||
try:
|
||||
self.selector.register(sock, EVENT_WRITE, self.current_task)
|
||||
except KeyError:
|
||||
raise ResourceBusy("The given resource is busy!") from None
|
||||
|
||||
def event_set(self, event):
|
||||
"""
|
||||
Sets an event
|
||||
"""
|
||||
|
||||
self.events.add(event)
|
||||
event.waiters.append(self.current_task)
|
||||
event.set = True
|
||||
self.reschedule_joinee()
|
||||
|
||||
def event_wait(self, event):
|
||||
"""
|
||||
Pauses the current task on an event
|
||||
"""
|
||||
|
||||
event.waiters.append(self.current_task)
|
||||
|
||||
|
||||
def cancel(self):
|
||||
"""
|
||||
Handler for the 'cancel' event, schedules the task to be cancelled later
|
||||
or does so straight away if it is safe to do so
|
||||
"""
|
||||
|
||||
if self.current_task.status in ("I/O", "sleep"):
|
||||
# We cancel right away
|
||||
self.do_cancel()
|
||||
else:
|
||||
self.current_task.cancel_pending = True # Cancellation is deferred
|
||||
|
||||
def wrap_socket(self, sock):
|
||||
"""
|
||||
Wraps a standard socket into an AsyncSocket object
|
||||
"""
|
||||
|
||||
return AsyncSocket(sock, self)
|
||||
|
||||
async def read_sock(self, sock: socket.socket, buffer: int):
|
||||
"""
|
||||
Reads from a socket asynchronously, waiting until the resource is
|
||||
available and returning up to buffer bytes from the socket
|
||||
"""
|
||||
|
||||
try:
|
||||
return sock.recv(buffer)
|
||||
except WantRead:
|
||||
await want_read(sock)
|
||||
return sock.recv(buffer)
|
||||
|
||||
async def accept_sock(self, sock: socket.socket):
|
||||
"""
|
||||
Accepts a socket connection asynchronously, waiting until the resource
|
||||
is available and returning the result of the accept() call
|
||||
"""
|
||||
|
||||
try:
|
||||
return sock.accept()
|
||||
except WantRead:
|
||||
await want_read(sock)
|
||||
return sock.accept()
|
||||
|
||||
async def sock_sendall(self, sock: socket.socket, data: bytes):
|
||||
"""
|
||||
Sends all the passed data, as bytes, trough the socket asynchronously
|
||||
"""
|
||||
|
||||
while data:
|
||||
try:
|
||||
sent_no = sock.send(data)
|
||||
except WantWrite:
|
||||
await want_write(sock)
|
||||
sent_no = sock.send(data)
|
||||
data = data[sent_no:]
|
||||
|
||||
async def close_sock(self, sock: socket.socket):
|
||||
"""
|
||||
Closes the socket asynchronously
|
||||
"""
|
||||
|
||||
await want_write(sock)
|
||||
self.selector.unregister(sock)
|
||||
return sock.close()
|
||||
|
||||
async def connect_sock(self, sock: socket.socket, addr: tuple):
|
||||
"""
|
||||
Connects a socket asynchronously
|
||||
"""
|
||||
|
||||
try: # "Borrowed" from curio
|
||||
return sock.connect(addr)
|
||||
except WantWrite:
|
||||
await want_write(sock)
|
||||
err = sock.getsockopt(SOL_SOCKET, SO_ERROR)
|
||||
if err != 0:
|
||||
raise OSError(err, f"Connect call failed: {addr}")
|
||||
"""
|
||||
The main runtime environment for giambio
|
||||
|
||||
Copyright (C) 2020 nocturn9x
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
"""
|
||||
|
||||
# Import libraries and internal resources
|
||||
import types
|
||||
import socket
|
||||
from time import sleep as wait
|
||||
from timeit import default_timer
|
||||
from .objects import Task, TimeQueue
|
||||
from socket import SOL_SOCKET, SO_ERROR
|
||||
from .traps import want_read, want_write
|
||||
from collections import deque
|
||||
from .socket import AsyncSocket, WantWrite, WantRead
|
||||
from selectors import DefaultSelector, EVENT_READ, EVENT_WRITE
|
||||
from .exceptions import (InternalError,
|
||||
CancelledError,
|
||||
ResourceBusy,
|
||||
)
|
||||
|
||||
|
||||
|
||||
class AsyncScheduler:
|
||||
"""
|
||||
An asynchronous scheduler implementation. Tries to mimic the threaded
|
||||
model in its simplicity, without using actual threads, but rather alternating
|
||||
across coroutines execution to let more than one thing at a time to proceed
|
||||
with its calculations. An attempt to fix the threaded model has been made
|
||||
without making the API unnecessarily complicated.
|
||||
A few examples are tasks cancellation and exception propagation.
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
"""
|
||||
Object constructor
|
||||
"""
|
||||
|
||||
# Tasks that are ready to run
|
||||
self.tasks = deque()
|
||||
# Selector object to perform I/O multiplexing
|
||||
self.selector = DefaultSelector()
|
||||
# This will always point to the currently running coroutine (Task object)
|
||||
self.current_task = None
|
||||
# Monotonic clock to keep track of elapsed time reliably
|
||||
self.clock = default_timer
|
||||
# Tasks that are asleep
|
||||
self.paused = TimeQueue(self.clock)
|
||||
# All active Event objects
|
||||
self.events = set()
|
||||
# Data to send back to a trap
|
||||
self.to_send = None
|
||||
# Have we ever ran?
|
||||
self.has_ran = False
|
||||
|
||||
def done(self):
|
||||
"""
|
||||
Returns True if there is work to do
|
||||
"""
|
||||
|
||||
if self.selector.get_map() or any([self.paused,
|
||||
self.tasks,
|
||||
self.events
|
||||
]):
|
||||
return False
|
||||
return True
|
||||
|
||||
def shutdown(self):
|
||||
"""
|
||||
Shuts down the event loop
|
||||
"""
|
||||
|
||||
# TODO: See if other teardown is required (massive join()?)
|
||||
self.selector.close()
|
||||
|
||||
def run(self):
|
||||
"""
|
||||
Starts the loop and 'listens' for events until there is work to do,
|
||||
then exits. This behavior kinda reflects a kernel, as coroutines can
|
||||
request the loop's functionality only trough some fixed entry points,
|
||||
which in turn yield and give execution control to the loop itself.
|
||||
"""
|
||||
|
||||
while True:
|
||||
try:
|
||||
if self.done():
|
||||
self.shutdown()
|
||||
break
|
||||
elif not self.tasks:
|
||||
if self.paused:
|
||||
# If there are no actively running tasks
|
||||
# we try to schedule the asleep ones
|
||||
self.awake_sleeping()
|
||||
if self.selector.get_map():
|
||||
# The next step is checking for I/O
|
||||
self.check_io()
|
||||
if self.events:
|
||||
# Try to awake event-waiting tasks
|
||||
self.check_events()
|
||||
# While there are tasks to run
|
||||
while self.tasks:
|
||||
# Sets the currently running task
|
||||
self.current_task = self.tasks.popleft()
|
||||
if self.current_task.cancel_pending:
|
||||
self.do_cancel()
|
||||
if self.to_send and self.current_task.status != "init":
|
||||
data = self.to_send
|
||||
else:
|
||||
data = None
|
||||
# Run a single step with the calculation
|
||||
method, *args = self.current_task.run(data)
|
||||
self.current_task.status = "run"
|
||||
self.current_task.steps += 1
|
||||
# Data has been sent, reset it to None
|
||||
if self.to_send and self.current_task != "init":
|
||||
self.to_send = None
|
||||
# Sneaky method call, thanks to David Beazley for this ;)
|
||||
getattr(self, method)(*args)
|
||||
except AttributeError: # If this happens, that's quite bad!
|
||||
raise InternalError("Uh oh! Something very bad just happened, did"
|
||||
" you try to mix primitives from other async libraries?") from None
|
||||
except CancelledError:
|
||||
self.current_task.status = "cancelled"
|
||||
self.current_task.cancelled = True
|
||||
self.current_task.cancel_pending = False
|
||||
self.join() # TODO: Investigate if a call to join() is needed
|
||||
except StopIteration as ret:
|
||||
# Coroutine ends
|
||||
self.current_task.status = "end"
|
||||
self.current_task.result = ret.value
|
||||
self.current_task.finished = True
|
||||
self.join()
|
||||
except BaseException as err:
|
||||
self.current_task.exc = err
|
||||
self.current_task.status = "crashed"
|
||||
self.join()
|
||||
|
||||
def do_cancel(self):
|
||||
"""
|
||||
Performs task cancellation by throwing CancelledError inside the current
|
||||
task in order to stop it from executing. The loop continues to execute
|
||||
as tasks are independent
|
||||
"""
|
||||
|
||||
# TODO: Do we need anything else?
|
||||
self.current_task.throw(CancelledError)
|
||||
|
||||
def get_running(self):
|
||||
"""
|
||||
Returns the current task
|
||||
"""
|
||||
|
||||
self.tasks.append(self.current_task)
|
||||
self.to_send = self.current_task
|
||||
|
||||
def check_events(self):
|
||||
"""
|
||||
Checks for ready or expired events and triggers them
|
||||
"""
|
||||
|
||||
for event in self.events.copy():
|
||||
if event.set:
|
||||
event.event_caught = True
|
||||
event.waiters
|
||||
self.tasks.extend(event.waiters)
|
||||
self.events.remove(event)
|
||||
|
||||
def awake_sleeping(self):
|
||||
"""
|
||||
Checks for and reschedules sleeping tasks
|
||||
"""
|
||||
|
||||
wait(max(0.0, self.paused[0][0] - self.clock()))
|
||||
# Sleep until the closest deadline in order not to waste CPU cycles
|
||||
while self.paused[0][0] < self.clock():
|
||||
# Reschedules tasks when their deadline has elapsed
|
||||
self.tasks.append(self.paused.get())
|
||||
if not self.paused:
|
||||
break
|
||||
|
||||
def check_io(self):
|
||||
"""
|
||||
Checks and schedules task to perform I/O
|
||||
"""
|
||||
|
||||
if self.tasks or self.events: # If there are tasks or events, never wait
|
||||
timeout = 0.0
|
||||
elif self.paused: # If there are asleep tasks, wait until the closest
|
||||
# deadline
|
||||
timeout = max(0.0, self.paused[0][0] - self.clock())
|
||||
else:
|
||||
timeout = None # If we _only_ have I/O to do, then wait indefinitely
|
||||
for key in dict(self.selector.get_map()).values():
|
||||
# We make sure we don't reschedule finished tasks
|
||||
if key.data.finished:
|
||||
key.data.last_io = ()
|
||||
self.selector.unregister(key.fileobj)
|
||||
if self.selector.get_map(): # If there is indeed tasks waiting on I/O
|
||||
io_ready = self.selector.select(timeout)
|
||||
# Get sockets that are ready and schedule their tasks
|
||||
for key, _ in io_ready:
|
||||
self.tasks.append(key.data) # Resource ready? Schedule its task
|
||||
|
||||
def start(self, func: types.FunctionType, *args):
|
||||
"""
|
||||
Starts the event loop from a sync context
|
||||
"""
|
||||
|
||||
entry = Task(func(*args), func.__name__ or str(func))
|
||||
self.tasks.append(entry)
|
||||
self.run()
|
||||
self.has_ran = True
|
||||
if entry.exc:
|
||||
raise entry.exc from None
|
||||
|
||||
def reschedule_joinee(self):
|
||||
"""
|
||||
Reschedules the joinee(s) of the
|
||||
currently running task, if any
|
||||
"""
|
||||
|
||||
self.tasks.extend(self.current_task.waiters)
|
||||
|
||||
def join(self):
|
||||
"""
|
||||
Handler for the 'join' event, does some magic to tell the scheduler
|
||||
to wait until the current coroutine ends
|
||||
"""
|
||||
|
||||
child = self.current_task
|
||||
child.joined = True
|
||||
if child.parent:
|
||||
child.waiters.append(child.parent)
|
||||
if child.finished:
|
||||
self.reschedule_joinee()
|
||||
elif child.exc:
|
||||
... # TODO: Handle exceptions
|
||||
|
||||
def sleep(self, seconds: int or float):
|
||||
"""
|
||||
Puts the caller to sleep for a given amount of seconds
|
||||
"""
|
||||
|
||||
if seconds:
|
||||
self.current_task.status = "sleep"
|
||||
self.paused.put(self.current_task, seconds)
|
||||
else:
|
||||
self.tasks.append(self.current_task)
|
||||
|
||||
# TODO: More generic I/O rather than just sockets
|
||||
def want_read(self, sock: socket.socket):
|
||||
"""
|
||||
Handler for the 'want_read' event, registers the socket inside the
|
||||
selector to perform I/0 multiplexing
|
||||
"""
|
||||
|
||||
self.current_task.status = "I/O"
|
||||
if self.current_task.last_io:
|
||||
if self.current_task.last_io == ("READ", sock):
|
||||
# Socket is already scheduled!
|
||||
return
|
||||
else:
|
||||
self.selector.unregister(sock)
|
||||
self.current_task.last_io = "READ", sock
|
||||
try:
|
||||
self.selector.register(sock, EVENT_READ, self.current_task)
|
||||
except KeyError:
|
||||
# The socket is already registered doing something else
|
||||
raise ResourceBusy("The given resource is busy!") from None
|
||||
|
||||
def want_write(self, sock: socket.socket):
|
||||
"""
|
||||
Handler for the 'want_write' event, registers the socket inside the
|
||||
selector to perform I/0 multiplexing
|
||||
"""
|
||||
|
||||
self.current_task.status = "I/O"
|
||||
if self.current_task.last_io:
|
||||
if self.current_task.last_io == ("WRITE", sock):
|
||||
# Socket is already scheduled!
|
||||
return
|
||||
else:
|
||||
# TODO: Inspect why modify() causes issues
|
||||
self.selector.unregister(sock)
|
||||
self.current_task.last_io = "WRITE", sock
|
||||
try:
|
||||
self.selector.register(sock, EVENT_WRITE, self.current_task)
|
||||
except KeyError:
|
||||
raise ResourceBusy("The given resource is busy!") from None
|
||||
|
||||
def event_set(self, event):
|
||||
"""
|
||||
Sets an event
|
||||
"""
|
||||
|
||||
self.events.add(event)
|
||||
event.waiters.append(self.current_task)
|
||||
event.set = True
|
||||
self.reschedule_joinee()
|
||||
|
||||
def event_wait(self, event):
|
||||
"""
|
||||
Pauses the current task on an event
|
||||
"""
|
||||
|
||||
event.waiters.append(self.current_task)
|
||||
|
||||
|
||||
def cancel(self):
|
||||
"""
|
||||
Handler for the 'cancel' event, schedules the task to be cancelled later
|
||||
or does so straight away if it is safe to do so
|
||||
"""
|
||||
|
||||
if self.current_task.status in ("I/O", "sleep"):
|
||||
# We cancel right away
|
||||
self.do_cancel()
|
||||
else:
|
||||
self.current_task.cancel_pending = True # Cancellation is deferred
|
||||
|
||||
def wrap_socket(self, sock):
|
||||
"""
|
||||
Wraps a standard socket into an AsyncSocket object
|
||||
"""
|
||||
|
||||
return AsyncSocket(sock, self)
|
||||
|
||||
async def read_sock(self, sock: socket.socket, buffer: int):
|
||||
"""
|
||||
Reads from a socket asynchronously, waiting until the resource is
|
||||
available and returning up to buffer bytes from the socket
|
||||
"""
|
||||
|
||||
try:
|
||||
return sock.recv(buffer)
|
||||
except WantRead:
|
||||
await want_read(sock)
|
||||
return sock.recv(buffer)
|
||||
|
||||
async def accept_sock(self, sock: socket.socket):
|
||||
"""
|
||||
Accepts a socket connection asynchronously, waiting until the resource
|
||||
is available and returning the result of the accept() call
|
||||
"""
|
||||
|
||||
try:
|
||||
return sock.accept()
|
||||
except WantRead:
|
||||
await want_read(sock)
|
||||
return sock.accept()
|
||||
|
||||
async def sock_sendall(self, sock: socket.socket, data: bytes):
|
||||
"""
|
||||
Sends all the passed data, as bytes, trough the socket asynchronously
|
||||
"""
|
||||
|
||||
while data:
|
||||
try:
|
||||
sent_no = sock.send(data)
|
||||
except WantWrite:
|
||||
await want_write(sock)
|
||||
sent_no = sock.send(data)
|
||||
data = data[sent_no:]
|
||||
|
||||
async def close_sock(self, sock: socket.socket):
|
||||
"""
|
||||
Closes the socket asynchronously
|
||||
"""
|
||||
|
||||
await want_write(sock)
|
||||
self.selector.unregister(sock)
|
||||
return sock.close()
|
||||
|
||||
async def connect_sock(self, sock: socket.socket, addr: tuple):
|
||||
"""
|
||||
Connects a socket asynchronously
|
||||
"""
|
||||
|
||||
try: # "Borrowed" from curio
|
||||
return sock.connect(addr)
|
||||
except WantWrite:
|
||||
await want_write(sock)
|
||||
err = sock.getsockopt(SOL_SOCKET, SO_ERROR)
|
||||
if err != 0:
|
||||
raise OSError(err, f"Connect call failed: {addr}")
|
||||
|
|
|
@ -1,60 +1,60 @@
|
|||
"""
|
||||
Exceptions for giambio
|
||||
|
||||
Copyright (C) 2020 nocturn9x
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
"""
|
||||
|
||||
|
||||
class GiambioError(Exception):
|
||||
"""
|
||||
Base class for giambio exceptions
|
||||
"""
|
||||
|
||||
...
|
||||
|
||||
|
||||
class InternalError(GiambioError):
|
||||
"""
|
||||
Internal exception
|
||||
"""
|
||||
|
||||
...
|
||||
|
||||
|
||||
class CancelledError(BaseException):
|
||||
"""
|
||||
Exception raised by the giambio.objects.Task.cancel() method
|
||||
to terminate a child task. This should NOT be catched, or
|
||||
at least it should be re-raised and never ignored
|
||||
"""
|
||||
|
||||
...
|
||||
|
||||
|
||||
class ResourceBusy(GiambioError):
|
||||
"""
|
||||
Exception that is raised when a resource is accessed by more than
|
||||
one task at a time
|
||||
"""
|
||||
|
||||
...
|
||||
|
||||
|
||||
class ResourceClosed(GiambioError):
|
||||
"""
|
||||
Raised when I/O is attempted on a closed resource
|
||||
"""
|
||||
|
||||
...
|
||||
"""
|
||||
Exceptions for giambio
|
||||
|
||||
Copyright (C) 2020 nocturn9x
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
"""
|
||||
|
||||
|
||||
class GiambioError(Exception):
|
||||
"""
|
||||
Base class for giambio exceptions
|
||||
"""
|
||||
|
||||
...
|
||||
|
||||
|
||||
class InternalError(GiambioError):
|
||||
"""
|
||||
Internal exception
|
||||
"""
|
||||
|
||||
...
|
||||
|
||||
|
||||
class CancelledError(BaseException):
|
||||
"""
|
||||
Exception raised by the giambio.objects.Task.cancel() method
|
||||
to terminate a child task. This should NOT be catched, or
|
||||
at least it should be re-raised and never ignored
|
||||
"""
|
||||
|
||||
...
|
||||
|
||||
|
||||
class ResourceBusy(GiambioError):
|
||||
"""
|
||||
Exception that is raised when a resource is accessed by more than
|
||||
one task at a time
|
||||
"""
|
||||
|
||||
...
|
||||
|
||||
|
||||
class ResourceClosed(GiambioError):
|
||||
"""
|
||||
Raised when I/O is attempted on a closed resource
|
||||
"""
|
||||
|
||||
...
|
||||
|
|
|
@ -1,158 +1,158 @@
|
|||
"""
|
||||
Various object wrappers and abstraction layers
|
||||
|
||||
Copyright (C) 2020 nocturn9x
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
"""
|
||||
|
||||
import types
|
||||
from .traps import join, cancel, event_set, event_wait
|
||||
from heapq import heappop, heappush
|
||||
from .exceptions import GiambioError
|
||||
from dataclasses import dataclass, field
|
||||
|
||||
|
||||
@dataclass
|
||||
class Task:
|
||||
|
||||
"""
|
||||
A simple wrapper around a coroutine object
|
||||
"""
|
||||
|
||||
coroutine: types.CoroutineType
|
||||
name: str
|
||||
cancelled: bool = False # True if the task gets cancelled
|
||||
exc: BaseException = None
|
||||
result: object = None
|
||||
finished: bool = False
|
||||
status: str = "init"
|
||||
steps: int = 0
|
||||
last_io: tuple = ()
|
||||
parent: object = None
|
||||
joined: bool= False
|
||||
cancel_pending: bool = False
|
||||
waiters: list = field(default_factory=list)
|
||||
|
||||
def run(self, what=None):
|
||||
"""
|
||||
Simple abstraction layer over coroutines' ``send`` method
|
||||
"""
|
||||
|
||||
return self.coroutine.send(what)
|
||||
|
||||
def throw(self, err: Exception):
|
||||
"""
|
||||
Simple abstraction layer over coroutines ``throw`` method
|
||||
"""
|
||||
|
||||
return self.coroutine.throw(err)
|
||||
|
||||
async def join(self):
|
||||
"""
|
||||
Joins the task
|
||||
"""
|
||||
|
||||
res = await join(self)
|
||||
if self.exc:
|
||||
raise self.exc
|
||||
return res
|
||||
|
||||
async def cancel(self):
|
||||
"""
|
||||
Cancels the task
|
||||
"""
|
||||
|
||||
await cancel(self)
|
||||
|
||||
def __del__(self):
|
||||
self.coroutine.close()
|
||||
|
||||
|
||||
class Event:
|
||||
"""
|
||||
A class designed similarly to threading.Event
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
"""
|
||||
Object constructor
|
||||
"""
|
||||
|
||||
self.set = False
|
||||
self.waiters = []
|
||||
self.event_caught = False
|
||||
|
||||
async def trigger(self):
|
||||
"""
|
||||
Sets the event, waking up all tasks that called
|
||||
pause() on us
|
||||
"""
|
||||
|
||||
if self.set:
|
||||
raise GiambioError("The event has already been set")
|
||||
await event_set(self)
|
||||
|
||||
async def wait(self):
|
||||
"""
|
||||
Waits until the event is set
|
||||
"""
|
||||
|
||||
await event_wait(self)
|
||||
|
||||
|
||||
class TimeQueue:
|
||||
"""
|
||||
An abstraction layer over a heap queue based on time. This is where
|
||||
sleeping tasks will be put when they are not running
|
||||
"""
|
||||
|
||||
def __init__(self, clock):
|
||||
"""
|
||||
Object constructor
|
||||
"""
|
||||
|
||||
self.clock = clock
|
||||
self.sequence = 0
|
||||
self.container = []
|
||||
|
||||
def __contains__(self, item):
|
||||
return item in self.container
|
||||
|
||||
def __iter__(self):
|
||||
return iter(self.container)
|
||||
|
||||
def __getitem__(self, item):
|
||||
return self.container.__getitem__(item)
|
||||
|
||||
def __bool__(self):
|
||||
return bool(self.container)
|
||||
|
||||
def __repr__(self):
|
||||
return f"TimeQueue({self.container}, clock={self.clock})"
|
||||
|
||||
def put(self, item, amount):
|
||||
"""
|
||||
Pushes an item onto the queue with its unique
|
||||
time amount and ID
|
||||
"""
|
||||
|
||||
heappush(self.container, (self.clock() + amount, self.sequence, item))
|
||||
self.sequence += 1
|
||||
|
||||
def get(self):
|
||||
"""
|
||||
Gets the first task that is meant to run
|
||||
"""
|
||||
|
||||
return heappop(self.container)[2]
|
||||
"""
|
||||
Various object wrappers and abstraction layers
|
||||
|
||||
Copyright (C) 2020 nocturn9x
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
"""
|
||||
|
||||
import types
|
||||
from .traps import join, cancel, event_set, event_wait
|
||||
from heapq import heappop, heappush
|
||||
from .exceptions import GiambioError
|
||||
from dataclasses import dataclass, field
|
||||
|
||||
|
||||
@dataclass
|
||||
class Task:
|
||||
|
||||
"""
|
||||
A simple wrapper around a coroutine object
|
||||
"""
|
||||
|
||||
coroutine: types.CoroutineType
|
||||
name: str
|
||||
cancelled: bool = False # True if the task gets cancelled
|
||||
exc: BaseException = None
|
||||
result: object = None
|
||||
finished: bool = False
|
||||
status: str = "init"
|
||||
steps: int = 0
|
||||
last_io: tuple = ()
|
||||
parent: object = None
|
||||
joined: bool= False
|
||||
cancel_pending: bool = False
|
||||
waiters: list = field(default_factory=list)
|
||||
|
||||
def run(self, what=None):
|
||||
"""
|
||||
Simple abstraction layer over coroutines' ``send`` method
|
||||
"""
|
||||
|
||||
return self.coroutine.send(what)
|
||||
|
||||
def throw(self, err: Exception):
|
||||
"""
|
||||
Simple abstraction layer over coroutines ``throw`` method
|
||||
"""
|
||||
|
||||
return self.coroutine.throw(err)
|
||||
|
||||
async def join(self):
|
||||
"""
|
||||
Joins the task
|
||||
"""
|
||||
|
||||
res = await join(self)
|
||||
if self.exc:
|
||||
raise self.exc
|
||||
return res
|
||||
|
||||
async def cancel(self):
|
||||
"""
|
||||
Cancels the task
|
||||
"""
|
||||
|
||||
await cancel(self)
|
||||
|
||||
def __del__(self):
|
||||
self.coroutine.close()
|
||||
|
||||
|
||||
class Event:
|
||||
"""
|
||||
A class designed similarly to threading.Event
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
"""
|
||||
Object constructor
|
||||
"""
|
||||
|
||||
self.set = False
|
||||
self.waiters = []
|
||||
self.event_caught = False
|
||||
|
||||
async def trigger(self):
|
||||
"""
|
||||
Sets the event, waking up all tasks that called
|
||||
pause() on us
|
||||
"""
|
||||
|
||||
if self.set:
|
||||
raise GiambioError("The event has already been set")
|
||||
await event_set(self)
|
||||
|
||||
async def wait(self):
|
||||
"""
|
||||
Waits until the event is set
|
||||
"""
|
||||
|
||||
await event_wait(self)
|
||||
|
||||
|
||||
class TimeQueue:
|
||||
"""
|
||||
An abstraction layer over a heap queue based on time. This is where
|
||||
sleeping tasks will be put when they are not running
|
||||
"""
|
||||
|
||||
def __init__(self, clock):
|
||||
"""
|
||||
Object constructor
|
||||
"""
|
||||
|
||||
self.clock = clock
|
||||
self.sequence = 0
|
||||
self.container = []
|
||||
|
||||
def __contains__(self, item):
|
||||
return item in self.container
|
||||
|
||||
def __iter__(self):
|
||||
return iter(self.container)
|
||||
|
||||
def __getitem__(self, item):
|
||||
return self.container.__getitem__(item)
|
||||
|
||||
def __bool__(self):
|
||||
return bool(self.container)
|
||||
|
||||
def __repr__(self):
|
||||
return f"TimeQueue({self.container}, clock={self.clock})"
|
||||
|
||||
def put(self, item, amount):
|
||||
"""
|
||||
Pushes an item onto the queue with its unique
|
||||
time amount and ID
|
||||
"""
|
||||
|
||||
heappush(self.container, (self.clock() + amount, self.sequence, item))
|
||||
self.sequence += 1
|
||||
|
||||
def get(self):
|
||||
"""
|
||||
Gets the first task that is meant to run
|
||||
"""
|
||||
|
||||
return heappop(self.container)[2]
|
||||
|
|
198
giambio/run.py
198
giambio/run.py
|
@ -1,99 +1,99 @@
|
|||
"""
|
||||
Helper methods and public API
|
||||
|
||||
Copyright (C) 2020 nocturn9x
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
"""
|
||||
|
||||
import socket
|
||||
import threading
|
||||
from .core import AsyncScheduler
|
||||
from .exceptions import GiambioError
|
||||
from .context import TaskManager
|
||||
from .socket import AsyncSocket
|
||||
from types import FunctionType, CoroutineType, GeneratorType
|
||||
|
||||
|
||||
thread_local = threading.local()
|
||||
|
||||
|
||||
def get_event_loop():
|
||||
"""
|
||||
Returns the event loop associated to the current
|
||||
thread
|
||||
"""
|
||||
|
||||
try:
|
||||
return thread_local.loop
|
||||
except AttributeError:
|
||||
raise GiambioError("no event loop set") from None
|
||||
|
||||
|
||||
def new_event_loop():
|
||||
"""
|
||||
Associates a new event loop to the current thread
|
||||
and deactivates the old one. This should not be
|
||||
called explicitly unless you know what you're doing
|
||||
"""
|
||||
|
||||
try:
|
||||
loop = thread_local.loop
|
||||
except AttributeError:
|
||||
thread_local.loop = AsyncScheduler()
|
||||
else:
|
||||
if not loop.done():
|
||||
raise GiambioError("cannot set event loop while running")
|
||||
else:
|
||||
thread_local.loop = AsyncScheduler()
|
||||
|
||||
|
||||
def run(func: FunctionType, *args):
|
||||
"""
|
||||
Starts the event loop from a synchronous entry point
|
||||
"""
|
||||
|
||||
if isinstance(func, (CoroutineType, GeneratorType)):
|
||||
raise RuntimeError("Looks like you tried to call giambio.run(your_func(arg1, arg2, ...)), that is wrong!"
|
||||
"\nWhat you wanna do, instead, is this: giambio.run(your_func, arg1, arg2, ...)")
|
||||
new_event_loop()
|
||||
thread_local.loop.start(func, *args)
|
||||
|
||||
|
||||
def clock():
|
||||
"""
|
||||
Returns the current clock time of the thread-local event
|
||||
loop
|
||||
"""
|
||||
|
||||
return thread_local.loop.clock()
|
||||
|
||||
|
||||
def wrap_socket(sock: socket.socket) -> AsyncSocket:
|
||||
"""
|
||||
Wraps a synchronous socket into a giambio.socket.AsyncSocket
|
||||
"""
|
||||
|
||||
return thread_local.loop.wrap_socket(sock)
|
||||
|
||||
|
||||
def create_pool():
|
||||
"""
|
||||
Creates an async pool
|
||||
"""
|
||||
|
||||
try:
|
||||
return TaskManager(thread_local.loop)
|
||||
except AttributeError:
|
||||
raise RuntimeError("It appears that giambio is not running, did you call giambio.async_pool()"
|
||||
" outside of an async context?") from None
|
||||
"""
|
||||
Helper methods and public API
|
||||
|
||||
Copyright (C) 2020 nocturn9x
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
"""
|
||||
|
||||
import socket
|
||||
import threading
|
||||
from .core import AsyncScheduler
|
||||
from .exceptions import GiambioError
|
||||
from .context import TaskManager
|
||||
from .socket import AsyncSocket
|
||||
from types import FunctionType, CoroutineType, GeneratorType
|
||||
|
||||
|
||||
thread_local = threading.local()
|
||||
|
||||
|
||||
def get_event_loop():
|
||||
"""
|
||||
Returns the event loop associated to the current
|
||||
thread
|
||||
"""
|
||||
|
||||
try:
|
||||
return thread_local.loop
|
||||
except AttributeError:
|
||||
raise GiambioError("no event loop set") from None
|
||||
|
||||
|
||||
def new_event_loop():
|
||||
"""
|
||||
Associates a new event loop to the current thread
|
||||
and deactivates the old one. This should not be
|
||||
called explicitly unless you know what you're doing
|
||||
"""
|
||||
|
||||
try:
|
||||
loop = thread_local.loop
|
||||
except AttributeError:
|
||||
thread_local.loop = AsyncScheduler()
|
||||
else:
|
||||
if not loop.done():
|
||||
raise GiambioError("cannot set event loop while running")
|
||||
else:
|
||||
thread_local.loop = AsyncScheduler()
|
||||
|
||||
|
||||
def run(func: FunctionType, *args):
|
||||
"""
|
||||
Starts the event loop from a synchronous entry point
|
||||
"""
|
||||
|
||||
if isinstance(func, (CoroutineType, GeneratorType)):
|
||||
raise RuntimeError("Looks like you tried to call giambio.run(your_func(arg1, arg2, ...)), that is wrong!"
|
||||
"\nWhat you wanna do, instead, is this: giambio.run(your_func, arg1, arg2, ...)")
|
||||
new_event_loop()
|
||||
thread_local.loop.start(func, *args)
|
||||
|
||||
|
||||
def clock():
|
||||
"""
|
||||
Returns the current clock time of the thread-local event
|
||||
loop
|
||||
"""
|
||||
|
||||
return thread_local.loop.clock()
|
||||
|
||||
|
||||
def wrap_socket(sock: socket.socket) -> AsyncSocket:
|
||||
"""
|
||||
Wraps a synchronous socket into a giambio.socket.AsyncSocket
|
||||
"""
|
||||
|
||||
return thread_local.loop.wrap_socket(sock)
|
||||
|
||||
|
||||
def create_pool():
|
||||
"""
|
||||
Creates an async pool
|
||||
"""
|
||||
|
||||
try:
|
||||
return TaskManager(thread_local.loop)
|
||||
except AttributeError:
|
||||
raise RuntimeError("It appears that giambio is not running, did you call giambio.async_pool()"
|
||||
" outside of an async context?") from None
|
||||
|
|
|
@ -1,100 +1,100 @@
|
|||
"""
|
||||
Basic abstraction layer for giambio asynchronous sockets
|
||||
|
||||
Copyright (C) 2020 nocturn9x
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
"""
|
||||
|
||||
|
||||
import socket
|
||||
from .exceptions import ResourceClosed
|
||||
from .traps import sleep
|
||||
|
||||
|
||||
# Stolen from curio
|
||||
try:
|
||||
from ssl import SSLWantReadError, SSLWantWriteError
|
||||
WantRead = (BlockingIOError, InterruptedError, SSLWantReadError)
|
||||
WantWrite = (BlockingIOError, InterruptedError, SSLWantWriteError)
|
||||
except ImportError:
|
||||
WantRead = (BlockingIOError, InterruptedError)
|
||||
WantWrite = (BlockingIOError, InterruptedError)
|
||||
|
||||
|
||||
class AsyncSocket(object):
|
||||
"""
|
||||
Abstraction layer for asynchronous TCP sockets
|
||||
"""
|
||||
|
||||
def __init__(self, sock: socket.socket, loop):
|
||||
self.sock = sock
|
||||
self.loop = loop
|
||||
self._closed = False
|
||||
self.sock.setblocking(False)
|
||||
|
||||
async def receive(self, max_size: int):
|
||||
"""
|
||||
Receives up to max_size bytes from a socket asynchronously
|
||||
"""
|
||||
|
||||
if self._closed:
|
||||
raise ResourceClosed("I/O operation on closed socket")
|
||||
return await self.loop.read_sock(self.sock, max_size)
|
||||
|
||||
async def accept(self):
|
||||
"""
|
||||
Accepts the socket, completing the 3-step TCP handshake asynchronously
|
||||
"""
|
||||
|
||||
if self._closed:
|
||||
raise ResourceClosed("I/O operation on closed socket")
|
||||
to_wrap = await self.loop.accept_sock(self.sock)
|
||||
return self.loop.wrap_socket(to_wrap[0]), to_wrap[1]
|
||||
|
||||
async def send_all(self, data: bytes):
|
||||
"""
|
||||
Sends all data inside the buffer asynchronously until it is empty
|
||||
"""
|
||||
|
||||
if self._closed:
|
||||
raise ResourceClosed("I/O operation on closed socket")
|
||||
return await self.loop.sock_sendall(self.sock, data)
|
||||
|
||||
async def close(self):
|
||||
"""
|
||||
Closes the socket asynchronously
|
||||
"""
|
||||
|
||||
if self._closed:
|
||||
raise ResourceClosed("I/O operation on closed socket")
|
||||
await self.loop.close_sock(self.sock)
|
||||
self._closed = True
|
||||
|
||||
async def connect(self, addr: tuple):
|
||||
"""
|
||||
Connects the socket to an endpoint
|
||||
"""
|
||||
|
||||
if self._closed:
|
||||
raise ResourceClosed("I/O operation on closed socket")
|
||||
await self.loop.connect_sock(self.sock, addr)
|
||||
|
||||
async def __aenter__(self):
|
||||
return self
|
||||
|
||||
async def __aexit__(self, *_):
|
||||
await self.close()
|
||||
|
||||
def __repr__(self):
|
||||
return f"giambio.socket.AsyncSocket({self.sock}, {self.loop})"
|
||||
"""
|
||||
Basic abstraction layer for giambio asynchronous sockets
|
||||
|
||||
Copyright (C) 2020 nocturn9x
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
"""
|
||||
|
||||
|
||||
import socket
|
||||
from .exceptions import ResourceClosed
|
||||
from .traps import sleep
|
||||
|
||||
|
||||
# Stolen from curio
|
||||
try:
|
||||
from ssl import SSLWantReadError, SSLWantWriteError
|
||||
WantRead = (BlockingIOError, InterruptedError, SSLWantReadError)
|
||||
WantWrite = (BlockingIOError, InterruptedError, SSLWantWriteError)
|
||||
except ImportError:
|
||||
WantRead = (BlockingIOError, InterruptedError)
|
||||
WantWrite = (BlockingIOError, InterruptedError)
|
||||
|
||||
|
||||
class AsyncSocket(object):
|
||||
"""
|
||||
Abstraction layer for asynchronous TCP sockets
|
||||
"""
|
||||
|
||||
def __init__(self, sock: socket.socket, loop):
|
||||
self.sock = sock
|
||||
self.loop = loop
|
||||
self._closed = False
|
||||
self.sock.setblocking(False)
|
||||
|
||||
async def receive(self, max_size: int):
|
||||
"""
|
||||
Receives up to max_size bytes from a socket asynchronously
|
||||
"""
|
||||
|
||||
if self._closed:
|
||||
raise ResourceClosed("I/O operation on closed socket")
|
||||
return await self.loop.read_sock(self.sock, max_size)
|
||||
|
||||
async def accept(self):
|
||||
"""
|
||||
Accepts the socket, completing the 3-step TCP handshake asynchronously
|
||||
"""
|
||||
|
||||
if self._closed:
|
||||
raise ResourceClosed("I/O operation on closed socket")
|
||||
to_wrap = await self.loop.accept_sock(self.sock)
|
||||
return self.loop.wrap_socket(to_wrap[0]), to_wrap[1]
|
||||
|
||||
async def send_all(self, data: bytes):
|
||||
"""
|
||||
Sends all data inside the buffer asynchronously until it is empty
|
||||
"""
|
||||
|
||||
if self._closed:
|
||||
raise ResourceClosed("I/O operation on closed socket")
|
||||
return await self.loop.sock_sendall(self.sock, data)
|
||||
|
||||
async def close(self):
|
||||
"""
|
||||
Closes the socket asynchronously
|
||||
"""
|
||||
|
||||
if self._closed:
|
||||
raise ResourceClosed("I/O operation on closed socket")
|
||||
await self.loop.close_sock(self.sock)
|
||||
self._closed = True
|
||||
|
||||
async def connect(self, addr: tuple):
|
||||
"""
|
||||
Connects the socket to an endpoint
|
||||
"""
|
||||
|
||||
if self._closed:
|
||||
raise ResourceClosed("I/O operation on closed socket")
|
||||
await self.loop.connect_sock(self.sock, addr)
|
||||
|
||||
async def __aenter__(self):
|
||||
return self
|
||||
|
||||
async def __aexit__(self, *_):
|
||||
await self.close()
|
||||
|
||||
def __repr__(self):
|
||||
return f"giambio.socket.AsyncSocket({self.sock}, {self.loop})"
|
||||
|
|
268
giambio/traps.py
268
giambio/traps.py
|
@ -1,134 +1,134 @@
|
|||
"""
|
||||
Implementation for all giambio traps, which are hooks
|
||||
into the event loop and allow it to switch tasks.
|
||||
These coroutines are the one and only way to interact
|
||||
with the event loop from the user's perspective, and
|
||||
the entire library is based on them
|
||||
|
||||
Copyright (C) 2020 nocturn9x
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
"""
|
||||
|
||||
|
||||
import types
|
||||
|
||||
|
||||
@types.coroutine
|
||||
def create_trap(method, *args):
|
||||
"""
|
||||
Creates and yields a trap. This
|
||||
is the lowest-level method to
|
||||
interact with the event loop
|
||||
"""
|
||||
|
||||
data = yield method, *args
|
||||
return data
|
||||
|
||||
|
||||
async def sleep(seconds: int):
|
||||
"""
|
||||
Pause the execution of an async function for a given amount of seconds,
|
||||
without blocking the entire event loop, which keeps watching for other events
|
||||
|
||||
This function is also useful as a sort of checkpoint, because it returns
|
||||
control to the scheduler, which can then switch to another task. If your code
|
||||
doesn't have enough calls to async functions (or 'checkpoints') this might
|
||||
prevent the scheduler from switching tasks properly. If you feel like this
|
||||
happens in your code, try adding a call to giambio.sleep(0) somewhere.
|
||||
This will act as a checkpoint without actually pausing the execution
|
||||
of your function, but it will allow the scheduler to switch tasks
|
||||
|
||||
:param seconds: The amount of seconds to sleep for
|
||||
:type seconds: int
|
||||
"""
|
||||
|
||||
assert seconds >= 0, "The time delay can't be negative"
|
||||
await create_trap("sleep", seconds)
|
||||
|
||||
|
||||
async def current_task():
|
||||
"""
|
||||
Gets the currently running task
|
||||
"""
|
||||
|
||||
return await create_trap("get_running")
|
||||
|
||||
|
||||
async def join(task):
|
||||
"""
|
||||
Awaits a given task for completion
|
||||
|
||||
:param task: The task to join
|
||||
:type task: class: Task
|
||||
"""
|
||||
|
||||
return await create_trap("join")
|
||||
|
||||
|
||||
async def cancel(task):
|
||||
"""
|
||||
Cancels the given task
|
||||
|
||||
The concept of cancellation is tricky, because there is no real way to 'stop'
|
||||
a task if not by raising an exception inside it and ignoring whatever it
|
||||
returns (and also hoping that the task won't cause collateral damage). It
|
||||
is highly recommended that when you write async code you take into account
|
||||
that it might be cancelled at any time. You might think to just ignore the
|
||||
cancellation exception and be done with it, but doing so *will* break your
|
||||
code, so if you really wanna do that be sure to re-raise it when done!
|
||||
"""
|
||||
|
||||
await create_trap("cancel")
|
||||
assert task.cancelled, f"Coroutine ignored CancelledError"
|
||||
|
||||
|
||||
async def want_read(stream):
|
||||
"""
|
||||
Notifies the event loop that a task wants to read from the given
|
||||
resource
|
||||
|
||||
:param stream: The resource that needs to be read
|
||||
"""
|
||||
|
||||
await create_trap("want_read", stream)
|
||||
|
||||
|
||||
async def want_write(stream):
|
||||
"""
|
||||
Notifies the event loop that a task wants to write on the given
|
||||
resource
|
||||
|
||||
:param stream: The resource that needs to be written
|
||||
"""
|
||||
|
||||
await create_trap("want_write", stream)
|
||||
|
||||
|
||||
async def event_set(event):
|
||||
"""
|
||||
Communicates to the loop that the given event object
|
||||
must be set. This is important as the loop constantly
|
||||
checks for active events to deliver them
|
||||
"""
|
||||
|
||||
await create_trap("event_set", event)
|
||||
|
||||
|
||||
async def event_wait(event):
|
||||
"""
|
||||
Notifies the event loop that the current task has to wait
|
||||
for the given event to trigger
|
||||
"""
|
||||
|
||||
await create_trap("event_wait", event)
|
||||
"""
|
||||
Implementation for all giambio traps, which are hooks
|
||||
into the event loop and allow it to switch tasks.
|
||||
These coroutines are the one and only way to interact
|
||||
with the event loop from the user's perspective, and
|
||||
the entire library is based on them
|
||||
|
||||
Copyright (C) 2020 nocturn9x
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
"""
|
||||
|
||||
|
||||
import types
|
||||
|
||||
|
||||
@types.coroutine
|
||||
def create_trap(method, *args):
|
||||
"""
|
||||
Creates and yields a trap. This
|
||||
is the lowest-level method to
|
||||
interact with the event loop
|
||||
"""
|
||||
|
||||
data = yield method, *args
|
||||
return data
|
||||
|
||||
|
||||
async def sleep(seconds: int):
|
||||
"""
|
||||
Pause the execution of an async function for a given amount of seconds,
|
||||
without blocking the entire event loop, which keeps watching for other events
|
||||
|
||||
This function is also useful as a sort of checkpoint, because it returns
|
||||
control to the scheduler, which can then switch to another task. If your code
|
||||
doesn't have enough calls to async functions (or 'checkpoints') this might
|
||||
prevent the scheduler from switching tasks properly. If you feel like this
|
||||
happens in your code, try adding a call to giambio.sleep(0) somewhere.
|
||||
This will act as a checkpoint without actually pausing the execution
|
||||
of your function, but it will allow the scheduler to switch tasks
|
||||
|
||||
:param seconds: The amount of seconds to sleep for
|
||||
:type seconds: int
|
||||
"""
|
||||
|
||||
assert seconds >= 0, "The time delay can't be negative"
|
||||
await create_trap("sleep", seconds)
|
||||
|
||||
|
||||
async def current_task():
|
||||
"""
|
||||
Gets the currently running task
|
||||
"""
|
||||
|
||||
return await create_trap("get_running")
|
||||
|
||||
|
||||
async def join(task):
|
||||
"""
|
||||
Awaits a given task for completion
|
||||
|
||||
:param task: The task to join
|
||||
:type task: class: Task
|
||||
"""
|
||||
|
||||
return await create_trap("join")
|
||||
|
||||
|
||||
async def cancel(task):
|
||||
"""
|
||||
Cancels the given task
|
||||
|
||||
The concept of cancellation is tricky, because there is no real way to 'stop'
|
||||
a task if not by raising an exception inside it and ignoring whatever it
|
||||
returns (and also hoping that the task won't cause collateral damage). It
|
||||
is highly recommended that when you write async code you take into account
|
||||
that it might be cancelled at any time. You might think to just ignore the
|
||||
cancellation exception and be done with it, but doing so *will* break your
|
||||
code, so if you really wanna do that be sure to re-raise it when done!
|
||||
"""
|
||||
|
||||
await create_trap("cancel")
|
||||
assert task.cancelled, f"Coroutine ignored CancelledError"
|
||||
|
||||
|
||||
async def want_read(stream):
|
||||
"""
|
||||
Notifies the event loop that a task wants to read from the given
|
||||
resource
|
||||
|
||||
:param stream: The resource that needs to be read
|
||||
"""
|
||||
|
||||
await create_trap("want_read", stream)
|
||||
|
||||
|
||||
async def want_write(stream):
|
||||
"""
|
||||
Notifies the event loop that a task wants to write on the given
|
||||
resource
|
||||
|
||||
:param stream: The resource that needs to be written
|
||||
"""
|
||||
|
||||
await create_trap("want_write", stream)
|
||||
|
||||
|
||||
async def event_set(event):
|
||||
"""
|
||||
Communicates to the loop that the given event object
|
||||
must be set. This is important as the loop constantly
|
||||
checks for active events to deliver them
|
||||
"""
|
||||
|
||||
await create_trap("event_set", event)
|
||||
|
||||
|
||||
async def event_wait(event):
|
||||
"""
|
||||
Notifies the event loop that the current task has to wait
|
||||
for the given event to trigger
|
||||
"""
|
||||
|
||||
await create_trap("event_wait", event)
|
||||
|
|
|
@ -0,0 +1,8 @@
|
|||
home = /usr
|
||||
implementation = CPython
|
||||
version_info = 3.7.3.final.0
|
||||
virtualenv = 20.1.0
|
||||
include-system-site-packages = false
|
||||
base-prefix = /usr
|
||||
base-exec-prefix = /usr
|
||||
base-executable = /usr/bin/python3
|
44
setup.py
44
setup.py
|
@ -1,22 +1,22 @@
|
|||
import setuptools
|
||||
|
||||
with open("README.md", "r") as readme:
|
||||
long_description = readme.read()
|
||||
|
||||
setuptools.setup(
|
||||
name="GiambIO",
|
||||
version="1.0",
|
||||
author="Nocturn9x aka IsGiambyy",
|
||||
author_email="hackhab@gmail.com",
|
||||
description="Asynchronous Python made easy (and friendly)",
|
||||
long_description=long_description,
|
||||
long_description_content_type="text/markdown",
|
||||
url="https://github.com/nocturn9x/giambio",
|
||||
packages=setuptools.find_packages(),
|
||||
classifiers=[
|
||||
"Programming Language :: Python :: 3",
|
||||
"Operating System :: OS Independent",
|
||||
"License :: OSI Approved :: Apache License 2.0",
|
||||
],
|
||||
python_requires=">=3.6",
|
||||
)
|
||||
import setuptools
|
||||
|
||||
with open("README.md", "r") as readme:
|
||||
long_description = readme.read()
|
||||
|
||||
setuptools.setup(
|
||||
name="GiambIO",
|
||||
version="1.0",
|
||||
author="Nocturn9x aka IsGiambyy",
|
||||
author_email="hackhab@gmail.com",
|
||||
description="Asynchronous Python made easy (and friendly)",
|
||||
long_description=long_description,
|
||||
long_description_content_type="text/markdown",
|
||||
url="https://github.com/nocturn9x/giambio",
|
||||
packages=setuptools.find_packages(),
|
||||
classifiers=[
|
||||
"Programming Language :: Python :: 3",
|
||||
"Operating System :: OS Independent",
|
||||
"License :: OSI Approved :: Apache License 2.0",
|
||||
],
|
||||
python_requires=">=3.6",
|
||||
)
|
||||
|
|
|
@ -1,42 +1,42 @@
|
|||
import giambio
|
||||
|
||||
|
||||
# A test for context managers
|
||||
|
||||
|
||||
async def countdown(n: int):
|
||||
print(f"Counting down from {n}!")
|
||||
while n > 0:
|
||||
print(f"Down {n}")
|
||||
n -= 1
|
||||
await giambio.sleep(1)
|
||||
# raise Exception("oh no man") # Uncomment to test propagation
|
||||
print("Countdown over")
|
||||
return 0
|
||||
|
||||
|
||||
async def countup(stop: int, step: int = 1):
|
||||
print(f"Counting up to {stop}!")
|
||||
x = 0
|
||||
while x < stop:
|
||||
print(f"Up {x}")
|
||||
x += 1
|
||||
await giambio.sleep(step)
|
||||
print("Countup over")
|
||||
return 1
|
||||
|
||||
|
||||
async def main():
|
||||
start = giambio.clock()
|
||||
try:
|
||||
async with giambio.create_pool() as pool:
|
||||
pool.spawn(countdown, 10)
|
||||
pool.spawn(countup, 5, 2)
|
||||
print("Children spawned, awaiting completion")
|
||||
except Exception as e:
|
||||
print(f"Got -> {type(e).__name__}: {e}")
|
||||
print(f"Task execution complete in {giambio.clock() - start:.2f} seconds")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
giambio.run(main)
|
||||
import giambio
|
||||
|
||||
|
||||
# A test for context managers
|
||||
|
||||
|
||||
async def countdown(n: int):
|
||||
print(f"Counting down from {n}!")
|
||||
while n > 0:
|
||||
print(f"Down {n}")
|
||||
n -= 1
|
||||
await giambio.sleep(1)
|
||||
# raise Exception("oh no man") # Uncomment to test propagation
|
||||
print("Countdown over")
|
||||
return 0
|
||||
|
||||
|
||||
async def countup(stop: int, step: int = 1):
|
||||
print(f"Counting up to {stop}!")
|
||||
x = 0
|
||||
while x < stop:
|
||||
print(f"Up {x}")
|
||||
x += 1
|
||||
await giambio.sleep(step)
|
||||
print("Countup over")
|
||||
return 1
|
||||
|
||||
|
||||
async def main():
|
||||
start = giambio.clock()
|
||||
try:
|
||||
async with giambio.create_pool() as pool:
|
||||
pool.spawn(countdown, 10)
|
||||
pool.spawn(countup, 5, 2)
|
||||
print("Children spawned, awaiting completion")
|
||||
except Exception as e:
|
||||
print(f"Got -> {type(e).__name__}: {e}")
|
||||
print(f"Task execution complete in {giambio.clock() - start:.2f} seconds")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
giambio.run(main)
|
||||
|
|
|
@ -1,35 +1,35 @@
|
|||
import giambio
|
||||
|
||||
|
||||
# A test for events
|
||||
|
||||
|
||||
async def child(ev: giambio.Event, pause: int):
|
||||
print("[child] Child is alive! Going to wait until notified")
|
||||
start_total = giambio.clock()
|
||||
await ev.wait()
|
||||
end_pause = giambio.clock() - start_total
|
||||
print(f"[child] Parent set the event, exiting in {pause} seconds")
|
||||
start_sleep = giambio.clock()
|
||||
await giambio.sleep(pause)
|
||||
end_sleep = giambio.clock() - start_sleep
|
||||
end_total = giambio.clock() - start_total
|
||||
print(f"[child] Done! Slept for {end_total} seconds total ({end_pause} paused, {end_sleep} sleeping), nice nap!")
|
||||
|
||||
|
||||
async def parent(pause: int = 1):
|
||||
async with giambio.create_pool() as pool:
|
||||
event = giambio.Event()
|
||||
print("[parent] Spawning child task")
|
||||
pool.spawn(child, event, pause + 2)
|
||||
start = giambio.clock()
|
||||
print(f"[parent] Sleeping {pause} second(s) before setting the event")
|
||||
await giambio.sleep(pause)
|
||||
await event.trigger()
|
||||
print("[parent] Event set, awaiting child")
|
||||
end = giambio.clock() - start
|
||||
print(f"[parent] Child exited in {end} seconds")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
giambio.run(parent, 3)
|
||||
import giambio
|
||||
|
||||
|
||||
# A test for events
|
||||
|
||||
|
||||
async def child(ev: giambio.Event, pause: int):
|
||||
print("[child] Child is alive! Going to wait until notified")
|
||||
start_total = giambio.clock()
|
||||
await ev.wait()
|
||||
end_pause = giambio.clock() - start_total
|
||||
print(f"[child] Parent set the event, exiting in {pause} seconds")
|
||||
start_sleep = giambio.clock()
|
||||
await giambio.sleep(pause)
|
||||
end_sleep = giambio.clock() - start_sleep
|
||||
end_total = giambio.clock() - start_total
|
||||
print(f"[child] Done! Slept for {end_total} seconds total ({end_pause} paused, {end_sleep} sleeping), nice nap!")
|
||||
|
||||
|
||||
async def parent(pause: int = 1):
|
||||
async with giambio.create_pool() as pool:
|
||||
event = giambio.Event()
|
||||
print("[parent] Spawning child task")
|
||||
pool.spawn(child, event, pause + 2)
|
||||
start = giambio.clock()
|
||||
print(f"[parent] Sleeping {pause} second(s) before setting the event")
|
||||
await giambio.sleep(pause)
|
||||
await event.trigger()
|
||||
print("[parent] Event set, awaiting child")
|
||||
end = giambio.clock() - start
|
||||
print(f"[parent] Child exited in {end} seconds")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
giambio.run(parent, 3)
|
||||
|
|
|
@ -1,25 +0,0 @@
|
|||
import giambio
|
||||
|
||||
|
||||
async def child(sleep: int, ident: int):
|
||||
start = giambio.clock() # This returns the current time from giambio's perspective
|
||||
print(f"[child {ident}] Gonna sleep for {sleep} seconds!")
|
||||
await giambio.sleep(sleep)
|
||||
end = giambio.clock() - start
|
||||
print(f"[child {ident}] I woke up! Slept for {end} seconds")
|
||||
|
||||
|
||||
async def main():
|
||||
print("[parent] Spawning children")
|
||||
task = giambio.spawn(child, 1, 1) # We spawn a child task
|
||||
task2 = giambio.spawn(child, 2, 2) # and why not? another one!
|
||||
start = giambio.clock()
|
||||
print("[parent] Children spawned, awaiting completion")
|
||||
await task.join()
|
||||
await task2.join()
|
||||
end = giambio.clock() - start
|
||||
print(f"[parent] Execution terminated in {end} seconds")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
giambio.run(main) # Start the async context
|
109
tests/server.py
109
tests/server.py
|
@ -1,54 +1,55 @@
|
|||
import giambio
|
||||
from giambio.socket import AsyncSocket
|
||||
import socket
|
||||
import logging
|
||||
import sys
|
||||
import traceback
|
||||
|
||||
|
||||
# A test to check for asynchronous I/O
|
||||
|
||||
|
||||
async def serve(address: tuple):
|
||||
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
||||
sock.bind(address)
|
||||
sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
|
||||
sock.listen(5)
|
||||
asock = giambio.wrap_socket(sock) # We make the socket an async socket
|
||||
logging.info(f"Serving asynchronously at {address[0]}:{address[1]}")
|
||||
async with giambio.create_pool() as pool:
|
||||
conn, addr = await asock.accept()
|
||||
logging.info(f"{addr[0]}:{addr[1]} connected")
|
||||
pool.spawn(handler, conn, addr)
|
||||
|
||||
|
||||
async def handler(sock: AsyncSocket, addr: tuple):
|
||||
addr = f"{addr[0]}:{addr[1]}"
|
||||
async with sock:
|
||||
await sock.send_all(b"Welcome to the server pal, feel free to send me something!\n")
|
||||
while True:
|
||||
await sock.send_all(b"-> ")
|
||||
data = await sock.receive(1024)
|
||||
if not data:
|
||||
break
|
||||
elif data == b"raise\n":
|
||||
await sock.send_all(b"I'm dead dude\n")
|
||||
raise TypeError("Oh, no, I'm gonna die!")
|
||||
to_send_back = data
|
||||
data = data.decode("utf-8").encode("unicode_escape")
|
||||
logging.info(f"Got: '{data.decode('utf-8')}' from {addr}")
|
||||
await sock.send_all(b"Got: " + to_send_back)
|
||||
logging.info(f"Echoed back '{data.decode('utf-8')}' to {addr}")
|
||||
logging.info(f"Connection from {addr} closed")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
port = int(sys.argv[1]) if len(sys.argv) > 1 else 1500
|
||||
logging.basicConfig(level=20, format="[%(levelname)s] %(asctime)s %(message)s", datefmt="%d/%m/%Y %p")
|
||||
try:
|
||||
giambio.run(serve, ("localhost", port))
|
||||
except (Exception, KeyboardInterrupt) as error: # Exceptions propagate!
|
||||
if isinstance(error, KeyboardInterrupt):
|
||||
logging.info("Ctrl+C detected, exiting")
|
||||
else:
|
||||
logging.error(f"Exiting due to a {type(error).__name__}: {error}")
|
||||
import giambio
|
||||
from giambio.socket import AsyncSocket
|
||||
import socket
|
||||
import logging
|
||||
import sys
|
||||
import traceback
|
||||
|
||||
|
||||
# A test to check for asynchronous I/O
|
||||
|
||||
|
||||
async def serve(address: tuple):
|
||||
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
||||
sock.bind(address)
|
||||
sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
|
||||
sock.listen(5)
|
||||
asock = giambio.wrap_socket(sock) # We make the socket an async socket
|
||||
logging.info(f"Serving asynchronously at {address[0]}:{address[1]}")
|
||||
async with giambio.create_pool() as pool:
|
||||
while True:
|
||||
conn, addr = await asock.accept()
|
||||
logging.info(f"{addr[0]}:{addr[1]} connected")
|
||||
pool.spawn(handler, conn, addr)
|
||||
print("oof done")
|
||||
|
||||
async def handler(sock: AsyncSocket, addr: tuple):
|
||||
addr = f"{addr[0]}:{addr[1]}"
|
||||
async with sock:
|
||||
await sock.send_all(b"Welcome to the server pal, feel free to send me something!\n")
|
||||
while True:
|
||||
await sock.send_all(b"-> ")
|
||||
data = await sock.receive(1024)
|
||||
if not data:
|
||||
break
|
||||
elif data == b"raise\n":
|
||||
await sock.send_all(b"I'm dead dude\n")
|
||||
raise TypeError("Oh, no, I'm gonna die!")
|
||||
to_send_back = data
|
||||
data = data.decode("utf-8").encode("unicode_escape")
|
||||
logging.info(f"Got: '{data.decode('utf-8')}' from {addr}")
|
||||
await sock.send_all(b"Got: " + to_send_back)
|
||||
logging.info(f"Echoed back '{data.decode('utf-8')}' to {addr}")
|
||||
logging.info(f"Connection from {addr} closed")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
port = int(sys.argv[1]) if len(sys.argv) > 1 else 1500
|
||||
logging.basicConfig(level=20, format="[%(levelname)s] %(asctime)s %(message)s", datefmt="%d/%m/%Y %p")
|
||||
try:
|
||||
giambio.run(serve, ("localhost", port))
|
||||
except (Exception, KeyboardInterrupt) as error: # Exceptions propagate!
|
||||
if isinstance(error, KeyboardInterrupt):
|
||||
logging.info("Ctrl+C detected, exiting")
|
||||
else:
|
||||
logging.error(f"Exiting due to a {type(error).__name__}: {error}")
|
||||
|
|
Loading…
Reference in New Issue