|
| 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'): |
| 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 | + <TABLE BORDER="0" CELLBORDER="1" CELLSPACING="0"> |
| 30 | + <TR><TD COLSPAN="2" BGCOLOR="#3F51B5"><FONT COLOR="white">{name}</FONT></TD></TR> |
| 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'''<TR> |
| 46 | + <TD BGCOLOR="{color}">{column.name}</TD> |
| 47 | + <TD BGCOLOR="{color}">{column.type} ({constraint_str})</TD> |
| 48 | + </TR>''' |
| 49 | + |
| 50 | + label += '</TABLE>>' |
| 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, tooltip=tooltip, color="#1E88E5", style="dashed") |
| 60 | + |
| 61 | + # Render the graph to a file and open it |
| 62 | + dot.render(output_file, view=True) |
| 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 | +class GenericUser(Base): |
| 101 | + __tablename__ = 'generic_user' |
| 102 | + email = Column(String, primary_key=True, index=True) |
| 103 | + external_id = Column(String, unique=True, nullable=False) |
| 104 | + is_active = Column(Boolean, default=True) |
| 105 | + is_blocked = Column(Boolean, default=False) |
| 106 | + last_ip_address = Column(String, nullable=True) |
| 107 | + last_user_agent = Column(String, nullable=True) |
| 108 | + last_estimated_location = Column(JSON, nullable=True) |
| 109 | + preferences = Column(JSON) |
| 110 | + registered_at = Column(DateTime, default=datetime.utcnow, index=True) |
| 111 | + last_login = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow, index=True) |
| 112 | + is_deleted = Column(Boolean, default=False) |
| 113 | + deleted_at = Column(DateTime, nullable=True) |
| 114 | + customer = relationship("Customer", uselist=False, back_populates="generic_user") |
| 115 | + content_creator = relationship("ContentCreator", uselist=False, back_populates="generic_user") |
| 116 | + user_sessions = relationship("UserSession", back_populates="generic_user") |
| 117 | + audit_logs = relationship("GenericAuditLog", back_populates="actor") |
| 118 | + notifications = relationship("GenericNotification", back_populates="recipient") |
| 119 | + |
| 120 | +class Customer(Base): |
| 121 | + __tablename__ = 'customer' |
| 122 | + email = Column(String, ForeignKey('generic_user.email'), primary_key=True, index=True) |
| 123 | + total_purchases = Column(Numeric(10, 10), default=0.0) |
| 124 | + generic_user = relationship("GenericUser", back_populates="customer") |
| 125 | + service_requests = relationship("ServiceRequest", back_populates="customer") |
| 126 | + subscriptions = relationship("GenericSubscription", back_populates="customer") |
| 127 | + subscription_usages = relationship("GenericSubscriptionUsage", back_populates="customer") |
| 128 | + billing_infos = relationship("GenericBillingInfo", back_populates="customer") |
| 129 | + feedbacks_provided = relationship("GenericFeedback", back_populates="customer") |
| 130 | + |
| 131 | +class ContentCreator(Base): |
| 132 | + __tablename__ = 'content_creator' |
| 133 | + email = Column(String, ForeignKey('generic_user.email'), primary_key=True, index=True) |
| 134 | + projects_created = Column(Integer, default=0) |
| 135 | + revenue_share = Column(Numeric(10, 10), default=0.7) |
| 136 | + total_earned = Column(Numeric(10, 10), default=0.0) |
| 137 | + last_project_created_at = Column(DateTime, nullable=True) |
| 138 | + generic_user = relationship("GenericUser", back_populates="content_creator") |
| 139 | + api_credit_logs = relationship("GenericAPICreditLog", back_populates="content_creator") |
| 140 | + api_keys = relationship("GenericAPIKey", back_populates="content_creator") |
| 141 | + feedbacks_received = relationship("GenericFeedback", back_populates="content_creator") |
| 142 | + |
| 143 | +class UserSession(Base): |
| 144 | + __tablename__ = 'user_session' |
| 145 | + id = Column(Integer, primary_key=True) |
| 146 | + user_email = Column(String, ForeignKey('generic_user.email'), nullable=False) |
| 147 | + session_token = Column(String, unique=True, nullable=False) |
| 148 | + expires_at = Column(DateTime, nullable=False) |
| 149 | + is_active = Column(Boolean, default=True) |
| 150 | + created_at = Column(DateTime, default=datetime.utcnow) |
| 151 | + updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow) |
| 152 | + generic_user = relationship("GenericUser", back_populates="user_sessions") |
| 153 | + |
| 154 | +class FileStorage(Base): |
| 155 | + __tablename__ = 'file_storage' |
| 156 | + id = Column(Integer, primary_key=True, index=True) |
| 157 | + file_data = Column(LargeBinary, nullable=False) |
| 158 | + file_type = Column(String, nullable=False) |
| 159 | + file_hash = Column(String, nullable=False, unique=True) |
| 160 | + upload_date = Column(DateTime, default=datetime.utcnow) |
| 161 | + |
| 162 | +class ServiceRequest(Base): |
| 163 | + __tablename__ = 'service_request' |
| 164 | + unique_id_for_sharing = Column(String, primary_key=True, index=True) |
| 165 | + status = Column(String, CheckConstraint("status IN ('pending', 'completed', 'failed')"), default='pending') |
| 166 | + ip_address = Column(String) |
| 167 | + request_time = Column(DateTime, default=datetime.utcnow, index=True) |
| 168 | + request_last_updated_time = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow) |
| 169 | + user_input = Column(JSON) |
| 170 | + input_data_string = Column(Text) |
| 171 | + api_request = Column(JSON) |
| 172 | + api_response = Column(JSON) |
| 173 | + api_session_id = Column(String, nullable=True, unique=True) |
| 174 | + total_cost = Column(Numeric(10, 10), nullable=True) |
| 175 | + customer_email = Column(String, ForeignKey('customer.email')) |
| 176 | + customer = relationship("Customer", back_populates="service_requests") |
| 177 | + |
| 178 | +# AuditLog |
| 179 | +class GenericAuditLog(Base): |
| 180 | + __tablename__ = 'generic_audit_log' |
| 181 | + id = Column(Integer, primary_key=True, index=True) |
| 182 | + action_type = Column(String, nullable=False, index=True) |
| 183 | + outcome = Column(String, nullable=True) |
| 184 | + field_affected = Column(String, nullable=True) |
| 185 | + prev_value = Column(JSON, nullable=True) |
| 186 | + new_value = Column(JSON, nullable=True) |
| 187 | + actor_email = Column(String, ForeignKey('generic_user.email'), index=True) |
| 188 | + related_request_id = Column(Integer, ForeignKey('generic_user_request.unique_id')) |
| 189 | + timestamp = Column(DateTime, default=datetime.utcnow) |
| 190 | + actor = relationship("GenericUser", back_populates="audit_logs") |
| 191 | + |
| 192 | +# Feedback |
| 193 | +class GenericFeedback(Base): |
| 194 | + __tablename__ = 'generic_feedback' |
| 195 | + id = Column(Integer, primary_key=True, index=True) |
| 196 | + score = Column(Integer, nullable=False) |
| 197 | + commentary = Column(Text, nullable=True) |
| 198 | + customer_email = Column(String, ForeignKey('customer.email'), index=True) |
| 199 | + content_creator_email = Column(String, ForeignKey('content_creator.email'), index=True) |
| 200 | + request_id = Column(Integer, ForeignKey('generic_user_request.unique_id')) |
| 201 | + last_updated = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow) |
| 202 | + is_removed = Column(Boolean, default=False) |
| 203 | + removed_at = Column(DateTime, nullable=True) |
| 204 | + customer = relationship("Customer", back_populates="feedbacks_provided") |
| 205 | + content_creator = relationship("ContentCreator", back_populates="feedbacks_received") |
| 206 | + |
| 207 | +# APIKeys |
| 208 | +class GenericAPIKey(Base): |
| 209 | + __tablename__ = 'generic_api_key' |
| 210 | + id = Column(Integer, primary_key=True, index=True) |
| 211 | + api_key = Column(String, unique=True, nullable=False) |
| 212 | + content_creator_email = Column(String, ForeignKey('content_creator.email'), index=True) |
| 213 | + is_active = Column(Boolean, default=True) |
| 214 | + is_revoked = Column(Boolean, default=False) |
| 215 | + expires_at = Column(DateTime, nullable=True) |
| 216 | + created_at = Column(DateTime, default=datetime.utcnow) |
| 217 | + content_creator = relationship("ContentCreator", back_populates="api_keys") |
| 218 | + |
| 219 | +# Notification |
| 220 | +class GenericNotification(Base): |
| 221 | + __tablename__ = 'generic_notification' |
| 222 | + id = Column(Integer, primary_key=True, index=True) |
| 223 | + recipient_email = Column(String, ForeignKey('generic_user.email'), index=True) |
| 224 | + notification_kind = Column(String, nullable=False) |
| 225 | + is_read = Column(Boolean, default=False) |
| 226 | + content = Column(Text, nullable=False) |
| 227 | + created_at = Column(DateTime, default=datetime.utcnow) |
| 228 | + read_at = Column(DateTime, nullable=True) |
| 229 | + recipient = relationship("GenericUser", back_populates="notifications") |
| 230 | + |
| 231 | +# APICreditLog |
| 232 | +class GenericAPICreditLog(Base): |
| 233 | + __tablename__ = 'generic_api_credit_log' |
| 234 | + id = Column(Integer, primary_key=True, index=True) |
| 235 | + timestamp = Column(DateTime, default=datetime.utcnow) |
| 236 | + is_paid = Column(Boolean, default=False) |
| 237 | + status = Column(String, default='pending') |
| 238 | + expense = Column(Numeric(10, 10), nullable=False) |
| 239 | + request_id = Column(Integer, ForeignKey('generic_user_request.unique_id')) |
| 240 | + token_count = Column(Integer, nullable=False) |
| 241 | + content_creator_email = Column(String, ForeignKey('content_creator.email')) |
| 242 | + content_creator = relationship("ContentCreator", back_populates="api_credit_logs") |
| 243 | + |
| 244 | +# SubscriptionType |
| 245 | +class GenericSubscriptionType(Base): |
| 246 | + __tablename__ = 'generic_subscription_type' |
| 247 | + id = Column(Integer, primary_key=True, index=True) |
| 248 | + name = Column(String, nullable=False) |
| 249 | + monthly_fee = Column(Numeric(10, 10), nullable=False) |
| 250 | + monthly_cap = Column(Integer, nullable=False) |
| 251 | + created_at = Column(DateTime, default=datetime.utcnow) |
| 252 | + updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow) |
| 253 | + is_removed = Column(Boolean, default=False) |
| 254 | + removed_at = Column(DateTime, nullable=True) |
| 255 | + subscriptions = relationship("GenericSubscription", back_populates="subscription_type") |
| 256 | + |
| 257 | +# Subscription |
| 258 | +class GenericSubscription(Base): |
| 259 | + __tablename__ = 'generic_subscription' |
| 260 | + id = Column(Integer, primary_key=True, index=True) |
| 261 | + customer_email = Column(String, ForeignKey('customer.email'), index=True) |
| 262 | + start_date = Column(DateTime, default=datetime.utcnow) |
| 263 | + end_date = Column(DateTime, nullable=True) |
| 264 | + current_use = Column(Integer, default=0) |
| 265 | + subscription_type_id = Column(Integer, ForeignKey('generic_subscription_type.id')) |
| 266 | + customer = relationship("Customer", back_populates="subscriptions") |
| 267 | + subscription_type = relationship("GenericSubscriptionType", back_populates="subscriptions") |
| 268 | + subscription_usages = relationship("GenericSubscriptionUsage", back_populates="subscription") |
| 269 | + |
| 270 | +# SubscriptionUsage |
| 271 | +class GenericSubscriptionUsage(Base): |
| 272 | + __tablename__ = 'generic_subscription_usage' |
| 273 | + id = Column(Integer, primary_key=True, index=True) |
| 274 | + customer_email = Column(String, ForeignKey('customer.email'), index=True) |
| 275 | + use_count = Column(Integer, default=0) |
| 276 | + last_use = Column(DateTime, nullable=True) |
| 277 | + subscription_id = Column(Integer, ForeignKey('generic_subscription.id')) |
| 278 | + subscription_type_id = Column(Integer, ForeignKey('generic_subscription_type.id')) |
| 279 | + customer = relationship("Customer", back_populates="subscription_usages") |
| 280 | + subscription = relationship("GenericSubscription", back_populates="subscription_usages") |
| 281 | + subscription_type = relationship("GenericSubscriptionType", backref="subscription_usages") |
| 282 | + |
| 283 | +# BillingInfo |
| 284 | +class GenericBillingInfo(Base): |
| 285 | + __tablename__ = 'generic_billing_info' |
| 286 | + id = Column(Integer, primary_key=True, index=True) |
| 287 | + customer_email = Column(String, ForeignKey('customer.email'), index=True) |
| 288 | + payment_type = Column(String, nullable=False) |
| 289 | + payment_data = Column(JSON) |
| 290 | + created_at = Column(DateTime, default=datetime.utcnow) |
| 291 | + updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow) |
| 292 | + is_removed = Column(Boolean, default=False) |
| 293 | + removed_at = Column(DateTime, nullable=True) |
| 294 | + customer = relationship("Customer", back_populates="billing_infos") |
| 295 | + |
| 296 | +models = [GenericUser, Customer, ContentCreator, UserSession, FileStorage, ServiceRequest, GenericAuditLog, GenericFeedback, GenericAPIKey, GenericNotification, GenericAPICreditLog, GenericSubscriptionType, GenericSubscription, GenericSubscriptionUsage, GenericBillingInfo] |
| 297 | + |
| 298 | + |
| 299 | +output_file_name = 'my_data_model_diagram' |
| 300 | +# Generate the diagram and add interactivity |
| 301 | +generate_data_model_diagram(models, output_file_name) |
| 302 | +add_web_font_and_interactivity('my_data_model_diagram.svg', 'my_interactive_data_model_diagram.svg') |
0 commit comments