├── MANIFEST.in ├── requirements.txt ├── .github └── workflows │ └── python-publish.yml ├── LICENSE ├── setup.py ├── .gitignore ├── README.md ├── sqlalchemy_data_model_visualizer.py └── my_interactive_data_model_diagram.svg /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include requirements.txt 2 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | sqlalchemy 2 | graphviz 3 | lxml 4 | -------------------------------------------------------------------------------- /.github/workflows/python-publish.yml: -------------------------------------------------------------------------------- 1 | name: Build and Publish to PyPI 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | release: 8 | types: [created] 9 | 10 | jobs: 11 | build-and-publish: 12 | runs-on: ubuntu-latest 13 | steps: 14 | - uses: actions/checkout@v3 15 | 16 | - name: Set up Python 17 | uses: actions/setup-python@v3 18 | with: 19 | python-version: '3.x' 20 | 21 | - name: Build wheel 22 | run: | 23 | python -m pip install --upgrade build 24 | python -m build 25 | 26 | - name: Publish to PyPI 27 | if: github.event_name == 'release' && github.event.action == 'created' 28 | run: | 29 | python -m pip install --upgrade twine 30 | twine upload dist/* 31 | env: 32 | TWINE_USERNAME: __token__ 33 | TWINE_PASSWORD: ${{ secrets.PYPI_API_TOKEN }} 34 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Jeff Emanuel 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import setup 2 | from pathlib import Path 3 | 4 | # Define the directory where this setup.py file is located 5 | here = Path(__file__).parent 6 | 7 | # Read the contents of README file 8 | long_description = (here / 'README.md').read_text(encoding='utf-8') 9 | 10 | # Read the contents of requirements file 11 | requirements = (here / 'requirements.txt').read_text(encoding='utf-8').splitlines() 12 | 13 | setup( 14 | name='sqlalchemy_data_model_visualizer', 15 | version='0.1.3', # Update the version number for new releases 16 | description='A tool to visualize SQLAlchemy data models with Graphviz.', 17 | long_description=long_description, 18 | long_description_content_type='text/markdown', 19 | author='Jeffrey Emanuel', 20 | author_email='jeff@pastel.network', 21 | url='https://github.com/Dicklesworthstone/sqlalchemy_data_model_visualizer', 22 | py_modules=['sqlalchemy_data_model_visualizer'], 23 | install_requires=requirements, 24 | classifiers=[ 25 | 'Development Status :: 3 - Alpha', 26 | 'Intended Audience :: Developers', 27 | 'License :: OSI Approved :: MIT License', 28 | 'Programming Language :: Python :: 3', 29 | 'Programming Language :: Python :: 3.8', 30 | 'Programming Language :: Python :: 3.9', 31 | 'Programming Language :: Python :: 3.10', 32 | 'Programming Language :: Python :: 3.11', 33 | ], 34 | license='MIT', 35 | keywords='sqlalchemy visualization graphviz data-model', 36 | include_package_data=True, # This tells setuptools to check MANIFEST.in for additional files 37 | ) 38 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | share/python-wheels/ 24 | *.egg-info/ 25 | .installed.cfg 26 | *.egg 27 | MANIFEST 28 | 29 | # PyInstaller 30 | # Usually these files are written by a python script from a template 31 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 32 | *.manifest 33 | *.spec 34 | 35 | # Installer logs 36 | pip-log.txt 37 | pip-delete-this-directory.txt 38 | 39 | # Unit test / coverage reports 40 | htmlcov/ 41 | .tox/ 42 | .nox/ 43 | .coverage 44 | .coverage.* 45 | .cache 46 | nosetests.xml 47 | coverage.xml 48 | *.cover 49 | *.py,cover 50 | .hypothesis/ 51 | .pytest_cache/ 52 | cover/ 53 | 54 | # Translations 55 | *.mo 56 | *.pot 57 | 58 | # Django stuff: 59 | *.log 60 | local_settings.py 61 | db.sqlite3 62 | db.sqlite3-journal 63 | 64 | # Flask stuff: 65 | instance/ 66 | .webassets-cache 67 | 68 | # Scrapy stuff: 69 | .scrapy 70 | 71 | # Sphinx documentation 72 | docs/_build/ 73 | 74 | # PyBuilder 75 | .pybuilder/ 76 | target/ 77 | 78 | # Jupyter Notebook 79 | .ipynb_checkpoints 80 | 81 | # IPython 82 | profile_default/ 83 | ipython_config.py 84 | 85 | # pyenv 86 | # For a library or package, you might want to ignore these files since the code is 87 | # intended to run in multiple environments; otherwise, check them in: 88 | # .python-version 89 | 90 | # pipenv 91 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 92 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 93 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 94 | # install all needed dependencies. 95 | #Pipfile.lock 96 | 97 | # poetry 98 | # Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. 99 | # This is especially recommended for binary packages to ensure reproducibility, and is more 100 | # commonly ignored for libraries. 101 | # https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control 102 | #poetry.lock 103 | 104 | # pdm 105 | # Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. 106 | #pdm.lock 107 | # pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it 108 | # in version control. 109 | # https://pdm.fming.dev/#use-with-ide 110 | .pdm.toml 111 | 112 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm 113 | __pypackages__/ 114 | 115 | # Celery stuff 116 | celerybeat-schedule 117 | celerybeat.pid 118 | 119 | # SageMath parsed files 120 | *.sage.py 121 | 122 | # Environments 123 | .env 124 | .venv 125 | env/ 126 | venv/ 127 | ENV/ 128 | env.bak/ 129 | venv.bak/ 130 | 131 | # Spyder project settings 132 | .spyderproject 133 | .spyproject 134 | 135 | # Rope project settings 136 | .ropeproject 137 | 138 | # mkdocs documentation 139 | /site 140 | 141 | # mypy 142 | .mypy_cache/ 143 | .dmypy.json 144 | dmypy.json 145 | 146 | # Pyre type checker 147 | .pyre/ 148 | 149 | # pytype static type analyzer 150 | .pytype/ 151 | 152 | # Cython debug symbols 153 | cython_debug/ 154 | 155 | # PyCharm 156 | # JetBrains specific template is maintained in a separate JetBrains.gitignore that can 157 | # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore 158 | # and can be added to the global gitignore or merged into this file. For a more nuclear 159 | # option (not recommended) you can uncomment the following to ignore the entire idea folder. 160 | #.idea/ 161 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # SQLAlchemy Data Model Visualizer 2 | 3 | ## Overview 4 | 5 | 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. 6 | 7 | ![Example Data Model Diagram](https://raw.githubusercontent.com/Dicklesworthstone/sqlalchemy_data_model_visualizer/main/my_interactive_data_model_diagram.svg) 8 | 9 | ## Try it Out Easily in Colab: 10 | 11 | [![Open In Colab](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/drive/1np5kPvDtdhq138eLHOGINYuTUMJo_wrj?usp=sharing) 12 | 13 | ## Features 14 | 15 | - Automatically maps SQLAlchemy ORM models to a directed graph. 16 | - Table-like representation of each model with fields, types, and constraints. 17 | - Export diagrams to SVG format for high-quality viewing and printing using Roboto font. 18 | 19 | ## Installation with pip and Usage: 20 | 21 | ```bash 22 | pip install sqlalchemy-data-model-visualizer 23 | 24 | # Suppose these are your SQLAlchemy data models defined above in the usual way, or imported from another file: 25 | models = [GenericUser, Customer, ContentCreator, UserSession, FileStorage, ServiceRequest, GenericAuditLog, GenericFeedback, GenericAPIKey, GenericNotification, GenericAPICreditLog, GenericSubscriptionType, GenericSubscription, GenericSubscriptionUsage, GenericBillingInfo] 26 | output_file_name = 'my_data_model_diagram' 27 | generate_data_model_diagram(models, output_file_name) 28 | add_web_font_and_interactivity('my_data_model_diagram.svg', 'my_interactive_data_model_diagram.svg') 29 | ``` 30 | 31 | ## Installation from Source 32 | 33 | To get started, clone the repository and install the required packages. 34 | 35 | ```bash 36 | git clone https://github.com/Dicklesworthstone/sqlalchemy_data_model_visualizer.git 37 | cd sqlalchemy_data_model_visualizer 38 | python3 -m venv venv 39 | source venv/bin/activate 40 | python3 -m pip install --upgrade pip 41 | python3 -m pip install wheel 42 | pip install -r requirements.txt 43 | ``` 44 | 45 | ## Requirements 46 | 47 | - Python 3.x 48 | - SQLAlchemy 49 | - Graphviz 50 | - lxml 51 | 52 | ## Usage 53 | 54 | ### Generate Data Model Diagram 55 | 56 | 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. 57 | 58 | Then, simply call the `generate_data_model_diagram` function. This will generate an SVG file with the name `my_data_model_diagram.svg`. 59 | 60 | ## API Documentation 61 | 62 | ### `generate_data_model_diagram(models, output_file='my_data_model_diagram', add_labels=True)` 63 | 64 | - `models`: List of SQLAlchemy models you want to visualize. 65 | - `output_file`: Name of the output SVG file. 66 | - `add_labels`: Set to False to hide labels on the edges between tables 67 | 68 | ## Contributing 69 | 70 | Contributions are welcome! Please open an issue or submit a pull request. 71 | 72 | ## License 73 | 74 | This project is licensed under the MIT License. 75 | 76 | --- 77 | 78 | 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: 79 | 80 | **[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. 81 | 82 | 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. 83 | 84 | **[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. 85 | 86 | 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. 87 | -------------------------------------------------------------------------------- /sqlalchemy_data_model_visualizer.py: -------------------------------------------------------------------------------- 1 | from datetime import datetime 2 | from typing import Optional 3 | from enum import Enum 4 | from decimal import Decimal 5 | from sqlalchemy.orm import sessionmaker, declarative_base, relationship 6 | from sqlalchemy import Column, String, DateTime, Integer, Numeric, Boolean, JSON, ForeignKey, LargeBinary, Text, UniqueConstraint, CheckConstraint, text as sql_text 7 | from sqlalchemy.ext.asyncio import create_async_engine, AsyncSession 8 | from sqlalchemy import inspect 9 | import graphviz 10 | from lxml import etree 11 | import os 12 | import re 13 | Base = declarative_base() 14 | 15 | def generate_data_model_diagram(models, output_file='my_data_model_diagram', add_labels=True, view_diagram=True): 16 | # Initialize graph with more advanced visual settings 17 | dot = graphviz.Digraph(comment='Interactive Data Models', format='svg', 18 | graph_attr={'bgcolor': '#EEEEEE', 'rankdir': 'TB', 'splines': 'spline'}, 19 | node_attr={'shape': 'none', 'fontsize': '12', 'fontname': 'Roboto'}, 20 | edge_attr={'fontsize': '10', 'fontname': 'Roboto'}) 21 | 22 | # Iterate through each SQLAlchemy model 23 | for model in models: 24 | insp = inspect(model) 25 | name = insp.class_.__name__ 26 | 27 | # Create an HTML-like label for each model as a rich table 28 | label = f'''< 29 | 30 | 31 | ''' 32 | 33 | for column in insp.columns: 34 | constraints = [] 35 | if column.primary_key: 36 | constraints.append("PK") 37 | if column.unique: 38 | constraints.append("Unique") 39 | if column.index: 40 | constraints.append("Index") 41 | 42 | constraint_str = ','.join(constraints) 43 | color = "#BBDEFB" 44 | 45 | label += f''' 46 | 47 | 48 | ''' 49 | 50 | label += '
{name}
{column.name}{column.type} ({constraint_str})
>' 51 | 52 | # Create the node with added hyperlink to detailed documentation 53 | dot.node(name, label=label, URL=f"http://{name}_details.html") 54 | 55 | # Add relationships with tooltips and advanced styling 56 | for rel in insp.relationships: 57 | target_name = rel.mapper.class_.__name__ 58 | tooltip = f"Relation between {name} and {target_name}" 59 | dot.edge(name, target_name, label=rel.key if add_labels else None, tooltip=tooltip, color="#1E88E5", style="dashed") 60 | 61 | # Render the graph to a file and open it 62 | dot.render(output_file, view=view_diagram) 63 | 64 | 65 | def add_web_font_and_interactivity(input_svg_file, output_svg_file): 66 | if not os.path.exists(input_svg_file): 67 | print(f"Error: {input_svg_file} does not exist.") 68 | return 69 | 70 | parser = etree.XMLParser(remove_blank_text=True) 71 | try: 72 | tree = etree.parse(input_svg_file, parser) 73 | except etree.XMLSyntaxError as e: 74 | print(f"Error parsing SVG: {e}") 75 | return 76 | 77 | root = tree.getroot() 78 | 79 | style_elem = etree.Element("style") 80 | style_elem.text = ''' 81 | @import url("https://fonts.googleapis.com/css?family=Roboto:400,400i,700,700i"); 82 | ''' 83 | root.insert(0, style_elem) 84 | 85 | for elem in root.iter(): 86 | if 'node' in elem.attrib.get('class', ''): 87 | elem.attrib['class'] = 'table-hover' 88 | if 'edge' in elem.attrib.get('class', ''): 89 | source = elem.attrib.get('source') 90 | target = elem.attrib.get('target') 91 | elem.attrib['class'] = f'edge-hover edge-from-{source} edge-to-{target}' 92 | 93 | tree.write(output_svg_file, pretty_print=True, xml_declaration=True, encoding='utf-8') 94 | 95 | # ________________________________________________________________ 96 | 97 | 98 | # [Insert your sqlalchemy data model classes here below:] 99 | 100 | use_demo = 0 101 | 102 | if use_demo: 103 | class GenericUser(Base): 104 | __tablename__ = 'generic_user' 105 | email = Column(String, primary_key=True, index=True) 106 | external_id = Column(String, unique=True, nullable=False) 107 | is_active = Column(Boolean, default=True) 108 | is_blocked = Column(Boolean, default=False) 109 | last_ip_address = Column(String, nullable=True) 110 | last_user_agent = Column(String, nullable=True) 111 | last_estimated_location = Column(JSON, nullable=True) 112 | preferences = Column(JSON) 113 | registered_at = Column(DateTime, default=datetime.utcnow, index=True) 114 | last_login = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow, index=True) 115 | is_deleted = Column(Boolean, default=False) 116 | deleted_at = Column(DateTime, nullable=True) 117 | customer = relationship("Customer", uselist=False, back_populates="generic_user") 118 | content_creator = relationship("ContentCreator", uselist=False, back_populates="generic_user") 119 | user_sessions = relationship("UserSession", back_populates="generic_user") 120 | audit_logs = relationship("GenericAuditLog", back_populates="actor") 121 | notifications = relationship("GenericNotification", back_populates="recipient") 122 | 123 | class Customer(Base): 124 | __tablename__ = 'customer' 125 | email = Column(String, ForeignKey('generic_user.email'), primary_key=True, index=True) 126 | total_purchases = Column(Numeric(10, 10), default=0.0) 127 | generic_user = relationship("GenericUser", back_populates="customer") 128 | service_requests = relationship("ServiceRequest", back_populates="customer") 129 | subscriptions = relationship("GenericSubscription", back_populates="customer") 130 | subscription_usages = relationship("GenericSubscriptionUsage", back_populates="customer") 131 | billing_infos = relationship("GenericBillingInfo", back_populates="customer") 132 | feedbacks_provided = relationship("GenericFeedback", back_populates="customer") 133 | 134 | class ContentCreator(Base): 135 | __tablename__ = 'content_creator' 136 | email = Column(String, ForeignKey('generic_user.email'), primary_key=True, index=True) 137 | projects_created = Column(Integer, default=0) 138 | revenue_share = Column(Numeric(10, 10), default=0.7) 139 | total_earned = Column(Numeric(10, 10), default=0.0) 140 | last_project_created_at = Column(DateTime, nullable=True) 141 | generic_user = relationship("GenericUser", back_populates="content_creator") 142 | api_credit_logs = relationship("GenericAPICreditLog", back_populates="content_creator") 143 | api_keys = relationship("GenericAPIKey", back_populates="content_creator") 144 | feedbacks_received = relationship("GenericFeedback", back_populates="content_creator") 145 | 146 | class UserSession(Base): 147 | __tablename__ = 'user_session' 148 | id = Column(Integer, primary_key=True) 149 | user_email = Column(String, ForeignKey('generic_user.email'), nullable=False) 150 | session_token = Column(String, unique=True, nullable=False) 151 | expires_at = Column(DateTime, nullable=False) 152 | is_active = Column(Boolean, default=True) 153 | created_at = Column(DateTime, default=datetime.utcnow) 154 | updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow) 155 | generic_user = relationship("GenericUser", back_populates="user_sessions") 156 | 157 | class FileStorage(Base): 158 | __tablename__ = 'file_storage' 159 | id = Column(Integer, primary_key=True, index=True) 160 | file_data = Column(LargeBinary, nullable=False) 161 | file_type = Column(String, nullable=False) 162 | file_hash = Column(String, nullable=False, unique=True) 163 | upload_date = Column(DateTime, default=datetime.utcnow) 164 | 165 | class ServiceRequest(Base): 166 | __tablename__ = 'service_request' 167 | unique_id_for_sharing = Column(String, primary_key=True, index=True) 168 | status = Column(String, CheckConstraint("status IN ('pending', 'completed', 'failed')"), default='pending') 169 | ip_address = Column(String) 170 | request_time = Column(DateTime, default=datetime.utcnow, index=True) 171 | request_last_updated_time = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow) 172 | user_input = Column(JSON) 173 | input_data_string = Column(Text) 174 | api_request = Column(JSON) 175 | api_response = Column(JSON) 176 | api_session_id = Column(String, nullable=True, unique=True) 177 | total_cost = Column(Numeric(10, 10), nullable=True) 178 | customer_email = Column(String, ForeignKey('customer.email')) 179 | customer = relationship("Customer", back_populates="service_requests") 180 | 181 | # AuditLog 182 | class GenericAuditLog(Base): 183 | __tablename__ = 'generic_audit_log' 184 | id = Column(Integer, primary_key=True, index=True) 185 | action_type = Column(String, nullable=False, index=True) 186 | outcome = Column(String, nullable=True) 187 | field_affected = Column(String, nullable=True) 188 | prev_value = Column(JSON, nullable=True) 189 | new_value = Column(JSON, nullable=True) 190 | actor_email = Column(String, ForeignKey('generic_user.email'), index=True) 191 | related_request_id = Column(Integer, ForeignKey('generic_user_request.unique_id')) 192 | timestamp = Column(DateTime, default=datetime.utcnow) 193 | actor = relationship("GenericUser", back_populates="audit_logs") 194 | 195 | # Feedback 196 | class GenericFeedback(Base): 197 | __tablename__ = 'generic_feedback' 198 | id = Column(Integer, primary_key=True, index=True) 199 | score = Column(Integer, nullable=False) 200 | commentary = Column(Text, nullable=True) 201 | customer_email = Column(String, ForeignKey('customer.email'), index=True) 202 | content_creator_email = Column(String, ForeignKey('content_creator.email'), index=True) 203 | request_id = Column(Integer, ForeignKey('generic_user_request.unique_id')) 204 | last_updated = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow) 205 | is_removed = Column(Boolean, default=False) 206 | removed_at = Column(DateTime, nullable=True) 207 | customer = relationship("Customer", back_populates="feedbacks_provided") 208 | content_creator = relationship("ContentCreator", back_populates="feedbacks_received") 209 | 210 | # APIKeys 211 | class GenericAPIKey(Base): 212 | __tablename__ = 'generic_api_key' 213 | id = Column(Integer, primary_key=True, index=True) 214 | api_key = Column(String, unique=True, nullable=False) 215 | content_creator_email = Column(String, ForeignKey('content_creator.email'), index=True) 216 | is_active = Column(Boolean, default=True) 217 | is_revoked = Column(Boolean, default=False) 218 | expires_at = Column(DateTime, nullable=True) 219 | created_at = Column(DateTime, default=datetime.utcnow) 220 | content_creator = relationship("ContentCreator", back_populates="api_keys") 221 | 222 | # Notification 223 | class GenericNotification(Base): 224 | __tablename__ = 'generic_notification' 225 | id = Column(Integer, primary_key=True, index=True) 226 | recipient_email = Column(String, ForeignKey('generic_user.email'), index=True) 227 | notification_kind = Column(String, nullable=False) 228 | is_read = Column(Boolean, default=False) 229 | content = Column(Text, nullable=False) 230 | created_at = Column(DateTime, default=datetime.utcnow) 231 | read_at = Column(DateTime, nullable=True) 232 | recipient = relationship("GenericUser", back_populates="notifications") 233 | 234 | # APICreditLog 235 | class GenericAPICreditLog(Base): 236 | __tablename__ = 'generic_api_credit_log' 237 | id = Column(Integer, primary_key=True, index=True) 238 | timestamp = Column(DateTime, default=datetime.utcnow) 239 | is_paid = Column(Boolean, default=False) 240 | status = Column(String, default='pending') 241 | expense = Column(Numeric(10, 10), nullable=False) 242 | request_id = Column(Integer, ForeignKey('generic_user_request.unique_id')) 243 | token_count = Column(Integer, nullable=False) 244 | content_creator_email = Column(String, ForeignKey('content_creator.email')) 245 | content_creator = relationship("ContentCreator", back_populates="api_credit_logs") 246 | 247 | # SubscriptionType 248 | class GenericSubscriptionType(Base): 249 | __tablename__ = 'generic_subscription_type' 250 | id = Column(Integer, primary_key=True, index=True) 251 | name = Column(String, nullable=False) 252 | monthly_fee = Column(Numeric(10, 10), nullable=False) 253 | monthly_cap = Column(Integer, nullable=False) 254 | created_at = Column(DateTime, default=datetime.utcnow) 255 | updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow) 256 | is_removed = Column(Boolean, default=False) 257 | removed_at = Column(DateTime, nullable=True) 258 | subscriptions = relationship("GenericSubscription", back_populates="subscription_type") 259 | 260 | # Subscription 261 | class GenericSubscription(Base): 262 | __tablename__ = 'generic_subscription' 263 | id = Column(Integer, primary_key=True, index=True) 264 | customer_email = Column(String, ForeignKey('customer.email'), index=True) 265 | start_date = Column(DateTime, default=datetime.utcnow) 266 | end_date = Column(DateTime, nullable=True) 267 | current_use = Column(Integer, default=0) 268 | subscription_type_id = Column(Integer, ForeignKey('generic_subscription_type.id')) 269 | customer = relationship("Customer", back_populates="subscriptions") 270 | subscription_type = relationship("GenericSubscriptionType", back_populates="subscriptions") 271 | subscription_usages = relationship("GenericSubscriptionUsage", back_populates="subscription") 272 | 273 | # SubscriptionUsage 274 | class GenericSubscriptionUsage(Base): 275 | __tablename__ = 'generic_subscription_usage' 276 | id = Column(Integer, primary_key=True, index=True) 277 | customer_email = Column(String, ForeignKey('customer.email'), index=True) 278 | use_count = Column(Integer, default=0) 279 | last_use = Column(DateTime, nullable=True) 280 | subscription_id = Column(Integer, ForeignKey('generic_subscription.id')) 281 | subscription_type_id = Column(Integer, ForeignKey('generic_subscription_type.id')) 282 | customer = relationship("Customer", back_populates="subscription_usages") 283 | subscription = relationship("GenericSubscription", back_populates="subscription_usages") 284 | subscription_type = relationship("GenericSubscriptionType", backref="subscription_usages") 285 | 286 | # BillingInfo 287 | class GenericBillingInfo(Base): 288 | __tablename__ = 'generic_billing_info' 289 | id = Column(Integer, primary_key=True, index=True) 290 | customer_email = Column(String, ForeignKey('customer.email'), index=True) 291 | payment_type = Column(String, nullable=False) 292 | payment_data = Column(JSON) 293 | created_at = Column(DateTime, default=datetime.utcnow) 294 | updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow) 295 | is_removed = Column(Boolean, default=False) 296 | removed_at = Column(DateTime, nullable=True) 297 | customer = relationship("Customer", back_populates="billing_infos") 298 | 299 | models = [GenericUser, Customer, ContentCreator, UserSession, FileStorage, ServiceRequest, GenericAuditLog, GenericFeedback, GenericAPIKey, GenericNotification, GenericAPICreditLog, GenericSubscriptionType, GenericSubscription, GenericSubscriptionUsage, GenericBillingInfo] 300 | 301 | 302 | output_file_name = 'my_data_model_diagram' 303 | # Generate the diagram and add interactivity 304 | generate_data_model_diagram(models, output_file_name, add_labels=True) 305 | add_web_font_and_interactivity('my_data_model_diagram.svg', 'my_interactive_data_model_diagram.svg') 306 | -------------------------------------------------------------------------------- /my_interactive_data_model_diagram.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 6 | 7 | 10 | 11 | %3 12 | 13 | 14 | 15 | GenericUser 16 | 17 | 18 | 19 | 20 | GenericUser 21 | 22 | 23 | email 24 | 25 | 26 | VARCHAR (PK,Index) 27 | 28 | 29 | external_id 30 | 31 | 32 | VARCHAR (Unique) 33 | 34 | 35 | is_active 36 | 37 | 38 | BOOLEAN () 39 | 40 | 41 | is_blocked 42 | 43 | 44 | BOOLEAN () 45 | 46 | 47 | last_ip_address 48 | 49 | 50 | VARCHAR () 51 | 52 | 53 | last_user_agent 54 | 55 | 56 | VARCHAR () 57 | 58 | 59 | last_estimated_location 60 | 61 | 62 | JSON () 63 | 64 | 65 | preferences 66 | 67 | 68 | JSON () 69 | 70 | 71 | registered_at 72 | 73 | 74 | DATETIME (Index) 75 | 76 | 77 | last_login 78 | 79 | 80 | DATETIME (Index) 81 | 82 | 83 | is_deleted 84 | 85 | 86 | BOOLEAN () 87 | 88 | 89 | deleted_at 90 | 91 | 92 | DATETIME () 93 | 94 | 95 | 96 | 97 | 98 | Customer 99 | 100 | 101 | 102 | 103 | Customer 104 | 105 | 106 | email 107 | 108 | 109 | VARCHAR (PK,Index) 110 | 111 | 112 | total_purchases 113 | 114 | 115 | NUMERIC(10, 10) () 116 | 117 | 118 | 119 | 120 | 121 | GenericUser->Customer 122 | 123 | 124 | 125 | 126 | 127 | 128 | customer 129 | 130 | 131 | 132 | ContentCreator 133 | 134 | 135 | 136 | 137 | ContentCreator 138 | 139 | 140 | email 141 | 142 | 143 | VARCHAR (PK,Index) 144 | 145 | 146 | projects_created 147 | 148 | 149 | INTEGER () 150 | 151 | 152 | revenue_share 153 | 154 | 155 | NUMERIC(10, 10) () 156 | 157 | 158 | total_earned 159 | 160 | 161 | NUMERIC(10, 10) () 162 | 163 | 164 | last_project_created_at 165 | 166 | 167 | DATETIME () 168 | 169 | 170 | 171 | 172 | 173 | GenericUser->ContentCreator 174 | 175 | 176 | 177 | 178 | 179 | 180 | content_creator 181 | 182 | 183 | 184 | UserSession 185 | 186 | 187 | 188 | 189 | UserSession 190 | 191 | 192 | id 193 | 194 | 195 | INTEGER (PK) 196 | 197 | 198 | user_email 199 | 200 | 201 | VARCHAR () 202 | 203 | 204 | session_token 205 | 206 | 207 | VARCHAR (Unique) 208 | 209 | 210 | expires_at 211 | 212 | 213 | DATETIME () 214 | 215 | 216 | is_active 217 | 218 | 219 | BOOLEAN () 220 | 221 | 222 | created_at 223 | 224 | 225 | DATETIME () 226 | 227 | 228 | updated_at 229 | 230 | 231 | DATETIME () 232 | 233 | 234 | 235 | 236 | 237 | GenericUser->UserSession 238 | 239 | 240 | 241 | 242 | 243 | 244 | user_sessions 245 | 246 | 247 | 248 | GenericAuditLog 249 | 250 | 251 | 252 | 253 | GenericAuditLog 254 | 255 | 256 | id 257 | 258 | 259 | INTEGER (PK,Index) 260 | 261 | 262 | action_type 263 | 264 | 265 | VARCHAR (Index) 266 | 267 | 268 | outcome 269 | 270 | 271 | VARCHAR () 272 | 273 | 274 | field_affected 275 | 276 | 277 | VARCHAR () 278 | 279 | 280 | prev_value 281 | 282 | 283 | JSON () 284 | 285 | 286 | new_value 287 | 288 | 289 | JSON () 290 | 291 | 292 | actor_email 293 | 294 | 295 | VARCHAR (Index) 296 | 297 | 298 | related_request_id 299 | 300 | 301 | INTEGER () 302 | 303 | 304 | timestamp 305 | 306 | 307 | DATETIME () 308 | 309 | 310 | 311 | 312 | 313 | GenericUser->GenericAuditLog 314 | 315 | 316 | 317 | 318 | 319 | 320 | audit_logs 321 | 322 | 323 | 324 | GenericNotification 325 | 326 | 327 | 328 | 329 | GenericNotification 330 | 331 | 332 | id 333 | 334 | 335 | INTEGER (PK,Index) 336 | 337 | 338 | recipient_email 339 | 340 | 341 | VARCHAR (Index) 342 | 343 | 344 | notification_kind 345 | 346 | 347 | VARCHAR () 348 | 349 | 350 | is_read 351 | 352 | 353 | BOOLEAN () 354 | 355 | 356 | content 357 | 358 | 359 | TEXT () 360 | 361 | 362 | created_at 363 | 364 | 365 | DATETIME () 366 | 367 | 368 | read_at 369 | 370 | 371 | DATETIME () 372 | 373 | 374 | 375 | 376 | 377 | GenericUser->GenericNotification 378 | 379 | 380 | 381 | 382 | 383 | 384 | notifications 385 | 386 | 387 | 388 | Customer->GenericUser 389 | 390 | 391 | 392 | 393 | 394 | 395 | generic_user 396 | 397 | 398 | 399 | ServiceRequest 400 | 401 | 402 | 403 | 404 | ServiceRequest 405 | 406 | 407 | unique_id_for_sharing 408 | 409 | 410 | VARCHAR (PK,Index) 411 | 412 | 413 | status 414 | 415 | 416 | VARCHAR () 417 | 418 | 419 | ip_address 420 | 421 | 422 | VARCHAR () 423 | 424 | 425 | request_time 426 | 427 | 428 | DATETIME (Index) 429 | 430 | 431 | request_last_updated_time 432 | 433 | 434 | DATETIME () 435 | 436 | 437 | user_input 438 | 439 | 440 | JSON () 441 | 442 | 443 | input_data_string 444 | 445 | 446 | TEXT () 447 | 448 | 449 | api_request 450 | 451 | 452 | JSON () 453 | 454 | 455 | api_response 456 | 457 | 458 | JSON () 459 | 460 | 461 | api_session_id 462 | 463 | 464 | VARCHAR (Unique) 465 | 466 | 467 | total_cost 468 | 469 | 470 | NUMERIC(10, 10) () 471 | 472 | 473 | customer_email 474 | 475 | 476 | VARCHAR () 477 | 478 | 479 | 480 | 481 | 482 | Customer->ServiceRequest 483 | 484 | 485 | 486 | 487 | 488 | 489 | service_requests 490 | 491 | 492 | 493 | GenericSubscription 494 | 495 | 496 | 497 | 498 | GenericSubscription 499 | 500 | 501 | id 502 | 503 | 504 | INTEGER (PK,Index) 505 | 506 | 507 | customer_email 508 | 509 | 510 | VARCHAR (Index) 511 | 512 | 513 | start_date 514 | 515 | 516 | DATETIME () 517 | 518 | 519 | end_date 520 | 521 | 522 | DATETIME () 523 | 524 | 525 | current_use 526 | 527 | 528 | INTEGER () 529 | 530 | 531 | subscription_type_id 532 | 533 | 534 | INTEGER () 535 | 536 | 537 | 538 | 539 | 540 | Customer->GenericSubscription 541 | 542 | 543 | 544 | 545 | 546 | 547 | subscriptions 548 | 549 | 550 | 551 | GenericSubscriptionUsage 552 | 553 | 554 | 555 | 556 | GenericSubscriptionUsage 557 | 558 | 559 | id 560 | 561 | 562 | INTEGER (PK,Index) 563 | 564 | 565 | customer_email 566 | 567 | 568 | VARCHAR (Index) 569 | 570 | 571 | use_count 572 | 573 | 574 | INTEGER () 575 | 576 | 577 | last_use 578 | 579 | 580 | DATETIME () 581 | 582 | 583 | subscription_id 584 | 585 | 586 | INTEGER () 587 | 588 | 589 | subscription_type_id 590 | 591 | 592 | INTEGER () 593 | 594 | 595 | 596 | 597 | 598 | Customer->GenericSubscriptionUsage 599 | 600 | 601 | 602 | 603 | 604 | 605 | subscription_usages 606 | 607 | 608 | 609 | GenericBillingInfo 610 | 611 | 612 | 613 | 614 | GenericBillingInfo 615 | 616 | 617 | id 618 | 619 | 620 | INTEGER (PK,Index) 621 | 622 | 623 | customer_email 624 | 625 | 626 | VARCHAR (Index) 627 | 628 | 629 | payment_type 630 | 631 | 632 | VARCHAR () 633 | 634 | 635 | payment_data 636 | 637 | 638 | JSON () 639 | 640 | 641 | created_at 642 | 643 | 644 | DATETIME () 645 | 646 | 647 | updated_at 648 | 649 | 650 | DATETIME () 651 | 652 | 653 | is_removed 654 | 655 | 656 | BOOLEAN () 657 | 658 | 659 | removed_at 660 | 661 | 662 | DATETIME () 663 | 664 | 665 | 666 | 667 | 668 | Customer->GenericBillingInfo 669 | 670 | 671 | 672 | 673 | 674 | 675 | billing_infos 676 | 677 | 678 | 679 | GenericFeedback 680 | 681 | 682 | 683 | 684 | GenericFeedback 685 | 686 | 687 | id 688 | 689 | 690 | INTEGER (PK,Index) 691 | 692 | 693 | score 694 | 695 | 696 | INTEGER () 697 | 698 | 699 | commentary 700 | 701 | 702 | TEXT () 703 | 704 | 705 | customer_email 706 | 707 | 708 | VARCHAR (Index) 709 | 710 | 711 | content_creator_email 712 | 713 | 714 | VARCHAR (Index) 715 | 716 | 717 | request_id 718 | 719 | 720 | INTEGER () 721 | 722 | 723 | last_updated 724 | 725 | 726 | DATETIME () 727 | 728 | 729 | is_removed 730 | 731 | 732 | BOOLEAN () 733 | 734 | 735 | removed_at 736 | 737 | 738 | DATETIME () 739 | 740 | 741 | 742 | 743 | 744 | Customer->GenericFeedback 745 | 746 | 747 | 748 | 749 | 750 | 751 | feedbacks_provided 752 | 753 | 754 | 755 | ContentCreator->GenericUser 756 | 757 | 758 | 759 | 760 | 761 | 762 | generic_user 763 | 764 | 765 | 766 | ContentCreator->GenericFeedback 767 | 768 | 769 | 770 | 771 | 772 | 773 | feedbacks_received 774 | 775 | 776 | 777 | GenericAPICreditLog 778 | 779 | 780 | 781 | 782 | GenericAPICreditLog 783 | 784 | 785 | id 786 | 787 | 788 | INTEGER (PK,Index) 789 | 790 | 791 | timestamp 792 | 793 | 794 | DATETIME () 795 | 796 | 797 | is_paid 798 | 799 | 800 | BOOLEAN () 801 | 802 | 803 | status 804 | 805 | 806 | VARCHAR () 807 | 808 | 809 | expense 810 | 811 | 812 | NUMERIC(10, 10) () 813 | 814 | 815 | request_id 816 | 817 | 818 | INTEGER () 819 | 820 | 821 | token_count 822 | 823 | 824 | INTEGER () 825 | 826 | 827 | content_creator_email 828 | 829 | 830 | VARCHAR () 831 | 832 | 833 | 834 | 835 | 836 | ContentCreator->GenericAPICreditLog 837 | 838 | 839 | 840 | 841 | 842 | 843 | api_credit_logs 844 | 845 | 846 | 847 | GenericAPIKey 848 | 849 | 850 | 851 | 852 | GenericAPIKey 853 | 854 | 855 | id 856 | 857 | 858 | INTEGER (PK,Index) 859 | 860 | 861 | api_key 862 | 863 | 864 | VARCHAR (Unique) 865 | 866 | 867 | content_creator_email 868 | 869 | 870 | VARCHAR (Index) 871 | 872 | 873 | is_active 874 | 875 | 876 | BOOLEAN () 877 | 878 | 879 | is_revoked 880 | 881 | 882 | BOOLEAN () 883 | 884 | 885 | expires_at 886 | 887 | 888 | DATETIME () 889 | 890 | 891 | created_at 892 | 893 | 894 | DATETIME () 895 | 896 | 897 | 898 | 899 | 900 | ContentCreator->GenericAPIKey 901 | 902 | 903 | 904 | 905 | 906 | 907 | api_keys 908 | 909 | 910 | 911 | UserSession->GenericUser 912 | 913 | 914 | 915 | 916 | 917 | 918 | generic_user 919 | 920 | 921 | 922 | GenericAuditLog->GenericUser 923 | 924 | 925 | 926 | 927 | 928 | 929 | actor 930 | 931 | 932 | 933 | GenericNotification->GenericUser 934 | 935 | 936 | 937 | 938 | 939 | 940 | recipient 941 | 942 | 943 | 944 | ServiceRequest->Customer 945 | 946 | 947 | 948 | 949 | 950 | 951 | customer 952 | 953 | 954 | 955 | GenericSubscription->Customer 956 | 957 | 958 | 959 | 960 | 961 | 962 | customer 963 | 964 | 965 | 966 | GenericSubscription->GenericSubscriptionUsage 967 | 968 | 969 | 970 | 971 | 972 | 973 | subscription_usages 974 | 975 | 976 | 977 | GenericSubscriptionType 978 | 979 | 980 | 981 | 982 | GenericSubscriptionType 983 | 984 | 985 | id 986 | 987 | 988 | INTEGER (PK,Index) 989 | 990 | 991 | name 992 | 993 | 994 | VARCHAR () 995 | 996 | 997 | monthly_fee 998 | 999 | 1000 | NUMERIC(10, 10) () 1001 | 1002 | 1003 | monthly_cap 1004 | 1005 | 1006 | INTEGER () 1007 | 1008 | 1009 | created_at 1010 | 1011 | 1012 | DATETIME () 1013 | 1014 | 1015 | updated_at 1016 | 1017 | 1018 | DATETIME () 1019 | 1020 | 1021 | is_removed 1022 | 1023 | 1024 | BOOLEAN () 1025 | 1026 | 1027 | removed_at 1028 | 1029 | 1030 | DATETIME () 1031 | 1032 | 1033 | 1034 | 1035 | 1036 | GenericSubscription->GenericSubscriptionType 1037 | 1038 | 1039 | 1040 | 1041 | 1042 | 1043 | subscription_type 1044 | 1045 | 1046 | 1047 | GenericSubscriptionUsage->Customer 1048 | 1049 | 1050 | 1051 | 1052 | 1053 | 1054 | customer 1055 | 1056 | 1057 | 1058 | GenericSubscriptionUsage->GenericSubscription 1059 | 1060 | 1061 | 1062 | 1063 | 1064 | 1065 | subscription 1066 | 1067 | 1068 | 1069 | GenericSubscriptionUsage->GenericSubscriptionType 1070 | 1071 | 1072 | 1073 | 1074 | 1075 | 1076 | subscription_type 1077 | 1078 | 1079 | 1080 | GenericBillingInfo->Customer 1081 | 1082 | 1083 | 1084 | 1085 | 1086 | 1087 | customer 1088 | 1089 | 1090 | 1091 | GenericFeedback->Customer 1092 | 1093 | 1094 | 1095 | 1096 | 1097 | 1098 | customer 1099 | 1100 | 1101 | 1102 | GenericFeedback->ContentCreator 1103 | 1104 | 1105 | 1106 | 1107 | 1108 | 1109 | content_creator 1110 | 1111 | 1112 | 1113 | GenericAPICreditLog->ContentCreator 1114 | 1115 | 1116 | 1117 | 1118 | 1119 | 1120 | content_creator 1121 | 1122 | 1123 | 1124 | GenericAPIKey->ContentCreator 1125 | 1126 | 1127 | 1128 | 1129 | 1130 | 1131 | content_creator 1132 | 1133 | 1134 | 1135 | FileStorage 1136 | 1137 | 1138 | 1139 | 1140 | FileStorage 1141 | 1142 | 1143 | id 1144 | 1145 | 1146 | INTEGER (PK,Index) 1147 | 1148 | 1149 | file_data 1150 | 1151 | 1152 | BLOB () 1153 | 1154 | 1155 | file_type 1156 | 1157 | 1158 | VARCHAR () 1159 | 1160 | 1161 | file_hash 1162 | 1163 | 1164 | VARCHAR (Unique) 1165 | 1166 | 1167 | upload_date 1168 | 1169 | 1170 | DATETIME () 1171 | 1172 | 1173 | 1174 | 1175 | 1176 | GenericSubscriptionType->GenericSubscription 1177 | 1178 | 1179 | 1180 | 1181 | 1182 | 1183 | subscriptions 1184 | 1185 | 1186 | 1187 | GenericSubscriptionType->GenericSubscriptionUsage 1188 | 1189 | 1190 | 1191 | 1192 | 1193 | 1194 | subscription_usages 1195 | 1196 | 1197 | 1198 | --------------------------------------------------------------------------------