├── .gitignore
├── INSTALL.md
├── LICENSE
├── Makefile
├── NEEDS.md
├── README.md
├── docs
├── Makefile
├── make.bat
└── source
│ ├── _images
│ ├── multiple_choice.gif
│ ├── share.png
│ ├── single_choice.gif
│ ├── to_do_list.gif
│ └── true_or_false.gif
│ ├── conf.py
│ ├── goodies.rst
│ ├── goodies_colored_expanders.rst
│ ├── goodies_echo.rst
│ ├── goodies_share.rst
│ ├── index.rst
│ ├── install.rst
│ ├── interactive_activities.rst
│ ├── interactive_activities_multiple_choice.rst
│ ├── interactive_activities_single_choice.rst
│ ├── interactive_activities_to_do_list.rst
│ ├── interactive_activities_true_or_false.rst
│ ├── intro.rst
│ ├── multipaging.rst
│ ├── multipaging_book.rst
│ ├── multipaging_chapter.rst
│ ├── multipaging_documentation.rst
│ ├── multipaging_native.rst
│ ├── multipaging_single.rst
│ └── roadmap.rst
├── examples
├── 01_to_do_list.md
├── 02_true_or_false.md
├── 03_single_choice.md
└── 04_multiple_choice.md
├── images
├── avatar.jpeg
├── github.png
├── linkedin.png
└── twitter.png
├── requirements.txt
├── setup.py
├── stb_examples
└── README.md
├── stbook_methods.py
└── streamlit_book
├── __init__.py
├── admin.py
├── answers.py
├── book_config.py
├── chapter_config.py
├── colored_expanders.py
├── echo.py
├── file_reader.py
├── floating_button.py
├── helpers.py
├── keywords.py
├── login.py
├── render.py
├── render_code_input.py
├── render_multiple_choice.py
├── render_single_choice.py
├── render_text_input.py
├── render_to_do_list.py
├── render_true_or_false.py
└── social_media.py
/.gitignore:
--------------------------------------------------------------------------------
1 | bookmark.txt
2 | stb_examples/
3 | tmp/
4 | stb_*.py
5 |
6 |
7 | # Byte-compiled / optimized / DLL files
8 | __pycache__/
9 | *.py[cod]
10 | *$py.class
11 |
12 | # C extensions
13 | *.so
14 |
15 | # Distribution / packaging
16 | venv/
17 | .Python
18 | .DS_Store
19 | */.DS_Store
20 | */*/.DS_Store
21 |
22 | build/
23 | develop-eggs/
24 | dist/
25 | downloads/
26 | eggs/
27 | .eggs/
28 | lib/
29 | lib64/
30 | parts/
31 | sdist/
32 | var/
33 | wheels/
34 | pip-wheel-metadata/
35 | share/python-wheels/
36 | *.egg-info/
37 | .installed.cfg
38 | *.egg
39 | MANIFEST
40 |
41 | # PyInstaller
42 | # Usually these files are written by a python script from a template
43 | # before PyInstaller builds the exe, so as to inject date/other infos into it.
44 | *.manifest
45 | *.spec
46 |
47 | # Installer logs
48 | pip-log.txt
49 | pip-delete-this-directory.txt
50 |
51 | # Unit test / coverage reports
52 | htmlcov/
53 | .tox/
54 | .nox/
55 | .coverage
56 | .coverage.*
57 | .cache
58 | nosetests.xml
59 | coverage.xml
60 | *.cover
61 | *.py,cover
62 | .hypothesis/
63 | .pytest_cache/
64 |
65 | # Translations
66 | *.mo
67 | *.pot
68 |
69 | # Django stuff:
70 | *.log
71 | local_settings.py
72 | db.sqlite3
73 | db.sqlite3-journal
74 |
75 | # Flask stuff:
76 | instance/
77 | .webassets-cache
78 |
79 | # Scrapy stuff:
80 | .scrapy
81 |
82 | # Sphinx documentation
83 | docs/_build/
84 |
85 | # PyBuilder
86 | target/
87 |
88 | # Jupyter Notebook
89 | .ipynb_checkpoints
90 |
91 | # IPython
92 | profile_default/
93 | ipython_config.py
94 |
95 | # pyenv
96 | .python-version
97 |
98 | # pipenv
99 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
100 | # However, in case of collaboration, if having platform-specific dependencies or dependencies
101 | # having no cross-platform support, pipenv may install dependencies that don't work, or not
102 | # install all needed dependencies.
103 | #Pipfile.lock
104 |
105 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow
106 | __pypackages__/
107 |
108 | # Celery stuff
109 | celerybeat-schedule
110 | celerybeat.pid
111 |
112 | # SageMath parsed files
113 | *.sage.py
114 |
115 | # Environments
116 | .env
117 | .venv
118 | env/
119 | venv/
120 | ENV/
121 | env.bak/
122 | venv.bak/
123 |
124 | # Spyder project settings
125 | .spyderproject
126 | .spyproject
127 |
128 | # Rope project settings
129 | .ropeproject
130 |
131 | # mkdocs documentation
132 | /site
133 |
134 | # mypy
135 | .mypy_cache/
136 | .dmypy.json
137 | dmypy.json
138 |
139 | # Pyre type checker
140 | .pyre/
141 |
--------------------------------------------------------------------------------
/INSTALL.md:
--------------------------------------------------------------------------------
1 | # Install requirements with a virtual environment
2 |
3 | ## Crear el ambiente
4 | virtualenv venv
5 |
6 | ## Activar ambiente
7 | source venv/bin/activate
8 |
9 | ## Instalar librerías
10 | pip install -r requirements.txt
11 |
12 | ## Desactivar Ambiente
13 | deactivate
14 |
15 | ## Borrar el ambiente
16 | rm -rf venv/
17 |
18 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2021 Sebastian Flores
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/Makefile:
--------------------------------------------------------------------------------
1 | # Minimal makefile for Sphinx documentation
2 | #
3 |
4 | # You can set these variables from the command line, and also
5 | # from the environment for the first two.
6 | SPHINXOPTS ?=
7 | SPHINXBUILD ?= sphinx-build
8 | SOURCEDIR = docs/source
9 | BUILDDIR = docs/build
10 |
11 | # Put it first so that "make" without argument is like "make help".
12 | help:
13 | @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)
14 |
15 | .PHONY: help Makefile
16 |
17 | make show:
18 | open docs/build/html/index.html
19 |
20 | make install:
21 | # New
22 | pip install -e /srv/pkg
23 | # old
24 | rm -f dist/*.tar.gz
25 | rm -f build/lib/streamlit_book/*.py
26 | rm -f /miniconda3/lib/python3.7/site-packages/streamlit_book/*.py
27 | python setup.py install
28 |
29 | version:
30 | nano streamlit_book/version_file.py
31 |
32 | test.pypi:
33 | rm -f dist/*.tar.gz
34 | python setup.py sdist
35 | python -m twine upload --repository testpypi dist/*
36 |
37 | pypi:
38 | rm -f dist/*.tar.gz
39 | python setup.py sdist
40 | python -m twine upload --repository pypi dist/*
41 |
42 | # Catch-all target: route all unknown targets to Sphinx using the new
43 | # "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS).
44 | %: Makefile
45 | @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)
46 |
--------------------------------------------------------------------------------
/NEEDS.md:
--------------------------------------------------------------------------------
1 | ADD to template.
2 |
3 | Makefile: create env, delete, activate, run, etc.
4 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # streamlit_book
2 |
3 | `streamlit_book` is a streamlit companion library, written in python+streamlit to create a interactive reader for the content on a given folder. It was developed on November 2021 during streamlit's hackathon - ended up being awarded one of the two best apps!
4 |
5 | ## Documentation
6 |
7 | All the documentation is at [readthedocs](https://streamlit-book.readthedocs.io/en/latest).
8 |
9 | ## Demos
10 |
11 | The list of all demos of the library for release 0.7.0. are:
12 |
13 | * [Demo Methods](https://stbook-methods.streamlit.app/): differente activities and methods.
14 | * [Demo Multipaging](https://stbook-multipaging.streamlit.app/): Different multipaging options (native and stbook).
15 |
16 | ## Examples
17 |
18 | Some apps using the library are:
19 |
20 | * [Happy Birds](https://notangrybirds.streamlit.app/) : A self contained example that mixes features of the library with a funny twist.
21 | * [The (confusion) Matrix](https://confusion-matrix.streamlit.app/): Take the blue pill to learn all about the confusion matrix.
22 | * [The Streamlitsaurus Rex](https://datasaurus.streamlit.app/): Will teach you to always visualize your data, and exhibits the mythical Datasaurus.
23 |
24 | ## How to use it
25 |
26 | Install it:
27 |
28 | ```bash
29 | pip install streamlit_book
30 | ```
31 |
32 | There are [different ways to use it](https://streamlit-book.readthedocs.io/en/latest/config.html), but in short just add to `streamlit_app.py` the function that list the files to be read (and other properties):
33 |
34 | ```python
35 | import streamlit as st
36 | import streamlit_book as stb
37 |
38 | # Streamlit page properties
39 | st.set_page_config()
40 |
41 | # Streamit book properties
42 | stb.set_book_config(menu_title="streamlit_book",
43 | menu_icon="lightbulb",
44 | options=[
45 | "What's new on v0.7.0?",
46 | "Core Features",
47 | ],
48 | paths=[
49 | "pages/00_whats_new.py", # single file
50 | "pages/01 Multitest", # a folder
51 | ],
52 | icons=[
53 | "code",
54 | "robot",
55 | ],
56 | save_answers=True,
57 | )
58 | ```
59 |
60 |
61 | ## Star History
62 |
63 | [](https://star-history.com/#sebastiandres/streamlit_book&Date)
64 |
--------------------------------------------------------------------------------
/docs/Makefile:
--------------------------------------------------------------------------------
1 | # Minimal makefile for Sphinx documentation
2 | #
3 |
4 | # You can set these variables from the command line, and also
5 | # from the environment for the first two.
6 | SPHINXOPTS ?=
7 | SPHINXBUILD ?= sphinx-build
8 | SOURCEDIR = source
9 | BUILDDIR = build
10 |
11 | # Put it first so that "make" without argument is like "make help".
12 | help:
13 | @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)
14 |
15 | .PHONY: help Makefile
16 |
17 | # Catch-all target: route all unknown targets to Sphinx using the new
18 | # "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS).
19 | %: Makefile
20 | @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)
21 |
--------------------------------------------------------------------------------
/docs/make.bat:
--------------------------------------------------------------------------------
1 | @ECHO OFF
2 |
3 | pushd %~dp0
4 |
5 | REM Command file for Sphinx documentation
6 |
7 | if "%SPHINXBUILD%" == "" (
8 | set SPHINXBUILD=sphinx-build
9 | )
10 | set SOURCEDIR=source
11 | set BUILDDIR=build
12 |
13 | if "%1" == "" goto help
14 |
15 | %SPHINXBUILD% >NUL 2>NUL
16 | if errorlevel 9009 (
17 | echo.
18 | echo.The 'sphinx-build' command was not found. Make sure you have Sphinx
19 | echo.installed, then set the SPHINXBUILD environment variable to point
20 | echo.to the full path of the 'sphinx-build' executable. Alternatively you
21 | echo.may add the Sphinx directory to PATH.
22 | echo.
23 | echo.If you don't have Sphinx installed, grab it from
24 | echo.http://sphinx-doc.org/
25 | exit /b 1
26 | )
27 |
28 | %SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O%
29 | goto end
30 |
31 | :help
32 | %SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O%
33 |
34 | :end
35 | popd
36 |
--------------------------------------------------------------------------------
/docs/source/_images/multiple_choice.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sebastiandres/streamlit_book/65e6e1896253d0a326f4d3e7af143aabc0139c76/docs/source/_images/multiple_choice.gif
--------------------------------------------------------------------------------
/docs/source/_images/share.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sebastiandres/streamlit_book/65e6e1896253d0a326f4d3e7af143aabc0139c76/docs/source/_images/share.png
--------------------------------------------------------------------------------
/docs/source/_images/single_choice.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sebastiandres/streamlit_book/65e6e1896253d0a326f4d3e7af143aabc0139c76/docs/source/_images/single_choice.gif
--------------------------------------------------------------------------------
/docs/source/_images/to_do_list.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sebastiandres/streamlit_book/65e6e1896253d0a326f4d3e7af143aabc0139c76/docs/source/_images/to_do_list.gif
--------------------------------------------------------------------------------
/docs/source/_images/true_or_false.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sebastiandres/streamlit_book/65e6e1896253d0a326f4d3e7af143aabc0139c76/docs/source/_images/true_or_false.gif
--------------------------------------------------------------------------------
/docs/source/conf.py:
--------------------------------------------------------------------------------
1 | # Configuration file for the Sphinx documentation builder.
2 | #
3 | # This file only contains a selection of the most common options. For a full
4 | # list see the documentation:
5 | # https://www.sphinx-doc.org/en/master/usage/configuration.html
6 |
7 | # -- Path setup --------------------------------------------------------------
8 |
9 | # If extensions (or modules to document with autodoc) are in another directory,
10 | # add these directories to sys.path here. If the directory is relative to the
11 | # documentation root, use os.path.abspath to make it absolute, like shown here.
12 | #
13 | import os
14 | import sys
15 | sys.path.insert(0, os.path.abspath('../../'))
16 | sys.path.insert(0, os.path.abspath('../../streamlit_book/'))
17 |
18 | print("sys.path:", sys.path)
19 | # -- Project information -----------------------------------------------------
20 |
21 | project = 'streamlit_book'
22 | copyright = '2022, Sebastian Flores Benner'
23 | author = 'Sebastian Flores Benner'
24 |
25 | # The full version, including alpha/beta/rc tags
26 | # Note: We import this to only define this in the src folder
27 |
28 | # -- General configuration ---------------------------------------------------
29 |
30 | # Read the Docs will set master doc to index instead
31 | # (or whatever it is you have specified in your settings).
32 | # See: https://stackoverflow.com/questions/56336234/build-fail-sphinx-error-contents-rst-not-found
33 | master_doc = 'index'
34 |
35 | # Add any Sphinx extension module names here, as strings. They can be
36 | # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom
37 | # ones.
38 | extensions = ['sphinx.ext.autodoc', 'sphinx.ext.autosectionlabel']
39 |
40 | # Add any paths that contain templates here, relative to this directory.
41 | templates_path = ['_templates']
42 |
43 | # List of patterns, relative to source directory, that match files and
44 | # directories to ignore when looking for source files.
45 | # This pattern also affects html_static_path and html_extra_path.
46 | exclude_patterns = []
47 |
48 |
49 | # -- Options for HTML output -------------------------------------------------
50 |
51 | # The theme to use for HTML and HTML Help pages. See the documentation for
52 | # a list of builtin themes.
53 | #
54 |
55 | html_theme = 'sphinx_rtd_theme'
56 | #html_theme = 'sphinx_documatt_theme'
57 |
58 | # Add any paths that contain custom static files (such as style sheets) here,
59 | # relative to this directory. They are copied after the builtin static files,
60 | # so a file named "default.css" will overwrite the builtin "default.css".
61 | html_static_path = ['_static']
62 |
63 | add_module_names = False
--------------------------------------------------------------------------------
/docs/source/goodies.rst:
--------------------------------------------------------------------------------
1 | Other goodies
2 | ===============================================
3 |
4 | But wait, there's more!
5 |
6 | .. include:: goodies_colored_expanders.rst
7 |
8 | .. include:: goodies_echo.rst
9 |
10 | .. include:: goodies_share.rst
11 |
--------------------------------------------------------------------------------
/docs/source/goodies_colored_expanders.rst:
--------------------------------------------------------------------------------
1 | Colored Expanders
2 | --------------------
3 |
4 | More colors to your expander!!! You just have to start the expander header with the right keyword.
5 |
6 | .. raw:: html
7 |
8 |
9 |
10 |
--------------------------------------------------------------------------------
/docs/source/goodies_echo.rst:
--------------------------------------------------------------------------------
1 | Echo
2 | ------
3 |
4 | The same old echo function from streamlit, but with an additional boolean parameter to control whether the output is shown or not. Intended to be paired with a checkbox, as shown on the example.
5 |
6 | .. autofunction:: __init__.echo
7 |
8 | **Interactive example:**
9 |
10 | .. raw:: html
11 |
12 |
13 |
14 |
15 |
16 |
--------------------------------------------------------------------------------
/docs/source/goodies_share.rst:
--------------------------------------------------------------------------------
1 | Share on Social Media
2 | -----------------------
3 |
4 | You can create share buttons for social media. This is based on the code I found at https://sharingbuttons.io/,
5 | which after a couple of hours of googling turned out to be the simplest way to share the content.
6 | Kudos to creator. It has some minor modifications, but it's mostly a wrapping of the code for stremlit.
7 |
8 | It requires as arguments the message and the url.
9 |
10 | .. autofunction:: __init__.share
11 |
12 | .. raw:: html
13 |
14 |
15 |
16 |
--------------------------------------------------------------------------------
/docs/source/index.rst:
--------------------------------------------------------------------------------
1 | Welcome
2 | ===============================================
3 |
4 | **streamlit_book** is an extension to the library streamlit, allowing an easier creation of streamlit apps with multipages and interactive activities.
5 | Jump to the introduction to see some examples or go to the documentation to see how to use it.
6 |
7 | .. toctree::
8 | :maxdepth: 2
9 | :caption: Content
10 |
11 | intro
12 | install
13 | multipaging
14 | multipaging_documentation
15 | interactive_activities
16 | goodies
17 | roadmap
--------------------------------------------------------------------------------
/docs/source/install.rst:
--------------------------------------------------------------------------------
1 | Installation
2 | ====================
3 |
4 | The repository for the code is hosted at ``_.
5 |
6 | From pypi
7 | ------------
8 |
9 | You can install the library from `pypi `_.
10 | This is the safe way. Don't stray from this path.
11 |
12 | .. code-block:: bash
13 |
14 | pip install streamlit_book
15 |
16 | On the `requirements.txt` file it should be simply the line:
17 |
18 | .. code-block:: none
19 |
20 | streamlit_book
21 |
22 | Or, optionally, for the specific version X.Y.Z:
23 |
24 | .. code-block:: none
25 |
26 | streamlit_book==X.Y.Z
27 |
28 | From repository
29 | --------------------
30 |
31 | You can install the library directly from the latest available version on github.
32 | This is good for testing the library, but might encounter some bugs, in which case you should let us know!
33 |
34 | .. code-block:: bash
35 |
36 | pip install git+https://github.com/sebastiandres/streamlit_book.git
37 |
38 | On the `requirements.txt` file it should be simply the line:
39 |
40 | .. code-block:: none
41 |
42 | git+https://github.com/sebastiandres/streamlit_book.git
43 |
--------------------------------------------------------------------------------
/docs/source/interactive_activities.rst:
--------------------------------------------------------------------------------
1 | Interactive Activities
2 | ===============================================
3 |
4 | There are different interactive activities to be used in your apps:
5 |
6 | .. include:: interactive_activities_true_or_false.rst
7 | .. include:: interactive_activities_single_choice.rst
8 | .. include:: interactive_activities_multiple_choice.rst
9 | .. include:: interactive_activities_to_do_list.rst
--------------------------------------------------------------------------------
/docs/source/interactive_activities_multiple_choice.rst:
--------------------------------------------------------------------------------
1 | Multiple choice question
2 | ----------------------------
3 |
4 | This functionality allows to ask a multiple choice question.
5 | It requires a question, the alternatives and the answer(s).
6 | Optionally, the texts for success, error and button can be customized.
7 |
8 | .. autofunction:: __init__.multiple_choice
9 |
10 | .. raw:: html
11 |
12 |
13 |
--------------------------------------------------------------------------------
/docs/source/interactive_activities_single_choice.rst:
--------------------------------------------------------------------------------
1 | Single choice question
2 | ------------------------
3 |
4 | This functionality allows to ask a single choice question.
5 | It requires a question, the alternatives and the (unique) answer.
6 | Optionally, the texts for success, error and button can be customized.
7 |
8 | .. autofunction:: __init__.single_choice
9 |
10 | .. raw:: html
11 |
12 |
13 |
--------------------------------------------------------------------------------
/docs/source/interactive_activities_to_do_list.rst:
--------------------------------------------------------------------------------
1 | To do list
2 | -------------
3 |
4 | This functionality allows to create to-do list.
5 | It only has as optional argument the text for success.
6 |
7 | .. autofunction:: __init__.to_do_list
8 |
9 | .. raw:: html
10 |
11 |
12 |
--------------------------------------------------------------------------------
/docs/source/interactive_activities_true_or_false.rst:
--------------------------------------------------------------------------------
1 | True or False question
2 | --------------------------
3 |
4 | This functionality allows to ask a true/false type of question.
5 | It requires a question and the solution as a True/False value.
6 | Optionally, the texts for success, error and button can be customized.
7 |
8 | .. autofunction:: __init__.true_or_false
9 |
10 | .. raw:: html
11 |
12 |
13 |
--------------------------------------------------------------------------------
/docs/source/intro.rst:
--------------------------------------------------------------------------------
1 | Introduction
2 | ============
3 |
4 | streamlit_book provide tools to create interactive multipaged apps
5 | on streamlit apps using python and simple folder convention.
6 |
7 | Features
8 | ----------
9 |
10 | * Reads and renders python files of the provided folders.
11 | * Multipaging compatible with native streamlit multipage. It can renders chapters (previous, next and reload buttons) or a book (collection of chapters).
12 | * Interactive activities: true or false question, single choice question, multiple choice question and to do list.
13 | * User answers can (optionally) be saved.
14 | * User's token to persist a session.
15 | * Admin view: stats on users and saved answers.
16 |
17 | Use streamlit_book to create more rich and interactive apps, activities and quizzes!
18 |
19 | Examples
20 | ----------
21 |
22 | * `Happy Birds `_ : A self contained example that mixes features of the library with a funny twist.
23 | * `The (confusion) Matrix `_: Take the blue pill to learn all about the confusion matrix.
24 | * `The Streamlitsaurus Rex `_: Will teach you to always visualize your data, and exhibits the mythical Datasaurus.
25 | * `SatSchool `_: interactive app how to manipulate and understand data from satellites in a range of environmental contexts.
26 | * `Template for multipaging `_: A template for a multipage app using streamlit_book, with public and private sections.
27 | * `stbook-methods.streamlitapp.com `_ : All the activities and goodies in streamlit_book!
28 | * `stbook-multipaging.streamlitapp.com `_ : How to do multipage with streamlit book!
29 |
30 | Are you using streamlit_book? `Let me know `_ and I will add it!
31 |
--------------------------------------------------------------------------------
/docs/source/multipaging.rst:
--------------------------------------------------------------------------------
1 | Multipaging: A short guide
2 | ===============================================
3 |
4 | There are ways to have a multipage content
5 |
6 | .. include:: multipaging_native.rst
7 | .. include:: multipaging_single.rst
8 | .. include:: multipaging_chapter.rst
9 | .. include:: multipaging_book.rst
10 |
--------------------------------------------------------------------------------
/docs/source/multipaging_book.rst:
--------------------------------------------------------------------------------
1 | Book: Having several chapters
2 | -------------------------------
3 |
4 | Requires a sidebar menu (like this demo), where each topic required a previous/next buttons.
5 |
6 | Use `stb.set_book_config`` to set the path and the configuration for the book.
7 |
8 |
9 | .. code-block:: python
10 |
11 | import streamlit as st
12 | import streamlit_book as stb
13 |
14 | # Streamlit webpage properties
15 | st.set_page_config()
16 |
17 | # Streamit book properties
18 | stb.set_book_config(menu_title="streamlit_book",
19 | menu_icon="lightbulb",
20 | options=[
21 | "What's new on v0.7.0?",
22 | "Core Features",
23 | ],
24 | paths=[
25 | "pages/00_whats_new.py", # single file
26 | "pages/01 Multitest", # a folder
27 | ],
28 | icons=[
29 | "code",
30 | "robot",
31 | ],
32 | save_answers=True,
33 | )
34 |
35 | Using the function `set_book_config`:
36 |
37 | * Will setup a sidebar menu with each of the chapter.
38 | * Each chapter is build with pagination for the provided file/folder.
39 |
40 | The capability to render several chapters was added in version 0.7.0, and makes a direct use of
41 | an awesome library to add a sidebar menu called (`streamlit_option_menu `_).
42 | Kudos to the creator. It delivers a professional look, and allows to add `icons by name `_ makes it a lot more user-friendly.
43 |
44 | See the function `set_book_config` required and optional parameters on the :ref:`Multipaging Documentation`.
45 |
46 | .. raw:: html
47 |
48 |
49 |
--------------------------------------------------------------------------------
/docs/source/multipaging_chapter.rst:
--------------------------------------------------------------------------------
1 | Chapter: A single document with multiple connected pages
2 | ----------------------------------------------------------
3 |
4 | * **Problem**: You want cute multipages, but you only need previous/next buttons.
5 | * **Solution**: Use method `set_chapter_config`` to set the path and other chapter configurations.
6 |
7 |
8 | .. code-block:: python
9 |
10 | import streamlit as st
11 | import streamlit_book as stb
12 |
13 | # Set wide display
14 | st.set_page_config()
15 |
16 | # Set multipage
17 | stb.set_chapter_config(path="pages/", save_answers=True)
18 |
19 | Using the function `set_chapter_config`:
20 |
21 | * Will setup the page navigation text/icons for a unique chapter.
22 | * Will sort the python and markdown files of the given path on lexigraphic order.
23 | * Will read and render the files into streamlit, allowing a enriched markdown/python of the implemented activities.
24 |
25 | See the function `set_chapter_config` required and optional parameters on the :ref:`Multipaging Documentation`.
26 |
27 | .. raw:: html
28 |
29 |
30 |
--------------------------------------------------------------------------------
/docs/source/multipaging_documentation.rst:
--------------------------------------------------------------------------------
1 | .. Multipaging Documentation:
2 |
3 | Multipaging Documentation
4 | -------------------------------
5 |
6 | .. autofunction:: __init__.set_book_config
7 |
8 | .. autofunction:: __init__.set_chapter_config
9 |
10 |
11 |
12 |
--------------------------------------------------------------------------------
/docs/source/multipaging_native.rst:
--------------------------------------------------------------------------------
1 | Native multipaging
2 | ---------------------
3 |
4 | * **Problem**: You don't want streamlit_book multipaging, but streamlit's native multipaging.
5 | * **Solution**: Just pust your files on `pages/` and import streamlit_book on the files where you need them. You can mix streamlit_book and streamlit's native multipaging.
6 |
7 | .. raw:: html
8 |
9 |
10 |
--------------------------------------------------------------------------------
/docs/source/multipaging_single.rst:
--------------------------------------------------------------------------------
1 | Single page
2 | -------------
3 |
4 | * **Problem**: You have one page to show, but you want to use the activities/questions provided on streamlit_book.
5 | * **Solution**: Just import the library streamlit_book without the setup and call the required functions.
6 |
7 | .. code-block:: python
8 |
9 | import streamlit as st
10 | import streamlit_book as stb
11 |
12 | st.set_page_config()
13 |
14 | # No need to initialize the streamlit_book library
15 | stb.true_or_false("Are you a robot?", False)
16 |
17 | .. raw:: html
18 |
19 |
20 |
--------------------------------------------------------------------------------
/docs/source/roadmap.rst:
--------------------------------------------------------------------------------
1 | Roadmap
2 | ============
3 |
4 | This is the roadmap for the next version of the library.
5 |
6 | * Preserving answers to questions - it currently resets the answers to the default values, even if answers have been provided/saved.
7 | * Setting a limit number of answers to a question - it currently allows unlimited answers.
8 | * Reimplement the Table of Contents.
9 | * Save when a user has accepted (do not repeat the dismiss).
10 | * More activities: Text entry, Code entry, File upload, ...
--------------------------------------------------------------------------------
/examples/01_to_do_list.md:
--------------------------------------------------------------------------------
1 | stb.to_do
2 | A (completely optional) description:
3 | - [x] task 1
4 | - [ ] task 2
5 | - [ ] task 3
6 | success: Congrats! You did it!
--------------------------------------------------------------------------------
/examples/02_true_or_false.md:
--------------------------------------------------------------------------------
1 | stb.true_or_false
2 | Is "Indiana Jones and the Last Crusade" one of the best movies?
3 | True
4 | success: You have chosen wisely
5 | failure: You chose poorly
6 | button: You must choose
--------------------------------------------------------------------------------
/examples/03_single_choice.md:
--------------------------------------------------------------------------------
1 | stb.single_choice
2 | Which is the current version of streamlit?
3 | - 0.0.1
4 | - 0.5.5
5 | + 1.2.0
6 | - 9.9.9
7 | success: Correct! As of Nov 13th, 2021, the current version of streamlit is 1.2.0.
8 | error: Wrong. Check the web!
9 | button: Check!
--------------------------------------------------------------------------------
/examples/04_multiple_choice.md:
--------------------------------------------------------------------------------
1 | stb.multiple-choice
2 | Which of the following are python libraries?
3 | [T] streamlit
4 | [F] pikachu
5 | [T] numpy
6 | [F] psyduck
7 | [T] matplotlib
--------------------------------------------------------------------------------
/images/avatar.jpeg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sebastiandres/streamlit_book/65e6e1896253d0a326f4d3e7af143aabc0139c76/images/avatar.jpeg
--------------------------------------------------------------------------------
/images/github.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sebastiandres/streamlit_book/65e6e1896253d0a326f4d3e7af143aabc0139c76/images/github.png
--------------------------------------------------------------------------------
/images/linkedin.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sebastiandres/streamlit_book/65e6e1896253d0a326f4d3e7af143aabc0139c76/images/linkedin.png
--------------------------------------------------------------------------------
/images/twitter.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sebastiandres/streamlit_book/65e6e1896253d0a326f4d3e7af143aabc0139c76/images/twitter.png
--------------------------------------------------------------------------------
/requirements.txt:
--------------------------------------------------------------------------------
1 | streamlit
2 | git+https://github.com/sebastiandres/streamlit_book.git
3 | #streamlit_book==0.7.3
--------------------------------------------------------------------------------
/setup.py:
--------------------------------------------------------------------------------
1 | from pathlib import Path
2 |
3 | import setuptools
4 |
5 | this_directory = Path(__file__).parent
6 | long_description = (this_directory / "README.md").read_text()
7 |
8 | setuptools.setup(
9 | name="streamlit-slides",
10 | version="0.7.8",
11 | author="Sebastián Flores Benner",
12 | author_email="sebastiandres@gmail.com",
13 | description="A streamlit companion library to create a interactive reader for the content on a given folder",
14 | long_description=long_description,
15 | long_description_content_type="text/markdown",
16 | url="https://github.com/sebastiandres/streamlit_book",
17 | packages=setuptools.find_packages(),
18 | include_package_data=True,
19 | classifiers=[],
20 | python_requires=">=3.7",
21 | install_requires=[
22 | "streamlit >= 0.63",
23 | 'streamlit-option-menu',
24 | ],
25 | extras_require={
26 | "devel": [
27 | "wheel",
28 | "pytest==7.4.0",
29 | "playwright==1.36.0",
30 | "requests==2.31.0",
31 | "pytest-playwright-snapshot==1.0",
32 | "pytest-rerunfailures==12.0",
33 | ]
34 | }
35 | )
--------------------------------------------------------------------------------
/stb_examples/README.md:
--------------------------------------------------------------------------------
1 | The stb_examples folder contains two typical examples of how to use streamlit-book.
2 | In order to test the examples on their own, and to avoid duplication, these are separated repos
3 | that should be downloaded by the interested contributor.
4 |
5 | Methods:
6 | ```
7 | cd stb_examples
8 | git clone git@github.com:sebastiandres/stbook_methods.git
9 | ```
10 |
11 | Multipaging:
12 | ```
13 | cd stb_examples
14 | git clone git@github.com:sebastiandres/stbook_multipaging.git
15 | ```
16 |
--------------------------------------------------------------------------------
/stbook_methods.py:
--------------------------------------------------------------------------------
1 | import importlib
2 | from stb_examples.stbook_methods import streamlit_app
3 |
4 | streamlit_app.main()
--------------------------------------------------------------------------------
/streamlit_book/__init__.py:
--------------------------------------------------------------------------------
1 | import os
2 | import sys
3 | sys.path.insert(0, os.path.abspath('.'))
4 |
5 | # The version of the library
6 | try:
7 | ### The main function to be used to read the files
8 | from chapter_config import set_chapter_config
9 | from book_config import set_book_config
10 | # General rendering
11 | from render import render_file
12 | # Some additional interesting functions
13 | from render_true_or_false import true_or_false
14 | from render_to_do_list import to_do_list
15 | from render_single_choice import single_choice
16 | from render_multiple_choice import multiple_choice
17 | from render_text_input import text_input
18 | from render_code_input import code_input
19 | # Other goodies
20 | from social_media import share
21 | from colored_expanders import add_color_to_expanders
22 | from floating_button import floating_button
23 | from echo import echo
24 | except:
25 | ### The main function to be used to read the files
26 | from .chapter_config import set_chapter_config
27 | from .book_config import set_book_config
28 | # General rendering
29 | from .render import render_file
30 | # Some additional interesting functions
31 | from .render_true_or_false import true_or_false
32 | from .render_to_do_list import to_do_list
33 | from .render_single_choice import single_choice
34 | from .render_multiple_choice import multiple_choice
35 | from .render_text_input import text_input
36 | from .render_code_input import code_input
37 | # Other goodies
38 | from .social_media import share
39 | from .colored_expanders import add_color_to_expanders
40 | from .floating_button import floating_button
41 | from .echo import echo
42 |
43 |
44 |
45 |
--------------------------------------------------------------------------------
/streamlit_book/admin.py:
--------------------------------------------------------------------------------
1 | import streamlit as st
2 | import os
3 | import pandas as pd
4 | import altair as alt
5 | try:
6 | from keywords import USERS_FILENAME, ANSWER_FILENAME
7 | from helpers import password_login, download_file, get_git_revision_short_hash
8 | except:
9 | from .keywords import USERS_FILENAME, ANSWER_FILENAME, ADMIN_PASSWORD
10 | from .helpers import password_login, download_file, get_git_revision_short_hash
11 |
12 | def admin_page():
13 | """"
14 | Shows a password page to the admin.
15 | """
16 | if "password_correct" not in st.session_state or not st.session_state["password_correct"]:
17 | admin_login_page()
18 | else:
19 | c1, c2 = st.columns(2)
20 | c1.title("Admin page")
21 | # Show configuration
22 | options = ["Configuration", "User info", "Answer info"]
23 | option = c2.selectbox("Choose an option", options)
24 | # Show users
25 | if option == options[0]:
26 | configuration()
27 | if option == options[1]:
28 | user_info()
29 | if option == options[2]:
30 | answers_info()
31 | return
32 |
33 | def configuration():
34 | """
35 | Shows available info on users.
36 | """
37 | st.header("Configuration")
38 | c1, c2, c3 = st.columns(3)
39 | c1.metric(label="Save answers?", value=f"{st.session_state.save_answers}")
40 | commit_hash = get_git_revision_short_hash()
41 | c2.metric(label="Current commit", value=f"{commit_hash}")
42 |
43 |
44 | def user_info():
45 | """
46 | Shows available info on users.
47 | """
48 | if os.path.exists(USERS_FILENAME):
49 | # If file exists, read the file
50 | df = pd.read_csv(USERS_FILENAME)
51 | # Provide some basic stats
52 | n_users = len(df)
53 | first_user = df.datetime.min()
54 | last_user = df.datetime.max()
55 | c1, c2, c3 = st.columns(3)
56 | c1.metric(label="# users", value=f"{n_users}")
57 | c2.metric(label="First user created on", value=f"{first_user}")
58 | c3.metric(label="Last user created on", value=f"{last_user}")
59 | # Plot the users by day
60 | df['day'] = pd.to_datetime(df['datetime'], format="%Y-%m-%d %H:%M:%S").dt.strftime('%Y-%m-%d')
61 | alt_df = df.groupby("day").count().rename(columns={"user_id": "# users"}).reset_index()
62 | alt_plot = alt.Chart(alt_df).mark_line(point=alt.OverlayMarkDef(color="blue")).encode(x="day", y="# users")
63 | #st.write(alt_df)
64 | st.altair_chart(alt_plot)
65 | # Allow to download the file
66 | download_file(df, button_label="Download user data", filename="users.csv")
67 | # Show the file or a graph
68 | #st.write(df)
69 | else:
70 | st.warning("No users file found.")
71 |
72 | def answers_info():
73 | """
74 | Shows available info on answers.
75 | """
76 | # Show answers
77 | if os.path.exists(ANSWER_FILENAME):
78 | df = pd.read_csv(ANSWER_FILENAME)
79 | # Provide some basic stats
80 | n_answers = len(df)
81 | n_unique_users = df["user_id"].nunique()
82 | n_unique_questions = df["question"].nunique()
83 | correctness = df["correct?"].mean()
84 | c1, c2, c3, c4 = st.columns(4)
85 | c1.metric(label="# answers", value=f"{n_answers}")
86 | c2.metric(label="# users on answers", value=f"{n_unique_users}")
87 | c3.metric(label="# questions", value=f"{n_unique_questions}")
88 | c4.metric(label="Average correct?", value=f"{100*correctness}%")
89 | # Plot the users by day
90 | df['day'] = pd.to_datetime(df['datetime'], format="%Y-%m-%d %H:%M:%S").dt.strftime('%Y-%m-%d')
91 | alt_df = df.groupby("day").count().rename(columns={"user_id": "# users"}).reset_index()
92 | alt_plot = alt.Chart(alt_df).mark_line(point=alt.OverlayMarkDef(color="blue")).encode(x="day", y="# users")
93 | #st.write(alt_df)
94 | st.altair_chart(alt_plot)
95 | # Allow to download the file
96 | c1, c2 = st.columns(2)
97 | df_last_answer = df.drop_duplicates(keep="last")
98 | download_file(df, "Download all answers", "all_answers.csv", where=c1)
99 | download_file(df_last_answer, "Download last answers", "last_answers.csv", where=c2)
100 | # Show the file or a graph
101 | #st.write(df)
102 | else:
103 | st.warning("No answers file found.")
104 |
105 | def admin_login_page():
106 | """"
107 | Shows a password page to the admin.
108 | """
109 | st.title("Admin Login")
110 | st.write("Please enter the password to access the admin page.")
111 | password_login("admin")
--------------------------------------------------------------------------------
/streamlit_book/answers.py:
--------------------------------------------------------------------------------
1 | import streamlit as st
2 | import os
3 |
4 | try:
5 | from helpers import get_datetime_string, get_git_revision_short_hash
6 | from keywords import ANSWER_FILENAME
7 | except:
8 | from .helpers import get_datetime_string, get_git_revision_short_hash
9 | from .keywords import ANSWER_FILENAME
10 |
11 | def create_answer_file():
12 | if not os.path.exists(ANSWER_FILENAME):
13 | with open(ANSWER_FILENAME, "w") as f:
14 | f.write("commit_hash,datetime,user_id,question,correct?,user_answer,correct_answer\n")
15 | return
16 |
17 | def save_answer(question, is_correct, user_answer, correct_answer):
18 | """
19 | Save the answer to the question.
20 | """
21 | # Parameters
22 | create_answer_file()
23 | commit_hash = st.session_state.commit_hash
24 | dt_string = get_datetime_string()
25 | user_id = st.session_state.user_id
26 | # Clean question and answers
27 | question = question.replace("\n", "\\n")
28 | user_answer = str(user_answer).replace("\n", "\\n")
29 | correct_answer = str(correct_answer).replace("\n", "\\n")
30 | with open(ANSWER_FILENAME, "a") as f:
31 | f.write(f'"{commit_hash}","{dt_string}","{user_id}","{question}","{is_correct}","{user_answer}","{correct_answer}"\n')
32 | return
--------------------------------------------------------------------------------
/streamlit_book/book_config.py:
--------------------------------------------------------------------------------
1 |
2 | import streamlit as st
3 | from glob import glob
4 | import os
5 | import pandas as pd
6 |
7 | try:
8 | from .file_reader import get_all_files, create_buttons
9 | from .login import create_new_user_and_token, get_user_from_token, get_token_from_user
10 | from .admin import admin_page
11 | from .helpers import get_git_revision_short_hash
12 | from .render import render_file
13 | from .chapter_config import set_chapter_config
14 | except Exception as e:
15 | print("Cannot import from . ", e)
16 | from file_reader import get_all_files, create_buttons
17 | from login import create_new_user_and_token, get_user_from_token, get_token_from_user
18 | from admin import admin_page
19 | from helpers import get_git_revision_short_hash
20 | from render import render_file
21 | from chapter_config import set_chapter_config
22 | except Exception as e:
23 | print("Cannot import! ", e)
24 |
25 | def set_book_config(options, paths,
26 | menu_title="Select a chapter",
27 | menu_icon="book",
28 | icons=None,
29 | orientation=None,
30 | styles=None,
31 | save_answers=False,
32 | display_page_info=True,
33 | ):
34 | """Creates a book using the streamlit_option_menu library.
35 | Renders each of the corresponding chapters based on their properties.
36 | Uses the same configurations used by `streamlit-option-menu
37 | `_
38 | and icons from `bootstrap-icons `_.
39 |
40 | :param options: List of chapter names to be displayed
41 | :type options: list of str
42 | :param paths: List of chapter paths containging the pages (py, md) to be displayed
43 | :type paths: list of str
44 | :param menu_title: Title of the menu, can be empty to be skipped.
45 | :type menu_title: str
46 | :param menu_icon: Icon to be used on the menu, from bootstrap icons.
47 | :type menu_icon: str
48 | :param icons: Icons to be used. Can be a single one used for all books, or a list of icons for each book.
49 | :type menu_icon: str or list of str
50 | :param orientation: Orientation of the menu. Can be "horizontal" or "vertical".
51 | :type orientation: str
52 | :param styles: Styles to be used. See the documentation of streamlit_option_menu.
53 | :type styles: dict
54 | :param save_answers: If True, it will save the answers in a csv file. Defaults to False.
55 | :type save_answers: bool
56 | :param display_page_info: If True, it will display the page info with the name and number. Defaults to True.
57 | :type display_page_info: bool
58 | :return: None
59 | """
60 | from streamlit_option_menu import option_menu
61 |
62 | # Initialize variables
63 | if "page_number" not in st.session_state:
64 | st.session_state.page_number = 0
65 | if "book_number" not in st.session_state:
66 | st.session_state.book_number = 0
67 |
68 | # Pack the arguments
69 | execution_dict = {
70 | "menu_title": menu_title,
71 | "options": options,
72 | }
73 | args = [menu_icon, icons, orientation, styles]
74 | if menu_icon is not None: execution_dict["menu_icon"] = menu_icon
75 | if icons is not None: execution_dict["icons"] = icons
76 | if orientation is not None: execution_dict["orientation"] = orientation
77 | if styles is not None: execution_dict["styles"] = styles
78 |
79 | # Execute the menu
80 | if orientation=="horizontal":
81 | selected_book_name = option_menu(**execution_dict)
82 | else:
83 | with st.sidebar:
84 | selected_book_name = option_menu(**execution_dict)
85 |
86 | # Update variables
87 | selected_book_number = options.index(selected_book_name)
88 | if st.session_state.book_number != selected_book_number:
89 | st.session_state.book_number = selected_book_number
90 | st.session_state.page_number = 0
91 |
92 | # Launch the corresponding chapter
93 | set_chapter_config(path=paths[selected_book_number], save_answers=save_answers, display_page_info=display_page_info)
94 |
--------------------------------------------------------------------------------
/streamlit_book/chapter_config.py:
--------------------------------------------------------------------------------
1 |
2 | import streamlit as st
3 | from glob import glob
4 | import os
5 | import pandas as pd
6 |
7 | try:
8 | from .file_reader import get_all_files, create_buttons
9 | from .login import create_new_user_and_token, get_user_from_token, get_token_from_user
10 | from .admin import admin_page
11 | from .helpers import get_git_revision_short_hash
12 | from .render import render_file
13 | except Exception as e:
14 | print("Cannot import from . ", e)
15 | from file_reader import get_all_files, create_buttons
16 | from login import create_new_user_and_token, get_user_from_token, get_token_from_user
17 | from admin import admin_page
18 | from helpers import get_git_revision_short_hash
19 | from render import render_file
20 | except Exception as e:
21 | print("Cannot import! ", e)
22 |
23 | def get_query_params():
24 | """
25 | Depending on streamlit's version, uses the previous or the new way to get the query params.
26 | Previous: st.experimental_get_query_params()
27 | New: st.query_params()
28 | """
29 | if hasattr(st, "query_params"):
30 | return st.query_params.to_dict()
31 | else:
32 | return st.experimental_get_query_params()
33 |
34 | def set_query_params(**kwargs):
35 | """
36 | Depending on streamlit's version, uses the previous or the new way to set the query params.
37 | Previous: st.experimental_set_query_params()
38 | New: st.set_query_params()
39 | """
40 | if hasattr(st, "query_params"):
41 | st.query_params.from_dict(**kwargs)
42 | else:
43 | st.experimental_set_query_params(**kwargs)
44 |
45 | def set_chapter_config(
46 | path="pages",
47 | toc=False,
48 | button="top",
49 | button_previous="⬅️",
50 | button_next="➡️",
51 | button_refresh="🔄",
52 | on_load_header=None,
53 | on_load_footer=None,
54 | save_answers=False,
55 | display_page_info=True,
56 | ):
57 | """Sets the book configuration, and displays the selected file.
58 |
59 | :param path: The path to root directory of the the files (py or md) to be rendered as pages of the book.
60 | :type path: string, dict
61 | :param toc: If True, it will display the table of contents for the files on the path. Defaults to False.
62 | :type toc: bool
63 | :param button: "top" (default behavior) or "bottom".
64 | :type button: str
65 | :param button_previous: icon or text for the previous button.
66 | :type button_previous: str
67 | :param button_next: icon or text for the next button.
68 | :type button_next: str
69 | :param button_refresh: icon or text for the refresh button.
70 | :type button_refresh: str
71 | :param on_load_header: function to be called before the page is loaded.
72 | :type on_load_header: function
73 | :param on_load_footer: function to be called after the page is loaded.
74 | :type on_load_footer: function
75 | :param save_answers: If True, it will save the answers in a csv file. Defaults to False.
76 | :type save_answers: bool
77 | :param display_page_info: If True, it will display the page info with the name and number. Defaults to True.
78 | :type display_page_info: bool
79 | :return: None
80 | """
81 | # Observation: File number goes from 0 to n-1.
82 |
83 | # Quick and dirty
84 | path = str(path)
85 |
86 | # Store the parameters
87 | if "save_answers" not in st.session_state:
88 | st.session_state.save_answers = save_answers
89 | if "commit_hash" not in st.session_state:
90 | st.session_state.commit_hash = get_git_revision_short_hash()
91 |
92 | # Admin Login: if requested by the user on the url, it redirects to a specific amin page
93 | # Never shows the content, it will stay on the admin page.
94 | query_params = get_query_params()
95 | if "user" in query_params and "admin" in query_params["user"]:
96 | # Here we handle everything related to the admin
97 | admin_page()
98 | return # Don't show anything else!
99 |
100 | # User login
101 | if "user_id" not in st.session_state:
102 | query_params = get_query_params()
103 | if "token" in query_params:
104 | # Here we must handle the user session
105 | token = query_params["token"][0] #Just consider the first one
106 | ## If the token is wrong, notify the user.
107 | user_id = get_user_from_token(token)
108 | if user_id is None:
109 | # Wrong token
110 | st.error("Wrong token! Use the regular url.")
111 | return
112 | else:
113 | # Correct token
114 | st.session_state.user_id = user_id
115 | st.session_state.token = token
116 | # Redirect to avoid the url with the token
117 | del query_params["token"]
118 | st.experimental_set_query_params(**query_params)
119 | else:
120 | # Token not in query params: create a new user and token
121 | user_id, token = create_new_user_and_token()
122 | st.session_state.user_id = user_id
123 | st.session_state.token = token
124 |
125 | # Save answers behavior
126 | if st.session_state.save_answers:
127 | if "warned_about_save_answers" not in st.session_state:
128 | def on_click():
129 | st.session_state.warned_about_save_answers = True
130 | c1, c2 = st.columns([8,1])
131 | user_url = f"?token={st.session_state.token}"
132 | c1.warning(f"Your answers will be saved. You can relaunch the app using the following custom url (Save it!).\n\n{user_url}")
133 | c2.markdown("\n\n")
134 | c2.markdown("\n\n")
135 | c2.button("Dismiss", on_click=on_click)
136 |
137 | # Get the files at path level (only files, not folders)
138 | file_list = get_all_files(path)
139 |
140 | # Check that we have at least 1 file to render
141 | if len(file_list) == 0:
142 | st.error(f"No files were found at the given path. Please check the provided path: {path}")
143 | return
144 |
145 | # Initialize the session state variables
146 | if "toc" not in st.session_state:
147 | st.session_state.toc = toc
148 |
149 | # Save button configuration
150 | if "button" not in st.session_state:
151 | st.session_state.button = button
152 |
153 | # Page number
154 | if "page_number" not in st.session_state:
155 | st.session_state.page_number = 0
156 |
157 | # Update file_fullpath
158 | selected_file_fullpath = file_list[st.session_state.page_number]
159 | caption_text = f"Page {st.session_state.page_number+1} of {st.session_state.total_files}. File: {selected_file_fullpath}"
160 |
161 | if st.session_state.toc:
162 | option = st.radio("Table of contents", options=file_list)
163 | st.session_state.page_number = file_list.index(option)
164 | st.button("Go to page", on_click=on_toc_table_click, key="gotopage")
165 | else:
166 | # Execute the on_load_header function
167 | if on_load_header:
168 | on_load_header()
169 |
170 | # If required, put the button on top of the page. Use columns for alignment
171 | if st.session_state.button in ["top", "both"]:
172 | create_buttons(caption_text,
173 | button_previous, button_next, button_refresh,
174 | display_page_info,
175 | key="top")
176 |
177 | # Render the file using the magic
178 | try:
179 | render_file(selected_file_fullpath)
180 | except Exception as e:
181 | st.exception(e)
182 |
183 | # If required, put the button on the bottom of the page. Use columns for alignment
184 | if st.session_state.button in ["bottom", "both"]:
185 | create_buttons(caption_text,
186 | button_previous, button_next, button_refresh,
187 | display_page_info,
188 | key="bottom")
189 |
190 | # Execute the on_load_footer function
191 | if on_load_footer:
192 | on_load_footer()
193 | return
--------------------------------------------------------------------------------
/streamlit_book/colored_expanders.py:
--------------------------------------------------------------------------------
1 | import streamlit as st
2 |
3 | def add_color_to_expanders():
4 | """
5 | Adds color to the expanders.
6 | Users don't need to call this function, is executed by default.
7 | """
8 | # Define your javascript
9 | my_js = """
10 | // Define the colors
11 | const color_dict = {
12 | "info": ["#EAF2FC", "#1D6777"],
13 | "success": ["#D7EED9", "#156C36"],
14 | "ok": ["#D7EED9", "#156C36"],
15 | "warning": ["#FFF4D9", "#947C2D"],
16 | "error": ["#FFDDDC", "#9D282C"]
17 | };
18 | // Get all the expander elements
19 | var expanderHeaders = window.parent.document.querySelectorAll('.streamlit-expanderHeader');
20 | var expanderContents = window.parent.document.querySelectorAll('.streamlit-expanderContent');
21 | for (var i = 0; i < expanderHeaders.length; i++) {
22 | let header = expanderHeaders[i];
23 | let content = expanderContents[i];
24 | text = header.innerText || header.textContent;
25 | // Check for text content
26 | for (let color in color_dict) {
27 | if (text.toLowerCase().startsWith(color)) {
28 | header.style.backgroundColor = color_dict[color][0];
29 | content.style.backgroundColor = color_dict[color][0];
30 | header.style.color = color_dict[color][1];
31 | content.style.color = color_dict[color][1];
32 | header.addEventListener("mouseover", function(event){header.style.color = 'red'})
33 | header.addEventListener("mouseout", function( event ) {header.style.color = color_dict[color][1]})
34 | }
35 | }
36 | }
37 | """
38 |
39 | # Wrapt the javascript as html code
40 | my_html = f""
41 |
42 | # Execute your app
43 | st.components.v1.html(my_html, height=0, width=0)
44 |
--------------------------------------------------------------------------------
/streamlit_book/echo.py:
--------------------------------------------------------------------------------
1 | import contextlib
2 | import re
3 | import textwrap
4 | import traceback
5 | from typing import List, Iterable, Optional
6 |
7 | _SPACES_RE = re.compile("\\s*")
8 | _EMPTY_LINE_RE = re.compile("\\s*\n")
9 |
10 | @contextlib.contextmanager
11 | def echo(code_location="above", show=True):
12 | """Whether to show the echoed code before or after the results of the executed code block.
13 | :param code_location: "above" or "below"
14 | :type lines: str
15 | :param show: Boolean to show or hide the code block
16 | :type lines: bool
17 | :return: None
18 | :rtype: none
19 | Copied and improved from `Streamlit's github `_
20 | """
21 |
22 | from streamlit import code, warning, empty, source_util
23 |
24 | if code_location == "below":
25 | show_code = code
26 | show_warning = warning
27 | else:
28 | placeholder = empty()
29 | show_code = placeholder.code
30 | show_warning = placeholder.warning
31 |
32 | try:
33 | # Get stack frame *before* running the echoed code. The frame's
34 | # line number will point to the `st.echo` statement we're running.
35 | frame = traceback.extract_stack()[-3]
36 | filename, start_line = frame.filename, frame.lineno
37 |
38 | # Read the file containing the source code of the echoed statement.
39 | with source_util.open_python_file(filename) as source_file:
40 | source_lines = source_file.readlines()
41 |
42 | # Get the indent of the first line in the echo block, skipping over any
43 | # empty lines.
44 | initial_indent = _get_initial_indent(source_lines[start_line:])
45 |
46 | # Iterate over the remaining lines in the source file
47 | # until we find one that's indented less than the rest of the
48 | # block. That's our end line.
49 | #
50 | # Note that this is *not* a perfect strategy, because
51 | # de-denting is not guaranteed to signal "end of block". (A
52 | # triple-quoted string might be dedented but still in the
53 | # echo block, for example.)
54 | # TODO: rewrite this to parse the AST to get the *actual* end of the block.
55 | lines_to_display: List[str] = []
56 | for line in source_lines[start_line:]:
57 | indent = _get_indent(line)
58 | if indent is not None and indent < initial_indent:
59 | break
60 | lines_to_display.append(line)
61 |
62 | code_string = textwrap.dedent("".join(lines_to_display))
63 |
64 | # Run the echoed code...
65 | yield
66 |
67 | # And draw the code string to the app!
68 | if show: # Improvement: only show if required!!!
69 | show_code(code_string, "python")
70 |
71 | except FileNotFoundError as err:
72 | show_warning("Unable to display code. %s" % err)
73 |
74 |
75 |
76 | def _get_initial_indent(lines: Iterable[str]) -> int:
77 | """Return the indent of the first non-empty line in the list.
78 | If all lines are empty, return 0.
79 | """
80 | for line in lines:
81 | indent = _get_indent(line)
82 | if indent is not None:
83 | return indent
84 |
85 | return 0
86 |
87 |
88 | def _get_indent(line: str) -> Optional[int]:
89 | """Get the number of whitespaces at the beginning of the given line.
90 | If the line is empty, or if it contains just whitespace and a newline,
91 | return None.
92 | """
93 | if _EMPTY_LINE_RE.match(line) is not None:
94 | return None
95 |
96 | match = _SPACES_RE.match(line)
97 | return match.end() if match is not None else 0
--------------------------------------------------------------------------------
/streamlit_book/file_reader.py:
--------------------------------------------------------------------------------
1 | import streamlit as st
2 | from glob import glob
3 | import os
4 | import pandas as pd
5 |
6 | try:
7 | from .render import render_file
8 | except:
9 | from render import render_file
10 |
11 | def on_toc_table_click():
12 | st.session_state.toc = False
13 | return
14 |
15 | def on_previous_click():
16 | """
17 | Updates the values of update_from_selectbox and update_from_button.
18 | Updates the page number to +1, or 0 if the last page was reached.
19 | """
20 | st.session_state.page_number = (st.session_state.page_number - 1) % st.session_state.total_files
21 | return
22 |
23 | def on_next_click():
24 | """
25 | Updates the values of update_from_selectbox and update_from_button.
26 | Updates the page number to +1, or 0 if the last page was reached.
27 | """
28 | st.session_state.page_number = (st.session_state.page_number + 1) % st.session_state.total_files
29 | return
30 |
31 | def on_refresh_click():
32 | return
33 |
34 | def create_buttons(caption_text,
35 | button_previous,
36 | button_next,
37 | button_refresh,
38 | display_page_info,
39 | key=""):
40 | """
41 | Function to create the navigation buttons.
42 | Only applies if more than 1 file is to be rendered.
43 | """
44 | # Skip if there is only one file
45 | if st.session_state.total_files <= 1:
46 | return
47 | # We have at least 2 files, so render the buttons
48 | if display_page_info:
49 | st.caption(caption_text)
50 | p = len(button_previous)
51 | n = len(button_next)
52 | r = len(button_refresh)
53 | b = max(p,n,r)
54 | # Dinamic resizing based on length of the button names
55 | # Would be even better if layout type was known
56 | c1, c2, c3, c4 = st.columns([b, b, b, max(b, 20-3*b)])
57 | if st.session_state.page_number!=0:
58 | c1.button(button_previous,
59 | help="Previous page", on_click=on_previous_click, key="previous_button_top"+key)
60 | if st.session_state.page_number!=st.session_state.total_files-1:
61 | c2.button(button_next,
62 | help="Next page", on_click=on_next_click, key="next_button_top"+key)
63 | c3.button(button_refresh,
64 | help="Refresh current page", on_click=on_refresh_click, key="switch_button_top"+key)
65 | return
66 |
67 | def get_all_files(path):
68 | """
69 | Returns a list of all files (python, markdown) in the given path,
70 | considering recursively all subfolders.
71 | The path can be a single file.
72 | It does not considers folders.
73 | Excludes files and folders starting with WIP (work in progress).
74 | It stores the total number of files (pages) in the session_state.
75 | """
76 | if path.endswith(".py"):
77 | py_files = [ path, ]
78 | else:
79 | py_files = glob(f"{path}/**/*.py", recursive=True)
80 | if path.endswith(".md"):
81 | md_files = [ path, ]
82 | else:
83 | md_files = glob(f"{path}/**/*.md", recursive=True)
84 | all_files = [_ for _ in sorted(py_files + md_files) if "/WIP" not in _]
85 | st.session_state.total_files = len(all_files)
86 | return all_files
87 |
88 | def is_file(label):
89 | """
90 | Checks if the given label is a file.
91 | """
92 | return label.endswith(".py") or label.endswith(".md")
93 |
94 | def get_options_at_path(user_selected_path: str, file_list: list):
95 | """
96 | Returns all the alternative items (folders and files) on the provided path
97 | """
98 | # Get the number of folders at the current level
99 | n_folders = len(user_selected_path.split("/"))
100 | # Get all the files at level
101 | subfolder_options = []
102 | for filepath in file_list:
103 | subfolder = "/".join(filepath.split("/")[:(n_folders+1)])
104 | if filepath.startswith(user_selected_path) and subfolder not in subfolder_options:
105 | subfolder_options.append(subfolder)
106 | options = [_.split("/")[-1] for _ in subfolder_options]
107 | return options
108 |
109 | def get_selection_box_args_at_path(user_selected_path: str, file_list: list, level: int, selected_option: str=""):
110 | """
111 | Search for items: folders, python files or markdown files.
112 | Only looks for files at the given path.
113 | It returns a dictionary, with the required keys and values required by the selectbox
114 | """
115 | # Get all options at certain level
116 | options = get_options_at_path(user_selected_path, file_list)
117 | # Get the current label index
118 | if selected_option:
119 | index = options.index(selected_option)
120 | else:
121 | index = 0
122 | # Get all options at certain level
123 | selection_box_args = {
124 | "label": f"Select {book_parts_convention(level)}:",
125 | "options": options,
126 | "index": index,
127 | "preset_option": options[index],
128 | "key": f"select_box_{level}",
129 | }
130 | return selection_box_args
131 |
132 | def get_selection_boxes_args_from_filepath(file_fullpath: str, base_path: str, file_list: list):
133 | """
134 | Creates a list of dictionaries, with the required keys and values required by the selectbox.
135 | """
136 | # The relative path are only the folders in the filepath not on the base_path (but not the file itself)
137 | relative_path = file_fullpath.replace(f"{base_path}/", "")
138 | # Get the intermediate folders, but not the file itself
139 | selected_labels = relative_path.split("/")
140 | # Initialize and populate the list of dictionaries
141 | selection_boxes_args = []
142 | working_path = base_path
143 | for level, label in enumerate(selected_labels):
144 | # Get the options of the selectbox
145 | selection_box_args = get_selection_box_args_at_path(working_path, file_list, level, label)
146 | selection_boxes_args.append(selection_box_args)
147 | # Increate the path
148 | working_path = working_path + "/" + label
149 | return selection_boxes_args
150 |
151 | def get_items(path: str):
152 | """
153 | # Search for folders, python files or markdown files.
154 | Only looks at precisely the level of the given path.
155 | It returns a dictionary, with keys the name to be rendered in the selectbox
156 | and with values another dictionary, with the type (file/folder) and the fullpath.
157 | """
158 | # Get all the files
159 | items_at_path = glob(f"{path}/*/") + glob(f"{path}/*.md") + glob(f"{path}/*.py")
160 | # Filter files that start with WIP (work in progress)
161 | items_at_path = [item for item in items_at_path if ("/WIP" not in item)]
162 | # Create
163 | item_dict = {}
164 | for i, my_item in enumerate(items_at_path):
165 | if my_item[-3:] in (".py", ".md"):
166 | item_name = my_item.split("/")[-1]
167 | render_name = item_name
168 | item_dict[render_name] = {"type":"file", "path":my_item}
169 | else:
170 | item_name = my_item.split("/")[-2]
171 | render_name = item_name
172 | item_dict[render_name] = {"type":"folder", "path":my_item}
173 | return item_dict
174 |
--------------------------------------------------------------------------------
/streamlit_book/floating_button.py:
--------------------------------------------------------------------------------
1 | import streamlit as st
2 |
3 | def add_floating_button(url, bootstrap_icon, icon_color, background_color):
4 | """
5 | Adds a floating button that opens a link to feedback.
6 | """
7 | url_html = f''
8 | button_html = """
9 |
11 |
24 | my_url
25 | """
26 | button_html = button_html.replace("my_background_color", background_color)
27 | button_html = button_html.replace("my_color", icon_color)
28 | button_html = button_html.replace("my_url", url_html)
29 | st.components.v1.html(button_html, height=100)
30 | return url, bootstrap_icon, icon_color, background_color
31 |
32 | def make_it_float(url, bootstrap_icon, icon_color, background_color):
33 | # Define your javascript
34 | my_js = """
35 | var containers = window.parent.document.querySelectorAll('.element-container');
36 | for (var i = 0; i < containers.length; i++) {
37 | //alert(i);
38 | let container = containers[i];
39 | let child = container.firstChild
40 | let name = child.tagName;
41 | if (name == 'IFRAME') {
42 | let docsrc = child.srcdoc;
43 | bool1 = docsrc.includes('my_url');
44 | bool2 = docsrc.includes('my_bootstrap_icon');
45 | bool3 = docsrc.includes('my_icon_color');
46 | bool4 = docsrc.includes('my_background_color');
47 | if (bool1 && bool2 && bool3 && bool4) {
48 | child.style.position = "fixed";
49 | child.style.bottom = "10px";
50 | child.style.right = "10px";
51 | child.style.zindex = "2000";
52 | }
53 | }
54 | }
55 | """
56 | my_js = my_js.replace("my_url", url)
57 | my_js = my_js.replace("my_bootstrap_icon", bootstrap_icon)
58 | my_js = my_js.replace("my_icon_color", icon_color)
59 | my_js = my_js.replace("my_background_color", background_color)
60 | # Wrapt the javascript as html code
61 | my_html = f""
62 | # Execute your app
63 | st.components.v1.html(my_html, height=0, width=2000)
64 |
65 | def floating_button(url, bootstrap_icon="chat-left-text-fill",
66 | icon_color="white", background_color="gray"):
67 | """
68 | """
69 | # Fix bootstrap icon if does not starts with "bi"
70 | if not bootstrap_icon.startswith("bi-"):
71 | bootstrap_icon = "bi-" + bootstrap_icon
72 | # Wrapt the javascript as html code
73 | properties = add_floating_button(url, bootstrap_icon, icon_color, background_color)
74 | make_it_float(*properties)
75 | return
--------------------------------------------------------------------------------
/streamlit_book/helpers.py:
--------------------------------------------------------------------------------
1 | from datetime import datetime
2 | import streamlit as st
3 | import subprocess
4 | import random
5 | import os
6 |
7 | try:
8 | from keywords import ADMIN_PASSWORD
9 | except:
10 | from .keywords import ADMIN_PASSWORD
11 |
12 | def password_entered():
13 | """
14 | Checks whether a password entered by the user is correct.
15 | Checks agains secrets (if defined) otherwise checks against hardcoded password.
16 | """
17 | # Get the password
18 | try:
19 | PASSWORD = st.secrets["ADMIN_PASSWORD"]
20 | except:
21 | PASSWORD = ADMIN_PASSWORD
22 | # Compare against password entered by the user
23 | if st.session_state["password"] == PASSWORD:
24 | st.session_state["password_correct"] = True
25 | del st.session_state["password"] # don't store password
26 | else:
27 | st.session_state["password_correct"] = False
28 | # Delete password from session state
29 | del PASSWORD
30 | return
31 |
32 | def password_login(user_id):
33 | if "password_correct" not in st.session_state:
34 | # First run, show input for password.
35 | st.text_input("Password", type="password", on_change=password_entered, key="password")
36 | elif not st.session_state["password_correct"]:
37 | # Password not correct, show input + error.
38 | st.text_input("Password", type="password", on_change=password_entered, key="password")
39 | st.error("😕 Password incorrect")
40 |
41 | def get_datetime_string():
42 | # datetime object containing current date and time
43 | dt_string = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
44 | return dt_string
45 |
46 | def get_git_revision_short_hash() -> str:
47 | """"
48 | Improved upon version found at
49 | https://stackoverflow.com/questions/14989858/get-the-current-git-hash-in-a-python-script/21901260#21901260"
50 | """
51 | try:
52 | return subprocess.check_output(['git', 'rev-parse', '--short', 'HEAD']).decode('ascii').strip()
53 | except:
54 | return "not_git"
55 |
56 | def download_file(df, button_label, filename, where=None):
57 | """
58 | """
59 | data_csv = df.to_csv().encode('utf-8')
60 | if where:
61 | btn = where.download_button(
62 | label=button_label,
63 | data=data_csv,
64 | file_name=filename,
65 | )
66 | else:
67 | btn = st.download_button(
68 | label=button_label,
69 | data=data_csv,
70 | file_name=filename,
71 | )
72 | return
73 |
74 | def get_token():
75 | t1 = get_random_string(3, "letters")
76 | t2 = get_random_string(3, "numbers")
77 | return t1 + t2
78 |
79 | def get_random_string(length, elements):
80 | if elements == "letters":
81 | elements = "abcdefghkmnopqrstxyz" # no confusing characters
82 | elif elements == "numbers":
83 | elements = "0123456789"
84 | else:
85 | elements = string.digits + string.ascii_letters
86 | # With combination of lower and upper case
87 | result_str = ''.join(random.choice(elements) for i in range(length))
88 | # print random string
89 | return result_str
--------------------------------------------------------------------------------
/streamlit_book/keywords.py:
--------------------------------------------------------------------------------
1 |
2 | def check_keyword(line, keyword):
3 | """
4 | Check if the line starts with the keyword
5 | Replaces - for _ for backwards compatibility.
6 | """
7 | new_line = line.replace("-", "_")
8 | return new_line.startswith(keyword)
9 |
10 | TODO_KEYWORD = "stb.to_do"
11 | TODO_COMPLETED = "- [x] "
12 | TODO_INCOMPLETE = "- [ ] "
13 | TODO_SUCCESS = "Bravo!"
14 |
15 | TRUE_FALSE_KEYWORD = "stb.true_or_false"
16 |
17 | SINGLE_CHOICE_KEYWORD = "stb.single_choice"
18 | SINGLE_CHOICE_CORRECT = "+ "
19 | SINGLE_CHOICE_WRONG = "- "
20 |
21 | MULTIPLE_CHOICE_KEYWORD = "stb.multiple_choice"
22 | MULTIPLE_CHOICE_TRUE = "[T] "
23 | MULTIPLE_CHOICE_FALSE = "[F] "
24 |
25 | TEXT_INPUT_KEYWORD = "stb.text_input"
26 |
27 | CODE_INPUT_KEYWORD = "stb.code_input"
28 |
29 | FILE_UPLOAD_KEYWORD = "stb.file_upload"
30 |
31 | SHARE_KEYWORD = "stb.share"
32 |
33 | BUTTON = "button:"
34 | DEFAULT_BUTTON_MESSAGE = "Check answer"
35 | SUCCESS = "success:"
36 | DEFAULT_SUCCESS_MESSAGE = "Correct answer"
37 | ERROR = "error:"
38 | DEFAULT_ERROR_MESSAGE = "Wrong answer"
39 |
40 | EXACT_TEXT = "equals:"
41 | CONTAINS_TEXT = "contains:"
42 | STARTS_WITH = "starts_with:"
43 | ENDS_WITH = "ends_with:"
44 |
45 | ASSERT = "assert:"
46 |
47 | USERS_FILENAME = "./tmp/users.csv"
48 | ANSWER_FILENAME = "./tmp/answers.csv"
49 | ADMIN_PASSWORD = "2lazy2change1password"
--------------------------------------------------------------------------------
/streamlit_book/login.py:
--------------------------------------------------------------------------------
1 | import os
2 | import pandas as pd
3 | import streamlit as st
4 |
5 | try:
6 | from helpers import get_datetime_string, download_file, get_token
7 | from keywords import USERS_FILENAME, ANSWER_FILENAME
8 | except:
9 | from .helpers import get_datetime_string, download_file, get_token
10 | from .keywords import USERS_FILENAME, ANSWER_FILENAME
11 |
12 | def create_user_df(user_id, token):
13 | """
14 | Create a dataframe with the user information.
15 | """
16 | dt_string = get_datetime_string()
17 | user_df = pd.DataFrame(columns=["user_id", "token", "datetime"], data=[[user_id, token, dt_string]])
18 | return user_df
19 |
20 | def get_user_from_token(query_token):
21 | df = pd.read_csv(USERS_FILENAME)
22 | m = df["token"] == query_token
23 | if m.any():
24 | user_for_token = df.user_id[m].values[0] # the first (and only) one
25 | return user_for_token
26 | else:
27 | return None
28 |
29 | def create_new_user_and_token():
30 | """
31 | Returns the user id and token.
32 | """
33 | # Create folder, if not there
34 | try:
35 | os.mkdir("./tmp/")
36 | except:
37 | pass
38 | # Check for user file, create if not there
39 | if not os.path.exists("./tmp/users.csv"):
40 | df0 = create_user_df(0,"abc123")
41 | df0.to_csv(USERS_FILENAME, index=False)
42 | # Take a new user
43 | df_users = pd.read_csv(USERS_FILENAME)
44 | user_id = df_users["user_id"].max() + 1
45 | user_token = get_token()
46 | df_new_user = create_user_df(user_id, user_token)
47 | df_users = pd.concat([df_users, df_new_user])
48 | df_users.to_csv(USERS_FILENAME, index=False)
49 | # Create the token
50 | return (user_id, user_token)
51 |
52 | def get_token_from_user(user_id):
53 | df = pd.read_csv(USERS_FILENAME)
54 | m = df["user_id"] == user_id
55 | if m.any():
56 | token = df.token[m].values[0]
57 | return token
58 | else:
59 | return None
--------------------------------------------------------------------------------
/streamlit_book/render.py:
--------------------------------------------------------------------------------
1 | import sys
2 | import types
3 |
4 | import streamlit as st
5 |
6 | try:
7 | from colored_expanders import add_color_to_expanders
8 | except:
9 | from .colored_expanders import add_color_to_expanders
10 |
11 |
12 | def _new_module(name: str) -> types.ModuleType:
13 | """Create a new module with the given name."""
14 | return types.ModuleType(name)
15 |
16 |
17 | def render_file(fullpath):
18 | """
19 | Renders the file (it's always a file and not a folder)
20 | given the fullpath (path and filename).
21 | Only admits python or markdown, also by construction.
22 | """
23 | fullpath_str = str(fullpath)
24 | if fullpath_str.endswith(".py"):
25 | # Create fake module. This gives us a name global namespace to
26 | # execute the code in.
27 | module = _new_module("__main__")
28 |
29 | # Install the fake module as the __main__ module. This allows
30 | # the pickle module to work inside the user's code, since it now
31 | # can know the module where the pickled objects stem from.
32 | # IMPORTANT: This means we can't use "if __name__ == '__main__'" in
33 | # our code, as it will point to the wrong module!!!
34 | sys.modules["__main__"] = module
35 |
36 | # Add special variables to the module's globals dict.
37 | # Note: The following is a requirement for the CodeHasher to
38 | # work correctly. The CodeHasher is scoped to
39 | # files contained in the directory of __main__.__file__, which we
40 | # assume is the main script directory.
41 | module.__dict__["__file__"] = fullpath
42 |
43 | # Execute as a regular python file
44 | with open(fullpath, "rb") as source_file:
45 | code = compile(source_file.read(), fullpath, "exec")
46 | exec(code, module.__dict__)
47 |
48 | add_color_to_expanders()
49 | elif fullpath_str.endswith(".md"):
50 | with open(fullpath, "r") as source_file:
51 | code = source_file.read()
52 | st.markdown(code)
53 | add_color_to_expanders()
54 | else:
55 | st.warning(f" ah: File extention not supported for file {fullpath}")
56 |
--------------------------------------------------------------------------------
/streamlit_book/render_code_input.py:
--------------------------------------------------------------------------------
1 | import streamlit as st
2 |
3 | try:
4 | from keywords import *
5 | from answers import save_answer
6 | except:
7 | from .keywords import *
8 | from .answers import save_answer
9 |
10 | def code_input(question, initial_code,
11 | contains=[],
12 | equals="",
13 | normalization_function=None,
14 | verification_function=None,
15 | success=DEFAULT_SUCCESS_MESSAGE,
16 | error=DEFAULT_ERROR_MESSAGE,
17 | button=DEFAULT_BUTTON_MESSAGE):
18 | """Render a text input question from the given parameters.
19 |
20 | :param question: question to be displayed before the multiple-choice options
21 | :type question: str
22 | :param initial_code:
23 | :type initial_code: str
24 | :param contains:
25 | :type contains: list of str
26 | :param equals:
27 | :type equals: str
28 | :param normalization_function:
29 | :type normalization_function: function
30 | :param verification_function:
31 | :type verification_function: function
32 | :param success: message to be displayed when the user answers correctly
33 | :type success: str, optional
34 | :param error: message to be displayed when the user answers incorrectly
35 | :type error: str, optional
36 | :param button: message to be displayed on the button that checks the answer
37 | :type button: str, optional
38 | :return: tuple of booleans with button press status and correctness of answer
39 | :rtype: tuple of bool
40 | """
41 | user_code = st.text_input(question, initial_code)
42 | # Check if the correct checkboxes are checked
43 | key = ("text-input:" + question + "/" + initial_code).lower().replace(" ", "_")
44 | if st.button(button, key=key):
45 | # Check answers
46 | if user_code == equals:
47 | if success:
48 | st.success(success)
49 | is_correct = True
50 | else:
51 | if error:
52 | st.error(error)
53 | is_correct = False
54 | # Save the answers, if required
55 | if "save_answers" in st.session_state and st.session_state.save_answers:
56 | correct_answer = "Depends, dynamically generated"
57 | save_answer(question, is_correct=is_correct, user_answer=user_code, correct_answer=correct_answer)
58 | # Return if button is pressed and answer evaluation
59 | return True, is_correct
60 | else:
61 | # Return if button is not pressed
62 | return False, None
--------------------------------------------------------------------------------
/streamlit_book/render_multiple_choice.py:
--------------------------------------------------------------------------------
1 | import streamlit as st
2 |
3 | try:
4 | from keywords import check_keyword
5 | from keywords import MULTIPLE_CHOICE_KEYWORD, MULTIPLE_CHOICE_FALSE, MULTIPLE_CHOICE_TRUE
6 | from keywords import BUTTON, SUCCESS, ERROR, DEFAULT_SUCCESS_MESSAGE, DEFAULT_ERROR_MESSAGE, DEFAULT_BUTTON_MESSAGE
7 | from answers import save_answer
8 | except:
9 | from .keywords import check_keyword
10 | from .keywords import MULTIPLE_CHOICE_KEYWORD, MULTIPLE_CHOICE_FALSE, MULTIPLE_CHOICE_TRUE
11 | from .keywords import BUTTON, SUCCESS, ERROR, DEFAULT_SUCCESS_MESSAGE, DEFAULT_ERROR_MESSAGE, DEFAULT_BUTTON_MESSAGE
12 | from .answers import save_answer
13 |
14 |
15 | def multiple_choice(question, options_dict,
16 | success=DEFAULT_SUCCESS_MESSAGE,
17 | error=DEFAULT_ERROR_MESSAGE,
18 | button=DEFAULT_BUTTON_MESSAGE):
19 | """Render a multiple choice question from the given parameters.
20 |
21 | :param question: question to be displayed before the multiple-choice options
22 | :type question: str
23 | :param options_dict: dictionary of options to be displayed, with the option text as key and the boolean answer as value
24 | :type options: dict
25 | :param answer: index (starting at 0) of the expected answer to the question
26 | :type answer: int
27 | :param success: message to be displayed when the user answers correctly
28 | :type success: str, optional
29 | :param error: message to be displayed when the user answers incorrectly
30 | :type error: str, optional
31 | :param button: message to be displayed on the button that checks the answer
32 | :type button: str, optional
33 | :return: tuple of booleans with button press status and correctness of answer
34 | :rtype: tuple of bool
35 | """
36 | cb_list = []
37 | if len(question)==0:
38 | st.error("Please provide a question")
39 | return None, None
40 | elif len(options_dict)<2:
41 | st.error("Must have at least 2 options")
42 | return None, None
43 | else:
44 | # Write the question
45 | st.markdown(question)
46 | # Write the options and append the user answers into the list cb_list
47 | for option, answer in options_dict.items():
48 | key = (question + option + str(answer)).lower().replace(" ", "_")
49 | # Post the option with no option
50 | cb = st.checkbox(option, value=False, key=key)
51 | cb_list.append(cb)
52 | # Check if the correct checkboxes are checked
53 | key = ("multiple-choice:" + question + "".join(options_dict.keys())).lower().replace(" ", "_")
54 | if st.button(button, key=key):
55 | # Check answers
56 | if all(u==a for u, a in zip(cb_list, options_dict.values())):
57 | if success:
58 | st.success(success)
59 | is_correct = True
60 | else:
61 | if error:
62 | st.error(error)
63 | is_correct = False
64 | # Save the answers, if required
65 | if "save_answers" in st.session_state and st.session_state.save_answers:
66 | user_answer = [list(options_dict.keys())[i] for i, cb in enumerate(cb_list) if cb]
67 | correct_answer = [key for key, val in options_dict.items() if val]
68 | save_answer(question, is_correct=is_correct, user_answer=user_answer, correct_answer=correct_answer)
69 | # Return if button is pressed and answer evaluation
70 | return True, is_correct
71 | else:
72 | # Return if button is not pressed
73 | return False, None
--------------------------------------------------------------------------------
/streamlit_book/render_single_choice.py:
--------------------------------------------------------------------------------
1 | import streamlit as st
2 |
3 | try:
4 | from keywords import check_keyword
5 | from keywords import SINGLE_CHOICE_KEYWORD, SINGLE_CHOICE_CORRECT, SINGLE_CHOICE_WRONG
6 | from keywords import BUTTON, SUCCESS, ERROR, DEFAULT_SUCCESS_MESSAGE, DEFAULT_ERROR_MESSAGE, DEFAULT_BUTTON_MESSAGE
7 | from answers import save_answer
8 | except:
9 | from .keywords import check_keyword
10 | from .keywords import SINGLE_CHOICE_KEYWORD, SINGLE_CHOICE_CORRECT, SINGLE_CHOICE_WRONG, BUTTON, SUCCESS, ERROR
11 | from .keywords import BUTTON, SUCCESS, ERROR, DEFAULT_SUCCESS_MESSAGE, DEFAULT_ERROR_MESSAGE, DEFAULT_BUTTON_MESSAGE
12 | from .answers import save_answer
13 |
14 |
15 | def single_choice(question, options, answer_index,
16 | success=DEFAULT_SUCCESS_MESSAGE,
17 | error=DEFAULT_ERROR_MESSAGE,
18 | button=DEFAULT_BUTTON_MESSAGE):
19 | """Renders a single-choice question from arguments.
20 |
21 | :param question: question to be displayed before the single-choice options
22 | :type question: str
23 | :param options: list of options to be displayed.
24 | :type options: str
25 | :param answer: index (starting at 0) of the expected answer to the question
26 | :type answer: int
27 | :param success: message to be displayed when the user answers correctly
28 | :type success: str, optional
29 | :param error: message to be displayed when the user answers incorrectly
30 | :type error: str, optional
31 | :param button: message to be displayed on the button that checks the answer
32 | :type button: str, optional
33 | :return: tuple of booleans with button press status and correctness of answer
34 | :rtype: tuple of bool
35 | """
36 | if len(question)==0:
37 | st.error("Please provide a question")
38 | return None, None
39 | elif len(options)<2:
40 | st.error("Must have at least 2 options")
41 | return None, None
42 | else:
43 | # Write the options with radio button: only one option can be selected
44 | user_answer = st.radio(question, options=options)
45 | # Check if correct answer
46 | key = ("single-choice:" + question + "".join(options)).lower().replace(" ", "_")
47 | if st.button(button, key=key):
48 | # Check ansers
49 | if options.index(user_answer) == answer_index:
50 | if success:
51 | st.success(success)
52 | is_correct = True
53 | else:
54 | if error:
55 | st.error(error)
56 | is_correct = False
57 | # Save the answers, if required
58 | if "save_answers" in st.session_state and st.session_state.save_answers:
59 | save_answer(question, is_correct=is_correct, user_answer=user_answer, correct_answer=options[answer_index])
60 | # Return if button is pressed and answer evaluation
61 | return True, is_correct
62 | else:
63 | # Return if button is not pressed
64 | return False, None
65 |
--------------------------------------------------------------------------------
/streamlit_book/render_text_input.py:
--------------------------------------------------------------------------------
1 | import streamlit as st
2 |
3 | try:
4 | from keywords import *
5 | from answers import save_answer
6 | except:
7 | from .keywords import *
8 | from .answers import save_answer
9 |
10 |
11 | def text_input(question, initial_answer,
12 | contains=[],
13 | equals="",
14 | normalization_function=None,
15 | verification_function=None,
16 | success=DEFAULT_SUCCESS_MESSAGE,
17 | error=DEFAULT_ERROR_MESSAGE,
18 | button=DEFAULT_BUTTON_MESSAGE):
19 | """Render a text input question from the given parameters.
20 |
21 | :param question: question to be displayed before the multiple-choice options
22 | :type question: str
23 | :param initial_answer:
24 | :type initial_answer: str
25 | :param contains:
26 | :type contains: list of str
27 | :param equals:
28 | :type equals: str
29 | :param normalization_function:
30 | :type normalization_function: function
31 | :param verification_function:
32 | :type normalization_function: function
33 | :param success: message to be displayed when the user answers correctly
34 | :type success: str, optional
35 | :param error: message to be displayed when the user answers incorrectly
36 | :type error: str, optional
37 | :param button: message to be displayed on the button that checks the answer
38 | :type button: str, optional
39 | :return: tuple of booleans with button press status and correctness of answer
40 | :rtype: tuple of bool
41 | """
42 | user_answer = st.text_input(question, initial_answer)
43 | # Check if the correct checkboxes are checked
44 | key = ("text-input:" + question + "/" + initial_answer).lower().replace(" ", "_")
45 | if st.button(button, key=key):
46 | # Check answers
47 | if user_answer == equals:
48 | if success:
49 | st.success(success)
50 | is_correct = True
51 | else:
52 | if error:
53 | st.error(error)
54 | is_correct = False
55 | # Save the answers, if required
56 | if "save_answers" in st.session_state and st.session_state.save_answers:
57 | correct_answer = "Depends, dynamically generated"
58 | save_answer(question, is_correct=is_correct, user_answer=user_answer, correct_answer=correct_answer)
59 | # Return if button is pressed and answer evaluation
60 | return True, is_correct
61 | else:
62 | # Return if button is not pressed
63 | return False, None
--------------------------------------------------------------------------------
/streamlit_book/render_to_do_list.py:
--------------------------------------------------------------------------------
1 | import streamlit as st
2 |
3 | try:
4 | from keywords import check_keyword
5 | from keywords import TODO_KEYWORD, TODO_COMPLETED, TODO_INCOMPLETE, TODO_SUCCESS, SUCCESS
6 | except:
7 | from .keywords import check_keyword
8 | from .keywords import TODO_KEYWORD, TODO_COMPLETED, TODO_INCOMPLETE, TODO_SUCCESS, SUCCESS
9 |
10 | def to_do_list(tasks, header="", success=TODO_SUCCESS):
11 | """ Renders the tasks as a to-do list, with optional header and success message.
12 | The tasks are a dictionary of tasks (supposed to be ordered as Python +3.6)
13 | with their completed (True) or to-do (False) status as a checkbox.
14 |
15 | :param tasks: dictionary of tasks in format string:bool
16 | :type tasks: dict
17 | :param header: description of the tasks
18 | :type header: str, optional
19 | :param success: success message
20 | :type success: str, optional
21 | :return: boolean with the exit status of the function
22 | :rtype: bool
23 | """
24 | if len(tasks)==0:
25 | st.error("There are no tasks to display.")
26 | cb_list = []
27 | st.markdown(header)
28 | for task, status in tasks.items():
29 | key = (header + task).lower().replace(" ", "_")
30 | cb = st.checkbox(task, value=status, key=key)
31 | cb_list.append(cb)
32 | if len(cb_list)>0 and all(cb for cb in cb_list):
33 | st.balloons()
34 | st.success(success)
35 | return True
36 | else:
37 | return False
--------------------------------------------------------------------------------
/streamlit_book/render_true_or_false.py:
--------------------------------------------------------------------------------
1 | import streamlit as st
2 |
3 | try:
4 | from keywords import check_keyword
5 | from keywords import TRUE_FALSE_KEYWORD, BUTTON, SUCCESS, ERROR, DEFAULT_BUTTON_MESSAGE, DEFAULT_SUCCESS_MESSAGE, DEFAULT_ERROR_MESSAGE
6 | from answers import save_answer
7 | except:
8 | from .keywords import check_keyword
9 | from .keywords import TRUE_FALSE_KEYWORD, BUTTON, SUCCESS, ERROR, DEFAULT_BUTTON_MESSAGE, DEFAULT_SUCCESS_MESSAGE, DEFAULT_ERROR_MESSAGE
10 | from .answers import save_answer
11 |
12 |
13 | def true_or_false(question, answer,
14 | success=DEFAULT_SUCCESS_MESSAGE,
15 | error=DEFAULT_ERROR_MESSAGE,
16 | button=DEFAULT_BUTTON_MESSAGE):
17 | """Renders a true or false question from arguments.
18 |
19 | :param question: question to be displayed before the true or false options
20 | :type question: str
21 | :param answer: expected answer to the question, can be True or False
22 | :type answer: str
23 | :param success: message to be displayed when the user answers correctly. If empty, no message is displayed.
24 | :type success: str, optional
25 | :param error: message to be displayed when the user answers incorrectly. If empty, no message is displayed.
26 | :type error: str, optional
27 | :param button: message to be displayed on the button that checks the answer
28 | :type button: str, optional
29 | :return: tuple of booleans(button_pressed, answer_correct) with the button status and correctness of answer
30 | :rtype: tuple of bool
31 | """
32 | if len(question)==0:
33 | st.error("Please provide a question")
34 | return None, None
35 | else:
36 | key = question.lower().replace(" ","")
37 | user_answer_str = st.radio(question, options=["True", "False"], key="TF"+key)
38 | user_answer = (user_answer_str == "True") # Convert to boolean
39 | if st.button(button, key="button"+key):
40 | # Reveal the answer
41 | if user_answer == answer:
42 | if success:
43 | st.success(success)
44 | is_correct = True
45 | else:
46 | if error:
47 | st.error(error)
48 | is_correct = False
49 | # Save the answer, if required
50 | if "save_answers" in st.session_state and st.session_state.save_answers:
51 | save_answer(question, is_correct=is_correct, user_answer=user_answer, correct_answer=answer)
52 | # Return if button is pressed and answer evaluation
53 | return True, is_correct
54 | else:
55 | # Not pressed yet
56 | return False, None
--------------------------------------------------------------------------------
/streamlit_book/social_media.py:
--------------------------------------------------------------------------------
1 | from streamlit.components.v1 import html
2 |
3 | try:
4 | from keywords import SHARE_KEYWORD
5 | from keywords import check_keyword
6 | except:
7 | from .keywords import SHARE_KEYWORD
8 | from .keywords import check_keyword
9 |
10 | def share_parser(lines):
11 | """Parses a list of lines into a dictionary with the parsed values.
12 |
13 | :param lines: list of lines
14 | :type lines: list
15 | :return: parsed values for text and url
16 | :rtype: dict
17 | """
18 | # Dict to store the parsed values
19 | parse_dict = {
20 | "my_text":"",
21 | "my_url":"",
22 | }
23 | for i, line in enumerate(lines):
24 | if i==0:
25 | if check_keyword(line, SHARE_KEYWORD):
26 | continue
27 | else:
28 | break
29 | elif i==1:
30 | parse_dict["my_text"] = line.strip()
31 | elif i==2:
32 | parse_dict["my_url"] = line.strip()
33 | return parse_dict
34 |
35 | def share_from_lines(lines):
36 | """Renders the share buttons from a list of lines.
37 |
38 | :param lines: list of lines
39 | :type lines: list
40 | :return: None
41 | """
42 | parse_dict = share_parser(lines)
43 | share(my_text=parse_dict["my_text"],
44 | my_url=parse_dict["my_url"], )
45 | return
46 |
47 | def share(my_text, my_url):
48 | """
49 | This function takes a url and a text and displays
50 | clickable sharing buttons in html.
51 |
52 | :param my_text: the text to share on social media
53 | :type my_text: str
54 | :param my_url: the url to share on social media
55 | :type my_url: str
56 | """
57 |
58 | # Define the css style for the sharing buttons
59 | my_css = """
60 | .resp-sharing-button__link,
61 | .resp-sharing-button__icon {
62 | display: inline-block
63 | }
64 |
65 | .resp-sharing-button__link {
66 | text-decoration: none;
67 | color: #fff;
68 | margin: 0.5em
69 | }
70 |
71 | .resp-sharing-button {
72 | border-radius: 5px;
73 | transition: 25ms ease-out;
74 | padding: 0.5em 0.75em;
75 | font-family: Helvetica Neue,Helvetica,Arial,sans-serif
76 | }
77 |
78 | .resp-sharing-button__icon svg {
79 | width: 1em;
80 | height: 1em;
81 | margin-right: 0.4em;
82 | vertical-align: top
83 | }
84 |
85 | .resp-sharing-button--small svg {
86 | margin: 0;
87 | vertical-align: middle
88 | }
89 |
90 | /* Non solid icons get a stroke */
91 | .resp-sharing-button__icon {
92 | stroke: #fff;
93 | fill: none
94 | }
95 |
96 | /* Solid icons get a fill */
97 | .resp-sharing-button__icon--solid,
98 | .resp-sharing-button__icon--solidcircle {
99 | fill: #fff;
100 | stroke: none
101 | }
102 |
103 | .resp-sharing-button--twitter {
104 | background-color: #55acee
105 | }
106 |
107 | .resp-sharing-button--twitter:hover {
108 | background-color: #2795e9
109 | }
110 |
111 | .resp-sharing-button--pinterest {
112 | background-color: #bd081c
113 | }
114 |
115 | .resp-sharing-button--pinterest:hover {
116 | background-color: #8c0615
117 | }
118 |
119 | .resp-sharing-button--facebook {
120 | background-color: #3b5998
121 | }
122 |
123 | .resp-sharing-button--facebook:hover {
124 | background-color: #2d4373
125 | }
126 |
127 | .resp-sharing-button--tumblr {
128 | background-color: #35465C
129 | }
130 |
131 | .resp-sharing-button--tumblr:hover {
132 | background-color: #222d3c
133 | }
134 |
135 | .resp-sharing-button--reddit {
136 | background-color: #5f99cf
137 | }
138 |
139 | .resp-sharing-button--reddit:hover {
140 | background-color: #3a80c1
141 | }
142 |
143 | .resp-sharing-button--google {
144 | background-color: #dd4b39
145 | }
146 |
147 | .resp-sharing-button--google:hover {
148 | background-color: #c23321
149 | }
150 |
151 | .resp-sharing-button--linkedin {
152 | background-color: #0077b5
153 | }
154 |
155 | .resp-sharing-button--linkedin:hover {
156 | background-color: #046293
157 | }
158 |
159 | .resp-sharing-button--email {
160 | background-color: #777
161 | }
162 |
163 | .resp-sharing-button--email:hover {
164 | background-color: #5e5e5e
165 | }
166 |
167 | .resp-sharing-button--xing {
168 | background-color: #1a7576
169 | }
170 |
171 | .resp-sharing-button--xing:hover {
172 | background-color: #114c4c
173 | }
174 |
175 | .resp-sharing-button--whatsapp {
176 | background-color: #25D366
177 | }
178 |
179 | .resp-sharing-button--whatsapp:hover {
180 | background-color: #1da851
181 | }
182 |
183 | .resp-sharing-button--hackernews {
184 | background-color: #FF6600
185 | }
186 | .resp-sharing-button--hackernews:hover, .resp-sharing-button--hackernews:focus { background-color: #FB6200 }
187 |
188 | .resp-sharing-button--vk {
189 | background-color: #507299
190 | }
191 |
192 | .resp-sharing-button--vk:hover {
193 | background-color: #43648c
194 | }
195 |
196 | .resp-sharing-button--facebook {
197 | background-color: #3b5998;
198 | border-color: #3b5998;
199 | }
200 |
201 | .resp-sharing-button--facebook:hover,
202 | .resp-sharing-button--facebook:active {
203 | background-color: #2d4373;
204 | border-color: #2d4373;
205 | }
206 |
207 | .resp-sharing-button--twitter {
208 | background-color: #55acee;
209 | border-color: #55acee;
210 | }
211 |
212 | .resp-sharing-button--twitter:hover,
213 | .resp-sharing-button--twitter:active {
214 | background-color: #2795e9;
215 | border-color: #2795e9;
216 | }
217 |
218 | .resp-sharing-button--email {
219 | background-color: #777777;
220 | border-color: #777777;
221 | }
222 |
223 | .resp-sharing-button--email:hover,
224 | .resp-sharing-button--email:active {
225 | background-color: #5e5e5e;
226 | border-color: #5e5e5e;
227 | }
228 |
229 | .resp-sharing-button--linkedin {
230 | background-color: #0077b5;
231 | border-color: #0077b5;
232 | }
233 |
234 | .resp-sharing-button--linkedin:hover,
235 | .resp-sharing-button--linkedin:active {
236 | background-color: #046293;
237 | border-color: #046293;
238 | }
239 |
240 | .resp-sharing-button--whatsapp {
241 | background-color: #25D366;
242 | border-color: #25D366;
243 | }
244 |
245 | .resp-sharing-button--whatsapp:hover,
246 | .resp-sharing-button--whatsapp:active {
247 | background-color: #1DA851;
248 | border-color: #1DA851;
249 | }
250 |
251 | .resp-sharing-button--telegram {
252 | background-color: #54A9EB;
253 | }
254 |
255 | .resp-sharing-button--telegram:hover {
256 | background-color: #4B97D1;}
257 | """
258 |
259 | # Define the html
260 | my_html = f"""
261 |
262 |
263 |
264 |