Repository: Dicklesworthstone/sqlalchemy_data_model_visualizer Branch: main Commit: f176a92537ab Files: 8 Total size: 26.6 KB Directory structure: gitextract_lqxycubh/ ├── .github/ │ └── workflows/ │ └── python-publish.yml ├── .gitignore ├── LICENSE ├── MANIFEST.in ├── README.md ├── requirements.txt ├── setup.py └── sqlalchemy_data_model_visualizer.py ================================================ FILE CONTENTS ================================================ ================================================ FILE: .github/workflows/python-publish.yml ================================================ name: Build and Publish to PyPI on: push: branches: - main release: types: [created] jobs: build-and-publish: runs-on: ubuntu-latest steps: - uses: actions/checkout@v3 - name: Set up Python uses: actions/setup-python@v3 with: python-version: '3.x' - name: Build wheel run: | python -m pip install --upgrade build python -m build - name: Publish to PyPI if: github.event_name == 'release' && github.event.action == 'created' run: | python -m pip install --upgrade twine twine upload dist/* env: TWINE_USERNAME: __token__ TWINE_PASSWORD: ${{ secrets.PYPI_API_TOKEN }} ================================================ FILE: .gitignore ================================================ # 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/#use-with-ide .pdm.toml # 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/ ================================================ FILE: LICENSE ================================================ MIT License Copyright (c) 2023 Jeff Emanuel 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. ================================================ FILE: MANIFEST.in ================================================ include requirements.txt ================================================ FILE: README.md ================================================ # SQLAlchemy Data Model Visualizer ## Overview This Python-based utility generates high-quality, readable visualizations of your SQLAlchemy ORM models with almost no effort. With a focus on clarity and detail, it uses Graphviz to render each model as a directed graph, making it easier to understand the relationships between tables in your database schema. ![Example Data Model Diagram](https://raw.githubusercontent.com/Dicklesworthstone/sqlalchemy_data_model_visualizer/main/my_interactive_data_model_diagram.svg) ## Try it Out Easily in Colab: [![Open In Colab](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/drive/1np5kPvDtdhq138eLHOGINYuTUMJo_wrj?usp=sharing) ## Features - Automatically maps SQLAlchemy ORM models to a directed graph. - Table-like representation of each model with fields, types, and constraints. - Export diagrams to SVG format for high-quality viewing and printing using Roboto font. ## Installation with pip and Usage: ```bash pip install sqlalchemy-data-model-visualizer # Suppose these are your SQLAlchemy data models defined above in the usual way, or imported from another file: models = [GenericUser, Customer, ContentCreator, UserSession, FileStorage, ServiceRequest, GenericAuditLog, GenericFeedback, GenericAPIKey, GenericNotification, GenericAPICreditLog, GenericSubscriptionType, GenericSubscription, GenericSubscriptionUsage, GenericBillingInfo] output_file_name = 'my_data_model_diagram' generate_data_model_diagram(models, output_file_name) add_web_font_and_interactivity('my_data_model_diagram.svg', 'my_interactive_data_model_diagram.svg') ``` ## Installation from Source To get started, clone the repository and install the required packages. ```bash git clone https://github.com/Dicklesworthstone/sqlalchemy_data_model_visualizer.git cd sqlalchemy_data_model_visualizer python3 -m venv venv source venv/bin/activate python3 -m pip install --upgrade pip python3 -m pip install wheel pip install -r requirements.txt ``` ## Requirements - Python 3.x - SQLAlchemy - Graphviz - lxml ## Usage ### Generate Data Model Diagram First, paste in your SQLAlchemy models. A set of fairly complex data models are provided in the code directly as an example-- just replace these with your own from your application. Then, simply call the `generate_data_model_diagram` function. This will generate an SVG file with the name `my_data_model_diagram.svg`. ## API Documentation ### `generate_data_model_diagram(models, output_file='my_data_model_diagram', add_labels=True)` - `models`: List of SQLAlchemy models you want to visualize. - `output_file`: Name of the output SVG file. - `add_labels`: Set to False to hide labels on the edges between tables ## Contributing Contributions are welcome! Please open an issue or submit a pull request. ## License This project is licensed under the MIT License. --- Thanks for your interest in my open-source project! I hope you find it useful. You might also find my commercial web apps useful, and I would really appreciate it if you checked them out: **[YoutubeTranscriptOptimizer.com](https://youtubetranscriptoptimizer.com)** makes it really quick and easy to paste in a YouTube video URL and have it automatically generate not just a really accurate direct transcription, but also a super polished and beautifully formatted written document that can be used independently of the video. The document basically sticks to the same material as discussed in the video, but it sounds much more like a real piece of writing and not just a transcript. It also lets you optionally generate quizzes based on the contents of the document, which can be either multiple choice or short-answer quizzes, and the multiple choice quizzes get turned into interactive HTML files that can be hosted and easily shared, where you can actually take the quiz and it will grade your answers and score the quiz for you. **[FixMyDocuments.com](https://fixmydocuments.com/)** lets you submit any kind of document— PDFs (including scanned PDFs that require OCR), MS Word and Powerpoint files, images, audio files (mp3, m4a, etc.) —and turn them into highly optimized versions in nice markdown formatting, from which HTML and PDF versions are automatically generated. Once converted, you can also edit them directly in the site using the built-in markdown editor, where it saves a running revision history and regenerates the PDF/HTML versions. In addition to just getting the optimized version of the document, you can also generate many other kinds of "derived documents" from the original: interactive multiple-choice quizzes that you can actually take and get graded on; slick looking presentation slides as PDF or HTML (using LaTeX and Reveal.js), an in-depth summary, a concept mind map (using Mermaid diagrams) and outline, custom lesson plans where you can select your target audience, a readability analysis and grade-level versions of your original document (good for simplifying concepts for students), Anki Flashcards that you can import directly into the Anki app or use on the site in a nice interface, and more. ================================================ FILE: requirements.txt ================================================ sqlalchemy graphviz lxml ================================================ FILE: setup.py ================================================ from setuptools import setup from pathlib import Path # Define the directory where this setup.py file is located here = Path(__file__).parent # Read the contents of README file long_description = (here / 'README.md').read_text(encoding='utf-8') # Read the contents of requirements file requirements = (here / 'requirements.txt').read_text(encoding='utf-8').splitlines() setup( name='sqlalchemy_data_model_visualizer', version='0.1.3', # Update the version number for new releases description='A tool to visualize SQLAlchemy data models with Graphviz.', long_description=long_description, long_description_content_type='text/markdown', author='Jeffrey Emanuel', author_email='jeff@pastel.network', url='https://github.com/Dicklesworthstone/sqlalchemy_data_model_visualizer', py_modules=['sqlalchemy_data_model_visualizer'], install_requires=requirements, classifiers=[ 'Development Status :: 3 - Alpha', 'Intended Audience :: Developers', 'License :: OSI Approved :: MIT License', 'Programming Language :: Python :: 3', 'Programming Language :: Python :: 3.8', 'Programming Language :: Python :: 3.9', 'Programming Language :: Python :: 3.10', 'Programming Language :: Python :: 3.11', ], license='MIT', keywords='sqlalchemy visualization graphviz data-model', include_package_data=True, # This tells setuptools to check MANIFEST.in for additional files ) ================================================ FILE: sqlalchemy_data_model_visualizer.py ================================================ from datetime import datetime from typing import Optional from enum import Enum from decimal import Decimal from sqlalchemy.orm import sessionmaker, declarative_base, relationship from sqlalchemy import Column, String, DateTime, Integer, Numeric, Boolean, JSON, ForeignKey, LargeBinary, Text, UniqueConstraint, CheckConstraint, text as sql_text from sqlalchemy.ext.asyncio import create_async_engine, AsyncSession from sqlalchemy import inspect import graphviz from lxml import etree import os import re Base = declarative_base() def generate_data_model_diagram(models, output_file='my_data_model_diagram', add_labels=True, view_diagram=True): # Initialize graph with more advanced visual settings dot = graphviz.Digraph(comment='Interactive Data Models', format='svg', graph_attr={'bgcolor': '#EEEEEE', 'rankdir': 'TB', 'splines': 'spline'}, node_attr={'shape': 'none', 'fontsize': '12', 'fontname': 'Roboto'}, edge_attr={'fontsize': '10', 'fontname': 'Roboto'}) # Iterate through each SQLAlchemy model for model in models: insp = inspect(model) name = insp.class_.__name__ # Create an HTML-like label for each model as a rich table label = f'''< ''' for column in insp.columns: constraints = [] if column.primary_key: constraints.append("PK") if column.unique: constraints.append("Unique") if column.index: constraints.append("Index") constraint_str = ','.join(constraints) color = "#BBDEFB" label += f'''''' label += '
{name}
{column.name} {column.type} ({constraint_str})
>' # Create the node with added hyperlink to detailed documentation dot.node(name, label=label, URL=f"http://{name}_details.html") # Add relationships with tooltips and advanced styling for rel in insp.relationships: target_name = rel.mapper.class_.__name__ tooltip = f"Relation between {name} and {target_name}" dot.edge(name, target_name, label=rel.key if add_labels else None, tooltip=tooltip, color="#1E88E5", style="dashed") # Render the graph to a file and open it dot.render(output_file, view=view_diagram) def add_web_font_and_interactivity(input_svg_file, output_svg_file): if not os.path.exists(input_svg_file): print(f"Error: {input_svg_file} does not exist.") return parser = etree.XMLParser(remove_blank_text=True) try: tree = etree.parse(input_svg_file, parser) except etree.XMLSyntaxError as e: print(f"Error parsing SVG: {e}") return root = tree.getroot() style_elem = etree.Element("style") style_elem.text = ''' @import url("https://fonts.googleapis.com/css?family=Roboto:400,400i,700,700i"); ''' root.insert(0, style_elem) for elem in root.iter(): if 'node' in elem.attrib.get('class', ''): elem.attrib['class'] = 'table-hover' if 'edge' in elem.attrib.get('class', ''): source = elem.attrib.get('source') target = elem.attrib.get('target') elem.attrib['class'] = f'edge-hover edge-from-{source} edge-to-{target}' tree.write(output_svg_file, pretty_print=True, xml_declaration=True, encoding='utf-8') # ________________________________________________________________ # [Insert your sqlalchemy data model classes here below:] use_demo = 0 if use_demo: class GenericUser(Base): __tablename__ = 'generic_user' email = Column(String, primary_key=True, index=True) external_id = Column(String, unique=True, nullable=False) is_active = Column(Boolean, default=True) is_blocked = Column(Boolean, default=False) last_ip_address = Column(String, nullable=True) last_user_agent = Column(String, nullable=True) last_estimated_location = Column(JSON, nullable=True) preferences = Column(JSON) registered_at = Column(DateTime, default=datetime.utcnow, index=True) last_login = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow, index=True) is_deleted = Column(Boolean, default=False) deleted_at = Column(DateTime, nullable=True) customer = relationship("Customer", uselist=False, back_populates="generic_user") content_creator = relationship("ContentCreator", uselist=False, back_populates="generic_user") user_sessions = relationship("UserSession", back_populates="generic_user") audit_logs = relationship("GenericAuditLog", back_populates="actor") notifications = relationship("GenericNotification", back_populates="recipient") class Customer(Base): __tablename__ = 'customer' email = Column(String, ForeignKey('generic_user.email'), primary_key=True, index=True) total_purchases = Column(Numeric(10, 10), default=0.0) generic_user = relationship("GenericUser", back_populates="customer") service_requests = relationship("ServiceRequest", back_populates="customer") subscriptions = relationship("GenericSubscription", back_populates="customer") subscription_usages = relationship("GenericSubscriptionUsage", back_populates="customer") billing_infos = relationship("GenericBillingInfo", back_populates="customer") feedbacks_provided = relationship("GenericFeedback", back_populates="customer") class ContentCreator(Base): __tablename__ = 'content_creator' email = Column(String, ForeignKey('generic_user.email'), primary_key=True, index=True) projects_created = Column(Integer, default=0) revenue_share = Column(Numeric(10, 10), default=0.7) total_earned = Column(Numeric(10, 10), default=0.0) last_project_created_at = Column(DateTime, nullable=True) generic_user = relationship("GenericUser", back_populates="content_creator") api_credit_logs = relationship("GenericAPICreditLog", back_populates="content_creator") api_keys = relationship("GenericAPIKey", back_populates="content_creator") feedbacks_received = relationship("GenericFeedback", back_populates="content_creator") class UserSession(Base): __tablename__ = 'user_session' id = Column(Integer, primary_key=True) user_email = Column(String, ForeignKey('generic_user.email'), nullable=False) session_token = Column(String, unique=True, nullable=False) expires_at = Column(DateTime, nullable=False) is_active = Column(Boolean, default=True) created_at = Column(DateTime, default=datetime.utcnow) updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow) generic_user = relationship("GenericUser", back_populates="user_sessions") class FileStorage(Base): __tablename__ = 'file_storage' id = Column(Integer, primary_key=True, index=True) file_data = Column(LargeBinary, nullable=False) file_type = Column(String, nullable=False) file_hash = Column(String, nullable=False, unique=True) upload_date = Column(DateTime, default=datetime.utcnow) class ServiceRequest(Base): __tablename__ = 'service_request' unique_id_for_sharing = Column(String, primary_key=True, index=True) status = Column(String, CheckConstraint("status IN ('pending', 'completed', 'failed')"), default='pending') ip_address = Column(String) request_time = Column(DateTime, default=datetime.utcnow, index=True) request_last_updated_time = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow) user_input = Column(JSON) input_data_string = Column(Text) api_request = Column(JSON) api_response = Column(JSON) api_session_id = Column(String, nullable=True, unique=True) total_cost = Column(Numeric(10, 10), nullable=True) customer_email = Column(String, ForeignKey('customer.email')) customer = relationship("Customer", back_populates="service_requests") # AuditLog class GenericAuditLog(Base): __tablename__ = 'generic_audit_log' id = Column(Integer, primary_key=True, index=True) action_type = Column(String, nullable=False, index=True) outcome = Column(String, nullable=True) field_affected = Column(String, nullable=True) prev_value = Column(JSON, nullable=True) new_value = Column(JSON, nullable=True) actor_email = Column(String, ForeignKey('generic_user.email'), index=True) related_request_id = Column(Integer, ForeignKey('generic_user_request.unique_id')) timestamp = Column(DateTime, default=datetime.utcnow) actor = relationship("GenericUser", back_populates="audit_logs") # Feedback class GenericFeedback(Base): __tablename__ = 'generic_feedback' id = Column(Integer, primary_key=True, index=True) score = Column(Integer, nullable=False) commentary = Column(Text, nullable=True) customer_email = Column(String, ForeignKey('customer.email'), index=True) content_creator_email = Column(String, ForeignKey('content_creator.email'), index=True) request_id = Column(Integer, ForeignKey('generic_user_request.unique_id')) last_updated = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow) is_removed = Column(Boolean, default=False) removed_at = Column(DateTime, nullable=True) customer = relationship("Customer", back_populates="feedbacks_provided") content_creator = relationship("ContentCreator", back_populates="feedbacks_received") # APIKeys class GenericAPIKey(Base): __tablename__ = 'generic_api_key' id = Column(Integer, primary_key=True, index=True) api_key = Column(String, unique=True, nullable=False) content_creator_email = Column(String, ForeignKey('content_creator.email'), index=True) is_active = Column(Boolean, default=True) is_revoked = Column(Boolean, default=False) expires_at = Column(DateTime, nullable=True) created_at = Column(DateTime, default=datetime.utcnow) content_creator = relationship("ContentCreator", back_populates="api_keys") # Notification class GenericNotification(Base): __tablename__ = 'generic_notification' id = Column(Integer, primary_key=True, index=True) recipient_email = Column(String, ForeignKey('generic_user.email'), index=True) notification_kind = Column(String, nullable=False) is_read = Column(Boolean, default=False) content = Column(Text, nullable=False) created_at = Column(DateTime, default=datetime.utcnow) read_at = Column(DateTime, nullable=True) recipient = relationship("GenericUser", back_populates="notifications") # APICreditLog class GenericAPICreditLog(Base): __tablename__ = 'generic_api_credit_log' id = Column(Integer, primary_key=True, index=True) timestamp = Column(DateTime, default=datetime.utcnow) is_paid = Column(Boolean, default=False) status = Column(String, default='pending') expense = Column(Numeric(10, 10), nullable=False) request_id = Column(Integer, ForeignKey('generic_user_request.unique_id')) token_count = Column(Integer, nullable=False) content_creator_email = Column(String, ForeignKey('content_creator.email')) content_creator = relationship("ContentCreator", back_populates="api_credit_logs") # SubscriptionType class GenericSubscriptionType(Base): __tablename__ = 'generic_subscription_type' id = Column(Integer, primary_key=True, index=True) name = Column(String, nullable=False) monthly_fee = Column(Numeric(10, 10), nullable=False) monthly_cap = Column(Integer, nullable=False) created_at = Column(DateTime, default=datetime.utcnow) updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow) is_removed = Column(Boolean, default=False) removed_at = Column(DateTime, nullable=True) subscriptions = relationship("GenericSubscription", back_populates="subscription_type") # Subscription class GenericSubscription(Base): __tablename__ = 'generic_subscription' id = Column(Integer, primary_key=True, index=True) customer_email = Column(String, ForeignKey('customer.email'), index=True) start_date = Column(DateTime, default=datetime.utcnow) end_date = Column(DateTime, nullable=True) current_use = Column(Integer, default=0) subscription_type_id = Column(Integer, ForeignKey('generic_subscription_type.id')) customer = relationship("Customer", back_populates="subscriptions") subscription_type = relationship("GenericSubscriptionType", back_populates="subscriptions") subscription_usages = relationship("GenericSubscriptionUsage", back_populates="subscription") # SubscriptionUsage class GenericSubscriptionUsage(Base): __tablename__ = 'generic_subscription_usage' id = Column(Integer, primary_key=True, index=True) customer_email = Column(String, ForeignKey('customer.email'), index=True) use_count = Column(Integer, default=0) last_use = Column(DateTime, nullable=True) subscription_id = Column(Integer, ForeignKey('generic_subscription.id')) subscription_type_id = Column(Integer, ForeignKey('generic_subscription_type.id')) customer = relationship("Customer", back_populates="subscription_usages") subscription = relationship("GenericSubscription", back_populates="subscription_usages") subscription_type = relationship("GenericSubscriptionType", backref="subscription_usages") # BillingInfo class GenericBillingInfo(Base): __tablename__ = 'generic_billing_info' id = Column(Integer, primary_key=True, index=True) customer_email = Column(String, ForeignKey('customer.email'), index=True) payment_type = Column(String, nullable=False) payment_data = Column(JSON) created_at = Column(DateTime, default=datetime.utcnow) updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow) is_removed = Column(Boolean, default=False) removed_at = Column(DateTime, nullable=True) customer = relationship("Customer", back_populates="billing_infos") models = [GenericUser, Customer, ContentCreator, UserSession, FileStorage, ServiceRequest, GenericAuditLog, GenericFeedback, GenericAPIKey, GenericNotification, GenericAPICreditLog, GenericSubscriptionType, GenericSubscription, GenericSubscriptionUsage, GenericBillingInfo] output_file_name = 'my_data_model_diagram' # Generate the diagram and add interactivity generate_data_model_diagram(models, output_file_name, add_labels=True) add_web_font_and_interactivity('my_data_model_diagram.svg', 'my_interactive_data_model_diagram.svg')