├── .gitignore ├── .travis.yml ├── AUTHORS ├── HACKING ├── HISTORY.rst ├── LICENSE ├── MANIFEST.in ├── NOTICE ├── README.rst ├── docs ├── Makefile ├── make.bat └── source │ ├── conf.py │ ├── getting-started.rst │ ├── index.rst │ └── reference.rst ├── examples └── cli.py ├── pychievements ├── __init__.py ├── achievements.py ├── backends.py ├── cli.py ├── icons.py ├── signals.py └── trackers.py ├── setup.py └── tests ├── __init__.py └── test_requirements.txt /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | [._]*.s[a-w][a-z] 3 | [._]s[a-w][a-z] 4 | *.un~ 5 | Session.vim 6 | .netrwhist 7 | *~ 8 | 9 | # Byte-compiled / optimized / DLL files 10 | __pycache__/ 11 | *.py[cod] 12 | 13 | # C extensions 14 | *.so 15 | 16 | # Distribution / packaging 17 | .Python 18 | env/ 19 | build/ 20 | develop-eggs/ 21 | dist/ 22 | eggs/ 23 | lib/ 24 | lib64/ 25 | parts/ 26 | sdist/ 27 | var/ 28 | *.egg-info/ 29 | .installed.cfg 30 | *.egg 31 | 32 | # PyInstaller 33 | # Usually these files are written by a python script from a template 34 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 35 | *.manifest 36 | *.spec 37 | 38 | # Installer logs 39 | pip-log.txt 40 | pip-delete-this-directory.txt 41 | 42 | # Unit test / coverage reports 43 | htmlcov/ 44 | .tox/ 45 | .coverage 46 | .cache 47 | nosetests.xml 48 | coverage.xml 49 | cover 50 | 51 | # Translations 52 | *.mo 53 | *.pot 54 | 55 | # Django stuff: 56 | *.log 57 | 58 | # Sphinx documentation 59 | docs/_build/ 60 | 61 | # PyBuilder 62 | target/ 63 | 64 | .ropeproject 65 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: python 2 | python: 3 | - "3.6" 4 | - "3.5" 5 | - "3.4" 6 | - "3.3" 7 | - "2.7" 8 | - "2.6" 9 | install: 10 | - "pip install -r tests/test_requirements.txt" 11 | script: 12 | - nosetests --with-coverage --cover-package=pychievements 13 | after_success: 14 | - coveralls 15 | -------------------------------------------------------------------------------- /AUTHORS: -------------------------------------------------------------------------------- 1 | Pychievements is written and maintained by Brian Knobbs and 2 | various contributors: 3 | 4 | 5 | Development Lead 6 | ```````````````` 7 | 8 | - Brian Knobbs 9 | 10 | 11 | Maintainers 12 | ``````````` 13 | 14 | 15 | Patches and Suggestions 16 | ``````````````````````` 17 | -------------------------------------------------------------------------------- /HACKING: -------------------------------------------------------------------------------- 1 | Where possible, please follow PEP8 with regard to coding style, except using a line width of 100 2 | instead of 80. 3 | 4 | Triple-quotes should always be """, single quotes are ' unless using " 5 | would result in less escaping within the string. 6 | 7 | All modules, functions, and methods should be well documented reStructuredText for 8 | Sphinx AutoDoc. 9 | 10 | All functionality should be available in pure Python. Optional C (via Cython) 11 | implementations may be written for performance reasons, but should never 12 | replace the Python implementation. 13 | 14 | Lastly, don't take yourself too seriously :)" 15 | -------------------------------------------------------------------------------- /HISTORY.rst: -------------------------------------------------------------------------------- 1 | History 2 | ------- 3 | 4 | 0.1.0 (2014-08-31) 5 | ++++++++++++++++++ 6 | 7 | * Initial Release! 8 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2014, Brian Knobbs 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | SOFTWARE. 20 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include HISTORY.rst README.rst LICENSE AUTHORS NOTICE 2 | -------------------------------------------------------------------------------- /NOTICE: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | Pychievements: The Python Achievements Framework! 2 | ================================================ 3 | 4 | |build| |docs| |coverage| |downloads| |license| 5 | 6 | .. |coverage| image:: https://img.shields.io/coveralls/PacketPerception/pychievements/master.svg 7 | :target: https://coveralls.io/r/PacketPerception/pychievements?branch=master 8 | 9 | .. |build| image:: https://img.shields.io/travis/PacketPerception/pychievements/master.svg 10 | :target: https://travis-ci.org/PacketPerception/pychievements 11 | 12 | .. |docs| image:: https://readthedocs.org/projects/pychievements/badge/?version=latest 13 | :target: http://pychievements.readthedocs.org/en/latest/ 14 | 15 | .. |downloads| image:: https://img.shields.io/pypi/dm/pychievements.svg 16 | :target: https://pypi.python.org/pypi/pychievements/ 17 | 18 | .. |license| image:: https://img.shields.io/pypi/l/pychievements.svg 19 | :target: https://pypi.python.org/pypi/pychievements/ 20 | 21 | **Pychievements** is a framework for creating and tracking achievements within a Python application. 22 | It includes functions specifically for creating command line applications, though it is flexible 23 | enough to be used for any application such as web applications. 24 | 25 | See the examples_ to get a good feel for what Pychievements offers. Documentation can be found RTD: 26 | http://pychievements.readthedocs.org/en/latest/ 27 | 28 | .. _examples: https://github.com/PacketPerception/pychievements/tree/master/examples 29 | 30 | 31 | Features: 32 | --------- 33 | - Create Achievements with any number of "goals" (based on levels) that can be reached 34 | - Flexible design makes it easy to customize the way levels are tracked 35 | - Easy to add new achievements later 36 | - Pluggable backend for storing tracked information in different formats to different locations 37 | - Achievements can be filtered by category or keywords 38 | - Easily specify "Icons" for individual goals within an achievement for dual states (achieved and 39 | unachieved) 40 | 41 | 42 | Example 43 | ------- 44 | 45 | A simple achievement. :: 46 | 47 | class MyAchievement(Achievement): 48 | name = "My Achievement" 49 | category = "achievements" 50 | keywords = ("my", "achievement") 51 | goals = ( 52 | {"level": 10, "name": "Level 1", "icon": icons.star, "description": "Level One"}, 53 | {"level": 20, "name": "Level 2", "icon": icons.star, "description": "Level Two"}, 54 | {"level": 30, "name": "Level 3", "icon": icons.star, "description": "Level Three"}, 55 | ) 56 | 57 | 58 | Increment a level for a user. :: 59 | 60 | tracker.increment(user_id, MyAchievment) 61 | 62 | 63 | Re-evaluating a level for a user based on arguments (requires the evaluate function to be defined 64 | or the Achievement). :: 65 | 66 | tracker.evaluate(user_id, MyAchievement, some, extra, args) 67 | 68 | 69 | Retrieve achievements. :: 70 | 71 | tracker.achievements() # retrieves all registered achievements in the tracker 72 | tracker.achieved(uid, achievement) # all achieved goals by uid for achievement 73 | tracker.unachieved(uid, achievement) # all unachieved goals by uid for achievement 74 | tracker.current(uid, achievement) # goal currently being worked torwards by uid 75 | 76 | 77 | Installation 78 | ------------ 79 | 80 | To install pychievements, simply: :: 81 | 82 | $ pip install pychievements 83 | 84 | 85 | License 86 | ------- 87 | 88 | Pychievements is licensed under the MIT License, see the LICENSE_. 89 | 90 | .. _LICENSE: http://github.com/PacketPerception/pychievements/blob/master/LICENSE 91 | 92 | 93 | Contribute 94 | ---------- 95 | 96 | If you'd like to contribute, simply fork `the repository`_, commit your changes 97 | to the **master** branch (or branch off of it), and send a pull request. Make 98 | sure you add yourself to AUTHORS_. 99 | 100 | 101 | .. _`the repository`: http://github.com/PacketPerception/pychievements 102 | .. _AUTHORS: http://github.com/PacketPerception/pychievements/blob/master/AUTHORS 103 | 104 | 105 | Roadmap 106 | ------- 107 | - More backends 108 | - More icons 109 | -------------------------------------------------------------------------------- /docs/Makefile: -------------------------------------------------------------------------------- 1 | # Makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line. 5 | SPHINXOPTS = 6 | SPHINXBUILD = sphinx-build 7 | PAPER = 8 | BUILDDIR = build 9 | 10 | # User-friendly check for sphinx-build 11 | ifeq ($(shell which $(SPHINXBUILD) >/dev/null 2>&1; echo $$?), 1) 12 | $(error The '$(SPHINXBUILD)' command was not found. Make sure you have Sphinx installed, then set the SPHINXBUILD environment variable to point to the full path of the '$(SPHINXBUILD)' executable. Alternatively you can add the directory with the executable to your PATH. If you don't have Sphinx installed, grab it from http://sphinx-doc.org/) 13 | endif 14 | 15 | # Internal variables. 16 | PAPEROPT_a4 = -D latex_paper_size=a4 17 | PAPEROPT_letter = -D latex_paper_size=letter 18 | ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) source 19 | # the i18n builder cannot share the environment and doctrees with the others 20 | I18NSPHINXOPTS = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) source 21 | 22 | .PHONY: help clean html dirhtml singlehtml pickle json htmlhelp qthelp devhelp epub latex latexpdf text man changes linkcheck doctest gettext 23 | 24 | help: 25 | @echo "Please use \`make ' where is one of" 26 | @echo " html to make standalone HTML files" 27 | @echo " dirhtml to make HTML files named index.html in directories" 28 | @echo " singlehtml to make a single large HTML file" 29 | @echo " pickle to make pickle files" 30 | @echo " json to make JSON files" 31 | @echo " htmlhelp to make HTML files and a HTML help project" 32 | @echo " qthelp to make HTML files and a qthelp project" 33 | @echo " devhelp to make HTML files and a Devhelp project" 34 | @echo " epub to make an epub" 35 | @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter" 36 | @echo " latexpdf to make LaTeX files and run them through pdflatex" 37 | @echo " latexpdfja to make LaTeX files and run them through platex/dvipdfmx" 38 | @echo " text to make text files" 39 | @echo " man to make manual pages" 40 | @echo " texinfo to make Texinfo files" 41 | @echo " info to make Texinfo files and run them through makeinfo" 42 | @echo " gettext to make PO message catalogs" 43 | @echo " changes to make an overview of all changed/added/deprecated items" 44 | @echo " xml to make Docutils-native XML files" 45 | @echo " pseudoxml to make pseudoxml-XML files for display purposes" 46 | @echo " linkcheck to check all external links for integrity" 47 | @echo " doctest to run all doctests embedded in the documentation (if enabled)" 48 | 49 | clean: 50 | rm -rf $(BUILDDIR)/* 51 | 52 | html: 53 | $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html 54 | @echo 55 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/html." 56 | 57 | dirhtml: 58 | $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml 59 | @echo 60 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml." 61 | 62 | singlehtml: 63 | $(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml 64 | @echo 65 | @echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml." 66 | 67 | pickle: 68 | $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle 69 | @echo 70 | @echo "Build finished; now you can process the pickle files." 71 | 72 | json: 73 | $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json 74 | @echo 75 | @echo "Build finished; now you can process the JSON files." 76 | 77 | htmlhelp: 78 | $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp 79 | @echo 80 | @echo "Build finished; now you can run HTML Help Workshop with the" \ 81 | ".hhp project file in $(BUILDDIR)/htmlhelp." 82 | 83 | qthelp: 84 | $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp 85 | @echo 86 | @echo "Build finished; now you can run "qcollectiongenerator" with the" \ 87 | ".qhcp project file in $(BUILDDIR)/qthelp, like this:" 88 | @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/Pychievements.qhcp" 89 | @echo "To view the help file:" 90 | @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/Pychievements.qhc" 91 | 92 | devhelp: 93 | $(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp 94 | @echo 95 | @echo "Build finished." 96 | @echo "To view the help file:" 97 | @echo "# mkdir -p $$HOME/.local/share/devhelp/Pychievements" 98 | @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/Pychievements" 99 | @echo "# devhelp" 100 | 101 | epub: 102 | $(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub 103 | @echo 104 | @echo "Build finished. The epub file is in $(BUILDDIR)/epub." 105 | 106 | latex: 107 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 108 | @echo 109 | @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex." 110 | @echo "Run \`make' in that directory to run these through (pdf)latex" \ 111 | "(use \`make latexpdf' here to do that automatically)." 112 | 113 | latexpdf: 114 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 115 | @echo "Running LaTeX files through pdflatex..." 116 | $(MAKE) -C $(BUILDDIR)/latex all-pdf 117 | @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." 118 | 119 | latexpdfja: 120 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 121 | @echo "Running LaTeX files through platex and dvipdfmx..." 122 | $(MAKE) -C $(BUILDDIR)/latex all-pdf-ja 123 | @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." 124 | 125 | text: 126 | $(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text 127 | @echo 128 | @echo "Build finished. The text files are in $(BUILDDIR)/text." 129 | 130 | man: 131 | $(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man 132 | @echo 133 | @echo "Build finished. The manual pages are in $(BUILDDIR)/man." 134 | 135 | texinfo: 136 | $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo 137 | @echo 138 | @echo "Build finished. The Texinfo files are in $(BUILDDIR)/texinfo." 139 | @echo "Run \`make' in that directory to run these through makeinfo" \ 140 | "(use \`make info' here to do that automatically)." 141 | 142 | info: 143 | $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo 144 | @echo "Running Texinfo files through makeinfo..." 145 | make -C $(BUILDDIR)/texinfo info 146 | @echo "makeinfo finished; the Info files are in $(BUILDDIR)/texinfo." 147 | 148 | gettext: 149 | $(SPHINXBUILD) -b gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale 150 | @echo 151 | @echo "Build finished. The message catalogs are in $(BUILDDIR)/locale." 152 | 153 | changes: 154 | $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes 155 | @echo 156 | @echo "The overview file is in $(BUILDDIR)/changes." 157 | 158 | linkcheck: 159 | $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck 160 | @echo 161 | @echo "Link check complete; look for any errors in the above output " \ 162 | "or in $(BUILDDIR)/linkcheck/output.txt." 163 | 164 | doctest: 165 | $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest 166 | @echo "Testing of doctests in the sources finished, look at the " \ 167 | "results in $(BUILDDIR)/doctest/output.txt." 168 | 169 | xml: 170 | $(SPHINXBUILD) -b xml $(ALLSPHINXOPTS) $(BUILDDIR)/xml 171 | @echo 172 | @echo "Build finished. The XML files are in $(BUILDDIR)/xml." 173 | 174 | pseudoxml: 175 | $(SPHINXBUILD) -b pseudoxml $(ALLSPHINXOPTS) $(BUILDDIR)/pseudoxml 176 | @echo 177 | @echo "Build finished. The pseudo-XML files are in $(BUILDDIR)/pseudoxml." 178 | -------------------------------------------------------------------------------- /docs/make.bat: -------------------------------------------------------------------------------- 1 | @ECHO OFF 2 | 3 | REM Command file for Sphinx documentation 4 | 5 | if "%SPHINXBUILD%" == "" ( 6 | set SPHINXBUILD=sphinx-build 7 | ) 8 | set BUILDDIR=build 9 | set ALLSPHINXOPTS=-d %BUILDDIR%/doctrees %SPHINXOPTS% source 10 | set I18NSPHINXOPTS=%SPHINXOPTS% source 11 | if NOT "%PAPER%" == "" ( 12 | set ALLSPHINXOPTS=-D latex_paper_size=%PAPER% %ALLSPHINXOPTS% 13 | set I18NSPHINXOPTS=-D latex_paper_size=%PAPER% %I18NSPHINXOPTS% 14 | ) 15 | 16 | if "%1" == "" goto help 17 | 18 | if "%1" == "help" ( 19 | :help 20 | echo.Please use `make ^` where ^ is one of 21 | echo. html to make standalone HTML files 22 | echo. dirhtml to make HTML files named index.html in directories 23 | echo. singlehtml to make a single large HTML file 24 | echo. pickle to make pickle files 25 | echo. json to make JSON files 26 | echo. htmlhelp to make HTML files and a HTML help project 27 | echo. qthelp to make HTML files and a qthelp project 28 | echo. devhelp to make HTML files and a Devhelp project 29 | echo. epub to make an epub 30 | echo. latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter 31 | echo. text to make text files 32 | echo. man to make manual pages 33 | echo. texinfo to make Texinfo files 34 | echo. gettext to make PO message catalogs 35 | echo. changes to make an overview over all changed/added/deprecated items 36 | echo. xml to make Docutils-native XML files 37 | echo. pseudoxml to make pseudoxml-XML files for display purposes 38 | echo. linkcheck to check all external links for integrity 39 | echo. doctest to run all doctests embedded in the documentation if enabled 40 | goto end 41 | ) 42 | 43 | if "%1" == "clean" ( 44 | for /d %%i in (%BUILDDIR%\*) do rmdir /q /s %%i 45 | del /q /s %BUILDDIR%\* 46 | goto end 47 | ) 48 | 49 | 50 | %SPHINXBUILD% 2> nul 51 | if errorlevel 9009 ( 52 | echo. 53 | echo.The 'sphinx-build' command was not found. Make sure you have Sphinx 54 | echo.installed, then set the SPHINXBUILD environment variable to point 55 | echo.to the full path of the 'sphinx-build' executable. Alternatively you 56 | echo.may add the Sphinx directory to PATH. 57 | echo. 58 | echo.If you don't have Sphinx installed, grab it from 59 | echo.http://sphinx-doc.org/ 60 | exit /b 1 61 | ) 62 | 63 | if "%1" == "html" ( 64 | %SPHINXBUILD% -b html %ALLSPHINXOPTS% %BUILDDIR%/html 65 | if errorlevel 1 exit /b 1 66 | echo. 67 | echo.Build finished. The HTML pages are in %BUILDDIR%/html. 68 | goto end 69 | ) 70 | 71 | if "%1" == "dirhtml" ( 72 | %SPHINXBUILD% -b dirhtml %ALLSPHINXOPTS% %BUILDDIR%/dirhtml 73 | if errorlevel 1 exit /b 1 74 | echo. 75 | echo.Build finished. The HTML pages are in %BUILDDIR%/dirhtml. 76 | goto end 77 | ) 78 | 79 | if "%1" == "singlehtml" ( 80 | %SPHINXBUILD% -b singlehtml %ALLSPHINXOPTS% %BUILDDIR%/singlehtml 81 | if errorlevel 1 exit /b 1 82 | echo. 83 | echo.Build finished. The HTML pages are in %BUILDDIR%/singlehtml. 84 | goto end 85 | ) 86 | 87 | if "%1" == "pickle" ( 88 | %SPHINXBUILD% -b pickle %ALLSPHINXOPTS% %BUILDDIR%/pickle 89 | if errorlevel 1 exit /b 1 90 | echo. 91 | echo.Build finished; now you can process the pickle files. 92 | goto end 93 | ) 94 | 95 | if "%1" == "json" ( 96 | %SPHINXBUILD% -b json %ALLSPHINXOPTS% %BUILDDIR%/json 97 | if errorlevel 1 exit /b 1 98 | echo. 99 | echo.Build finished; now you can process the JSON files. 100 | goto end 101 | ) 102 | 103 | if "%1" == "htmlhelp" ( 104 | %SPHINXBUILD% -b htmlhelp %ALLSPHINXOPTS% %BUILDDIR%/htmlhelp 105 | if errorlevel 1 exit /b 1 106 | echo. 107 | echo.Build finished; now you can run HTML Help Workshop with the ^ 108 | .hhp project file in %BUILDDIR%/htmlhelp. 109 | goto end 110 | ) 111 | 112 | if "%1" == "qthelp" ( 113 | %SPHINXBUILD% -b qthelp %ALLSPHINXOPTS% %BUILDDIR%/qthelp 114 | if errorlevel 1 exit /b 1 115 | echo. 116 | echo.Build finished; now you can run "qcollectiongenerator" with the ^ 117 | .qhcp project file in %BUILDDIR%/qthelp, like this: 118 | echo.^> qcollectiongenerator %BUILDDIR%\qthelp\Pychievements.qhcp 119 | echo.To view the help file: 120 | echo.^> assistant -collectionFile %BUILDDIR%\qthelp\Pychievements.ghc 121 | goto end 122 | ) 123 | 124 | if "%1" == "devhelp" ( 125 | %SPHINXBUILD% -b devhelp %ALLSPHINXOPTS% %BUILDDIR%/devhelp 126 | if errorlevel 1 exit /b 1 127 | echo. 128 | echo.Build finished. 129 | goto end 130 | ) 131 | 132 | if "%1" == "epub" ( 133 | %SPHINXBUILD% -b epub %ALLSPHINXOPTS% %BUILDDIR%/epub 134 | if errorlevel 1 exit /b 1 135 | echo. 136 | echo.Build finished. The epub file is in %BUILDDIR%/epub. 137 | goto end 138 | ) 139 | 140 | if "%1" == "latex" ( 141 | %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex 142 | if errorlevel 1 exit /b 1 143 | echo. 144 | echo.Build finished; the LaTeX files are in %BUILDDIR%/latex. 145 | goto end 146 | ) 147 | 148 | if "%1" == "latexpdf" ( 149 | %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex 150 | cd %BUILDDIR%/latex 151 | make all-pdf 152 | cd %BUILDDIR%/.. 153 | echo. 154 | echo.Build finished; the PDF files are in %BUILDDIR%/latex. 155 | goto end 156 | ) 157 | 158 | if "%1" == "latexpdfja" ( 159 | %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex 160 | cd %BUILDDIR%/latex 161 | make all-pdf-ja 162 | cd %BUILDDIR%/.. 163 | echo. 164 | echo.Build finished; the PDF files are in %BUILDDIR%/latex. 165 | goto end 166 | ) 167 | 168 | if "%1" == "text" ( 169 | %SPHINXBUILD% -b text %ALLSPHINXOPTS% %BUILDDIR%/text 170 | if errorlevel 1 exit /b 1 171 | echo. 172 | echo.Build finished. The text files are in %BUILDDIR%/text. 173 | goto end 174 | ) 175 | 176 | if "%1" == "man" ( 177 | %SPHINXBUILD% -b man %ALLSPHINXOPTS% %BUILDDIR%/man 178 | if errorlevel 1 exit /b 1 179 | echo. 180 | echo.Build finished. The manual pages are in %BUILDDIR%/man. 181 | goto end 182 | ) 183 | 184 | if "%1" == "texinfo" ( 185 | %SPHINXBUILD% -b texinfo %ALLSPHINXOPTS% %BUILDDIR%/texinfo 186 | if errorlevel 1 exit /b 1 187 | echo. 188 | echo.Build finished. The Texinfo files are in %BUILDDIR%/texinfo. 189 | goto end 190 | ) 191 | 192 | if "%1" == "gettext" ( 193 | %SPHINXBUILD% -b gettext %I18NSPHINXOPTS% %BUILDDIR%/locale 194 | if errorlevel 1 exit /b 1 195 | echo. 196 | echo.Build finished. The message catalogs are in %BUILDDIR%/locale. 197 | goto end 198 | ) 199 | 200 | if "%1" == "changes" ( 201 | %SPHINXBUILD% -b changes %ALLSPHINXOPTS% %BUILDDIR%/changes 202 | if errorlevel 1 exit /b 1 203 | echo. 204 | echo.The overview file is in %BUILDDIR%/changes. 205 | goto end 206 | ) 207 | 208 | if "%1" == "linkcheck" ( 209 | %SPHINXBUILD% -b linkcheck %ALLSPHINXOPTS% %BUILDDIR%/linkcheck 210 | if errorlevel 1 exit /b 1 211 | echo. 212 | echo.Link check complete; look for any errors in the above output ^ 213 | or in %BUILDDIR%/linkcheck/output.txt. 214 | goto end 215 | ) 216 | 217 | if "%1" == "doctest" ( 218 | %SPHINXBUILD% -b doctest %ALLSPHINXOPTS% %BUILDDIR%/doctest 219 | if errorlevel 1 exit /b 1 220 | echo. 221 | echo.Testing of doctests in the sources finished, look at the ^ 222 | results in %BUILDDIR%/doctest/output.txt. 223 | goto end 224 | ) 225 | 226 | if "%1" == "xml" ( 227 | %SPHINXBUILD% -b xml %ALLSPHINXOPTS% %BUILDDIR%/xml 228 | if errorlevel 1 exit /b 1 229 | echo. 230 | echo.Build finished. The XML files are in %BUILDDIR%/xml. 231 | goto end 232 | ) 233 | 234 | if "%1" == "pseudoxml" ( 235 | %SPHINXBUILD% -b pseudoxml %ALLSPHINXOPTS% %BUILDDIR%/pseudoxml 236 | if errorlevel 1 exit /b 1 237 | echo. 238 | echo.Build finished. The pseudo-XML files are in %BUILDDIR%/pseudoxml. 239 | goto end 240 | ) 241 | 242 | :end 243 | -------------------------------------------------------------------------------- /docs/source/conf.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 3 | # Pychievements documentation build configuration file, created by 4 | # sphinx-quickstart on Mon Sep 1 01:28:58 2014. 5 | # 6 | # This file is execfile()d with the current directory set to its 7 | # containing dir. 8 | # 9 | # Note that not all possible configuration values are present in this 10 | # autogenerated file. 11 | # 12 | # All configuration values have a default; values that are commented out 13 | # serve to show the default. 14 | 15 | import sys 16 | import os 17 | sys.path.insert(0, os.path.abspath(os.path.join('..', '..'))) 18 | 19 | import pychievements 20 | 21 | # -- General configuration ------------------------------------------------ 22 | 23 | # If your documentation needs a minimal Sphinx version, state it here. 24 | #needs_sphinx = '1.0' 25 | 26 | # Add any Sphinx extension module names here, as strings. They can be 27 | # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom 28 | # ones. 29 | extensions = [ 30 | 'sphinx.ext.autodoc', 31 | 'sphinx.ext.coverage', 32 | 'sphinx.ext.viewcode', 33 | ] 34 | 35 | # Add any paths that contain templates here, relative to this directory. 36 | templates_path = ['_templates'] 37 | 38 | # The suffix of source filenames. 39 | source_suffix = '.rst' 40 | 41 | # The encoding of source files. 42 | #source_encoding = 'utf-8-sig' 43 | 44 | # The master toctree document. 45 | master_doc = 'index' 46 | 47 | # General information about the project. 48 | project = u'Pychievements' 49 | copyright = u'2014, Brian Knobbs' 50 | 51 | # The version info for the project you're documenting, acts as replacement for 52 | # |version| and |release|, also used in various other places throughout the 53 | # built documents. 54 | # 55 | # The short X.Y version. 56 | version = pychievements.__version__ 57 | # The full version, including alpha/beta/rc tags. 58 | release = version 59 | 60 | # The language for content autogenerated by Sphinx. Refer to documentation 61 | # for a list of supported languages. 62 | #language = None 63 | 64 | # There are two options for replacing |today|: either, you set today to some 65 | # non-false value, then it is used: 66 | #today = '' 67 | # Else, today_fmt is used as the format for a strftime call. 68 | #today_fmt = '%B %d, %Y' 69 | 70 | # List of patterns, relative to source directory, that match files and 71 | # directories to ignore when looking for source files. 72 | exclude_patterns = [] 73 | 74 | # The reST default role (used for this markup: `text`) to use for all 75 | # documents. 76 | #default_role = None 77 | 78 | # If true, '()' will be appended to :func: etc. cross-reference text. 79 | #add_function_parentheses = True 80 | 81 | # If true, the current module name will be prepended to all description 82 | # unit titles (such as .. function::). 83 | #add_module_names = True 84 | 85 | # If true, sectionauthor and moduleauthor directives will be shown in the 86 | # output. They are ignored by default. 87 | #show_authors = False 88 | 89 | # The name of the Pygments (syntax highlighting) style to use. 90 | pygments_style = 'sphinx' 91 | 92 | # A list of ignored prefixes for module index sorting. 93 | #modindex_common_prefix = [] 94 | 95 | # If true, keep warnings as "system message" paragraphs in the built documents. 96 | #keep_warnings = False 97 | 98 | 99 | # -- Options for HTML output ---------------------------------------------- 100 | 101 | # The theme to use for HTML and HTML Help pages. See the documentation for 102 | # a list of builtin themes. 103 | html_theme = 'default' 104 | 105 | # Theme options are theme-specific and customize the look and feel of a theme 106 | # further. For a list of options available for each theme, see the 107 | # documentation. 108 | #html_theme_options = {} 109 | 110 | # Add any paths that contain custom themes here, relative to this directory. 111 | #html_theme_path = [] 112 | 113 | # The name for this set of Sphinx documents. If None, it defaults to 114 | # " v documentation". 115 | #html_title = None 116 | 117 | # A shorter title for the navigation bar. Default is the same as html_title. 118 | #html_short_title = None 119 | 120 | # The name of an image file (relative to this directory) to place at the top 121 | # of the sidebar. 122 | #html_logo = None 123 | 124 | # The name of an image file (within the static path) to use as favicon of the 125 | # docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 126 | # pixels large. 127 | #html_favicon = None 128 | 129 | # Add any paths that contain custom static files (such as style sheets) here, 130 | # relative to this directory. They are copied after the builtin static files, 131 | # so a file named "default.css" will overwrite the builtin "default.css". 132 | html_static_path = ['_static'] 133 | 134 | # Add any extra paths that contain custom files (such as robots.txt or 135 | # .htaccess) here, relative to this directory. These files are copied 136 | # directly to the root of the documentation. 137 | #html_extra_path = [] 138 | 139 | # If not '', a 'Last updated on:' timestamp is inserted at every page bottom, 140 | # using the given strftime format. 141 | #html_last_updated_fmt = '%b %d, %Y' 142 | 143 | # If true, SmartyPants will be used to convert quotes and dashes to 144 | # typographically correct entities. 145 | #html_use_smartypants = True 146 | 147 | # Custom sidebar templates, maps document names to template names. 148 | #html_sidebars = {} 149 | 150 | # Additional templates that should be rendered to pages, maps page names to 151 | # template names. 152 | #html_additional_pages = {} 153 | 154 | # If false, no module index is generated. 155 | #html_domain_indices = True 156 | 157 | # If false, no index is generated. 158 | #html_use_index = True 159 | 160 | # If true, the index is split into individual pages for each letter. 161 | #html_split_index = False 162 | 163 | # If true, links to the reST sources are added to the pages. 164 | #html_show_sourcelink = True 165 | 166 | # If true, "Created using Sphinx" is shown in the HTML footer. Default is True. 167 | #html_show_sphinx = True 168 | 169 | # If true, "(C) Copyright ..." is shown in the HTML footer. Default is True. 170 | #html_show_copyright = True 171 | 172 | # If true, an OpenSearch description file will be output, and all pages will 173 | # contain a tag referring to it. The value of this option must be the 174 | # base URL from which the finished HTML is served. 175 | #html_use_opensearch = '' 176 | 177 | # This is the file name suffix for HTML files (e.g. ".xhtml"). 178 | #html_file_suffix = None 179 | 180 | # Output file base name for HTML help builder. 181 | htmlhelp_basename = 'pychievements' 182 | 183 | 184 | # -- Options for LaTeX output --------------------------------------------- 185 | 186 | #latex_elements = { 187 | # The paper size ('letterpaper' or 'a4paper'). 188 | #'papersize': 'letterpaper', 189 | 190 | # The font size ('10pt', '11pt' or '12pt'). 191 | #'pointsize': '10pt', 192 | 193 | # Additional stuff for the LaTeX preamble. 194 | #'preamble': '', 195 | #} 196 | 197 | # Grouping the document tree into LaTeX files. List of tuples 198 | # (source start file, target name, title, 199 | # author, documentclass [howto, manual, or own class]). 200 | #latex_documents = [ 201 | #('index', 'Pychievements.tex', u'Pychievements Documentation', 202 | #u'Brian Knobbs', 'manual'), 203 | #] 204 | 205 | # The name of an image file (relative to this directory) to place at the top of 206 | # the title page. 207 | #latex_logo = None 208 | 209 | # For "manual" documents, if this is true, then toplevel headings are parts, 210 | # not chapters. 211 | #latex_use_parts = False 212 | 213 | # If true, show page references after internal links. 214 | #latex_show_pagerefs = False 215 | 216 | # If true, show URL addresses after external links. 217 | #latex_show_urls = False 218 | 219 | # Documents to append as an appendix to all manuals. 220 | #latex_appendices = [] 221 | 222 | # If false, no module index is generated. 223 | #latex_domain_indices = True 224 | 225 | 226 | # -- Options for manual page output --------------------------------------- 227 | 228 | # One entry per manual page. List of tuples 229 | # (source start file, name, description, authors, manual section). 230 | man_pages = [ 231 | ('index', 'pychievements', u'Pychievements Documentation', 232 | [u'Brian Knobbs'], 1) 233 | ] 234 | 235 | # If true, show URL addresses after external links. 236 | #man_show_urls = False 237 | 238 | 239 | # -- Options for Texinfo output ------------------------------------------- 240 | 241 | # Grouping the document tree into Texinfo files. List of tuples 242 | # (source start file, target name, title, author, 243 | # dir menu entry, description, category) 244 | #texinfo_documents = [ 245 | #('index', 'Pychievements', u'Pychievements Documentation', 246 | #u'Brian Knobbs', 'Pychievements', 'One line description of project.', 247 | #'Miscellaneous'), 248 | #] 249 | 250 | # Documents to append as an appendix to all manuals. 251 | #texinfo_appendices = [] 252 | 253 | # If false, no module index is generated. 254 | #texinfo_domain_indices = True 255 | 256 | # How to display URL addresses: 'footnote', 'no', or 'inline'. 257 | #texinfo_show_urls = 'footnote' 258 | 259 | # If true, do not generate a @detailmenu in the "Top" node's menu. 260 | #texinfo_no_detailmenu = False 261 | -------------------------------------------------------------------------------- /docs/source/getting-started.rst: -------------------------------------------------------------------------------- 1 | .. _getting-started: 2 | 3 | Getting Started 4 | =============== 5 | 6 | *Getting Started* will guide you through creating your first achievements an introducing you to 7 | the pieces of the system. From there, visit the :doc:`reference` for in-depth information. 8 | 9 | Installing Pychievements 10 | ------------------------ 11 | 12 | Pychievements can be installed with ``pip``: 13 | :: 14 | 15 | $ pip install pychievements 16 | 17 | .. note: 18 | There are no required dependencies for pychievements. If you would like to the ``cli`` tools, 19 | then then you will have to install the *clint* library. You can use pip to install the 20 | optional ``cli`` dependencies like so: ``pip install pychievements[cli]`` 21 | 22 | 23 | Introduction to Pychievments 24 | ---------------------------- 25 | 26 | Pychievements has a number of modules that you'll need to be at least familiar with: 27 | 28 | tracker 29 | The default achievement tracker for pychievements is instantiated at import and is used to 30 | track all registered achievements for specific *tracked_ids*. 31 | 32 | Achievement 33 | Base Achievment class. 34 | 35 | icons.Icon 36 | Base Icon class. Icons are used to know what to display for a given goal within an 37 | achievement and have two states, achieved and unachieved. 38 | 39 | backends.AchievementBackend 40 | Pychievements have pluggable backends for storing tracked achievement data. The default 41 | ``AchievementBackend`` simply keeps everyting in memory, meaning it will be lost when the 42 | application is closed. The backend the tracker is using can be updated with the 43 | ``set_backend`` method. 44 | 45 | signals 46 | You can register functions as callbacks that can *recieve* Pychievement signals. Signals can 47 | be generated when a level is changed, when a new goal is reached, or when all goals have 48 | been achieved for a given achievement. 49 | 50 | 51 | Achievements 52 | ^^^^^^^^^^^^ 53 | 54 | At the core of Pychievements is the ``Achievement`` class. It is used to define goals that are 55 | obtained at specified *levels*. Levels are simply an integer. At a minimum, achievements must have 56 | the following attributes defined: 57 | 58 | * name : Display name of your achievement 59 | * category : Defaults to "achievements" 60 | * goals : A tuple of *goals*. A *goal* is a dictionary with the following keys: *["level", "name", 61 | "icon", "description"]* 62 | 63 | An example Achievement class: :: 64 | 65 | class MyAchievement(Achievement): 66 | name = "My Achievement" 67 | category = "achievements" 68 | keywords = ("my", "achievement") 69 | goals = ( 70 | {"level": 10, "name": "Level 1", "icon": icons.star, "description": "Level One" }, 71 | {"level": 20, "name": "Level 2", "icon": icons.star, "description": "Level Two" }, 72 | {"level": 30, "name": "Level 3", "icon": icons.star, "description": "Level Three" }, 73 | ) 74 | 75 | An achievements current level for an id can tracked with either the ``increment`` or ``evaluate`` 76 | functions, which the achievment can override to provide custom level manipulation. 77 | 78 | 79 | The Tracker 80 | ^^^^^^^^^^^ 81 | 82 | A singleton ``tracker`` is created on import that is available as ``pychievements.tracker``. The 83 | tracker provides an interface for interacting with registered achievements for a given 84 | ``tracked_id``. The tracker lets you: 85 | 86 | * increment level of an achievement for a tracked_id 87 | * evaluate level of an achievement for a tracked_id 88 | * query all achievements by category or keywords 89 | * query all achieved goals of an achievement for a tracked_id 90 | * query all unachieved goals of an achievement for a tracked_id 91 | * query the current goal being worked towards of an achievement for a tracked_id 92 | 93 | The tracker works with the configured backend to store and retrieve all of the tracked levels. 94 | 95 | 96 | Icons 97 | ^^^^^ 98 | 99 | Icons are simple classes that provide the "icon" (or what is displayed) for an achievment goal. It 100 | must define what to display for when the goal has been achieved (``achieved``) or not 101 | (``unachieved``) 102 | 103 | 104 | Examples 105 | -------- 106 | 107 | The easiest way to get started to check out the examples in the `examples`_ folder in the 108 | `the repository`_. Then check out the :doc:`reference` for more information. 109 | 110 | .. _examples: https://github.com/PacketPerception/pychievements/tree/master/examples 111 | .. _`the repository`: https://github.com/PacketPerception/pychievements 112 | -------------------------------------------------------------------------------- /docs/source/index.rst: -------------------------------------------------------------------------------- 1 | ################################################# 2 | Pychievements, the Python Achievements Framework! 3 | ################################################# 4 | 5 | **Pychievements** is a framework for creating and tracking achievements within a Python application. 6 | It includes functions specifically for creating commandline applications, though it is flexible 7 | enough to be used for any application such as web applications. 8 | 9 | See the examples_ to get a good feel for what Pychievements offers. Source can be found on github_. 10 | 11 | .. _examples: https://github.com/PacketPerception/pychievements/tree/master/examples 12 | .. _github: https://github.com/PacketPerception/pychievements 13 | 14 | .. toctree:: 15 | :maxdepth: 2 16 | 17 | getting-started 18 | reference 19 | 20 | Contribute 21 | ---------- 22 | 23 | If you'd like to contribute, simply fork `the repository`_, commit your changes 24 | to the **master** branch (or branch off of it), and send a pull request. Make 25 | sure you add yourself to AUTHORS_. 26 | 27 | 28 | License 29 | ------- 30 | 31 | Pychievements is license under the MIT license. You can find it in github, LICENSE_ 32 | 33 | .. _`the repository`: https://github.com/PacketPerception/pychievements 34 | .. _LICENSE: http://github.com/PacketPerception/pychievements/blob/master/LICENSE 35 | .. _AUTHORS: http://github.com/PacketPerception/pychievements/blob/master/AUTHORS 36 | 37 | 38 | 39 | Indices and tables 40 | ================== 41 | 42 | * :ref:`genindex` 43 | * :ref:`modindex` 44 | * :ref:`search` 45 | -------------------------------------------------------------------------------- /docs/source/reference.rst: -------------------------------------------------------------------------------- 1 | .. _reference: 2 | 3 | ========= 4 | Reference 5 | ========= 6 | 7 | Achievements 8 | ------------ 9 | 10 | .. automodule:: pychievements.achievements 11 | :members: 12 | 13 | Icons 14 | ----- 15 | 16 | .. automodule:: pychievements.icons 17 | :members: 18 | 19 | Trackers 20 | -------- 21 | 22 | .. automodule:: pychievements.trackers 23 | :members: 24 | 25 | Signals 26 | ------- 27 | 28 | .. automodule:: pychievements.signals 29 | :members: 30 | 31 | Backends 32 | -------- 33 | 34 | .. automodule:: pychievements.backends 35 | :members: 36 | 37 | CLI 38 | --- 39 | 40 | .. automodule:: pychievements.cli 41 | :members: 42 | -------------------------------------------------------------------------------- /examples/cli.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | """ 3 | A simple command line shell tool with achievements! 4 | 5 | Requires the ``clint`` python library to be installed to use the ``pychievements.cli`` commands. 6 | """ 7 | 8 | import sys 9 | import cmd 10 | from pychievements import tracker, Achievement, icons 11 | from pychievements.signals import receiver, goal_achieved, highest_level_achieved 12 | from pychievements.cli import print_goal, print_goals_for_tracked 13 | 14 | 15 | class TheLister(Achievement): 16 | """ 17 | Create a simple Achievement with 4 goals that can be reached 18 | """ 19 | name = 'The Lister' 20 | category = 'cli' 21 | keywords = ('cli', 'commands', 'ls') 22 | goals = ( 23 | {'level': 5, 'name': 'Getting Interested', 24 | 'icon': icons.star, 'description': 'Used `ls` 5 times'}, 25 | {'level': 10, 'name': 'Peruser of lists', 26 | 'icon': icons.star, 'description': 'Used `ls` 10 times'}, 27 | {'level': 15, 'name': 'The listing master', 'icon': icons.star, 28 | 'description': 'Used `ls` 15 times'}, 29 | {'level': 20, 'name': 'All your lists are belong to us!', 30 | 'icon': icons.star, 'description': 'Used `ls` 20 times'}, 31 | ) 32 | # Achievements must be registered with the tracker before they can be used 33 | tracker.register(TheLister) 34 | 35 | 36 | class TheCreator(Achievement): 37 | """ 38 | Achievements can have as many goals as they like 39 | """ 40 | name = 'The Creator' 41 | category = 'cli' 42 | keywords = ('cli', 'commands', 'create', 'modifiers') 43 | goals = ( 44 | {'level': 1, 'name': 'My First Creation', 45 | 'icon': icons.unicodeCheck, 'description': 'and it\'s so beautiful....'}, 46 | {'level': 5, 'name': 'Green thumb', 47 | 'icon': icons.unicodeCheckBox, 'description': 'You\'ve created at least 5 objects!'}, 48 | {'level': 10, 'name': 'Clever thinker', 49 | 'icon': icons.star, 'description': 'More than 10 new creations are all because of you.'}, 50 | {'level': 17, 'name': 'Almost an adult', 51 | 'icon': icons.star, 'description': 'Just about 18.'}, 52 | {'level': 15, 'name': 'True Inspiration', 53 | 'icon': icons.star, 'description': 'Or did you steal your ideas for these 15 items? Hmm?'}, 54 | {'level': 20, 'name': 'Divine Creator', 55 | 'icon': icons.star, 'description': 'All the world bows to your divine inspiration.'}, 56 | ) 57 | 58 | def evaluate(self, old_objects, new_objects, *args, **kwargs): 59 | """ TheCreator uses evalute instead of increment so it can increment the level based on the 60 | number of objects created and not have to count each one. Remember, evalute must return the 61 | achieved achievements after evaluating. """ 62 | self._current += new_objects - old_objects 63 | return self.achieved 64 | tracker.register(TheCreator) 65 | 66 | 67 | @receiver(goal_achieved) 68 | def new_goal(tracked_id, achievement, goals, **kwargs): 69 | """ 70 | We've setup some signal receivers so when a new goal has been reached we can print out a message 71 | to our user. It is possible to achieve more than one goal at once, though this will be called 72 | once for an achievement update. 73 | """ 74 | for g in goals: 75 | print_goal(g, True) 76 | 77 | 78 | @receiver(highest_level_achieved) 79 | def check_if_all_completed(tracked_id, **kwargs): 80 | """ 81 | Another signal reciever where we will check to see if any goals are unmet after we know we've 82 | achieved the highest level for a single achievement. 83 | """ 84 | unachieved = [] 85 | for a in tracker.achievements(): 86 | unachieved += tracker.unachieved(tracked_id, a) 87 | if not unachieved: 88 | print('\n\nYou\'ve achieved the highest level of every achievement possible! Congrats!') 89 | 90 | 91 | class MyCLIProgram(cmd.Cmd): 92 | """ 93 | Simple command shell that lets us create objects and then list them. We're not performing 94 | multi-user tracking in our shell, so the tracked_id for all commands will just be 'userid'. 95 | """ 96 | intro = 'The Achievement Oriented Command Line! Use ctrl+c to exit' 97 | prompt = '(do stuff) ' 98 | 99 | def __init__(self, *args, **kwargs): 100 | cmd.Cmd.__init__(self, *args, **kwargs) 101 | self._objects = [] 102 | 103 | def do_ls(self, arg): 104 | """ List created objects """ 105 | for _ in self._objects: 106 | print(_) 107 | # every time we run 'ls', increment the level for TheListener 108 | tracker.increment('userid', TheLister) 109 | 110 | def do_create(self, arg): 111 | """ Create objects (e.g. create 1 2 3 4 5)""" 112 | old = len(self._objects) 113 | self._objects += arg.split() 114 | # Have TheCreator update our level based on the number of objects we just created 115 | tracker.evaluate('userid', TheCreator, old, len(self._objects)) 116 | 117 | def do_remove(self, arg): 118 | """ Remove objects """ 119 | for o in arg.split(): 120 | if o in self._objects: 121 | self._objects.remove(o) 122 | 123 | def do_achievements(self, arg): 124 | """ List achievements. Can specify 'all' to see all achievements, or 'current' to see 125 | achievements currently working towards. Shows achieved achievements by default 126 | """ 127 | showall = arg.lower() == 'all' 128 | current = arg.lower() == 'current' 129 | print('') 130 | print_goals_for_tracked('userid', achieved=True, unachieved=showall, only_current=current, 131 | level=True) 132 | 133 | def do_exit(self, arg): 134 | sys.exit(0) 135 | 136 | def do_EOF(self, arg): 137 | sys.exit(0) 138 | 139 | if __name__ == '__main__': 140 | MyCLIProgram().cmdloop() 141 | -------------------------------------------------------------------------------- /pychievements/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | 3 | Pychievements! 4 | 5 | The Python achievements framework!! 6 | 7 | """ 8 | 9 | from __future__ import absolute_import 10 | 11 | from .trackers import AchievementTracker 12 | from .achievements import Achievement 13 | tracker = AchievementTracker() 14 | 15 | __all__ = ['tracker', 'Achievement'] 16 | 17 | 18 | __title__ = 'pychievements' 19 | __version__ = '0.1.2' 20 | __build__ = 0x000102 21 | __auther__ = 'Brian Knobbs' 22 | __license__ = 'MIT' 23 | __copyright__ = 'Copyright 2014 Brian Knobbs' 24 | __docformat__ = 'restructuredtext' 25 | -------------------------------------------------------------------------------- /pychievements/achievements.py: -------------------------------------------------------------------------------- 1 | class Achievement(object): 2 | """ 3 | Base Achievement class. 4 | 5 | An achievement primarily consists of 'goals', being levels that can be reached. Instances of 6 | Achievements are used to track progress, and the current level for individual IDs. For this, 7 | an Achievement implements a number of functions to interact with the current level. 8 | Achievements can also have a ``category`` (string) and ``keywords`` (tuple of strings) that can 9 | be used to filter Achievements. 10 | 11 | Goals are defined as a tuple of tuples with the format: 12 | 13 | .. code-block:: python 14 | 15 | goals = ( 16 | {'level': 10, 'name': 'Level 1', 'icon': icons.star, 'description': 'Level One'}, 17 | {'level': 20, 'name': 'Level 2', 'icon': icons.star, 'description': 'Level Two'}, 18 | {'level': 30, 'name': 'Level 3', 'icon': icons.star, 'description': 'Level Three'}, 19 | ) 20 | 21 | 22 | Arguments: 23 | 24 | level 25 | A positive integer that must be reached (greater than or equal) to be considered 'met' 26 | 27 | name 28 | A short name for the level 29 | 30 | icon 31 | The ``Icon`` to represent the level before it has been achieved. This must be an 32 | :py:mod:`pychievements.icons.Icon` class. 33 | 34 | .. note:: 35 | There are simple ASCII icons available from :py:mod:`pychievements.icons` 36 | 37 | description 38 | A longer description of the level. 39 | 40 | 41 | 42 | Achievements can be updated in two ways: ``increment`` and ``evaluate``. Increment increments 43 | the current level given an optional set of arguments, where evaluate performs a custom 44 | evaluation a sets the current level based on that evaluation. 45 | 46 | Increment is best used when the application is aware of achievement tracking, and calls 47 | to increment can be placed throughout the application. 48 | 49 | Evaluate is best used when actions may happen externally, and cannot be tracked using repeated 50 | calls to increment. Evaluate will also return the list of achieved goals after it has performed 51 | its evaluation. 52 | 53 | An Achievement can be initialized with a ``current`` level, for example when restoring for a 54 | saved state. 55 | """ 56 | name = 'Achievement' 57 | category = 'achievements' 58 | keywords = tuple() 59 | goals = tuple() 60 | 61 | def __init__(self, current=0): 62 | self._current = current 63 | self.goals = sorted(self.goals, key=lambda g: g['level']) # make sure our goals are sorted 64 | 65 | def __repr__(self): 66 | return '<{0} category:\'{1}\' keywords:{2} {3}>'.format(self.name, self.category, 67 | self.keywords, self._current) 68 | 69 | @property 70 | def current(self): 71 | """ 72 | Returns the current level being achieved (meaning haven't achieved yet) as a tuple: 73 | 74 | :: 75 | (current_level, (required_level, name, icon, description)) 76 | 77 | If all achievements have been achieved, the current level is returned with a None: 78 | 79 | :: 80 | (current_level, None) 81 | """ 82 | g = [_ for _ in self.goals if self._current < _['level']] 83 | if g: 84 | return (self._current, g[0]) 85 | return (self._current, None) 86 | 87 | @property 88 | def achieved(self): 89 | """ 90 | Returns a list of achieved goals 91 | """ 92 | return [_ for _ in self.goals if self._current >= _['level']] 93 | 94 | @property 95 | def unachieved(self): 96 | """ 97 | Returns a list of goals that have not been met yet 98 | """ 99 | return [_ for _ in self.goals if self._current < _['level']] 100 | 101 | def increment(self, amount=1, *args, **kwargs): 102 | """ 103 | Increases the current level. Achievements can redefine this function to take options to 104 | increase the level based on given arguments. By default, this will simply increment the 105 | current count by ``amount`` (which defaults to 1). 106 | """ 107 | self._current = self._current + amount 108 | 109 | def evaluate(self, *args, **kwargs): 110 | """ 111 | Performs a custom evaluation to set the current level of an achievement. Returns a list of 112 | achieved goals after the level is determined. 113 | """ 114 | return self.achieved 115 | 116 | def set_level(self, level): 117 | """ 118 | Overrides the current level with the given level 119 | """ 120 | self._current = level 121 | -------------------------------------------------------------------------------- /pychievements/backends.py: -------------------------------------------------------------------------------- 1 | import sqlite3 2 | 3 | 4 | class AchievementBackend(object): 5 | """ 6 | AchievementBackend 7 | 8 | Achievement backends implement the getting/setting/updating of achievements for ``tracked_id``. 9 | Achievements in the system are tracked for a specific, unique ID, ``tracked_id``. 10 | 11 | AchievementBackend is the most basic implementation of an AchievementBackend, storing all 12 | tracked information in memory and never persisting it. All of the functions of an 13 | AchievementBackend work to retrieve an ``Achievement`` instance for a given ``tracked_id``, and 14 | run the appropriate function on it, storing the results. In the least, storing results for a 15 | specific achievement, for a specific ``target_id`` should include the ``target_id``, the 16 | ``Achievement`` class name (``Achievement.__name__``), and the current level 17 | (``Achievement.current``) 18 | 19 | .. note:: 20 | AchievementBackend is NOT thread safe 21 | """ 22 | def __init__(self): 23 | self._tracked = {} 24 | 25 | def achievement_for_id(self, tracked_id, achievement): 26 | """ Retrieves the current ``Achievement`` for the given ``tracked_id``. If the given 27 | ``tracked_id`` does not exist yet, it should be created. Also, if the given ``tracked_id`` 28 | hasn't tracked the given ``Achievement`` yet, a new instance of the ``Achievement`` should 29 | be created for the given ``tracked_id``""" 30 | if tracked_id not in self._tracked: 31 | self._tracked[tracked_id] = {} 32 | if achievement.__name__ not in self._tracked[tracked_id]: 33 | self._tracked[tracked_id][achievement.__name__] = achievement() 34 | return self._tracked[tracked_id][achievement.__name__] 35 | 36 | def achievements_for_id(self, tracked_id, achievements): 37 | """ 38 | Returns the current achievement for each achievement in ``achievements`` for the given 39 | tracked_id """ 40 | r = [] 41 | for a in achievements: 42 | r.append(self.achievement_for_id(tracked_id, a)) 43 | return r 44 | 45 | def set_level_for_id(self, tracked_id, achievement, level): 46 | """ Set the ``level`` for an ``Achievement`` for the given ``tracked_id`` """ 47 | if tracked_id not in self._tracked: 48 | self._tracked[tracked_id] = {} 49 | if achievement.__name__ not in self._tracked[tracked_id]: 50 | self._tracked[tracked_id][achievement.__name__] = achievement(current=level) 51 | self._tracked[tracked_id][achievement.__name__].set_level(level) 52 | 53 | def get_tracked_ids(self): 54 | return self._tracked.keys() 55 | 56 | def remove_id(self, tracked_id): 57 | """ Removes *tracked_id* from the backend """ 58 | if tracked_id in self._tracked: 59 | del self._tracked[tracked_id] 60 | 61 | 62 | class SQLiteAchievementBackend(AchievementBackend): 63 | """ 64 | Stores achievement data in a SQLite database. 65 | 66 | Arguments: 67 | 68 | dbfile 69 | The full path and file name to store the SQLite database 70 | 71 | To use, create the backend and then use the :py:func:`set_backend` method of the tracker. 72 | 73 | .. code-block:: python 74 | 75 | mybackend = SQLiteAchievementBackend('/some/db.file') 76 | tracker.set_backend(mybackend) 77 | """ 78 | def __init__(self, dbfile): 79 | self.conn = sqlite3.connect(dbfile) 80 | with self.conn: 81 | c = self.conn.cursor() 82 | c.execute('create table if not exists pychievements (tracked_id text, ' 83 | 'achievement text, level integer)') 84 | 85 | def achievement_for_id(self, tracked_id, achievement): 86 | with self.conn: 87 | c = self.conn.cursor() 88 | c.execute('select level from pychievements where achievement=? and tracked_id=?', 89 | (achievement.__name__, str(tracked_id))) 90 | rows = c.fetchall() 91 | if not rows: 92 | c.execute('insert into pychievements values(?, ?, ?)', 93 | (str(tracked_id), achievement.__name__, 0)) 94 | return achievement(current=0) 95 | return achievement(current=rows[0][0]) 96 | 97 | def achievements_for_id(self, tracked_id, achievements): 98 | r = [] 99 | achievements = dict((_.__name__, _) for _ in achievements) 100 | with self.conn: 101 | c = self.conn.cursor() 102 | c.execute('select achievement, level from pychievements where tracked_id=? and ' 103 | 'achievement in (%s)' % ','.join('?'*len(achievements.keys())), 104 | [str(tracked_id)] + list(achievements.keys())) 105 | rows = c.fetchall() 106 | for i, _ in enumerate(rows): 107 | r.append(achievements[_[0]](current=_[1])) 108 | return r 109 | 110 | def set_level_for_id(self, tracked_id, achievement, level): 111 | with self.conn: 112 | c = self.conn.cursor() 113 | c.execute('update pychievements set level=? where achievement=? and tracked_id=?', 114 | (level, achievement.__name__, str(tracked_id))) 115 | 116 | def get_tracked_ids(self): 117 | with self.conn: 118 | c = self.conn.cursor() 119 | c.execute('select distinct tracked_id from pychievements') 120 | rows = c.fetchall() 121 | return [_[0] for _ in rows] 122 | 123 | def remove_id(self, tracked_id): 124 | with self.conn: 125 | c = self.conn.cursor() 126 | c.execute('delete from pychievements where tracked_id=?', (str(tracked_id),)) 127 | -------------------------------------------------------------------------------- /pychievements/cli.py: -------------------------------------------------------------------------------- 1 | from . import tracker as _defaulttracker 2 | from .achievements import Achievement 3 | from inspect import isclass as _isclass 4 | 5 | try: 6 | from itertools import zip_longest as _zip_longest 7 | except ImportError: 8 | from itertools import izip_longest as _zip_longest 9 | 10 | 11 | def print_goal(goal, achieved=False, level=None, indent=2): 12 | """ Print a goals description with its icon. Achieved (True/False) will choose the correct icon 13 | from the goal. If a level is specified, a tracker line will be added under the icon showing 14 | the current level out of the required level for the goal. If level is > the required level, 15 | achieved will be set to true. 16 | """ 17 | from clint.textui import puts 18 | from clint.textui import indent as _indent 19 | from clint.textui.cols import columns, console_width 20 | if level is not None and level >= goal['level']: 21 | achieved = True 22 | icon = (goal['icon'].achieved() if achieved else goal['icon'].unachieved()).split('\n') 23 | maxiw = max([len(str(_)) for _ in icon]) 24 | descw = console_width({})-maxiw-(indent + 4) 25 | desc = '{0}\n{1}\n\n{2}'.format(goal['name'], '-'*len(goal['name']), 26 | columns([goal['description'], descw])).split('\n') 27 | if level is not None: 28 | if level > goal['level']: 29 | level = goal['level'] 30 | maxitw = max([len(_) for _ in icon]) 31 | icon.append(("%d/%d" % (level, goal['level'])).center(maxitw)) 32 | with _indent(indent): 33 | for i, d in _zip_longest(icon, desc): 34 | puts("{1:{0}} {2}".format(maxiw, str(i) if i is not None else "", 35 | d.strip() if d is not None else "")) 36 | 37 | 38 | def print_goals(achievement_or_iter, indent=2): 39 | """ 40 | Displays all of the available goals registered for the given achievement(s) 41 | """ 42 | from clint.textui import puts 43 | from clint.textui.cols import console_width 44 | from clint.textui import indent as _indent 45 | if _isclass(achievement_or_iter) and issubclass(achievement_or_iter, Achievement): 46 | achievement_or_iter = [achievement_or_iter] 47 | 48 | for achievement in achievement_or_iter: 49 | with _indent(indent): 50 | puts("{0}\n{1}\n".format(achievement.name, '='*(console_width({})-indent-2))) 51 | for goal in achievement.goals: 52 | print_goal(goal, True, indent=indent) 53 | puts("\n") 54 | 55 | 56 | def print_goals_for_tracked(tracked_id, achievement_or_iter=None, achieved=True, unachieved=False, 57 | only_current=False, level=False, category=None, keywords=[], 58 | indent=2, tracker=None): 59 | """ 60 | Prints goals for a specific ``tracked_id`` from as tracked by a ``tracker``. By default, this 61 | will print out all achieved goals for every achievement in the ``tracker``. 62 | 63 | Arguments: 64 | 65 | achievment_or_iter 66 | If ``None``, this will print goals for all achievements registered with the ``tracker``. 67 | Otherwise an ``Achievement`` or list of achievements can be given to show goals for. 68 | 69 | achieved 70 | If True, prints out goals that have allready been achieved. 71 | 72 | unachieved 73 | If True, prints out goals that have not been achieved. 74 | 75 | only_current 76 | If True, only prints the goal currently being worked on (next to be achieved). This will 77 | override the ``achieved`` and ``unachieved`` options. 78 | 79 | category 80 | Category to filter achievements from the tracker. 81 | 82 | keywords 83 | Keywords to filter achievements from the tracker. 84 | 85 | level 86 | If True, show the current level with the achievements 87 | 88 | tracker 89 | The tracker to use for getting information about achievements and ``tracked_id``. If 90 | ``tracker`` is ``None``, this will default to using the default tracker. 91 | """ 92 | from clint.textui import puts 93 | from clint.textui import indent as _indent 94 | from clint.textui.cols import console_width 95 | if tracker is None: 96 | tracker = _defaulttracker 97 | 98 | if achievement_or_iter is None: 99 | achievement_or_iter = tracker.achievements() 100 | elif _isclass(achievement_or_iter) and issubclass(achievement_or_iter, Achievement): 101 | achievement_or_iter = [achievement_or_iter] 102 | 103 | for achievement in achievement_or_iter: 104 | with _indent(indent): 105 | puts("{0}\n{1}\n".format(achievement.name, '='*(console_width({})-indent-2))) 106 | current = tracker.current(tracked_id, achievement) 107 | cl = None if not level else current[0] 108 | if only_current: 109 | print_goal(current[1], level=current[0], indent=indent) 110 | else: 111 | goals = tracker.achieved(tracked_id, achievement) if achieved else [] 112 | goals += tracker.unachieved(tracked_id, achievement) if unachieved else [] 113 | for goal in goals: 114 | print_goal(goal, current[0] >= goal['level'], level=cl, indent=indent) 115 | puts("\n") 116 | -------------------------------------------------------------------------------- /pychievements/icons.py: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | """ 3 | pychievments.icons includes the :py:mod:`Icon` class as well as a number of pre-defined icons useful 4 | for CLI applications. 5 | 6 | * unicodeCheck 7 | * unicdeCheckBox 8 | * star 9 | 10 | """ 11 | 12 | 13 | class ColorCatcher(object): 14 | def __getattr__(self, name): 15 | return lambda s: s 16 | try: 17 | from clint.textui import colored 18 | except ImportError: 19 | colored = ColorCatcher() 20 | 21 | 22 | class Icon(object): 23 | """ 24 | Simple class to represent an ``Icon`` for an achievement. It provides to functions, 25 | ``achieved``, and ``unachieved``, which will return the displayable icon for the appropriate 26 | state. 27 | 28 | The base Icon class can be used without modification to create simple text Icons, e.g.: 29 | 30 | .. code-block:: python 31 | 32 | star = Icon(unachieved=' No ', achieved=' Yes ') 33 | 34 | """ 35 | def __init__(self, unachieved='', achieved=''): 36 | self._unachieved = unachieved 37 | self._achieved = achieved 38 | 39 | def unachieved(self, tracked_id=None, achievement=None): 40 | """ Returns the unachieved icon """ 41 | return self._unachieved 42 | 43 | def achieved(self, tracked_id=None, achievement=None): 44 | """ Returns the achieved icon """ 45 | return self._achieved 46 | 47 | 48 | ############################################################################################ 49 | # Some built-in unicode icons 50 | unicodeCheckBox = Icon('\n\n ☐ \n', '\n\n ☑ \n') 51 | unicodeCheck = Icon('\n\n ✗ \n', '\n\n ✓ \n') 52 | 53 | ############################################################################################ 54 | # Some built-in ASCII Art icons 55 | star = Icon(colored.white(""" .. 56 | .88. 57 | .8 8. 58 | ........8 8........ 59 | D88888 8888888 60 | .88 88~. 61 | ,88 88D 62 | 88 88..88 88. 63 | D 88. .88 D 64 | D 8. .8 8 65 | .D. .D. 66 | """), colored.yellow(""" .. 67 | .88. 68 | .8888. 69 | ........888888........ 70 | D8888888888888888888 71 | .88888888888888~. 72 | ,888888888888D 73 | 888888..888888. 74 | D8888. .8888D 75 | D88. .888 76 | .D. .D. 77 | """)) 78 | 79 | 80 | # from http://www.chris.com/ascii/index.php?art=animals/birds%20(land) 81 | _ROADRUNNER_STR = r""" 82 | .==,_ 83 | .===,_`\ 84 | .====,_ ` \ .====,__ 85 | .==-,`~. \ `:`.__, 86 | `~~=-. \ /^^^ 87 | `~~=. \ / 88 | `~. \ / 89 | ~. \____./ 90 | `.=====) 91 | ___.--~~~--.__ 92 | ___\.--~~~ ~~~---.._|/ 93 | ~~~" / 94 | ' 95 | """ 96 | roadrunner = Icon(colored.white(_ROADRUNNER_STR), colored.yellow(_ROADRUNNER_STR)) 97 | 98 | _EAGLE_STR = r""" 99 | ___ 100 | ,-' >---. ,---. 101 | / ,o)' `. / `. 102 | '| ( ,_ ) | `. 103 | ,--| -.,' `./ ; `. 104 | / | `. : . ` 105 | / |:. `- , \ :.\\ 106 | | ,-|' \-.___,' :\ ;::\\ 107 | |, ::'\ , `. ,.::\ :(- 108 | |: |:; \,'\ ). / .:.. ,:::::\ `\\ 109 | | |,: ` `/ `-/ ::::::::::::::\ ; 110 | | |: ::::::::::::::.\ | 111 | \ |:., :::::::::: ` |; | 112 | \ `.:' ::.,::::::: `: \ || | 113 | \ \ . ,:::::::,::: . ( `-'| | 114 | `. \ ::::,`':(::' ` |\ \ : | 115 | \ :-:. `:: \ ` | \ \ \ | 116 | `' |:' `' /`. `. \ : `' \| 117 | / \ \ `._/ `'`-` | 118 | __ / \, ,\ _\\ `. 119 | _/ ,\- (`' `-',-','-,"-. 120 | /,-(,- \_\ (-'(,---.:.) 121 | """ 122 | eagle = Icon(colored.white(_EAGLE_STR), colored.yellow(_EAGLE_STR)) 123 | 124 | 125 | _BEE_STR = r""" 126 | 127 | ...vvvv)))))). 128 | /~~\ ,,,c(((((((((((((((((/ 129 | /~~c \. .vv)))))))))))))))))))\`` 130 | G_G__ ,,(((KKKK//////////////' 131 | ,Z~__ '@,gW@@AKXX~MW,gmmmz==m_. 132 | iP,dW@!,A@@@@@@@@@@@@@@@A` ,W@@A\c 133 | ]b_.__zf !P~@@@@@*P~b.~+=m@@@*~ g@Ws. 134 | ~` ,2W2m. '\[ ['~~c'M7 _gW@@A`'s 135 | v=XX)====Y- [ [ \c/*@@@*~ g@@i 136 | /v~ !.!. '\c7+sg@@@@@s. 137 | // 'c'c '\c7*X7~~~~ 138 | ]/ ~=Xm_ '~=(Gm_. 139 | """ 140 | bee = Icon(colored.white(_BEE_STR), colored.yellow(_BEE_STR)) 141 | 142 | _EARTH_STR = r""" 143 | ,,,,,, 144 | o#'9MMHb':'-,o, 145 | .oH":HH$' "' ' -*R&o, 146 | dMMM*""'`' .oM"HM?. 147 | ,MMM' "HLbd< ?&H\\ 148 | .:MH ."\ ` MM MM&b 149 | . "*H - &MMMMMMMMMH: 150 | . dboo MMMMMMMMMMMM. 151 | . dMMMMMMb *MMMMMMMMMP. 152 | . MMMMMMMP *MMMMMP . 153 | `#MMMMM MM6P , 154 | ' `MMMP" HM*`, 155 | ' :MM .- , 156 | '. `#?.. . ..' 157 | -. . .- 158 | ''-.oo,oo.-'' 159 | """ 160 | earth = Icon(colored.white(_EARTH_STR), colored.yellow(_EARTH_STR)) 161 | 162 | book = Icon(colored.white(""" 163 | _.-"\\ 164 | _.-" \\ 165 | ,-" \\ 166 | ( \ \\ 167 | \ \ \\ 168 | \ \ \\ 169 | \ \ _.-; 170 | \ \ _.-" : 171 | \ \,-" _.-" 172 | \( _.-" 173 | `--" 174 | """), colored.yellow(""" 175 | _.--._ _.--._ 176 | ,-=.-":;:;:;\':;:;:;"-._ 177 | \\\:;:;:;:;:;\:;:;:;:;:;\\ 178 | \\\:;:;:;:;:;\:;:;:;:;:;\\ 179 | \\\:;:;:;:;:;\:;:;:;:;:;\\ 180 | \\\:;:;:;:;:;\:;::;:;:;:\\ 181 | \\\;:;::;:;:;\:;:;:;::;:\\ 182 | \\\;;:;:_:--:\:_:--:_;:;\\ 183 | \\\_.-" : "-._\\ 184 | \`_..--"--.;.--""--.._=> 185 | """)) 186 | -------------------------------------------------------------------------------- /pychievements/signals.py: -------------------------------------------------------------------------------- 1 | import sys 2 | import threading 3 | 4 | 5 | def _make_id(target): 6 | if hasattr(target, '__func__'): 7 | return (id(target.__self__), id(target.__func__)) 8 | return id(target) 9 | 10 | NONE_ID = _make_id(None) 11 | 12 | 13 | class Signal(object): 14 | """ 15 | Base class for all signals 16 | 17 | Internal attributes: 18 | 19 | receivers 20 | { receiverkey(id): receiver } 21 | """ 22 | def __init__(self): 23 | self.receivers = [] 24 | self.lock = threading.Lock() 25 | 26 | def connect(self, receiver, sender=None, dispatch_uid=None): 27 | """ 28 | Connect receiver to sender for signal. 29 | 30 | Arguments: 31 | 32 | receiver 33 | A function or an instance method which is to recieve signals. 34 | 35 | sender 36 | The sender to which the receiver should respond. Must be None to recieve events 37 | from any sender. 38 | 39 | dispatch_uid 40 | An identifier used to uniquely identify a particular instance of a receiver. This 41 | will usually be a string, though it may be anything hashable. 42 | 43 | """ 44 | if dispatch_uid: 45 | lookup_key = (dispatch_uid, _make_id(sender)) 46 | else: 47 | lookup_key = (_make_id(receiver), _make_id(sender)) 48 | 49 | with self.lock: 50 | for r_key, _ in self.receivers: 51 | if r_key == lookup_key: 52 | break 53 | else: 54 | self.receivers.append((lookup_key, receiver)) 55 | 56 | def disconnect(self, receiver=None, sender=None, dispatch_uid=None): 57 | """ 58 | Disconnect receiver from sender for signal. 59 | 60 | Arguments: 61 | 62 | receiver 63 | The registered receiver to disconnect. May be none if 64 | dispatch_uid is specified. 65 | 66 | sender 67 | The registered sender to disconnect 68 | 69 | dispatch_uid 70 | the unique identifier of the receiver to disconnect 71 | """ 72 | if dispatch_uid: 73 | lookup_key = (dispatch_uid, _make_id(sender)) 74 | else: 75 | lookup_key = (_make_id(receiver), _make_id(sender)) 76 | 77 | with self.lock: 78 | for index in range(len(self.receivers)): 79 | (r_key, _) = self.receivers[index] 80 | if r_key == lookup_key: 81 | del self.receivers[index] 82 | break 83 | 84 | def has_listeners(self, sender=None): 85 | return bool(self._receivers(sender)) 86 | 87 | def send(self, sender, **named): 88 | """ 89 | Send signal from sender to all connected receivers. 90 | 91 | If any receiver raises an error, the error propagates back through send, 92 | terminating the dispatch loop, so it is quite possible to not have all 93 | receivers called if a raises an error. 94 | 95 | Arguments: 96 | 97 | sender 98 | The sender of the signal Either a specific object or None. 99 | 100 | named 101 | Named arguments which will be passed to receivers. 102 | 103 | Returns a list of tuple pairs [(receiver, response), ... ]. 104 | """ 105 | responses = [] 106 | for receiver in self._receivers(sender): 107 | response = receiver(signal=self, sender=sender, **named) 108 | responses.append((receiver, response)) 109 | return responses 110 | 111 | def send_robust(self, sender, **named): 112 | """ 113 | Send signal from sender to all connected receivers catching errors. 114 | 115 | Arguments: 116 | 117 | sender 118 | The sender of the signal. Can be any python object (normally one 119 | registered with a connect if you actually want something to 120 | occur). 121 | 122 | named 123 | Named arguments which will be passed to receivers. These 124 | arguments must be a subset of the argument names defined in 125 | providing_args. 126 | 127 | Return a list of tuple pairs [(receiver, response), ... ]. 128 | 129 | If any receiver raises an error (specifically any subclass of 130 | Exception), the error instance is returned as the result for that 131 | receiver. The traceback is always attached to the error at 132 | ``__traceback__``. 133 | """ 134 | responses = [] 135 | 136 | # Call each receiver with whatever arguments it can accept. 137 | # Return a list of tuple pairs [(receiver, response), ... ]. 138 | for receiver in self._receivers(sender): 139 | try: 140 | response = receiver(signal=self, sender=sender, **named) 141 | except Exception as err: 142 | if not hasattr(err, '__traceback__'): 143 | err.__traceback__ = sys.exc_info()[2] 144 | responses.append((receiver, err)) 145 | else: 146 | responses.append((receiver, response)) 147 | return responses 148 | 149 | def _receivers(self, sender): 150 | """ 151 | Filter sequence of receivers to get receivers for sender. 152 | """ 153 | with self.lock: 154 | senderkey = _make_id(sender) 155 | receivers = [] 156 | for (_, r_senderkey), receiver in self.receivers: 157 | if r_senderkey == NONE_ID or r_senderkey == senderkey: 158 | receivers.append(receiver) 159 | return receivers 160 | 161 | 162 | def receiver(signal, **kwargs): 163 | """ 164 | A decorator for connecting receivers to signals. Used by passing in the 165 | signal (or list of signals) and keyword arguments to connect:: 166 | 167 | @receiver(goal_achieved) 168 | def signal_receiver(sender, **kwargs): 169 | ... 170 | 171 | @receiver([goal_achieved, level_increased], sender=tracker) 172 | def signals_receiver(sender, **kwargs): 173 | ... 174 | 175 | """ 176 | def _decorator(func): 177 | if isinstance(signal, (list, tuple)): 178 | for s in signal: 179 | s.connect(func, **kwargs) 180 | else: 181 | signal.connect(func, **kwargs) 182 | return func 183 | return _decorator 184 | 185 | 186 | goal_achieved = Signal() 187 | level_increased = Signal() 188 | highest_level_achieved = Signal() 189 | -------------------------------------------------------------------------------- /pychievements/trackers.py: -------------------------------------------------------------------------------- 1 | from .achievements import Achievement 2 | from .backends import AchievementBackend 3 | from .signals import goal_achieved, level_increased, highest_level_achieved 4 | from inspect import isclass as _isclass 5 | 6 | 7 | class AlreadyRegistered(Exception): 8 | pass 9 | 10 | 11 | class NotRegistered(Exception): 12 | pass 13 | 14 | 15 | class AchievementTracker(object): 16 | """ 17 | AchievementTracker tracks achievements and current levels for ``tracked_id`` using a configured 18 | achievement backend. 19 | 20 | A default instance of Achievement tracker is created as a singleton when pycheivements is 21 | imported as ``pychievements.tracker``. Most often, this is what you will want to use. 22 | 23 | Arguments: 24 | 25 | backend: 26 | The backend to use for storing/retrieving achievement data. If ``None``, the default 27 | :py:class:`AchievementBackend` will be used, which stores all data in memory. 28 | 29 | .. note:: 30 | The backend the tracker is using can be updated at any time using the :py:func:`set_backend` 31 | function. 32 | """ 33 | def __init__(self, backend=None): 34 | self._registry = [] 35 | self._backend = AchievementBackend() if backend is None else backend 36 | 37 | def set_backend(self, backend): 38 | """ 39 | Configures a new backend for storing achievement data. 40 | """ 41 | if not isinstance(backend, AchievementBackend): 42 | raise ValueError('Backend must be an instance of an AchievementBackend') 43 | self._backend = backend 44 | 45 | def register(self, achievement_or_iterable, **options): 46 | """ 47 | Registers the given achievement(s) to be tracked. 48 | """ 49 | if _isclass(achievement_or_iterable) and issubclass(achievement_or_iterable, Achievement): 50 | achievement_or_iterable = [achievement_or_iterable] 51 | for achievement in achievement_or_iterable: 52 | if not achievement.category: 53 | raise ValueError('Achievements must specify a category, could not register ' 54 | '%s' % achievement.__name__) 55 | if achievement in self._registry: 56 | raise AlreadyRegistered('The achievement %s is already ' 57 | 'registered' % achievement.__name__) 58 | if achievement is not Achievement: 59 | self._registry.append(achievement) 60 | 61 | def unregister(self, achievement_or_iterable): 62 | """ 63 | Un-registers the given achievement(s). 64 | 65 | If an achievement isn't already registered, this will raise NotRegistered. 66 | """ 67 | if _isclass(achievement_or_iterable) and issubclass(achievement_or_iterable, Achievement): 68 | achievement_or_iterable = [achievement_or_iterable] 69 | for achievement in achievement_or_iterable: 70 | if achievement not in self._registry: 71 | raise NotRegistered('The achievement %s is not registered' % achievement.__name__) 72 | self._registry.remove(achievement) 73 | 74 | def is_registered(self, achievement): 75 | """ 76 | Check if an achievement is registered with this `AchievementTracker` 77 | """ 78 | return achievement in self._registry 79 | 80 | def achievements(self, category=None, keywords=[]): 81 | """ 82 | Returns all registered achievements. 83 | 84 | Arguments: 85 | 86 | category 87 | Filters returned achievements by category. This is a strict string match. 88 | 89 | keywords 90 | Filters returned achievements by keywords. Returned achievements will match all 91 | given keywords 92 | """ 93 | achievements = [] 94 | for achievement in self._registry: 95 | if category is None or achievement.category == category: 96 | if not keywords or all([_ in achievement.keywords for _ in keywords]): 97 | achievements.append(achievement) 98 | return achievements 99 | 100 | def achievement_for_id(self, tracked_id, achievement): 101 | """ 102 | Returns ``Achievement`` for a given ``tracked_id``. Achievement can be an ``Achievement`` 103 | class or a string of the name of an achievement class that has been registered with this 104 | tracker. 105 | 106 | Raises NotRegistered if the given achievement is not registered with the tracker. 107 | 108 | If ``tracked_id`` has not been tracked yet by this tracker, it will be created. 109 | """ 110 | if isinstance(achievement, Achievement): 111 | achievement = achievement.__class__.__name__ 112 | elif _isclass(achievement) and issubclass(achievement, Achievement): 113 | achievement = achievement.__name__ 114 | 115 | a = [_ for _ in self._registry if _.__name__ == achievement] 116 | if a: 117 | return self._backend.achievement_for_id(tracked_id, a[0]) 118 | raise NotRegistered('The achievement %s is not registered with this tracker' % achievement) 119 | 120 | def achievements_for_id(self, tracked_id, category=None, keywords=[]): 121 | """ Returns all of the achievements for tracked_id that match the given category and 122 | keywords """ 123 | return self._backend.achievements_for_id(tracked_id, self.achievements(category, keywords)) 124 | 125 | def _check_signals(self, tracked_id, achievement, old_level, old_achieved): 126 | cur_level = achievement.current[0] 127 | if old_level < cur_level: 128 | level_increased.send_robust(self, tracked_id=tracked_id, achievement=achievement) 129 | if old_achieved != achievement.achieved: 130 | new_goals = [_ for _ in achievement.achieved if _ not in old_achieved] 131 | goal_achieved.send_robust(self, tracked_id=tracked_id, achievement=achievement, 132 | goals=new_goals) 133 | if not achievement.unachieved: 134 | highest_level_achieved.send_robust(self, tracked_id=tracked_id, 135 | achievement=achievement) 136 | return new_goals 137 | return False 138 | 139 | def increment(self, tracked_id, achievement, amount=1, *args, **kwargs): 140 | """ 141 | Increments an achievement for a given ``tracked_id``. Achievement can be an ``Achievement`` 142 | class or a string of the name of an achievement class that has been registered with this 143 | tracker. 144 | 145 | Raises NotRegistered if the given achievement is not registered with the tracker. 146 | 147 | If ``tracked_id`` has not been tracked yet by this tracker, it will be created before 148 | incrementing. 149 | 150 | Returns an list of achieved goals if a new goal was reached, or False 151 | """ 152 | achievement = self.achievement_for_id(tracked_id, achievement) 153 | cur_level = achievement.current[0] 154 | achieved = achievement.achieved[:] 155 | achievement.increment(amount, *args, **kwargs) 156 | self._backend.set_level_for_id(tracked_id, achievement.__class__, achievement.current[0]) 157 | return self._check_signals(tracked_id, achievement, cur_level, achieved) 158 | 159 | def evaluate(self, tracked_id, achievement, *args, **kwargs): 160 | """ 161 | Evaluates an achievement for a given ``tracked_id``. Achievement can be an ``Achievement`` 162 | class or a string of the name of an achievement class that has been registered with 163 | this tracker. 164 | 165 | Raises NotRegistered if the given achievement is not registered with the tracker. 166 | 167 | If ``tracked_id`` has not been tracked yet by this tracker, it will be created before 168 | evaluating. 169 | 170 | Returns list of achieved goals for the given achievement after evaluation 171 | """ 172 | achievement = self.achievement_for_id(tracked_id, achievement) 173 | cur_level = achievement.current[0] 174 | achieved = achievement.achieved[:] 175 | result = achievement.evaluate(*args, **kwargs) 176 | self._backend.set_level_for_id(tracked_id, achievement.__class__, achievement.current[0]) 177 | self._check_signals(tracked_id, achievement, cur_level, achieved) 178 | return result 179 | 180 | def current(self, tracked_id, achievement): 181 | """ 182 | Returns ``current`` for a given tracked_id. See :ref:``Achievement`` 183 | """ 184 | achievement = self.achievement_for_id(tracked_id, achievement) 185 | return achievement.current 186 | 187 | def achieved(self, tracked_id, achievement): 188 | """ 189 | Returns ``achieved`` for a given tracked_id. See :ref:``Achievement`` 190 | """ 191 | achievement = self.achievement_for_id(tracked_id, achievement) 192 | return achievement.achieved 193 | 194 | def unachieved(self, tracked_id, achievement): 195 | """ 196 | Returns ``unachieved`` for a given tracked_id. See :ref:``Achievement`` 197 | """ 198 | achievement = self.achievement_for_id(tracked_id, achievement) 199 | return achievement.unachieved 200 | 201 | def set_level(self, tracked_id, achievement, level): 202 | """ 203 | Returns ``set_level`` for a given tracked_id. See :ref:``Achievement`` 204 | """ 205 | achievement = self.achievement_for_id(tracked_id, achievement) 206 | cur_level = achievement.current[0] 207 | achieved = achievement.achieved[:] 208 | achievement.set_level(level) 209 | self._backend.set_level_for_id(tracked_id, achievement.__class__, achievement.current[0]) 210 | self._check_signals(tracked_id, achievement, cur_level, achieved) 211 | 212 | def get_tracked_ids(self): 213 | """ Returns all tracked ids """ 214 | return self._backend.get_tracked_ids() 215 | 216 | def remove_id(self, tracked_id): 217 | """ Remove all tracked information for tracked_id """ 218 | self._backend.remove_id(tracked_id) 219 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | import os 5 | import sys 6 | 7 | try: 8 | from setuptools import setup 9 | except ImportError: 10 | from distutils.core import setup 11 | 12 | import pychievements 13 | 14 | 15 | def publish(): 16 | """Publish to PyPi""" 17 | os.system("python setup.py sdist upload") 18 | 19 | if sys.argv[-1] == "publish": 20 | publish() 21 | sys.exit() 22 | 23 | setup( 24 | name='pychievements', 25 | version=pychievements.__version__, 26 | description='Python Achievements Framework', 27 | long_description=open('README.rst').read() + '\n\n' + open('HISTORY.rst').read(), 28 | author='Brian Knobbs', 29 | author_email='brian@packetperception.org', 30 | url='https://github.com/PacketPerception/pychievements', 31 | packages=[ 32 | 'pychievements', 33 | ], 34 | extras_require={ 35 | 'cli': ["clint"] 36 | }, 37 | license='MIT', 38 | classifiers=( 39 | 'Development Status :: 4 - Beta', 40 | 'Intended Audience :: Developers', 41 | 'Natural Language :: English', 42 | 'License :: OSI Approved :: MIT License', 43 | 'Programming Language :: Python', 44 | 'Programming Language :: Python :: 2', 45 | 'Programming Language :: Python :: 3', 46 | 'Topic :: Software Development :: Libraries', 47 | 'Topic :: Software Development :: Libraries :: Application Frameworks', 48 | ), 49 | ) 50 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- 1 | import os 2 | import random 3 | import unittest 4 | import tempfile 5 | 6 | from pychievements import Achievement, icons 7 | from pychievements import cli 8 | from pychievements.trackers import AchievementTracker, NotRegistered, AlreadyRegistered 9 | from pychievements.backends import SQLiteAchievementBackend 10 | from pychievements.signals import receiver, goal_achieved, level_increased, highest_level_achieved 11 | 12 | 13 | def AchievementFactory(name): 14 | attrs = {'name': name, 'category': random.choice(CATEGORIES), 15 | 'keywords': random.sample(KEYWORDS, random.randrange(1, len(KEYWORDS))), 16 | 'goals': tuple({'level': _, 'name': str(_), 'icon': icons.star, 'description': str(_)} 17 | for _ in range(15, random.randrange(25, 50, 5), 5))} 18 | return type(name, (Achievement,), attrs) 19 | 20 | 21 | CATEGORIES = ['itaque', 'sapiente', 'consectetur', 'voluptate', 'iusto', 'sint'] 22 | KEYWORDS = ['qui', 'officiis', 'expedita', 'saepe', 'placeat', 'perferendis', 'vitae', 23 | 'ullam', 'aut', 'aut', 'enim', 'sint'] 24 | TRACKED_IDS = [3324, 608, 'deserunt', 'omnis', 'excepturi', 'dolores', 'vero'] 25 | ACHIEVEMENTS = [AchievementFactory("Achieve%d" % _) for _ in range(0, random.randrange(5, 10))] 26 | 27 | 28 | class TrackerTests(unittest.TestCase): 29 | def setUp(self): 30 | self.tracker = AchievementTracker() 31 | self.tracker.register(ACHIEVEMENTS) 32 | 33 | def test_bad_backend(self): 34 | self.assertRaises(ValueError, self.tracker.set_backend, AchievementTracker) 35 | 36 | def test_register_list(self): 37 | # achievements are added as a list in setUp 38 | self.assertEquals(len(self.tracker.achievements()), len(ACHIEVEMENTS)) 39 | 40 | def test_register_single(self): 41 | self.tracker = AchievementTracker() 42 | for a in ACHIEVEMENTS: 43 | self.tracker.register(a) 44 | self.assertEquals(len(self.tracker.achievements()), len(ACHIEVEMENTS)) 45 | self.assertRaises(AlreadyRegistered, self.tracker.register, ACHIEVEMENTS[0]) 46 | 47 | def test_register_missing_cat(self): 48 | achiev = AchievementFactory('testFail') 49 | achiev.category = None 50 | self.assertRaises(ValueError, self.tracker.register, achiev) 51 | 52 | def test_unregister(self): 53 | self.tracker.unregister(ACHIEVEMENTS[0]) 54 | self.assertEquals(len(self.tracker.achievements()), len(ACHIEVEMENTS)-1) 55 | self.assertRaises(NotRegistered, self.tracker.unregister, ACHIEVEMENTS[0]) 56 | 57 | def test_is_registered(self): 58 | self.assertTrue(self.tracker.is_registered(ACHIEVEMENTS[0])) 59 | self.tracker.unregister(ACHIEVEMENTS[1]) 60 | self.assertFalse(self.tracker.is_registered(ACHIEVEMENTS[1])) 61 | 62 | def test_achievements(self): 63 | self.assertEqual(len(self.tracker.achievements()), len(ACHIEVEMENTS)) 64 | 65 | def test_achievements_category(self): 66 | cat = random.choice(CATEGORIES) 67 | num_achieves = len([_ for _ in ACHIEVEMENTS if _.category == cat]) 68 | self.assertEqual(len(self.tracker.achievements(category=cat)), num_achieves) 69 | 70 | def test_achievements_keywords(self): 71 | keys = random.sample(KEYWORDS, 2) 72 | num_achieves = len([_ for _ in ACHIEVEMENTS 73 | if keys[0] in _.keywords and keys[1] in _.keywords]) 74 | self.assertEqual(len(self.tracker.achievements(keywords=keys)), num_achieves) 75 | 76 | def test_achievement_for_id(self): 77 | tid = random.choice(TRACKED_IDS) 78 | achiev = self.tracker.achievement_for_id(tid, random.choice(ACHIEVEMENTS)) 79 | repr(achiev) 80 | self.assertEqual(self.tracker.current(tid, achiev), achiev.current) 81 | 82 | self.assertRaises(NotRegistered, self.tracker.achievement_for_id, tid, 'NotRegistered') 83 | 84 | def test_increment(self): 85 | tid = random.choice(TRACKED_IDS) 86 | num_increment = random.randint(20, 60) 87 | for _ in range(num_increment): 88 | self.tracker.increment(tid, random.choice(self.tracker.achievements())) 89 | total = sum([_.current[0] for _ in self.tracker.achievements_for_id(tid)]) 90 | self.assertEqual(total, num_increment) 91 | 92 | def test_evaluate(self): 93 | tid = random.choice(TRACKED_IDS) 94 | self.assertEqual(self.tracker.evaluate(tid, random.choice(ACHIEVEMENTS)), []) 95 | 96 | def test_set_level(self): 97 | tid = 'randomeID' 98 | achiev = random.choice(ACHIEVEMENTS) 99 | self.tracker.set_level(tid, achiev, 100) 100 | self.assertEqual(self.tracker.current(tid, achiev)[0], 100) 101 | 102 | def test_remove_id(self): 103 | tid = random.choice(TRACKED_IDS) 104 | for _ in TRACKED_IDS: 105 | self.tracker.increment(_, random.choice(ACHIEVEMENTS)) 106 | self.tracker.remove_id(tid) 107 | print(self.tracker.get_tracked_ids()) 108 | self.assertEqual(len(self.tracker.get_tracked_ids()), len(TRACKED_IDS)-1) 109 | 110 | 111 | class AchievementBackenedTests(unittest.TestCase): 112 | # only tests things that haven't been hit in TrackerTests 113 | def setUp(self): 114 | self.tracker = AchievementTracker() 115 | self.tracker.register(ACHIEVEMENTS) 116 | 117 | def test_set_level_for_id(self): 118 | self.tracker._backend.set_level_for_id('newid', random.choice(ACHIEVEMENTS), 100) 119 | 120 | 121 | class SQLiteBackendTests(unittest.TestCase): 122 | def setUp(self): 123 | self.dbfile = tempfile.NamedTemporaryFile(delete=False) 124 | self.dbfile.close() 125 | self.backend = SQLiteAchievementBackend(self.dbfile.name) 126 | self.tracker = AchievementTracker() 127 | self.tracker.set_backend(self.backend) 128 | self.tracker.register(ACHIEVEMENTS) 129 | 130 | def tearDown(self): 131 | os.remove(self.dbfile.name) 132 | 133 | def test_increment(self): 134 | tid = random.choice(TRACKED_IDS) 135 | num_increment = random.randint(20, 60) 136 | for _ in range(num_increment): 137 | self.tracker.increment(tid, random.choice(self.tracker.achievements())) 138 | total = sum([_.current[0] for _ in self.tracker.achievements_for_id(tid)]) 139 | self.assertEqual(total, num_increment) 140 | 141 | def test_achievement_for_id(self): 142 | tid = random.choice(TRACKED_IDS) 143 | achiev = self.tracker.achievement_for_id(tid, random.choice(ACHIEVEMENTS)) 144 | self.assertEqual(self.tracker.current(tid, achiev), achiev.current) 145 | self.assertRaises(NotRegistered, self.tracker.achievement_for_id, tid, 'NotRegistered') 146 | 147 | def test_set_level(self): 148 | tid = 'randomID' 149 | achiev = random.choice(ACHIEVEMENTS) 150 | self.tracker.set_level(tid, achiev, 100) 151 | self.assertEqual(self.tracker.current(tid, achiev)[0], 100) 152 | 153 | def test_remove_id(self): 154 | tid = random.choice(TRACKED_IDS) 155 | for _ in TRACKED_IDS: 156 | self.tracker.increment(_, random.choice(ACHIEVEMENTS)) 157 | self.tracker.remove_id(tid) 158 | self.assertEqual(len(self.tracker.get_tracked_ids()), len(TRACKED_IDS)-1) 159 | 160 | 161 | @receiver([goal_achieved, level_increased, highest_level_achieved]) 162 | def recv(*args, **kwargs): 163 | pass 164 | 165 | 166 | @receiver(goal_achieved) 167 | def recv2(*args, **kwargs): 168 | pass 169 | 170 | 171 | class recvClass: 172 | def __self__(self): 173 | return self 174 | 175 | def __func__(self, *args, **kwargs): 176 | pass 177 | 178 | 179 | class SignalsTest(unittest.TestCase): 180 | def setUp(self): 181 | self.tracker = AchievementTracker() 182 | self.tracker.register(ACHIEVEMENTS) 183 | self.signal_received = False 184 | 185 | def test_goal_achieved(self): 186 | rec = lambda s=self, *args, **kwargs: setattr(s, 'signal_received', True) 187 | goal_achieved.connect(rec, dispatch_uid='test') 188 | 189 | tid = random.choice(TRACKED_IDS) 190 | achiev = self.tracker.achievement_for_id(tid, random.choice(ACHIEVEMENTS)) 191 | self.tracker.set_level(tid, achiev, achiev.goals[-2]['level']+1) 192 | self.assertTrue(self.signal_received) 193 | goal_achieved.disconnect(rec, dispatch_uid='test') 194 | 195 | def test_level_increased(self): 196 | rec = lambda s=self, *args, **kwargs: setattr(s, 'signal_received', True) 197 | receiver(rec) 198 | level_increased.connect(rec) 199 | 200 | tid = random.choice(TRACKED_IDS) 201 | achiev = self.tracker.achievement_for_id(tid, random.choice(ACHIEVEMENTS)) 202 | self.tracker.increment(tid, achiev) 203 | self.assertTrue(self.signal_received) 204 | level_increased.disconnect(rec) 205 | 206 | def test_highest_level_achieved(self): 207 | rec = lambda s=self, *args, **kwargs: setattr(s, 'signal_received', True) 208 | highest_level_achieved.connect(rec) 209 | 210 | tid = random.choice(TRACKED_IDS) 211 | achiev = self.tracker.achievement_for_id(tid, random.choice(ACHIEVEMENTS)) 212 | self.tracker.set_level(tid, achiev, achiev.goals[-1]['level']+1) 213 | self.assertTrue(self.signal_received) 214 | highest_level_achieved.disconnect(rec) 215 | 216 | def test_duplicate_reciever(self): 217 | highest_level_achieved.connect(recvClass) 218 | highest_level_achieved.connect(recvClass) 219 | self.assertTrue(highest_level_achieved.has_listeners()) 220 | highest_level_achieved.disconnect(recvClass) 221 | 222 | def test_signal_send(self): 223 | rec = lambda s=self, *args, **kwargs: setattr(s, 'signal_received', True) 224 | highest_level_achieved.connect(rec) 225 | highest_level_achieved.send(self) 226 | highest_level_achieved.disconnect(rec) 227 | 228 | def test_callback_exception(self): 229 | def raise_exc(*args, **kwargs): 230 | raise Exception('test') 231 | highest_level_achieved.connect(raise_exc) 232 | r = highest_level_achieved.send_robust(self) 233 | self.assertNotEqual(r, []) 234 | highest_level_achieved.disconnect(raise_exc) 235 | 236 | 237 | class IconsTests(unittest.TestCase): 238 | def test_color_catcher(self): 239 | import sys 240 | del sys.modules['pychievements.icons'] 241 | textui = sys.modules['clint.textui'] 242 | sys.modules['clint.textui'] = sys.modules['nose'] 243 | import pychievements.icons 244 | c = pychievements.icons.ColorCatcher() 245 | self.assertEqual(c.red('test'), 'test') 246 | sys.modules['clint.textui'] = textui 247 | 248 | 249 | class CLITests(unittest.TestCase): 250 | def setUp(self): 251 | self.tracker = AchievementTracker() 252 | self.tracker.register(ACHIEVEMENTS) 253 | 254 | def test_print_goal(self): 255 | cli.print_goal(ACHIEVEMENTS[0].goals[0], achieved=True, level=100) 256 | 257 | def test_print_goals(self): 258 | cli.print_goals(ACHIEVEMENTS[0]) 259 | 260 | def test_print_goals_for_tracked(self): 261 | cli.print_goals_for_tracked(random.choice(TRACKED_IDS), unachieved=True) 262 | cli.print_goals_for_tracked(random.choice(TRACKED_IDS), ACHIEVEMENTS[0], unachieved=True, 263 | tracker=self.tracker) 264 | cli.print_goals_for_tracked(random.choice(TRACKED_IDS), ACHIEVEMENTS, unachieved=True, 265 | tracker=self.tracker) 266 | cli.print_goals_for_tracked(random.choice(TRACKED_IDS), only_current=True, 267 | tracker=self.tracker) 268 | 269 | 270 | if __name__ == '__main__': 271 | unittest.main() 272 | -------------------------------------------------------------------------------- /tests/test_requirements.txt: -------------------------------------------------------------------------------- 1 | # These requirements are for running pychievements tests 2 | clint 3 | coverage 4 | python-coveralls 5 | nose 6 | --------------------------------------------------------------------------------