Repository: stonebase-mx/python-packages.stonebase.mx (language: python)

Issue:
Create a streamlit utils extension with common shared tooling
https://github.com/streamlit/streamlit/issues/3426 


Discussion / hints:

Fix patch:
diff --git a/.github/workflows/check-style.yaml b/.github/workflows/check-style.yaml
index 4ccdfdb..cbccddf 100644
--- a/.github/workflows/check-style.yaml
+++ b/.github/workflows/check-style.yaml
@@ -9,6 +9,7 @@ jobs:
         path:
           - rhdzmota
           - extensions/hello_world
+          - extensions/streamlit_webapps
     name: Style Check ${{ matrix.path }}
     runs-on: ubuntu-20.04
     steps:
diff --git a/.github/workflows/check-types.yaml b/.github/workflows/check-types.yaml
index 07114af..98d42c1 100644
--- a/.github/workflows/check-types.yaml
+++ b/.github/workflows/check-types.yaml
@@ -9,6 +9,7 @@ jobs:
         path:
           - rhdzmota/src
           - extensions/hello_world/src
+          - extensions/streamlit_webapps/src
   build:
     name: Type Check ${{ matrix.path }}
     runs-on: ubuntu-20.04
diff --git a/.github/workflows/pypi-publish.yaml b/.github/workflows/pypi-publish.yaml
index f8c66e6..adf56ff 100644
--- a/.github/workflows/pypi-publish.yaml
+++ b/.github/workflows/pypi-publish.yaml
@@ -14,6 +14,7 @@ jobs:
         path:
           - rhdzmota
           - extensions/hello_world
+          - extensions/streamlit_webapps
     # Check if PR was closed due to a merge.
     if: github.event.pull_request.merged == true
     # Create a job on Ubuntu
diff --git a/extensions/streamlit_webapps/CHANGELOG.md b/extensions/streamlit_webapps/CHANGELOG.md
new file mode 100644
index 0000000..b382346
--- /dev/null
+++ b/extensions/streamlit_webapps/CHANGELOG.md
@@ -0,0 +1,11 @@
+# Extension Changelog: streamlit_webapps
+
+Release types:
+* `major`: Non-backward compatible new features or refactors, or bugfixes.
+* `minor`: Backward compatible new features, refactors, or bugfixes.
+* `patch`: Minor backward compatible changes such as typos, documentations, small bugs, etc.
+
+## `0.1.0` Genesis - First Implementation
+
+* Date: `TBD`
+* Release Type: `minor`
diff --git a/extensions/streamlit_webapps/LICENSE b/extensions/streamlit_webapps/LICENSE
new file mode 100644
index 0000000..e69de29
diff --git a/extensions/streamlit_webapps/MANIFEST.in b/extensions/streamlit_webapps/MANIFEST.in
new file mode 100644
index 0000000..1c50de7
--- /dev/null
+++ b/extensions/streamlit_webapps/MANIFEST.in
@@ -0,0 +1,4 @@
+include LICENSE
+include README.md
+include requirements.txt
+include src/main/rhdzmota/ext/streamlit_webapps_version
diff --git a/extensions/streamlit_webapps/README.md b/extensions/streamlit_webapps/README.md
new file mode 100644
index 0000000..d75c94c
--- /dev/null
+++ b/extensions/streamlit_webapps/README.md
@@ -0,0 +1,96 @@
+# RHDZMOTA EXT: streamlit_webapps
+
+Package extensions have 2 main purposes:
+* Split the dependency sets into several smaller python projects
+* Serve as domain-specific packages (i.e., split functionality).
+
+## About the `streamlit_webapps` extension
+
+The streamlit extension contains some common useful abstractions to be used when working on creating streamlit applications.
+
+The starting abstraction is the `PageView` class, which provides an "easy to reason about" pattern to follow when working on creating webapps. To get a sneak-peak of the functionality, consider creating a `webapp.py` script with the following content:
+
+```python
+from rhdzmota.ext.streamlit_webapps.page_view import PageView
+
+
+if __name__ == "__main__":
+    with PageView() as page:
+        page.view()
+```
+* Run with: `streamlit run webapp.py
+
+With a single import you can have a fully functional streamlit page with content (in this case, a "non implemented" message.
+
+The `PageView` abstractions allow you to specify the page configuration via class initialization such as:
+* `PageView(page_title="Demo", page_layout="wide", favicon_path="path/to/favicon.png")`
+
+You can provide a custom streamlit implementation to a page-view via:
+* `Inline` (discouraged) via the `PageView.infline` class constructor.
+* `Inheritance` (recommended) by providing an implementation for the `view` instance method.
+
+Code Example: Using inheritance to provide a custom streamlit implementation.
+
+```python
+import random
+import datetime as dt
+
+import streamlit as st
+
+from rhdzmota.ext.streamlit_webapps.page_view import PageView
+
+class WelcomeView(PageView):
+    def view(self, **kwargs):
+        st.markdown("# My Custom Webapp")
+        with st.form(f"form-{self.refname}"):
+            num_input = int(st.number_input("Num. of Options (input)", min_value=2, step=1))
+            num_output = int(st.number_input("Num. of Winners", min_value=1, step=1))
+            exclusive_select = st.checkbox("Exclusive Select")
+            submitted = st.form_submit_button("Submit")
+
+        if not submitted:
+            return
+
+        values = [f"Option-{i}" for i in range(num_input)]
+        timestamp = dt.datetime.now().isoformat()
+        sep = "\n* "
+        result_prefix = f"Result (TS: {dt.datetime.now().isoformat()}) "
+        if not exclusive_select:
+            output = result_prefix + sep + sep.join(random.choices(values, k=num_output))
+            return st.markdown(output)
+        random.shuffle(values)
+        output = result_prefix + sep + sep.join(values[:num_output] if num_output < num_input else values)
+        return st.markdown(output)
+
+
+if __name__ == "__main__":
+    with WelcomeView(page_title="Demo", page_layout="wide") as page:
+        page.view()
+
+```
+
+## Linked Installation
+
+Most extensions can be installed indirectly via the main package by providing an extra-dependency tag. Example:
+
+```commandline
+$ pip install 'rhdzmota[ext.hello_world]'
+```
+* General pattern: `'rhdzmota[ext.<extension-slug>]'`
+
+## Standalone Installations
+
+Package extensions can also be installed independently. For example:
+
+```commandline
+$ pip install rhdzmota_extension_hello_world
+```
+* General pattern: `rhdzmota_extension_<extension-slug>`
+
+You can also install them locally:
+
+```commandline
+$ EXT_BUILD_LOCAL=1 pip install -e extensions/hello_world
+```
+* General pattern: `EXT_BUILD_LOCAL=1 pip install -e path/to/extension`
+
diff --git a/extensions/streamlit_webapps/requirements.txt b/extensions/streamlit_webapps/requirements.txt
new file mode 100644
index 0000000..f06fd51
--- /dev/null
+++ b/extensions/streamlit_webapps/requirements.txt
@@ -0,0 +1,7 @@
+# Extension dependencies:
+streamlit==1.31.1
+sentry_sdk==1.40.6
+PyJWT==2.3.0
+
+# Should always have `rhdzmota` as a dependency
+rhdzmota
diff --git a/extensions/streamlit_webapps/setup.py b/extensions/streamlit_webapps/setup.py
new file mode 100644
index 0000000..095f910
--- /dev/null
+++ b/extensions/streamlit_webapps/setup.py
@@ -0,0 +1,77 @@
+import os
+from setuptools import setup, find_namespace_packages
+
+# Environ variables
+EXT_CODEBASE_PATH = os.environ.get(
+    "EXT_CODEBASE_PATH",
+    default=os.path.join("src", "main")
+)
+EXT_BUILD_LOCAL = int(os.environ.get(
+    "EXT_BUILD_LOCAL",
+    default="0",
+))
+
+with open("README.md", "r") as file:
+    readme = file.read()
+
+with open("requirements.txt", "r") as file:
+    requirements = [
+        req.strip()
+        for req in file.read().splitlines()
+        if req and not req.startswith("#")
+    ]
+    # If local build, do not install `rhdzmota` from pypi
+    requirements.pop(requirements.index("rhdzmota"))
+
+version_filename = "streamlit_webapps_version"
+version_filepath = os.path.join(
+    EXT_CODEBASE_PATH,
+    "rhdzmota",
+    "ext",
+    version_filename,
+)
+
+with open(version_filepath, "r") as file:
+    version = file.readline().strip()
+
+setup(
+    name="rhdzmota_extension_streamlit_webapps",
+    version=version,
+    description=(
+        "RHDZMOTA Extension App: streamlit_webapps"
+    ),
+    long_description=readme,
+    long_description_content_type="text/markdown",
+    author="Rodrigo H. Mota",
+    author_email="info@rhdzmota.com",
+    url="https://github.com/rhdzmota/package.rhdzmota.com",
+    classifiers=[
+        "Typing :: Typed",
+        "Topic :: Scientific/Engineering",
+        "Topic :: Software Development :: Libraries :: Python Modules",
+        "Programming Language :: Python :: 3 :: Only",
+        "Programming Language :: Python :: 3.10",
+    ],
+    package_dir={
+        "": EXT_CODEBASE_PATH,
+    },
+    package_data={
+        "": [
+            os.path.join(
+                "rhdzmota",
+                "ext",
+                "streamlit_webapps_version",
+            ),
+        ],
+    },
+    packages=[
+        package
+        for package in find_namespace_packages(where=EXT_CODEBASE_PATH)
+        if package.startswith("rhdzmota")
+    ],
+    include_package_data=True,
+    install_requires=requirements,
+    python_requires=">=3.10, <4",
+    license="TBD",
+    zip_safe=False,
+)
diff --git a/extensions/streamlit_webapps/src/main/examples/ex1_plain_page_view.py b/extensions/streamlit_webapps/src/main/examples/ex1_plain_page_view.py
new file mode 100644
index 0000000..44e611f
--- /dev/null
+++ b/extensions/streamlit_webapps/src/main/examples/ex1_plain_page_view.py
@@ -0,0 +1,6 @@
+from rhdzmota.ext.streamlit_webapps.page_view import PageView
+
+
+if __name__ == "__main__":
+    with PageView(page_title="EX1", page_layout="wide") as page:
+        page.view()
diff --git a/extensions/streamlit_webapps/src/main/examples/ex2_custom_page_inline.py b/extensions/streamlit_webapps/src/main/examples/ex2_custom_page_inline.py
new file mode 100644
index 0000000..84a80be
--- /dev/null
+++ b/extensions/streamlit_webapps/src/main/examples/ex2_custom_page_inline.py
@@ -0,0 +1,17 @@
+import streamlit as st
+
+from rhdzmota.ext.streamlit_webapps.page_view import PageView
+
+
+def view(*args, **kwargs):
+    st.markdown("# My Custom View")
+    st.info("This is the example 2!")
+    st.error(
+        "Consider that we do not recommend inline-views; "
+        "see example 3 for an alternative using inheritance."
+    )
+
+
+if __name__ == "__main__":
+    with PageView.inline("CustomView", func=view)(page_title="Demo", page_layout="centered") as page:
+        page.view()
diff --git a/extensions/streamlit_webapps/src/main/examples/ex3_custom_page_inheritance.py b/extensions/streamlit_webapps/src/main/examples/ex3_custom_page_inheritance.py
new file mode 100644
index 0000000..3086310
--- /dev/null
+++ b/extensions/streamlit_webapps/src/main/examples/ex3_custom_page_inheritance.py
@@ -0,0 +1,15 @@
+import streamlit as st
+
+from rhdzmota.ext.streamlit_webapps.page_view import PageView
+
+
+class CustomPageView(PageView):
+
+    def view(self, **kwargs):
+        st.markdown("# My Custom View")
+        st.info("This is the example 3!")
+
+
+if __name__ == "__main__":
+    with CustomPageView(page_title="Demo", page_layout="centered") as page:
+        page.view()
diff --git a/extensions/streamlit_webapps/src/main/examples/ex4_multiple_views_pingpong.py b/extensions/streamlit_webapps/src/main/examples/ex4_multiple_views_pingpong.py
new file mode 100644
index 0000000..a49b261
--- /dev/null
+++ b/extensions/streamlit_webapps/src/main/examples/ex4_multiple_views_pingpong.py
@@ -0,0 +1,32 @@
+import streamlit as st
+
+from rhdzmota.ext.streamlit_webapps.page_view import PageView
+from rhdzmota.ext.streamlit_webapps.page_view_switcher import PageViewSwitcher
+
+
+class PingView(PageView):
+
+    def view(self, **kwargs):
+        st.markdown("## Ping!")
+        st.info("You are in the ping view...")
+        if st.button("Go pong!"):
+            return self.forward(PongView.refname)
+
+
+class PongView(PageView):
+
+    def view(self, **kwargs):
+        st.markdown("## Pong!")
+        st.info("You are in the pong view...")
+        if st.button("Go ping!"):
+            return self.forward(PingView.refname)
+
+
+if __name__ == "__main__":
+    ping = PingView(page_title="Ping", page_layout="centered")
+    pong = PongView(page_title="Pong", page_layout="centered")
+    switcher = PageViewSwitcher.from_page_views(
+        switcher_name="pingpong",
+        page_views=[ping, pong],  # Order is not relevant here.
+    )
+    switcher.run(initial_page_key=ping.refname)
diff --git a/extensions/streamlit_webapps/src/main/examples/ex5_mutiple_views_with_header_pingpong.py b/extensions/streamlit_webapps/src/main/examples/ex5_mutiple_views_with_header_pingpong.py
new file mode 100644
index 0000000..afa3335
--- /dev/null
+++ b/extensions/streamlit_webapps/src/main/examples/ex5_mutiple_views_with_header_pingpong.py
@@ -0,0 +1,43 @@
+import streamlit as st
+
+from rhdzmota.ext.streamlit_webapps.page_view_switcher import PageViewSwitcher
+from rhdzmota.ext.streamlit_webapps.page_view import (
+    PageViewHeader,
+    PageView,
+)
+
+
+class PingPongHeader(PageViewHeader):
+    def on_start(self):
+        st.markdown("# PingPong Header")
+        st.text("This text is part of the header.")
+        if self.history_exists() and st.button("Return"):
+            self.backward()
+
+
+class PingView(PageView, PingPongHeader):
+
+    def view(self, **kwargs):
+        st.markdown("## Ping!")
+        st.info("You are in the ping view...")
+        if st.button("Go pong!"):
+            return self.forward(PongView.refname)
+
+
+class PongView(PageView, PingPongHeader):
+
+    def view(self, **kwargs):
+        st.markdown("## Pong!")
+        st.info("You are in the pong view...")
+        if st.button("Go ping!"):
+            return self.forward(PingView.refname)
+
+
+if __name__ == "__main__":
+    ping = PingView(page_title="Ping", page_layout="centered")
+    pong = PongView(page_title="Pong", page_layout="centered")
+    switcher = PageViewSwitcher.from_page_views(
+        switcher_name="pingpong",
+        page_views=[ping, pong],  # Order is not relevant here.
+    )
+    switcher.run(initial_page_key=ping.refname)
diff --git a/extensions/streamlit_webapps/src/main/rhdzmota/ext/streamlit_webapps/__init__.py b/extensions/streamlit_webapps/src/main/rhdzmota/ext/streamlit_webapps/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/extensions/streamlit_webapps/src/main/rhdzmota/ext/streamlit_webapps/components/__init__.py b/extensions/streamlit_webapps/src/main/rhdzmota/ext/streamlit_webapps/components/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/extensions/streamlit_webapps/src/main/rhdzmota/ext/streamlit_webapps/components/catalog.py b/extensions/streamlit_webapps/src/main/rhdzmota/ext/streamlit_webapps/components/catalog.py
new file mode 100644
index 0000000..6f20323
--- /dev/null
+++ b/extensions/streamlit_webapps/src/main/rhdzmota/ext/streamlit_webapps/components/catalog.py
@@ -0,0 +1,10 @@
+import enum
+
+from .comp_simple_email_access_request import SimpleEmailAccessRequest
+
+
+class CompCatalog(enum.Enum):
+    SIMPLE_EMAIL_ACCESS_REQUEST = SimpleEmailAccessRequest
+
+    def configure(self, **kwargs):
+        return self.value(**kwargs)
diff --git a/extensions/streamlit_webapps/src/main/rhdzmota/ext/streamlit_webapps/components/comp_simple_email_access_request.py b/extensions/streamlit_webapps/src/main/rhdzmota/ext/streamlit_webapps/components/comp_simple_email_access_request.py
new file mode 100644
index 0000000..02aef1b
--- /dev/null
+++ b/extensions/streamlit_webapps/src/main/rhdzmota/ext/streamlit_webapps/components/comp_simple_email_access_request.py
@@ -0,0 +1,45 @@
+from dataclasses import dataclass
+from typing import Optional
+
+import streamlit as st
+
+from rhdzmota.ext.streamlit_webapps.page_view import PageView
+
+
+@dataclass(frozen=True, slots=True, kw_only=True)
+class SimpleEmailAccessRequest(PageView):
+    reference_page_on_success: str
+    reference_page_on_failure: str
+    reference_page_request_email_verification_code: str
+    overwrite_component_subtitle: Optional[str] = None
+    overwrite_submit_button_label: Optional[str] = None
+
+    @property
+    def submit_button_label(self) -> str:
+        return self.overwrite_submit_button_label or "Submit"
+
+    @property
+    def component_subtitle(self) -> str:
+        return self.overwrite_component_subtitle or "Request Access Via Email"
+
+    def view(self, **kwargs):
+        st.markdown(f"## {self.component_subtitle}")
+        with st.form("form-" + self.refname):
+            email = st.text_input("Corporate email")
+            passcode = st.text_input("Passcode (optional)")
+            submitted = st.form_submit_button(self.submit_button_label)
+        if not submitted:
+            return
+        st.session_state["email_main"] = email
+        # TODO: Implement email validation
+        if "@" not in email:
+            return st.error(f"Email validation error.")
+        # Verify that the email contains a valid domain
+        _, domain = email.split("@")
+        # TODO: Domain verification via environ
+        if not passcode:
+            # TODO: Implement email verification
+            return self.forward("canonical_error")
+        # TODO: Verify if passcode is valid
+        elif True:
+            return self.forward(self.reference_page_on_success)
diff --git a/extensions/streamlit_webapps/src/main/rhdzmota/ext/streamlit_webapps/page_view.py b/extensions/streamlit_webapps/src/main/rhdzmota/ext/streamlit_webapps/page_view.py
new file mode 100644
index 0000000..20e5fe5
--- /dev/null
+++ b/extensions/streamlit_webapps/src/main/rhdzmota/ext/streamlit_webapps/page_view.py
@@ -0,0 +1,139 @@
+import json
+from dataclasses import dataclass, field
+from typing import Callable, Optional
+
+import streamlit as st
+
+
+class PageViewDescriptorRefname:
+
+    def __get__(self, obj, objtype=None):
+        # We should be able to always get the class
+        cls = objtype or type(obj)
+        default = getattr(
+            cls,
+            "overwrite_refname",
+        ) or cls.__name__
+        if not obj:
+            return default
+        return getattr(
+            obj,
+            "overwrite_instance_refname",
+        ) or default
+
+
+class PageViewSessionKeys:
+    session_key_page_history = "page_history"
+    session_key_page_current = "page_current"
+    session_key_page_current_input_kwargs = "page_current_input_kwargs"
+
+    @classmethod
+    def history_exists(cls) -> bool:
+        records = st.session_state.get(
+            cls.session_key_page_history,
+            [],
+        )
+        return len(records) > 0
+
+
+class PageViewDescriptors:
+    overwrite_refname: Optional[str] = None
+    refname = PageViewDescriptorRefname()
+
+
+@dataclass(frozen=True, slots=True, kw_only=True)
+class PageViewHeader(PageViewSessionKeys):
+    def on_start(self):
+        pass
+
+
+@dataclass(frozen=True, slots=True, kw_only=True)
+class PageView(PageViewDescriptors, PageViewSessionKeys):
+    page_title: Optional[str] = None
+    page_layout: Optional[str] = None
+    favicon_path: Optional[str] = None
+    initial_sidebar_state: Optional[str] = None
+    menu_items: dict = field(default_factory=dict)
+    page_configs_path: Optional[str] = None
+    overwrite_instance_refname: Optional[str] = None
+    page_configs_kwargs: dict = field(default_factory=dict)
+
+    @classmethod
+    def inline(cls, view_name: str, func: Callable, **kwargs) -> 'PageView':
+        return type(view_name, (cls, ), {"view": func, **kwargs})
+
+    @property
+    def configs_page(self) -> dict:
+        page_configs_constructor = {
+            "page_title": self.page_title,
+            "page_icon": self.favicon_path,
+            "menu_items": self.menu_items,
+            # TODO: Add enum validation
+            "layout": self.page_layout or "centered",
+            "initial_sidebar_state": self.initial_sidebar_state or "auto",
+        }
+        if not self.page_configs_path:
+            return page_configs_constructor
+        with open(self.page_configs_path, "r") as file:
+            content = file.read()
+        return {
+            **json.loads(content),
+            **page_configs_constructor,
+        }
+
+    def __enter__(self) -> 'PageView':
+        # Page config should only be executed ONCE and
+        # must be the first streamlit command.
+        st.set_page_config(**self.configs_page)
+        # Register the current page in the sesion state
+        st.session_state[self.session_key_page_current] = self.refname
+        # Register the current page as the first page in history
+        key_page_history = self.session_key_page_history
+        if key_page_history not in st.session_state:
+            st.session_state[key_page_history] = []
+        # Execute on-start method (you can use this as a page header)
+        if isinstance(self, PageViewHeader):
+            self.on_start()
+        return self
+
+    def __exit__(self, *args):
+        # TODO: To be defined...
+        pass
+
+    def forward(
+            self,
+            page_key: str,
+            exclude_from_history: bool = False,
+            **kwargs,
+    ):
+        # Get the current page
+        page_source = st.session_state[self.session_key_page_current]
+        page_source_kwargs = st.session_state.get(self.session_key_page_current_input_kwargs) or {}
+        # Set the next page flow stage and propagate kwargs
+        st.session_state[self.session_key_page_current] = page_key
+        st.session_state[self.session_key_page_current_input_kwargs] = kwargs
+        # Set the page-source as part of the page-history
+        if not exclude_from_history:
+            st.session_state[self.session_key_page_history].append((page_source, page_source_kwargs))
+        # Rerun to apply changes
+        st.rerun()
+
+    def backward(self):
+        page_before, page_before_kwargs = st.session_state[self.session_key_page_history].pop(-1)
+        self.forward(
+            page_key=page_before,
+            exclude_from_history=True,
+            **page_before_kwargs,
+        )
+
+    def view(self, **kwargs):
+        st.markdown("# Not Implemented :shrug:")
+
+
+class CanonicalErrorView(PageView):
+    overwrite_refname: Optional[str] = "canonical_error"
+
+    def view(self, **kwargs):
+        st.error("An issue was detected")
+        if st.button("Return"):
+            self.backward()
diff --git a/extensions/streamlit_webapps/src/main/rhdzmota/ext/streamlit_webapps/page_view_switcher.py b/extensions/streamlit_webapps/src/main/rhdzmota/ext/streamlit_webapps/page_view_switcher.py
new file mode 100644
index 0000000..37f5829
--- /dev/null
+++ b/extensions/streamlit_webapps/src/main/rhdzmota/ext/streamlit_webapps/page_view_switcher.py
@@ -0,0 +1,45 @@
+import enum
+from typing import List
+
+import streamlit as st
+
+from rhdzmota.ext.streamlit_webapps.page_view import (
+    PageView,
+    CanonicalErrorView,
+)
+
+
+class PageViewSwitcher(enum.Enum):
+
+    @classmethod
+    def from_page_views(
+            cls,
+            switcher_name: str,
+            page_views: List[PageView],
+            disable_canonical_error: bool = False,
+    ) -> 'PageViewSwitcher':
+        canonical_error_item = ("canonical_error", CanonicalErrorView())
+        return cls(
+            value=switcher_name,
+            names=[
+                (page_view.refname, page_view)
+                for page_view in page_views
+            ] + ([] if disable_canonical_error else [canonical_error_item]),
+        )
+
+    @classmethod
+    def run(cls, initial_page_key: str, **initial_page_kwargs):
+        # On first execution, save the inital page key with their corresponding kwargs
+        page_current = st.session_state.get(PageView.session_key_page_current, None)
+        if not page_current:
+            st.session_state[PageView.session_key_page_current] = initial_page_key
+            st.session_state[PageView.session_key_page_current_input_kwargs] = initial_page_kwargs
+        # Execute the page defined on the session-state
+
+        with cls[page_current or initial_page_key].value as current_page_instance:
+            current_page_instance.view(
+                **st.session_state.get(
+                    PageView.session_key_page_current_input_kwargs,
+                    {}
+                )
+            )
diff --git a/extensions/streamlit_webapps/src/main/rhdzmota/ext/streamlit_webapps/settings.py b/extensions/streamlit_webapps/src/main/rhdzmota/ext/streamlit_webapps/settings.py
new file mode 100644
index 0000000..52e96ba
--- /dev/null
+++ b/extensions/streamlit_webapps/src/main/rhdzmota/ext/streamlit_webapps/settings.py
@@ -0,0 +1,9 @@
+import os
+import enum
+
+
+class Env(enum.Enum):
+    EXT_APP_NAME = os.environ.get(
+        "EXT_APP_NAME",
+        default="streamlit_webapps",
+    )
diff --git a/extensions/streamlit_webapps/src/main/rhdzmota/ext/streamlit_webapps_version b/extensions/streamlit_webapps/src/main/rhdzmota/ext/streamlit_webapps_version
new file mode 100644
index 0000000..6e8bf73
--- /dev/null
+++ b/extensions/streamlit_webapps/src/main/rhdzmota/ext/streamlit_webapps_version
@@ -0,0 +1,1 @@
+0.1.0


Test patch:
diff --git a/.github/workflows/check-tests.yaml b/.github/workflows/check-tests.yaml
index b9c7ef5..006d344 100644
--- a/.github/workflows/check-tests.yaml
+++ b/.github/workflows/check-tests.yaml
@@ -9,6 +9,7 @@ jobs:
         path:
           - rhdzmota
           - extensions/hello_world
+          - extensions/streamlit_webapps
     name: Testing Check ${{ matrix.path }}
     runs-on: ubuntu-20.04
     steps:
diff --git a/extensions/streamlit_webapps/src/test/test_rhdzmota/test_ext/test_streamlit_webapps/__init__.py b/extensions/streamlit_webapps/src/test/test_rhdzmota/test_ext/test_streamlit_webapps/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/extensions/streamlit_webapps/src/test/test_rhdzmota/test_ext/test_streamlit_webapps/test_settings.py b/extensions/streamlit_webapps/src/test/test_rhdzmota/test_ext/test_streamlit_webapps/test_settings.py
new file mode 100644
index 0000000..dd20f3a
--- /dev/null
+++ b/extensions/streamlit_webapps/src/test/test_rhdzmota/test_ext/test_streamlit_webapps/test_settings.py
@@ -0,0 +1,6 @@
+from rhdzmota.ext.streamlit_webapps.settings import Env
+
+
+def test_env_app_name():
+    app_name = "streamlit_webapps"
+    assert Env.EXT_APP_NAME.value.startswith(app_name)
diff --git a/extensions/{{cookiecutter.project_slug}}/src/test/test_rhdzmota/test_ext/test_{{cookiecutter.project_slug}}/test_settings.py b/extensions/{{cookiecutter.project_slug}}/src/test/test_rhdzmota/test_ext/test_{{cookiecutter.project_slug}}/test_settings.py
index 3779fcc..fccac06 100644
--- a/extensions/{{cookiecutter.project_slug}}/src/test/test_rhdzmota/test_ext/test_{{cookiecutter.project_slug}}/test_settings.py
+++ b/extensions/{{cookiecutter.project_slug}}/src/test/test_rhdzmota/test_ext/test_{{cookiecutter.project_slug}}/test_settings.py
@@ -3,4 +3,4 @@
 
 def test_env_app_name():
     app_name = "{{cookiecutter.project_slug}}"
-    assert Env.EXT_APP_NAME.startswith(app_name)
+    assert Env.EXT_APP_NAME.value.startswith(app_name)