From c388542844f31712d3743cda05350df6008da259 Mon Sep 17 00:00:00 2001 From: anubrag Date: Wed, 17 Jan 2024 23:35:19 +0530 Subject: [PATCH 1/2] added mail module --- nextpy/backend/module/email/__init__.py | 4 - nextpy/backend/module/mail/__init__.py | 16 +++ nextpy/backend/module/mail/config.py | 19 ++++ nextpy/backend/module/mail/email_template.py | 15 +++ nextpy/backend/module/mail/exceptions.py | 8 ++ nextpy/backend/module/mail/message.py | 61 +++++++++++ nextpy/backend/module/mail/sender.py | 70 ++++++++++++ poetry.lock | 63 ++++++++++- pyproject.toml | 3 +- .../backend/module/mail/test_email_module.py | 103 ++++++++++++++++++ tests/backend/module/mail/test_template.html | 10 ++ 11 files changed, 362 insertions(+), 10 deletions(-) delete mode 100644 nextpy/backend/module/email/__init__.py create mode 100644 nextpy/backend/module/mail/__init__.py create mode 100644 nextpy/backend/module/mail/config.py create mode 100644 nextpy/backend/module/mail/email_template.py create mode 100644 nextpy/backend/module/mail/exceptions.py create mode 100644 nextpy/backend/module/mail/message.py create mode 100644 nextpy/backend/module/mail/sender.py create mode 100644 tests/backend/module/mail/test_email_module.py create mode 100644 tests/backend/module/mail/test_template.html diff --git a/nextpy/backend/module/email/__init__.py b/nextpy/backend/module/email/__init__.py deleted file mode 100644 index 3f7d01b3..00000000 --- a/nextpy/backend/module/email/__init__.py +++ /dev/null @@ -1,4 +0,0 @@ -# This file has been modified by the Nextpy Team in 2023 using AI tools and automation scripts. -# We have rigorously tested these modifications to ensure reliability and performance. Based on successful test results, we are confident in the quality and stability of these changes. - -"""The email package builts on top of fastapi-mail.""" diff --git a/nextpy/backend/module/mail/__init__.py b/nextpy/backend/module/mail/__init__.py new file mode 100644 index 00000000..a3ecbd4c --- /dev/null +++ b/nextpy/backend/module/mail/__init__.py @@ -0,0 +1,16 @@ +"""The email package.""" + +from .config import EmailConfig +from .message import EmailMessage +from .sender import EmailSender +from .email_template import EmailTemplateManager +from .exceptions import EmailConfigError, EmailSendError + +__all__ = [ + 'EmailConfig', + 'EmailSender', + 'EmailMessage', + 'EmailTemplateManager', + 'EmailConfigError', + 'EmailSendError' +] diff --git a/nextpy/backend/module/mail/config.py b/nextpy/backend/module/mail/config.py new file mode 100644 index 00000000..b54cea5e --- /dev/null +++ b/nextpy/backend/module/mail/config.py @@ -0,0 +1,19 @@ +from pydantic import BaseModel, EmailStr + + +class EmailConfig(BaseModel): + """Configuration class for the email module.""" + + MAIL_SERVER: str # SMTP server address + MAIL_PORT: int # SMTP server port + MAIL_USERNAME: str # SMTP username + MAIL_PASSWORD: str # SMTP password + MAIL_USE_TLS: bool = False # Whether to use TLS + MAIL_USE_SSL: bool = False # Whether to use SSL + MAIL_DEFAULT_SENDER: EmailStr # Default email sender + MAIL_TEMPLATE_FOLDER: str = None # Path to the email templates directory + MAIL_MAX_EMAILS: int = None # Maximum number of emails to send + MAIL_SUPPRESS_SEND: bool = False # Suppress sending emails for testing + + class Config: + env_prefix = "EMAIL_" # Prefix for environment variable configuration diff --git a/nextpy/backend/module/mail/email_template.py b/nextpy/backend/module/mail/email_template.py new file mode 100644 index 00000000..fc4c3b61 --- /dev/null +++ b/nextpy/backend/module/mail/email_template.py @@ -0,0 +1,15 @@ +from jinja2 import Environment, FileSystemLoader, select_autoescape + +from .config import EmailConfig + + +class EmailTemplateManager: + def __init__(self, config: EmailConfig): + self.env = Environment( + loader=FileSystemLoader(config.MAIL_TEMPLATE_FOLDER), + autoescape=select_autoescape(['html', 'xml']) + ) + + def render_template(self, template_name: str, context: dict) -> str: + template = self.env.get_template(template_name) + return template.render(context) diff --git a/nextpy/backend/module/mail/exceptions.py b/nextpy/backend/module/mail/exceptions.py new file mode 100644 index 00000000..5dbcb41b --- /dev/null +++ b/nextpy/backend/module/mail/exceptions.py @@ -0,0 +1,8 @@ +class EmailError(Exception): + """Base class for email errors.""" + +class EmailConfigError(EmailError): + """Raised for configuration related errors.""" + +class EmailSendError(EmailError): + """Raised when sending an email fails.""" diff --git a/nextpy/backend/module/mail/message.py b/nextpy/backend/module/mail/message.py new file mode 100644 index 00000000..e23d14e3 --- /dev/null +++ b/nextpy/backend/module/mail/message.py @@ -0,0 +1,61 @@ +import mimetypes +import os +from email import encoders +from email.mime.base import MIMEBase +from enum import Enum +from typing import Dict, List, Optional, Union + +from pydantic import BaseModel, EmailStr, validator + + +class MessageType(str, Enum): + PLAIN = "plain" + HTML = "html" + +class MultipartSubtypeEnum(str, Enum): + MIXED = "mixed" + DIGEST = "digest" + ALTERNATIVE = "alternative" + RELATED = "related" + REPORT = "report" + SIGNED = "signed" + ENCRYPTED = "encrypted" + FORM_DATA = "form-data" + MIXED_REPLACE = "x-mixed-replace" + BYTERANGE = "byterange" + +class EmailMessage(BaseModel): + recipients: List[EmailStr] + subject: str + body: Optional[str] = None + html_body: Optional[str] = None + sender: Optional[EmailStr] = None + cc: List[EmailStr] = [] + bcc: List[EmailStr] = [] + attachments: List[str] = [] # List of file paths for attachments + template_body: Optional[Union[dict, list, str]] = None # Template context + subtype: MessageType = MessageType.PLAIN + multipart_subtype: MultipartSubtypeEnum = MultipartSubtypeEnum.MIXED + headers: Optional[Dict[str, str]] = None + + @validator('attachments', each_item=True) + def validate_attachment(cls, v): + if isinstance(v, str) and os.path.isfile(v) and os.access(v, os.R_OK): + return v + raise ValueError("Attachment must be a readable file path") + + def create_attachment(self, filepath: str) -> MIMEBase: + """Creates a MIMEBase object for the given attachment file.""" + ctype, encoding = mimetypes.guess_type(filepath) + if ctype is None or encoding is not None: + ctype = 'application/octet-stream' + maintype, subtype = ctype.split('/', 1) + with open(filepath, 'rb') as fp: + attachment = MIMEBase(maintype, subtype) + attachment.set_payload(fp.read()) + encoders.encode_base64(attachment) + attachment.add_header('Content-Disposition', 'attachment', filename=os.path.basename(filepath)) + return attachment + + class Config: + arbitrary_types_allowed = True diff --git a/nextpy/backend/module/mail/sender.py b/nextpy/backend/module/mail/sender.py new file mode 100644 index 00000000..3238046f --- /dev/null +++ b/nextpy/backend/module/mail/sender.py @@ -0,0 +1,70 @@ +from email.mime.multipart import MIMEMultipart +from email.mime.text import MIMEText +from typing import Optional + +import aiosmtplib + +from .config import EmailConfig +from .email_template import EmailTemplateManager +from .exceptions import EmailSendError +from .message import EmailMessage + + +class EmailSender: + def __init__(self, config: EmailConfig, template_manager: Optional[EmailTemplateManager] = None): + self.config = config + self.template_manager = template_manager + + async def send_email(self, message: EmailMessage, template_name: Optional[str] = None) -> None: + """Sends an email message.""" + if self.config.MAIL_SUPPRESS_SEND: + return + + smtp = aiosmtplib.SMTP() + + try: + await smtp.connect(hostname=self.config.MAIL_SERVER, port=self.config.MAIL_PORT, use_tls=self.config.MAIL_USE_TLS) + if self.config.MAIL_USE_TLS or self.config.MAIL_USE_SSL: + await smtp.starttls() + if self.config.MAIL_USERNAME and self.config.MAIL_PASSWORD: + await smtp.login(self.config.MAIL_USERNAME, self.config.MAIL_PASSWORD) + + mime_message = await self._create_mime_message(message, template_name) + await smtp.send_message(mime_message) + except Exception as error: + # Consider adding logging here + raise EmailSendError(f"Failed to send email: {error}") + finally: + if smtp.is_connected: + await smtp.quit() + + + async def _create_mime_message( + self, message: EmailMessage, template_name: Optional[str] = None + ) -> MIMEMultipart: + """Creates a MIME message from an EmailMessage object.""" + mime_message = MIMEMultipart("mixed" if message.attachments else "alternative") + mime_message["Subject"] = message.subject + mime_message["From"] = ( + message.sender if message.sender else self.config.MAIL_DEFAULT_SENDER + ) + mime_message["To"] = ", ".join(message.recipients) + + # If a template is provided, render the email content using the template + if template_name and self.template_manager: + rendered_content = self.template_manager.render_template( + template_name, message.template_body + ) + mime_message.attach(MIMEText(rendered_content, "html")) + else: + if message.body: + mime_message.attach(MIMEText(message.body, "plain")) + if message.html_body: + mime_message.attach(MIMEText(message.html_body, "html")) + + # Handling attachments + for attachment_path in message.attachments: + attachment = message.create_attachment(attachment_path) + mime_message.attach(attachment) + + return mime_message diff --git a/poetry.lock b/poetry.lock index 30932ee6..583095b4 100644 --- a/poetry.lock +++ b/poetry.lock @@ -112,6 +112,22 @@ files = [ [package.dependencies] frozenlist = ">=1.1.0" +[[package]] +name = "aiosmtplib" +version = "3.0.1" +description = "asyncio SMTP client" +category = "main" +optional = false +python-versions = ">=3.8,<4.0" +files = [ + {file = "aiosmtplib-3.0.1-py3-none-any.whl", hash = "sha256:abcceae7e820577307b4cda2041b2c25e5121469c0e186764ddf8e15b12064cd"}, + {file = "aiosmtplib-3.0.1.tar.gz", hash = "sha256:43580604b152152a221598be3037f0ae6359c2817187ac4433bd857bc3fc6513"}, +] + +[package.extras] +docs = ["furo (>=2023.9.10,<2024.0.0)", "sphinx (>=7.0.0,<8.0.0)", "sphinx-copybutton (>=0.5.0,<0.6.0)", "sphinx_autodoc_typehints (>=1.24.0,<2.0.0)"] +uvloop = ["uvloop (>=0.18,<0.19)"] + [[package]] name = "alembic" version = "1.13.1" @@ -800,6 +816,42 @@ files = [ {file = "distro-1.9.0.tar.gz", hash = "sha256:2fa77c6fd8940f116ee1d6b94a2f90b13b5ea8d019b98bc8bafdcabcdd9bdbed"}, ] +[[package]] +name = "dnspython" +version = "2.4.2" +description = "DNS toolkit" +category = "main" +optional = false +python-versions = ">=3.8,<4.0" +files = [ + {file = "dnspython-2.4.2-py3-none-any.whl", hash = "sha256:57c6fbaaeaaf39c891292012060beb141791735dbb4004798328fc2c467402d8"}, + {file = "dnspython-2.4.2.tar.gz", hash = "sha256:8dcfae8c7460a2f84b4072e26f1c9f4101ca20c071649cb7c34e8b6a93d58984"}, +] + +[package.extras] +dnssec = ["cryptography (>=2.6,<42.0)"] +doh = ["h2 (>=4.1.0)", "httpcore (>=0.17.3)", "httpx (>=0.24.1)"] +doq = ["aioquic (>=0.9.20)"] +idna = ["idna (>=2.1,<4.0)"] +trio = ["trio (>=0.14,<0.23)"] +wmi = ["wmi (>=1.5.1,<2.0.0)"] + +[[package]] +name = "email-validator" +version = "2.1.0.post1" +description = "A robust email address syntax and deliverability validation library." +category = "main" +optional = false +python-versions = ">=3.8" +files = [ + {file = "email_validator-2.1.0.post1-py3-none-any.whl", hash = "sha256:c973053efbeddfef924dc0bd93f6e77a1ea7ee0fce935aea7103c7a3d6d2d637"}, + {file = "email_validator-2.1.0.post1.tar.gz", hash = "sha256:a4b0bd1cf55f073b924258d19321b1f3aa74b4b5a71a42c305575dba920e1a44"}, +] + +[package.dependencies] +dnspython = ">=2.0.0" +idna = ">=2.0.0" + [[package]] name = "exceptiongroup" version = "1.2.0" @@ -1269,14 +1321,14 @@ files = [ [[package]] name = "ipykernel" -version = "6.28.0" +version = "6.29.0" description = "IPython Kernel for Jupyter" category = "dev" optional = false python-versions = ">=3.8" files = [ - {file = "ipykernel-6.28.0-py3-none-any.whl", hash = "sha256:c6e9a9c63a7f4095c0a22a79f765f079f9ec7be4f2430a898ddea889e8665661"}, - {file = "ipykernel-6.28.0.tar.gz", hash = "sha256:69c11403d26de69df02225916f916b37ea4b9af417da0a8c827f84328d88e5f3"}, + {file = "ipykernel-6.29.0-py3-none-any.whl", hash = "sha256:076663ca68492576f051e4af7720d33f34383e655f2be0d544c8b1c9de915b2f"}, + {file = "ipykernel-6.29.0.tar.gz", hash = "sha256:b5dd3013cab7b330df712891c96cd1ab868c27a7159e606f762015e9bf8ceb3f"}, ] [package.dependencies] @@ -1299,7 +1351,7 @@ cov = ["coverage[toml]", "curio", "matplotlib", "pytest-cov", "trio"] docs = ["myst-parser", "pydata-sphinx-theme", "sphinx", "sphinx-autodoc-typehints", "sphinxcontrib-github-alt", "sphinxcontrib-spelling", "trio"] pyqt5 = ["pyqt5"] pyside6 = ["pyside6"] -test = ["flaky", "ipyparallel", "pre-commit", "pytest (>=7.0)", "pytest-asyncio", "pytest-cov", "pytest-timeout"] +test = ["flaky", "ipyparallel", "pre-commit", "pytest (>=7.0)", "pytest-asyncio (==0.23.2)", "pytest-cov", "pytest-timeout"] [[package]] name = "ipython" @@ -2494,6 +2546,7 @@ files = [ ] [package.dependencies] +email-validator = {version = ">=1.0.3", optional = true, markers = "extra == \"email\""} typing-extensions = ">=4.2.0" [package.extras] @@ -4689,4 +4742,4 @@ testing = ["big-O", "jaraco.functools", "jaraco.itertools", "more-itertools", "p [metadata] lock-version = "2.0" python-versions = "^3.8" -content-hash = "f858d77a327ef9175faa6593455e7e56277aa68ebdac20a877f40995e80fc9f2" +content-hash = "dec3a720b0436264e305dc74f25d764ae2823dda2de4c822ac89d59da9aa9cdc" diff --git a/pyproject.toml b/pyproject.toml index 05a53d31..8648c3c3 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -21,7 +21,7 @@ gunicorn = "^20.1.0" httpx = ">=0.24.0,<0.26.0" jinja2 = "^3.1.2" psutil = "^5.9.4" -pydantic = "^1.10.2" +pydantic = {version = "^1.10.2", extras = ["email"]} python-multipart = "^0.0.5" python-socketio = "^5.7.0" redis = "^4.3.5" @@ -50,6 +50,7 @@ websockets = ">=10.4" pyjokes = "^0.6.0" pylint = "^3.0.3" charset-normalizer = "^3.3.2" +aiosmtplib = "^3.0.1" [tool.poetry.group.dev.dependencies] pytest = "^7.1.2" diff --git a/tests/backend/module/mail/test_email_module.py b/tests/backend/module/mail/test_email_module.py new file mode 100644 index 00000000..46653bc2 --- /dev/null +++ b/tests/backend/module/mail/test_email_module.py @@ -0,0 +1,103 @@ +# test_email_module.py +from pathlib import Path +import aiosmtplib +from unittest.mock import AsyncMock, MagicMock, patch +import pytest +import pytest_asyncio +from email.mime.multipart import MIMEMultipart + +from nextpy.backend.module.mail.config import EmailConfig +from nextpy.backend.module.mail.email_template import EmailTemplateManager +from nextpy.backend.module.mail.sender import EmailSender +from nextpy.backend.module.mail.message import EmailMessage +from nextpy.backend.module.mail.exceptions import EmailSendError + +# Fixture for Email Configuration +@pytest.fixture +def email_config(): + # Specify the directory where your test_template.html is located + test_template_dir = Path(__file__).parent + return EmailConfig( + MAIL_SERVER="smtp.example.com", + MAIL_PORT=587, + MAIL_USERNAME="user@example.com", + MAIL_PASSWORD="password", + MAIL_USE_TLS=True, + MAIL_USE_SSL=False, + MAIL_DEFAULT_SENDER="sender@example.com", + MAIL_TEMPLATE_FOLDER=str(test_template_dir) # Set the directory where test_template.html is located + ) + +@pytest.fixture +def mock_template_manager(): + # Mock your EmailTemplateManager here if needed, or provide a real instance + return MagicMock(spec=EmailTemplateManager) + +# Fixture for Email Template Manager +@pytest.fixture +def email_template_manager(email_config): + return EmailTemplateManager(email_config) + +# Fixture for Email Sender +@pytest.fixture +def email_sender(email_config, email_template_manager): + return EmailSender(email_config, email_template_manager) + +# Test for Email Template Rendering +def test_template_rendering(email_template_manager): + rendered_content = email_template_manager.render_template("test_template.html", {"name": "Test"}) + assert "Test" in rendered_content + +# Test for Sending Email +@pytest.mark.asyncio +async def test_send_email(email_config, mock_template_manager): + # Mock the SMTP client + with patch('aiosmtplib.SMTP') as mock_smtp: + mock_smtp.return_value.connect = AsyncMock() + + email_sender = EmailSender(email_config, mock_template_manager) + + # Mock the connect, starttls, login, send_message and quit methods + mock_smtp.return_value.connect = AsyncMock() + mock_smtp.return_value.starttls = AsyncMock() + mock_smtp.return_value.login = AsyncMock() + mock_smtp.return_value.send_message = AsyncMock() + mock_smtp.return_value.quit = AsyncMock() + + # Properly instantiate EmailMessage + message = EmailMessage( + recipients=["recipient@example.com"], + subject="Test Subject", + body="Test email body" + ) + await email_sender.send_email(message) + + # Assertions to ensure methods were called + mock_smtp.return_value.connect.assert_called_once() + mock_smtp.return_value.starttls.assert_called_once() + mock_smtp.return_value.login.assert_called_once() + mock_smtp.return_value.send_message.assert_called_once() + mock_smtp.return_value.quit.assert_called_once() + + +# Test for Handling Email Sending Failure +@pytest.mark.asyncio +async def test_send_email_failure(email_config, mock_template_manager): + # Mock the SMTP client + with patch('aiosmtplib.SMTP') as mock_smtp: + mock_smtp.return_value.connect = AsyncMock(side_effect=aiosmtplib.errors.SMTPConnectError("Connection error")) + mock_smtp.return_value.is_connected = True + mock_smtp.return_value.quit = AsyncMock() + + email_sender = EmailSender(email_config, mock_template_manager) + + # Properly instantiate EmailMessage + message = EmailMessage( + recipients=["recipient@example.com"], + subject="Test Email", + body="This is a test email." + ) + email_sender.config.MAIL_SERVER = "invalid.server.com" # Intentionally incorrect + + with pytest.raises(EmailSendError): + await email_sender.send_email(message) diff --git a/tests/backend/module/mail/test_template.html b/tests/backend/module/mail/test_template.html new file mode 100644 index 00000000..ff524923 --- /dev/null +++ b/tests/backend/module/mail/test_template.html @@ -0,0 +1,10 @@ + + + + Test Email + + +

Hello {{ name }}

+

This is a test email template.

+ + \ No newline at end of file From 041b285197bca2c4213af260d2b0edb75759c902 Mon Sep 17 00:00:00 2001 From: anubrag Date: Thu, 18 Jan 2024 12:08:28 +0530 Subject: [PATCH 2/2] update unstyed --- app-examples/unstyled_example/.gitignore | 4 ++ .../unstyled_example/assets/favicon.ico | Bin 0 -> 15406 bytes .../unstyled_example/assets/github.svg | 10 ++++ .../assets/gradient_underline.svg | 2 + app-examples/unstyled_example/assets/icon.svg | 3 + .../unstyled_example/assets/logo_darkmode.svg | 2 + .../unstyled_example/assets/paneleft.svg | 13 +++++ .../assets/text_logo_darkmode.svg | 2 + .../unstyled_example/__init__.py | 3 + .../unstyled_example/unstyled_example.py | 54 ++++++++++++++++++ app-examples/unstyled_example/xtconfig.py | 5 ++ nextpy/__init__.py | 7 ++- nextpy/__init__.pyi | 19 ++++-- nextpy/frontend/components/__init__.py | 1 - nextpy/frontend/components/proxy/__init__.py | 6 +- nextpy/frontend/components/proxy/unstyled.py | 26 +++++++++ 16 files changed, 145 insertions(+), 12 deletions(-) create mode 100644 app-examples/unstyled_example/.gitignore create mode 100644 app-examples/unstyled_example/assets/favicon.ico create mode 100644 app-examples/unstyled_example/assets/github.svg create mode 100644 app-examples/unstyled_example/assets/gradient_underline.svg create mode 100644 app-examples/unstyled_example/assets/icon.svg create mode 100644 app-examples/unstyled_example/assets/logo_darkmode.svg create mode 100644 app-examples/unstyled_example/assets/paneleft.svg create mode 100644 app-examples/unstyled_example/assets/text_logo_darkmode.svg create mode 100644 app-examples/unstyled_example/unstyled_example/__init__.py create mode 100644 app-examples/unstyled_example/unstyled_example/unstyled_example.py create mode 100644 app-examples/unstyled_example/xtconfig.py create mode 100644 nextpy/frontend/components/proxy/unstyled.py diff --git a/app-examples/unstyled_example/.gitignore b/app-examples/unstyled_example/.gitignore new file mode 100644 index 00000000..eab0d4b0 --- /dev/null +++ b/app-examples/unstyled_example/.gitignore @@ -0,0 +1,4 @@ +*.db +*.py[cod] +.web +__pycache__/ \ No newline at end of file diff --git a/app-examples/unstyled_example/assets/favicon.ico b/app-examples/unstyled_example/assets/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..1f55d3be060bb3a53118ff759b317978debf9f28 GIT binary patch literal 15406 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 + + + + + + + + + diff --git a/app-examples/unstyled_example/assets/gradient_underline.svg b/app-examples/unstyled_example/assets/gradient_underline.svg new file mode 100644 index 00000000..36ff3730 --- /dev/null +++ b/app-examples/unstyled_example/assets/gradient_underline.svg @@ -0,0 +1,2 @@ + + \ No newline at end of file diff --git a/app-examples/unstyled_example/assets/icon.svg b/app-examples/unstyled_example/assets/icon.svg new file mode 100644 index 00000000..f7ee063b --- /dev/null +++ b/app-examples/unstyled_example/assets/icon.svg @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/app-examples/unstyled_example/assets/logo_darkmode.svg b/app-examples/unstyled_example/assets/logo_darkmode.svg new file mode 100644 index 00000000..e90f7463 --- /dev/null +++ b/app-examples/unstyled_example/assets/logo_darkmode.svg @@ -0,0 +1,2 @@ + + \ No newline at end of file diff --git a/app-examples/unstyled_example/assets/paneleft.svg b/app-examples/unstyled_example/assets/paneleft.svg new file mode 100644 index 00000000..ac9c5040 --- /dev/null +++ b/app-examples/unstyled_example/assets/paneleft.svg @@ -0,0 +1,13 @@ + + + + + + + + + + + + + diff --git a/app-examples/unstyled_example/assets/text_logo_darkmode.svg b/app-examples/unstyled_example/assets/text_logo_darkmode.svg new file mode 100644 index 00000000..e395e3d7 --- /dev/null +++ b/app-examples/unstyled_example/assets/text_logo_darkmode.svg @@ -0,0 +1,2 @@ + + \ No newline at end of file diff --git a/app-examples/unstyled_example/unstyled_example/__init__.py b/app-examples/unstyled_example/unstyled_example/__init__.py new file mode 100644 index 00000000..847433fd --- /dev/null +++ b/app-examples/unstyled_example/unstyled_example/__init__.py @@ -0,0 +1,3 @@ +# This file has been modified by the Nextpy Team in 2023 using AI tools and automation scripts. +# We have rigorously tested these modifications to ensure reliability and performance. Based on successful test results, we are confident in the quality and stability of these changes. + diff --git a/app-examples/unstyled_example/unstyled_example/unstyled_example.py b/app-examples/unstyled_example/unstyled_example/unstyled_example.py new file mode 100644 index 00000000..d7a037ea --- /dev/null +++ b/app-examples/unstyled_example/unstyled_example/unstyled_example.py @@ -0,0 +1,54 @@ +import nextpy as xt + + +class CounterState(xt.State): + value: int = 0 + + def change_value(self, amount): + self.value += amount + + +def create_button(label, amount): + return xt.unstyled.button( + label, + on_click=lambda: CounterState.change_value(amount) + ) + + +def index() -> xt.Component: + heading_color = xt.match( + CounterState.value, + (0, "red"), + (4, "blue"), + (8, "green"), + (12, "orange"), + (16, "lime"), + (20, "orange"), + "black" + ) + + return xt.unstyled.flex( + xt.unstyled.heading( + CounterState.value, + color="white", + background_color=heading_color, + as_="h2" + ), + xt.unstyled.flex( + create_button("decrement", -1), + create_button("increment", 1), + gap="2" + ), + align_items="center", + direction="column", + gap="2" + ) + + +# Global styles defined as a Python dictionary +style = { + "text_align": "center", +} + +app = xt.App(style=style) +app.add_page(index) diff --git a/app-examples/unstyled_example/xtconfig.py b/app-examples/unstyled_example/xtconfig.py new file mode 100644 index 00000000..0477aec0 --- /dev/null +++ b/app-examples/unstyled_example/xtconfig.py @@ -0,0 +1,5 @@ +import nextpy as xt + +config = xt.Config( + app_name="unstyled_example", +) \ No newline at end of file diff --git a/nextpy/__init__.py b/nextpy/__init__.py index 2d8a537a..d2f31aea 100644 --- a/nextpy/__init__.py +++ b/nextpy/__init__.py @@ -289,6 +289,7 @@ "nextpy.frontend.components.el": ["el"], "nextpy.frontend.components.moment.moment": ["MomentDelta"], "nextpy.frontend.page": ["page"], + "nextpy.frontend.components.proxy": ["animation", "unstyled"], "nextpy.frontend.style": ["color_mode", "style", "toggle_color_mode"], "nextpy.frontend.components.recharts": [ "area_chart", "bar_chart", "line_chart", "composed_chart", "pie_chart", @@ -300,7 +301,6 @@ "polar_angle_axis", "polar_grid", "polar_radius_axis", ], "nextpy.utils": ["utils"], - "nextpy.frontend.components.proxy": ["animation"], } @@ -354,6 +354,9 @@ def __getattr__(name: str) -> Type: module = importlib.import_module("nextpy.frontend.components.proxy") return module.animation + # Custom alias handling for 'unstyled' + if name == "unstyled": + return importlib.import_module("nextpy.frontend.components.proxy.unstyled") try: # Check for import of a module that is not in the mapping. @@ -371,4 +374,4 @@ def __getattr__(name: str) -> Type: getattr(module, name) if name != _MAPPING[name].rsplit(".")[-1] else module ) except ModuleNotFoundError: - raise AttributeError(f"module 'nextpy' has no attribute {name}") from None + raise AttributeError(f"module 'nextpy' has no attribute {name}") from None \ No newline at end of file diff --git a/nextpy/__init__.pyi b/nextpy/__init__.pyi index 1f049903..27d4b44b 100644 --- a/nextpy/__init__.pyi +++ b/nextpy/__init__.pyi @@ -1,4 +1,4 @@ -# This file has been modified by the Nextpy Team in 2023 using AI tools and automation scripts. +# This file has been modified by the Nextpy Team in 2023 using AI tools and automation scripts. # We have rigorously tested these modifications to ensure reliability and performance. Based on successful test results, we are confident in the quality and stability of these changes. from nextpy.backend import admin as admin @@ -257,7 +257,9 @@ from nextpy.frontend.components import center as center from nextpy.frontend.components import checkbox as checkbox from nextpy.frontend.components import checkbox_group as checkbox_group from nextpy.frontend.components import circular_progress as circular_progress -from nextpy.frontend.components import circular_progress_label as circular_progress_label +from nextpy.frontend.components import ( + circular_progress_label as circular_progress_label, +) from nextpy.frontend.components import circle as circle from nextpy.frontend.components import code as code from nextpy.frontend.components import code_block as code_block @@ -341,8 +343,12 @@ from nextpy.frontend.components import moment as moment from nextpy.frontend.components import multi_select as multi_select from nextpy.frontend.components import multi_select_option as multi_select_option from nextpy.frontend.components import next_link as next_link -from nextpy.frontend.components import number_decrement_stepper as number_decrement_stepper -from nextpy.frontend.components import number_increment_stepper as number_increment_stepper +from nextpy.frontend.components import ( + number_decrement_stepper as number_decrement_stepper, +) +from nextpy.frontend.components import ( + number_increment_stepper as number_increment_stepper, +) from nextpy.frontend.components import number_input as number_input from nextpy.frontend.components import number_input_field as number_input_field from nextpy.frontend.components import number_input_stepper as number_input_stepper @@ -365,7 +371,9 @@ from nextpy.frontend.components import progress as progress from nextpy.frontend.components import radio as radio from nextpy.frontend.components import radio_group as radio_group from nextpy.frontend.components import range_slider as range_slider -from nextpy.frontend.components import range_slider_filled_track as range_slider_filled_track +from nextpy.frontend.components import ( + range_slider_filled_track as range_slider_filled_track, +) from nextpy.frontend.components import range_slider_thumb as range_slider_thumb from nextpy.frontend.components import range_slider_track as range_slider_track from nextpy.frontend.components import responsive_grid as responsive_grid @@ -455,6 +463,7 @@ from nextpy.build.config import Config as Config from nextpy.build.config import DBConfig as DBConfig from nextpy import constants as constants from nextpy.constants import Env as Env + # from nextpy.frontend.custom_components import custom_components as custom_components from nextpy.frontend.components import el as el from nextpy.backend import event as event diff --git a/nextpy/frontend/components/__init__.py b/nextpy/frontend/components/__init__.py index 45a21556..11039d51 100644 --- a/nextpy/frontend/components/__init__.py +++ b/nextpy/frontend/components/__init__.py @@ -18,4 +18,3 @@ from .react_player import * from .recharts import * from .suneditor import * - diff --git a/nextpy/frontend/components/proxy/__init__.py b/nextpy/frontend/components/proxy/__init__.py index 66ef53aa..8c1c2423 100644 --- a/nextpy/frontend/components/proxy/__init__.py +++ b/nextpy/frontend/components/proxy/__init__.py @@ -1,8 +1,6 @@ -# This file has been modified by the Nextpy Team in 2023 using AI tools and automation scripts. -# We have rigorously tested these modifications to ensure reliability and performance. Based on successful test results, we are confident in the quality and stability of these changes. - """Contains proxy components.""" from .animation import animation +from .unstyled import * # Make sure this line is correctly importing headless -__all__ = ["animation"] +__all__ = ["animation", "headless"] \ No newline at end of file diff --git a/nextpy/frontend/components/proxy/unstyled.py b/nextpy/frontend/components/proxy/unstyled.py new file mode 100644 index 00000000..7816b1f4 --- /dev/null +++ b/nextpy/frontend/components/proxy/unstyled.py @@ -0,0 +1,26 @@ +"""Unstyled Components Alias.""" +# File: nextpy/frontend/components/proxy/unstyled.py + +import sys +from nextpy.frontend.components.radix.themes import * +from nextpy.frontend.components.radix.themes.components import * +from nextpy.frontend.components.radix.themes.layout import * +from nextpy.frontend.components.radix.themes.typography import * +from nextpy.frontend.components.radix.primitives import * + +class Unstyled: + def __getattr__(self, item): + # Check in each submodule for the component + for module in [themes, components, layout, typography, primitives]: + try: + return getattr(module, item) + except AttributeError: + continue + # If not found, raise an attribute error + raise AttributeError(f"No component named '{item}' in unstyled module") + +# Create an instance of the Unstyled class +unstyled = Unstyled() + +# Optionally, you can define __all__ for explicit exports +__all__ = [name for name in dir() if not name.startswith("_") and name != 'Unstyled']