From dfb03ad223030eb98d356a820888ceeebc255f45 Mon Sep 17 00:00:00 2001 From: Fasil | Python/Odoo Developer Date: Sun, 24 Nov 2024 11:03:48 +0300 Subject: [PATCH] Release v0.1.0 - Enhanced Methods --- .github/workflows/publish.yml | 27 +++++ .github/workflows/python-apk.yml | 49 --------- .gitignore | 162 ----------------------------- LICENCE | 21 ++++ MANIFEST.in | 5 + README.md | 140 ++++++++----------------- assets/icon.png | Bin 7048 -> 0 bytes assets/logo_128.png | Bin 3461 -> 0 bytes assets/logo_512.png | Bin 6939 -> 0 bytes core/__init__.py | 2 - core/base.py | 108 ------------------- core/controls.py | 53 ---------- core/db.py | 47 --------- main.py | 47 --------- manifest.json | 10 -- pyproject.toml | 97 +++++++++++++++++ requirements.txt | 1 - src/flet_model/__init__.py | 6 ++ src/flet_model/model.py | 172 +++++++++++++++++++++++++++++++ src/flet_model/router.py | 86 ++++++++++++++++ tests/test_model.py | 31 ++++++ views/main_view.py | 163 ----------------------------- views/second_view.py | 27 ----- 23 files changed, 489 insertions(+), 765 deletions(-) create mode 100644 .github/workflows/publish.yml delete mode 100644 .github/workflows/python-apk.yml delete mode 100644 .gitignore create mode 100644 LICENCE create mode 100644 MANIFEST.in delete mode 100644 assets/icon.png delete mode 100644 assets/logo_128.png delete mode 100644 assets/logo_512.png delete mode 100644 core/__init__.py delete mode 100644 core/base.py delete mode 100644 core/controls.py delete mode 100644 core/db.py delete mode 100644 main.py delete mode 100644 manifest.json create mode 100644 pyproject.toml delete mode 100644 requirements.txt create mode 100644 src/flet_model/__init__.py create mode 100644 src/flet_model/model.py create mode 100644 src/flet_model/router.py create mode 100644 tests/test_model.py delete mode 100644 views/main_view.py delete mode 100644 views/second_view.py diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml new file mode 100644 index 0000000..2d129bb --- /dev/null +++ b/.github/workflows/publish.yml @@ -0,0 +1,27 @@ +name: Publish to PyPI + +on: + push: + tags: + - 'v*' + +jobs: + build-and-publish: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + - name: Set up Python + uses: actions/setup-python@v4 + with: + python-version: "3.x" + - name: Install build dependencies + run: | + python -m pip install --upgrade pip + pip install build twine + - name: Build package + run: python -m build + - name: Publish to PyPI + env: + TWINE_USERNAME: __token__ + TWINE_PASSWORD: ${{ secrets.PYPI_API_TOKEN }} + run: twine upload dist/* \ No newline at end of file diff --git a/.github/workflows/python-apk.yml b/.github/workflows/python-apk.yml deleted file mode 100644 index 65c2c5f..0000000 --- a/.github/workflows/python-apk.yml +++ /dev/null @@ -1,49 +0,0 @@ -name: Python and Flutter Build - -on: - push: - branches: - - main - -jobs: - build: - runs-on: ubuntu-latest - - steps: - - name: Checkout code - uses: actions/checkout@v2 - - - name: Set up Python 3.12.1 - uses: actions/setup-python@v2 - with: - python-version: 3.12.1 - - - name: Install dependencies - run: | - python -m pip install --upgrade pip - pip install -r requirements.txt - - - name: Set up Android SDK - uses: android-actions/setup-android@v2 - - - name: Set up Flutter - uses: subosito/flutter-action@v2 - with: - flutter-version: '3.19.0' # La versión de Flutter que estás utilizando - - - name: Build with Flet - run: | - flet build apk --product FletApp --project FletModel - - - name: Create Release - uses: actions/upload-artifact@v2 - with: - name: package - path: | - build/ - dist/ - bin/ - lib/ - include/ - src/ - if-no-files-found: error diff --git a/.gitignore b/.gitignore deleted file mode 100644 index 82f9275..0000000 --- a/.gitignore +++ /dev/null @@ -1,162 +0,0 @@ -# Byte-compiled / optimized / DLL files -__pycache__/ -*.py[cod] -*$py.class - -# C extensions -*.so - -# Distribution / packaging -.Python -build/ -develop-eggs/ -dist/ -downloads/ -eggs/ -.eggs/ -lib/ -lib64/ -parts/ -sdist/ -var/ -wheels/ -share/python-wheels/ -*.egg-info/ -.installed.cfg -*.egg -MANIFEST - -# PyInstaller -# Usually these files are written by a python script from a template -# before PyInstaller builds the exe, so as to inject date/other infos into it. -*.manifest -*.spec - -# Installer logs -pip-log.txt -pip-delete-this-directory.txt - -# Unit test / coverage reports -htmlcov/ -.tox/ -.nox/ -.coverage -.coverage.* -.cache -nosetests.xml -coverage.xml -*.cover -*.py,cover -.hypothesis/ -.pytest_cache/ -cover/ - -# Translations -*.mo -*.pot - -# Django stuff: -*.log -local_settings.py -db.sqlite3 -db.sqlite3-journal - -# Flask stuff: -instance/ -.webassets-cache - -# Scrapy stuff: -.scrapy - -# Sphinx documentation -docs/_build/ - -# PyBuilder -.pybuilder/ -target/ - -# Jupyter Notebook -.ipynb_checkpoints - -# IPython -profile_default/ -ipython_config.py - -# pyenv -# For a library or package, you might want to ignore these files since the code is -# intended to run in multiple environments; otherwise, check them in: -# .python-version - -# pipenv -# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. -# However, in case of collaboration, if having platform-specific dependencies or dependencies -# having no cross-platform support, pipenv may install dependencies that don't work, or not -# install all needed dependencies. -#Pipfile.lock - -# poetry -# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. -# This is especially recommended for binary packages to ensure reproducibility, and is more -# commonly ignored for libraries. -# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control -#poetry.lock - -# pdm -# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. -#pdm.lock -# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it -# in version control. -# https://pdm.fming.dev/latest/usage/project/#working-with-version-control -.pdm.toml -.pdm-python -.pdm-build/ - -# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm -__pypackages__/ - -# Celery stuff -celerybeat-schedule -celerybeat.pid - -# SageMath parsed files -*.sage.py - -# Environments -.env -.venv -env/ -venv/ -ENV/ -env.bak/ -venv.bak/ - -# Spyder project settings -.spyderproject -.spyproject - -# Rope project settings -.ropeproject - -# mkdocs documentation -/site - -# mypy -.mypy_cache/ -.dmypy.json -dmypy.json - -# Pyre type checker -.pyre/ - -# pytype static type analyzer -.pytype/ - -# Cython debug symbols -cython_debug/ - -# PyCharm -# JetBrains specific template is maintained in a separate JetBrains.gitignore that can -# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore -# and can be added to the global gitignore or merged into this file. For a more nuclear -# option (not recommended) you can uncomment the following to ignore the entire idea folder. -#.idea/ diff --git a/LICENCE b/LICENCE new file mode 100644 index 0000000..40f1d9f --- /dev/null +++ b/LICENCE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2024 Fasil + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. \ No newline at end of file diff --git a/MANIFEST.in b/MANIFEST.in new file mode 100644 index 0000000..4fe739a --- /dev/null +++ b/MANIFEST.in @@ -0,0 +1,5 @@ +include LICENSE +include README.md +include src/flet_model/py.typed +recursive-include examples *.py +recursive-include tests *.py \ No newline at end of file diff --git a/README.md b/README.md index fd1fc45..5d51d12 100644 --- a/README.md +++ b/README.md @@ -1,117 +1,65 @@ - # Flet Model -This repository contains a simple Flet application demonstrating a basic structure and usage of the Flet framework. +A Model-based router for Flet applications that simplifies the creation of multi-page applications. -## Repository Structure +## Installation -```plaintext -Flet-Model/ -├── assets -│ └── icon.png -├── core -│ └── base.py -├── views -│ └── main_view.py (Add add all flet views here) -├── main.py -└── manifest.json +```bash +pip install flet-model ``` -### File Descriptions - -- **assets/icon.png**: Placeholder for assets like icons and images. -- **core/base.py**: Contains the core classes and logic for the application. -- **views/main_view.py**: Example view demonstrating the usage of the `Model` and `Control` classes. -- **main.py**: Entry point for the Flet application, handling routing and initial setup. -- **manifest.json**: Configuration file specifying application metadata and views. - -## Main Components - -### main.py - -This is the entry point of the application. It loads the configuration from `manifest.json`, dynamically imports the required modules, and sets up the routing and error handling. - -### core/base.py +## Usage -Defines the `Control` and `Model` classes which are used to represent UI components and views in the application. +Here's a simple example of how to use Flet Model: -- **Control**: Wrapper for Flet controls with additional properties. -- **Model**: Base class for creating views. It handles the creation and arrangement of `Control` instances. +```python +import flet as ft +from flet_model import main, Model -### views/main_view.py +class FirstView(Model): + route = 'first' + vertical_alignment = ft.MainAxisAlignment.CENTER + horizontal_alignment = ft.CrossAxisAlignment.CENTER -An example view demonstrating how to use the `Model` and `Control` classes. + appbar = ft.AppBar( + title=ft.Text("First View"), + center_title=True, + bgcolor=ft.Colors.SURFACE) + controls = [ + ft.ElevatedButton("Go to Second Page", on_click="go_second") + ] -```python -#main_view -import flet as ft -from core.base import Model, Control + def go_second(self, e): + self.page.go('first/second') -class MainView(Model): - route = '/' +class SecondView(Model): + route = 'second' + vertical_alignment = ft.MainAxisAlignment.CENTER + horizontal_alignment = ft.CrossAxisAlignment.CENTER appbar = ft.AppBar( - leading=ft.Icon(ft.icons.PALETTE), - leading_width=40, - title=ft.Text("AppBar Example"), - center_title=False, - bgcolor=ft.colors.SURFACE_VARIANT, - actions=[ - ft.IconButton(ft.icons.WB_SUNNY_OUTLINED), - ft.IconButton(ft.icons.FILTER_3), - ft.PopupMenuButton( - items=[ - ft.PopupMenuItem(text="Item 1"), - ] - ), - ], - ) - - name = Control(ft.TextField(label="Name"), sequence=1) - age = Control(ft.TextField(label="Age", keyboard_type=ft.KeyboardType.NUMBER), sequence=2) - submit_button = Control(ft.ElevatedButton(text="Submit", on_click="on_click_submit"), sequence=3) - - def on_click_submit(self, e): - print("Submitted") + title=ft.Text("Second View"), + center_title=True, + bgcolor=ft.Colors.SURFACE) + controls = [ + ft.ElevatedButton("Go to First", on_click="go_first") + ] + + def go_first(self, e): + self.page.go('first') + +# Run the Flet app +ft.app(target=main) ``` ## Features -- can be use for string for assign action in Control class -- - - -## Usage -1. Clone the repository: - ```bash - git clone https://github.com/fasilwdr/Flet-Model.git - cd Flet-Model - ``` - - Create views under views folder and map it into manifest.json - ```json - { - "name": "Flet App", - "short_name": "Flet App", - "version": "1.0.1", - "views": [ - "views/main_view.py", - "views/second_view.py" - ] - } - ``` - -2. Install the required dependencies: - ```bash - pip install flet - ``` - -3. Run the application: - ```bash - flet main.py - ``` - -The application should start, and you will see a simple form with fields for Name and Age, and a Submit button. +- Model-based view definition +- Automatic route handling +- Event binding +- Support for nested routes +- Easy navigation between views ## License -This project is licensed under the MIT License. +This project is licensed under the MIT License. \ No newline at end of file diff --git a/assets/icon.png b/assets/icon.png deleted file mode 100644 index f6c45a73c07d8fb1c6edfc776fac67b8ea637947..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 7048 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 diff --git a/assets/logo_128.png b/assets/logo_128.png deleted file mode 100644 index 22a177a74f0e87ec8230e965af402276f96d154b..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 3461 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 O17^>hKjWOANcw+jmH?;# diff --git a/core/__init__.py b/core/__init__.py deleted file mode 100644 index d740490..0000000 --- a/core/__init__.py +++ /dev/null @@ -1,2 +0,0 @@ -from . import base -from . import controls \ No newline at end of file diff --git a/core/base.py b/core/base.py deleted file mode 100644 index db6715e..0000000 --- a/core/base.py +++ /dev/null @@ -1,108 +0,0 @@ -# core/base.py -import flet as ft -import inspect - - -class Model: - route = None - controls = [] - back_route = None - appbar = None - bottom_appbar = None - auto_scroll = None - bgcolor = None - drawer = None - end_drawer = None - fullscreen_dialog = None - floating_action_button = None - floating_action_button_location = None - navigation_bar = None - horizontal_alignment = ft.CrossAxisAlignment.START - on_scroll_interval = 10 - on_keyboard_event = None - padding = 10 - scroll = None - on_scroll = None - spacing = 10 - vertical_alignment = ft.MainAxisAlignment.START - overlay_controls = [] - - def __init__(self, page): - self.page = page - - def on_view_pop(self, e): - self.page.go(self.back_route) - - def init(self): - pass - - def post_init(self): - pass - - def create_view(self): - controls = self.controls - if self.overlay_controls: - for overlay in self.overlay_controls: - self.page.overlay.append(overlay) - if self.back_route: - self.page.on_view_pop = self.on_view_pop - if self.on_keyboard_event: - self.page.on_keyboard_event = self.on_keyboard_event - - self.init() - - #Add Other Controls to bind event handlers - controls_to_bind = controls + self.overlay_controls + [self.appbar, self.bottom_appbar, self.drawer, self.navigation_bar] - # Dynamically bind event handlers - self.bind_event_handlers(controls_to_bind) - - view = ft.View( - route=self.route, - controls=controls, - appbar=self.appbar, - bottom_appbar=self.bottom_appbar, - auto_scroll=self.auto_scroll, - bgcolor=self.bgcolor, - drawer=self.drawer, - end_drawer=self.end_drawer, - fullscreen_dialog=self.fullscreen_dialog, - floating_action_button=self.floating_action_button, - floating_action_button_location=self.floating_action_button_location, - horizontal_alignment=self.horizontal_alignment, - on_scroll_interval=self.on_scroll_interval, - padding=self.padding, - scroll=self.scroll, - spacing=self.spacing, - vertical_alignment=self.vertical_alignment, - navigation_bar=self.navigation_bar, - on_scroll=self.on_scroll, - ) - self.post_init() - return view - - def bind_event_handlers(self, controls): - # Event handler attributes to look for - event_attrs = ['on_click', 'on_hover', 'on_long_press', 'on_change', 'on_dismiss'] - - for control in controls: - for attr in event_attrs: - if hasattr(control, attr): - handler = getattr(control, attr) - if isinstance(handler, str) and hasattr(self, handler): - # Bind the event handler - setattr(control, attr, getattr(self, handler)) - - # If the control has nested controls, bind their event handlers too - if hasattr(control, 'controls'): - self.bind_event_handlers(control.controls) - if hasattr(control, 'content'): - self.bind_event_handlers([control.content]) - - -def view_model(page): - # Scan through subclasses of Model to find a matching route - for cls in Model.__subclasses__(): - if cls.route == page.route: - return cls(page).create_view() # Pass the page object to the class - # If no matching class is found - raise ValueError("No view available for the given route") diff --git a/core/controls.py b/core/controls.py deleted file mode 100644 index e42c615..0000000 --- a/core/controls.py +++ /dev/null @@ -1,53 +0,0 @@ -import flet as ft -from datetime import datetime, date - - -def UserError(page, text): - snackbar = ft.SnackBar(content=ft.Text(text, color=ft.colors.WHITE, text_align="center"), open=True, bgcolor=ft.colors.RED) - page.show_snack_bar(snackbar) - - -def UserInfo(page, text): - snackbar = ft.SnackBar(content=ft.Text(text, color=ft.colors.WHITE, text_align="center"), open=True, bgcolor=ft.colors.BLUE) - page.show_snack_bar(snackbar) - - -def UserWarning(page, text): - snackbar = ft.SnackBar(content=ft.Text(text, color=ft.colors.BLACK, text_align="center"), open=True, bgcolor=ft.colors.YELLOW) - page.show_snack_bar(snackbar) - - -def get_formated_date(date_string, format): - date_object = datetime.strptime(date_string, "%Y-%m-%dT%H:%M:%S.%f") - formatted_date = date_object.strftime(format) - return formatted_date - - -class DateField(ft.Container): - def __init__(self, label, value='', col=None, format='%d-%m-%Y'): - super().__init__() - self.value = value - self.format = format - self.date_picker = ft.DatePicker( - on_change=self.change_date, - current_date=datetime.strptime(self.value, '%d-%m-%Y').date() if self.value else None, - first_date=date(2023, 10, 1), - ) - self.content = ft.Row(controls=[ - ft.Text(label), - ft.IconButton(ft.icons.DATE_RANGE, on_click=self.pick_date), - ft.Text(self.value) - ]) - self.col = col - # self.border = ft.InputBorder.NONE - self.padding = 5 - self.bgcolor = ft.colors.SURFACE - - def pick_date(self, e): - self.page.overlay.append(self.date_picker) - self.page.update() - self.date_picker.pick_date() - def change_date(self, e): - self.value = get_formated_date(e.data, self.format) - self.content.controls[-1].value = self.value - self.content.controls[-1].update() \ No newline at end of file diff --git a/core/db.py b/core/db.py deleted file mode 100644 index 7b22fc6..0000000 --- a/core/db.py +++ /dev/null @@ -1,47 +0,0 @@ -import sqlite3 - - -def execute_query(query, params=None): - """ - Execute a given query on the SQLite database. - - Parameters: - - query (str): The SQL query to be executed. - - params (tuple, optional): The parameters to be used with the query. - - Returns: - - list: The fetched results from the query if it is a SELECT query. - - None: If the query is not a SELECT query. - """ - conn = sqlite3.connect('assets/data.db') - cursor = conn.cursor() - - try: - if params: - cursor.execute(query, params) - else: - cursor.execute(query) - - if query.strip().lower().startswith('select'): - results = cursor.fetchall() - conn.close() - return results - else: - conn.commit() - except sqlite3.Error as e: - print(f"An error occurred: {e}") - finally: - conn.close() - - -# Create table query -create_table_query = ''' -CREATE TABLE IF NOT EXISTS stb ( - id INTEGER PRIMARY KEY, - odoo_id INTEGER, - name VARCHAR(255) -); -''' - -# Execute the create table query -execute_query(create_table_query) diff --git a/main.py b/main.py deleted file mode 100644 index a9f16bb..0000000 --- a/main.py +++ /dev/null @@ -1,47 +0,0 @@ -import flet as ft -from core.base import view_model -import json - -pattern = r'^[A-Z0-9!@#$%^&*()-_=+\\\[\]{}|;:\'",.<>/?]*$' - -# Load and parse the config.json -with open('manifest.json', 'r') as file: - manifest = json.load(file) - -# Dynamically import the module -for addon_path in manifest['views']: - module_name = addon_path.split('.')[0].replace('/', '.') - __import__(module_name) - - -def main(page: ft.Page): - def on_error_page(e): - if page.route != '/': - page.go('/') - else: - page.client_storage.clear() - page.update() - page.go('/') - - def on_route_change(e): - page.views.clear() - # Use view_model from the dynamically imported modules - page.views.append(view_model(page)) - page.update() - page.data = { - 'manifest': manifest - } - page.title = manifest.get('name', False) or 'Flet App' - page.on_route_change = on_route_change - # page.on_error = on_error_page - if manifest.get('default_theme_mode'): - page.theme_mode = manifest.get('default_theme_mode') - if manifest.get('color_scheme_seed'): - page.theme = ft.theme.Theme(color_scheme_seed=manifest.get('color_scheme_seed')) - page.scroll = ft.ScrollMode.AUTO - if not page.client_storage.get("manifest"): - page.route = '/login' - page.go(page.route) - - -ft.app(target=main, assets_dir='assets') diff --git a/manifest.json b/manifest.json deleted file mode 100644 index 1ddd2ef..0000000 --- a/manifest.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "name": "Flet App", - "short_name": "Flet App", - "version": "1.0.1", - "default_theme_mode": "light", - "views": [ - "views/main_view.py", - "views/second_view.py" - ] -} \ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..f6776a9 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,97 @@ +[build-system] +requires = ["hatchling"] +build-backend = "hatchling.build" + +[project] +name = "flet-model" +version = "0.1.0" +description = "A Model-based router for Flet applications that simplifies the creation of multi-page applications" +readme = "README.md" +authors = [{ name = "Fasil", email = "fasilwdr@hotmail.com" }] +license = { file = "LICENSE" } +classifiers = [ + "Development Status :: 3 :: Alpha", + "Intended Audience :: Developers", + "License :: OSI Approved :: MIT License", + "Operating System :: OS Independent", + "Programming Language :: Python", + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3.7", + "Programming Language :: Python :: 3.8", + "Programming Language :: Python :: 3.9", + "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", + "Topic :: Software Development :: Libraries :: Python Modules", + "Topic :: Software Development :: User Interfaces", +] +keywords = ["flet", "router", "model", "navigation", "gui", "mvc", "ui", "framework"] +dependencies = [ + "flet>=0.10.0", +] +requires-python = ">=3.7" + +[project.optional-dependencies] +dev = [ + "pytest>=7.0.0", + "pytest-cov", # for coverage reporting + "black", # for formatting + "isort", # for import sorting + "flake8", # for linting + "mypy", # for type checking + "build", # for building package + "twine", # for publishing + "sphinx", # for documentation + "sphinx-rtd-theme", # for documentation theme +] + +[project.urls] +Homepage = "https://github.com/fasilwdr/Flet-Model" +Documentation = "https://github.com/fasilwdr/Flet-Model#readme" +Repository = "https://github.com/fasilwdr/Flet-Model.git" +Issues = "https://github.com/fasilwdr/Flet-Model/issues" +Changelog = "https://github.com/fasilwdr/Flet-Model/releases" + +[tool.black] +line-length = 88 +target-version = ['py37'] +include = '\.pyi?$' +extend-exclude = ''' +# A regex preceded with ^/ will apply only to files and directories +# in the root of the project. +^/docs/ +''' + +[tool.isort] +profile = "black" +multi_line_output = 3 +include_trailing_comma = true +force_grid_wrap = 0 +use_parentheses = true +line_length = 88 +known_first_party = ["flet_model"] + +[tool.pytest.ini_options] +minversion = "7.0" +addopts = "-ra -q --cov=flet_model --cov-report=term-missing" +testpaths = [ + "tests", +] +python_files = ["test_*.py"] +pythonpath = [ + "src" +] + +[tool.mypy] +python_version = "3.7" +warn_return_any = true +warn_unused_configs = true +disallow_untyped_defs = true +disallow_incomplete_defs = true +check_untyped_defs = true +disallow_untyped_decorators = true +no_implicit_optional = true +warn_redundant_casts = true +warn_unused_ignores = true +warn_no_return = true +warn_unreachable = true +strict_optional = true \ No newline at end of file diff --git a/requirements.txt b/requirements.txt deleted file mode 100644 index 8d97ed6..0000000 --- a/requirements.txt +++ /dev/null @@ -1 +0,0 @@ -flet==0.22.0 \ No newline at end of file diff --git a/src/flet_model/__init__.py b/src/flet_model/__init__.py new file mode 100644 index 0000000..438a8ca --- /dev/null +++ b/src/flet_model/__init__.py @@ -0,0 +1,6 @@ +from .model import Model +from .router import Router + +__version__ = "0.1.0" + +__all__ = ["Model", "Router"] \ No newline at end of file diff --git a/src/flet_model/model.py b/src/flet_model/model.py new file mode 100644 index 0000000..90d8933 --- /dev/null +++ b/src/flet_model/model.py @@ -0,0 +1,172 @@ +from typing import List, Optional, Callable, Any, Union, Dict +import flet as ft +from functools import lru_cache +import weakref + + +class Model: + """Base class for creating view models in Flet applications.""" + + route: str = None + controls: Union[List[ft.Control], Callable[[], List[ft.Control]]] = [] + appbar: Optional[ft.AppBar] = None + bottom_appbar: Optional[ft.BottomAppBar] = None + auto_scroll: Optional[bool] = None + bgcolor: Optional[str] = None + drawer: Optional[ft.NavigationDrawer] = None + end_drawer: Optional[ft.NavigationDrawer] = None + fullscreen_dialog: Optional[bool] = None + floating_action_button: Optional[ft.FloatingActionButton] = None + floating_action_button_location: Optional[str] = None + navigation_bar: Optional[ft.NavigationBar] = None + horizontal_alignment: ft.CrossAxisAlignment = ft.CrossAxisAlignment.START + on_scroll_interval: int = 10 + on_keyboard_event: Optional[Callable] = None + padding: int = 10 + scroll: Optional[bool] = None + on_scroll: Optional[Callable] = None + spacing: int = 10 + vertical_alignment: ft.MainAxisAlignment = ft.MainAxisAlignment.START + overlay_controls: List[ft.Control] = [] + + # Class-level cache for event handlers + _event_handler_cache: Dict[str, Callable] = {} + + def __init__(self, page: ft.Page): + """Initialize the model with a Flet page instance.""" + self.page = page + self.view: Optional[ft.View] = None + self._control_cache = {} + + @lru_cache(maxsize=32) + def get_cached_controls(self) -> List[ft.Control]: + """Cache and return controls if they're generated by a callable.""" + if callable(self.controls): + return self.controls() + return self.controls + + def init(self) -> None: + """Initialize the model. Override this method for setup logic.""" + pass + + def post_init(self) -> None: + """Post-initialization hook. Override for logic after view creation.""" + pass + + def create_view(self) -> ft.View: + """Create and return a Flet View instance based on model properties.""" + # Use cached controls + controls = self.get_cached_controls() + + # Handle overlay controls efficiently + if self.overlay_controls: + self.page.overlay.extend(self.overlay_controls) + + # Efficient event binding + if self.on_keyboard_event: + self.page.on_keyboard_event = self.on_keyboard_event + if self.on_scroll: + self.page.on_scroll = self.on_scroll + + # Run init in thread + self.page.run_thread(self.init) + + # Collect all controls that need event binding + controls_to_bind = [] + controls_to_bind.extend(controls) + controls_to_bind.extend(self.overlay_controls) + if self.floating_action_button: + controls_to_bind.append(self.floating_action_button) + if self.bottom_appbar: + controls_to_bind.append(self.bottom_appbar) + + self.bind_event_handlers(controls_to_bind) + + # Create view with all properties + self.view = ft.View( + route=self.route, + controls=controls, + appbar=self.appbar, + bottom_appbar=self.bottom_appbar, + auto_scroll=self.auto_scroll, + bgcolor=self.bgcolor, + drawer=self.drawer, + end_drawer=self.end_drawer, + fullscreen_dialog=self.fullscreen_dialog, + floating_action_button=self.floating_action_button, + floating_action_button_location=self.floating_action_button_location, + horizontal_alignment=self.horizontal_alignment, + on_scroll_interval=self.on_scroll_interval, + padding=self.padding, + scroll=self.scroll, + spacing=self.spacing, + vertical_alignment=self.vertical_alignment, + navigation_bar=self.navigation_bar, + on_scroll=self.on_scroll, + ) + + self.page.run_thread(self.post_init) + return self.view + + def update(self) -> None: + """Update the view with current model properties.""" + if not self.view: + return + + # Batch update all properties + updates = { + 'controls': self.get_cached_controls(), + 'appbar': self.appbar, + 'bottom_appbar': self.bottom_appbar, + 'auto_scroll': self.auto_scroll, + 'bgcolor': self.bgcolor, + 'drawer': self.drawer, + 'end_drawer': self.end_drawer, + 'fullscreen_dialog': self.fullscreen_dialog, + 'floating_action_button': self.floating_action_button, + 'floating_action_button_location': self.floating_action_button_location, + 'horizontal_alignment': self.horizontal_alignment, + 'on_scroll_interval': self.on_scroll_interval, + 'padding': self.padding, + 'scroll': self.scroll, + 'spacing': self.spacing, + 'vertical_alignment': self.vertical_alignment, + 'navigation_bar': self.navigation_bar, + } + + for attr, value in updates.items(): + if hasattr(self.view, attr): + setattr(self.view, attr, value) + + self.view.update() + + def bind_event_handlers(self, controls: List[ft.Control]) -> None: + """Recursively bind event handlers to controls with caching.""" + event_attrs = ('on_click', 'on_hover', 'on_long_press', 'on_change', 'on_dismiss') + + for control in controls: + if not control: + continue + + control_id = id(control) + if control_id in self._control_cache: + continue + + self._control_cache[control_id] = True + + for attr in event_attrs: + if hasattr(control, attr): + handler = getattr(control, attr) + if isinstance(handler, str) and hasattr(self, handler): + # Cache the handler + if handler not in self._event_handler_cache: + self._event_handler_cache[handler] = getattr(self, handler) + setattr(control, attr, self._event_handler_cache[handler]) + + # Recursively bind nested controls + if hasattr(control, 'controls') and control.controls: + self.bind_event_handlers(control.controls) + if hasattr(control, 'content') and control.content: + self.bind_event_handlers([control.content]) + if hasattr(control, 'header') and control.header: + self.bind_event_handlers([control.header]) \ No newline at end of file diff --git a/src/flet_model/router.py b/src/flet_model/router.py new file mode 100644 index 0000000..6f41fca --- /dev/null +++ b/src/flet_model/router.py @@ -0,0 +1,86 @@ +from typing import Dict, Optional, Type +import flet as ft +from .model import Model + + +class Router: + """Router class for handling navigation in Flet applications.""" + + _instance: Optional['Router'] = None + _routes: Dict[str, Model] = {} + _page: Optional[ft.Page] = None + _view_cache: Dict[str, ft.View] = {} # Cache for views + + def __new__(cls, *args, **kwargs): + if cls._instance is None: + cls._instance = super(Router, cls).__new__(cls) + return cls._instance + + def __init__(self, *route_maps: Dict[str, Model]): + """Initialize the router with route mappings.""" + if not self._routes: + self._routes = {} + for route_map in route_maps: + self._routes.update(route_map) + + if route_maps and list(route_maps[0].values()): + first_model = list(route_maps[0].values())[0] + self._page = first_model.page + self._setup_routing() + + def _setup_routing(self) -> None: + """Set up route handling and initialize default route.""" + if not self._page: + return + + self._page.on_route_change = self._handle_route_change + self._page.on_view_pop = self._handle_view_pop + + if not self._page.route or self._page.route == '/': + default_route = next(iter(self._routes.keys())) + self._page.route = default_route + self._page.go(default_route) + + def _handle_route_change(self, e: ft.RouteChangeEvent) -> None: + """Handle route changes and update view stack with caching.""" + route_parts = self._page.route.lstrip('/').split('/') + self._page.views.clear() + current_route = '' + + for part in route_parts: + if part: + current_route = f"{current_route}/{part}" if current_route else part + if part in self._routes: + # Check view cache first + if part not in self._view_cache: + self._view_cache[part] = self._routes[part].create_view() + self._page.views.append(self._view_cache[part]) + + self._page.update() + + def _handle_view_pop(self, e: ft.ViewPopEvent) -> None: + """Handle back navigation.""" + if len(self._page.views) > 1: + self._page.views.pop() + routes = self._page.route.split('/') + routes.pop() + self._page.go('/'.join(routes)) + self._page.update() + + @classmethod + def register_route(cls, route: str, model_class: Type[Model]) -> None: + """Register a new route with its corresponding model class.""" + if cls._instance and cls._instance._page: + cls._instance._routes[route] = model_class(cls._instance._page) + # Clear view cache for this route + if route in cls._instance._view_cache: + del cls._instance._view_cache[route] + + @classmethod + def get_current_model(cls) -> Optional[Model]: + """Get the model instance for the current route.""" + if not (cls._instance and cls._instance._page and cls._instance._page.route): + return None + + current_route = cls._instance._page.route.split('/')[-1] + return cls._instance._routes.get(current_route) \ No newline at end of file diff --git a/tests/test_model.py b/tests/test_model.py new file mode 100644 index 0000000..e86aaf4 --- /dev/null +++ b/tests/test_model.py @@ -0,0 +1,31 @@ +import pytest +import flet as ft +from flet_model import Model + + +def test_model_initialization(): + page = ft.Page() + + class TestModel(Model): + route = "test" + controls = [ft.Text("Test")] + + model = TestModel(page) + assert model.route == "test" + assert len(model.controls) == 1 + + +def test_model_view_creation(): + page = ft.Page() + + class TestModel(Model): + route = "test" + controls = [ft.Text("Test")] + appbar = ft.AppBar(title=ft.Text("Test")) + + model = TestModel(page) + view = model.create_view() + + assert view.route == "test" + assert len(view.controls) == 1 + assert isinstance(view.appbar, ft.AppBar) \ No newline at end of file diff --git a/views/main_view.py b/views/main_view.py deleted file mode 100644 index a98aa6f..0000000 --- a/views/main_view.py +++ /dev/null @@ -1,163 +0,0 @@ -#main_view -import flet as ft -from core.base import Model -from core.controls import UserError, UserInfo, UserWarning -import datetime - - -class MainView(Model): - route = '/' - vertical_alignment = ft.MainAxisAlignment.CENTER - horizontal_alignment = ft.CrossAxisAlignment.CENTER - - appbar = ft.AppBar( - leading=ft.IconButton(ft.icons.PALETTE, on_click="check"), - leading_width=40, - title=ft.Text("Main View"), - center_title=True, - bgcolor=ft.colors.SURFACE_VARIANT) - - navigation_bar = ft.NavigationBar( - destinations=[ - ft.NavigationDestination(icon=ft.icons.EXPLORE, label="Explore"), - ft.NavigationDestination(icon=ft.icons.COMMUTE, label="Commute"), - ft.NavigationDestination( - icon=ft.icons.BOOKMARK_BORDER, - selected_icon=ft.icons.BOOKMARK, - label="Explore", - ), - ] - ) - drawer = ft.NavigationDrawer( - controls=[ - ft.Container(height=12), - ft.NavigationDrawerDestination( - label="Item 1", - icon=ft.icons.DOOR_BACK_DOOR_OUTLINED, - selected_icon_content=ft.Icon(ft.icons.DOOR_BACK_DOOR), - ), - ft.Divider(thickness=2), - ft.NavigationDrawerDestination( - icon_content=ft.Icon(ft.icons.MAIL_OUTLINED), - label="Item 2", - selected_icon=ft.icons.MAIL, - ), - ft.NavigationDrawerDestination( - icon_content=ft.Icon(ft.icons.PHONE_OUTLINED), - label="Item 3", - selected_icon=ft.icons.PHONE, - ), - ], - ) - - # Banner - def close_banner(e): - e.control.page.close_banner() - print("banner closed") - - banner = ft.Banner( - bgcolor=ft.colors.AMBER_100, - leading=ft.Icon(ft.icons.WARNING_AMBER_ROUNDED, color=ft.colors.AMBER, size=40), - content=ft.Text( - "Oops, there were some errors while trying to delete the file. What would you like me to do?" - ), - actions=[ - ft.TextButton("Retry", on_click=close_banner), - ft.TextButton("Ignore", on_click=close_banner), - ft.TextButton("Cancel", on_click=close_banner), - ], - ) - - # AlertDialog - dlg = ft.AlertDialog( - title=ft.Text("Hello, you!") - ) - - dlg_modal = ft.AlertDialog( - modal=True, - title=ft.Text("Please confirm"), - content=ft.Text("Do you really want to delete all those files?"), - actions=[ - ft.TextButton("Yes", on_click=lambda e: e.control.page.close_dialog()), - ft.TextButton("No", on_click=lambda e: e.control.page.close_dialog()), - ], - actions_alignment=ft.MainAxisAlignment.END, - on_dismiss=lambda e: print("Modal dialog dismissed!"), - ) - - actions = ft.Dropdown( - options=[ - ft.dropdown.Option("Open Drawer"), - ft.dropdown.Option("Show Banner"), - # ft.dropdown.Option("Open DatePicker"), - ft.dropdown.Option("Show Dialog"), - ft.dropdown.Option("Show Dialog Modal"), - ft.dropdown.Option("Show BottomSheet"), - ft.dropdown.Option("Check UserError (SnackBar)"), - ft.dropdown.Option("Check UserInfo (SnackBar)"), - ft.dropdown.Option("Check UserWarning (SnackBar)"), - ft.dropdown.Option("Go to Second Page"), - ], - ) - # BottomSheet - bottom_sheet = ft.BottomSheet( - ft.Container( - ft.Column( - [ - ft.Text("This is sheet's content!"), - ft.ElevatedButton("Close bottom sheet", on_click=lambda e: e.control.page.close_bottom_sheet()), - ], - tight=True, - ), - padding=10, - ), - open=True, - on_dismiss=lambda e: print("Bottom Sheet dismissed!"), - ) - - # DatePicker - def change_date(self, e): - print(f"Date picker changed, value is {self.date_picker.value}") - - def date_picker_dismissed(self, e): - print(f"Date picker dismissed, value is {self.date_picker.value}") - - # date_picker = ft.DatePicker( - # on_change="change_date", - # on_dismiss="date_picker_dismissed", - # first_date=datetime.datetime(2023, 10, 1), - # last_date=datetime.datetime(2024, 10, 1), - # ) - # - # overlay_controls = [date_picker] - - controls = [ - actions, - ft.ElevatedButton("Go", on_click='on_click_check_button') - ] - - def on_click_check_button(self, e): - if self.actions.value == 'Check UserError (SnackBar)': - return UserError(self.page, "Error Message") - elif self.actions.value == 'Check UserInfo (SnackBar)': - return UserInfo(self.page, "Info Message") - elif self.actions.value == 'Check UserWarning (SnackBar)': - return UserWarning(self.page, "Warning Message") - if self.actions.value == 'Open Drawer': - self.page.show_drawer(self.drawer) - elif self.actions.value == 'Go to Second Page': - self.page.go('/second') - elif self.actions.value == 'Open DatePicker': - self.date_picker.pick_date() - elif self.actions.value == 'Show Banner': - self.page.show_banner(self.banner) - elif self.actions.value == 'Show Dialog': - self.page.show_dialog(self.dlg) - elif self.actions.value == 'Show Dialog Modal': - self.page.show_dialog(self.dlg_modal) - elif self.actions.value == 'Show BottomSheet': - self.page.show_bottom_sheet(self.bottom_sheet) - - - - diff --git a/views/second_view.py b/views/second_view.py deleted file mode 100644 index cb43c38..0000000 --- a/views/second_view.py +++ /dev/null @@ -1,27 +0,0 @@ -#main_view -import flet as ft -from core.base import Model - - -class SecondView(Model): - route = '/second' - back_route = '/' - - vertical_alignment = ft.MainAxisAlignment.CENTER - horizontal_alignment = ft.CrossAxisAlignment.CENTER - - # def init(self): - # print("init") - - appbar = ft.AppBar( - leading=ft.Icon(ft.icons.PALETTE), - leading_width=40, - title=ft.Text("Second View"), - center_title=True, - bgcolor=ft.colors.SURFACE_VARIANT) - - controls = [ - ft.Text("Second Page"), - ft.ElevatedButton("Go Home", on_click=lambda e: e.control.page.go('/')) - ] -