djangoで作った

はじめに

こんにちは。

今日は、コーディングの補助アプリを作ってみました。

[機能1]SQLのテーブル構造を整理して表示する

テーブル作る!

データを追加する!

ビューを作る!

出来上がったビューを表示!

viewやテーブルの構造が一目でわかる!

[機能2]Pythonのエラーメッセージを整理して表示する

エラーコードを入力!

ケッテイボタンを押すと・・・

エラーの影響が出たコードの場所をピックアップ!

ダブルクリックで詳細を確認することもできます。(キーボードのj,kや↑↓で移動可能)

[開発環境]

  • OS: macOS (M3チップ)
  • 言語: Python 3.14
  • 仮想環境: venv
  • DBSQLite
  • ツール管理: Homebrew (tree, Pythonなどのインストールに使用)

[ディレクトリ構造]

非常にややこしいディレクトリ名にしてしまいました。。。🙇‍♀️🙇‍♀️🙇‍♀️

・view_sql:全体の管理

・error_analyze:処理の詳細を管理

※error_analyzeという名前ですが、SQLの可視化の処理もここで書いてます。

tree -L 2
.
├── README.md
├── config
│   ├── __init__.py
│   ├── __pycache__
│   ├── asgi.py
│   ├── settings.py
│   ├── urls.py
│   └── wsgi.py
├── db.sqlite3
├── error_analyze ←コードのエラー分析とSQL可視化の具体的な処理内容を管理
│   ├── __init__.py
│   ├── __pycache__
│   ├── admin.py
│   ├── apps.py
│   ├── migrations
│   ├── models.py
│   ├── templates
│   ├── tests.py
│   ├── urls.py
│   └── views.py
├── kakeibo.db
├── manage.py
├── memo.md
├── tools
│   └── test_broken.py
├── venv
│   ├── bin
│   ├── include
│   ├── lib
│   └── pyvenv.cfg
└── view_sql ←どのアプリを動かすか、どのDBを使うか、など全体ルールを管理します。
    ├── __init__.py
    ├── __pycache__
    ├── admin.py
    ├── asgi.py
    ├── migrations
    ├── models.py
    ├── settings.py
    ├── tests.py
    ├── urls.py
    ├── views.py
    └── wsgi.py

全体のコードはこちらにあります。

全体のコードへジャンプ

環境を作る

環境構築

# 仮想環境作成
python3 -m venv venv
# 有効化
source venv/bin/activate
# ライブラリのインストール
pip install django
# インストールできたか確認
python -m django --version
>>6.0.3
# プロジェクト作成
django-admin startproject config .
# アプリ作成
python manage.py startapp myapp
# マイグレーションの実行
python manage.py migrate
# 開発用サーバーの起動
python manage.py runserver

[メモ]python3 -m venv venvについて

Python3:python呼び出し
-m venv:標準ライブラリのvenvをモジュール(-m)として実行
venv:ライブラリ管理用ディレクトリ作成

# バージョンを指定する(Version3.9の場合)
python3.9 -m venv venv-3.9

[メモ]manage.pyについて

setdefaultメソッドを呼び出す

今回は、第1引数のDJANGO_SETTINGS_MODULEという環境変数は特に何も設定していません。

第2引数のconfig.settingsに設定しています。

manage.py

os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'config.settings')

os.py

key(DJANGO_SETTINGS_MODULE)がなければ,value(config.settings)をkeyに設定します。

    def setdefault(self, key, value):
        if key not in self:
            self[key] = value
        return self[key]

manage.py

続いて、execute_from_command_lineメソッドを呼び出します。

from django.core.management import execute_from_command_line

execute_from_command_line(sys.argv)

management>__init__.pyi

pyiファイルなので、中身が見えませんでした。

def execute_from_command_line(argv: list[str] | None = ...) -> None: ...

ブラウザで検索「github django」します。

(ブログにリンクを埋め込みできなかったので、ベタ貼りで失礼します><)

https://github.com/django/django/blob/6b90f8a8d6994dc62cd91dde911fe56ec3389494/django/core/management/init.py#L440

django/core/management/__init__.py

ManagementUtilityクラスを呼び出していますね。

def execute_from_command_line(argv=None):
"""Run a ManagementUtility."""
utility = ManagementUtility(argv)
utility.execute()

__init__.pyのManagementUtilityクラス

引数を自分のプロパティとして保持します。

def __init__(self, argv=None):
        self.argv = argv or sys.argv[:]
        self.prog_name = os.path.basename(self.argv[0])
        if self.prog_name == "__main__.py":
            self.prog_name = "python -m django"
        self.settings_exception = None

サブコマンド(命令)を抽出します。

def execute(self):
        try:
            subcommand = self.argv[1]
        except IndexError:
            subcommand = "help"  # Display help if no arguments were given.

self.argv[2:](サブコマンド以降の引数すべて)を解析し、もし --settings があればそれを適用します。

parser = CommandParser(
            prog=self.prog_name,
            usage="%(prog)s subcommand [options] [args]",
            add_help=False,
            allow_abbrev=False,
        )
        parser.add_argument("--settings")
        parser.add_argument("--pythonpath")
        parser.add_argument("args", nargs="*")
options, args = parser.parse_known_args(self.argv[2:])

そして、最終的なコマンド実行へ引き継ぎます。

self.fetch_command(subcommand).run_from_argv(self.argv)

解説

◾️error_analyze>views.py

lineage関数:SQLのテーブルやビューどのテーブルからデータを持ってきているのか調べます。

def get_lineage(cur, table_name, depth=0):
    lineage_data = []
    indent = depth * 30
    cur.execute("SELECT type, sql FROM sqlite_master WHERE LOWER(name)=LOWER(?)", (table_name,))
    info = cur.fetchone()

1.実体のテーブルなのかビューなのか、DB管理情報のsqlite_masterから調べます。

    columns, sample_data = [], []
    
    try:
        cur.execute(f"PRAGMA table_info({table_name})")
        columns = [col[1] for col in cur.fetchall()]
        cur.execute(f"SELECT * FROM {table_name} LIMIT 5")
        sample_data = [list(row) for row in cur.fetchall()]
    except: pass

2.レコードの情報を取得します。

node_data = {'name': table_name, 'type': 'SOURCE', 'indent': indent, 'columns': columns, 'data': sample_data}

3.取得した情報を辞書型で管理します。

if info:
        if info[0].upper() == 'VIEW':
            node_data['type'] = 'VIEW'
            lineage_data.append(node_data)
            view_sql = info[1]
            # 正規表現を強化: FROMやJOINの後の単語を抽出
            # カンマ区切りやサブクエリ、エイリアスに対応
            found_tables = re.findall(r"(?:FROM|JOIN)\s+([a-zA-Z0-9_]+)", view_sql, re.IGNORECASE)
            # 重複排除と予約語の除外
            for p in sorted(set(found_tables), key=found_tables.index):
                if p.upper() not in ["SELECT", "AS", "WHERE", "GROUP", "ORDER", "LEFT", "RIGHT", "INNER", "OUTER"]:
                    lineage_data.extend(get_lineage(cur, p, depth + 1))
        else:
            lineage_data.append(node_data)
    return lineage_data

4.関数の最初あたりで作成したリスト(lineage_data)に全てのデータを追加していきます。

lineage_data.extend(get_lineage(cur, p, depth + 1))

とありますが、再起処理を使うことで、ビューどのテーブルからデータを持ってきているか、調べることができます。

index関数:ユーザーが入力したテキスト(以下、入力データと呼ぶ)の内容を見て、SQLとトレースバッグのどちらかを判断し、適切な処理に振り分けます。

viz_result, sql_result, status_message = None, None, None
raw_input = request.POST.get("error_log", "") if request.method == "POST" else ""
db_path = os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))), 'kakeibo.db')

raw_input:入力データが送られてきた時のみ、処理します。エラーログが送られてきた時は空で返します。

db_path:DBがある場所を特定します。

if request.method == "POST" and raw_input:
  clean_input = raw_input.strip()
    upper_input = clean_input.upper()

データを全て大文字に直します。

if any(upper_input.startswith(cmd) for cmd in ["CREATE", "DROP", "INSERT", "DELETE", "UPDATE"]):
   with sqlite3.connect(db_path) as conn:
      conn.executescript(raw_input)

入力データに”CREATE”, “DROP”, “INSERT”, “DELETE”, “UPDATE”のどれかが含まれている場合、

dbに接続します。

target_tables = re.findall(r"FROM\s+([a-zA-Z0-9_]+)", clean_input, re.IGNORECASE)

ここから、SQL可視化とエラー分析で処理が分かれていきます。

まずは、SQL可視化の方からです。

入力データがSELECTだった場合、SELECT文からテーブル名を抽出します。

lineage_tree.extend(get_lineage(cur, name))
sql_result = {'lineage': lineage_tree}

そして先ほど紹介したget_lineage関数を呼び出します。

結果は、sql_resultという変数に保持されます。

return render(request, 'analyzer/index.html', {
   'viz_result': viz_result, 'sql_result': sql_result,
   'status_message': status_message, 'raw_error': raw_input
})

最後に、sql_resultをHTMLテンプレートに流し込み、最終的な画面としてユーザーに届けています。

続いてエラー分析の方ですね。

まず、エラーログによくある「ファイル名、行番号、関数名」のパターンを抽出します。

trace_steps = re.findall(r'File\s+"(.+?)",\s*line\s*(\d+),\s*in\s*(.+)', raw_input)
if trace_steps:
   steps = []
   for i, (path, line, func) in enumerate(trace_steps):
       line_num = int(line)
       code_all = []

もし一つでも「ファイル名と行番号のセット」が見つかったら分析を進めます。

何も見つからなければ、スルーされます。

if os.path.exists(path):
   with open(path, 'r', encoding='utf-8') as f:
   for idx, text in enumerate(f.readlines()):
       curr = idx + 1
       code_all.append({'num': curr, 'text': text.rstrip(), 'is_target': (curr == line_num)})
else: 
   code_all = [{'num': line_num, 'text': "/* FILE_NOT_FOUND */", 'is_target': True}]

表示用データの作成を行います。

'is_target': (curr == line_num)

ここでは、いま読み込んでいる行番号とエラーログに書かれていた行番号が一致するかを判定し、TrueかFalseを記録します。

code_all = [{'num': line_num, 'text': "/* FILE_NOT_FOUND */", 'is_target': True}]

ファイルが見つからなければ、”/* FILE_NOT_FOUND */”と表示します。

except: 
     code_all = [{'num': line_num, 'text': "/* LOAD_ERROR */", 'is_target': True}]
     steps.append({'file': os.path.basename(path), 'line': line, 'func': func, 'indent': i * 20, 'code_all': code_all, 'target_line': line_num})

code_allにエラーの内容を入れ込みます。

’text’には、本物のエラーコードの代わりに、/* LOAD_ERROR */ という短いテキストを入れます。

‘num’にはline_num は保持し、’is_target ‘も True にすることで、エラーが起こった箇所の情報は保持します。

結果、画面にはコードは出ないが、「何行目でエラーだったか」という事実だけは表示されます。

return render(request, 'analyzer/index.html', {
   'viz_result': viz_result, 'sql_result': sql_result,
   'status_message': status_message, 'raw_error': raw_input
})

SQL可視化と同じようにHTMLテンプレートに流し込み、最終的な画面としてユーザーに届けます。

おわりに

周辺設定(フロントエンドや urls.py / settings.py 等)の修正箇所については、今回は説明を割愛します。

Djangoの動作原理については、こちらの解説記事で紹介していますので、ぜひチェックしてみてください。

ありがとうございました!

今回登場したコード全体

config>asgi.py

"""
ASGI config for config project.

It exposes the ASGI callable as a module-level variable named ``application``.

For more information on this file, see
https://docs.djangoproject.com/en/6.0/howto/deployment/asgi/
"""

import os

from django.core.asgi import get_asgi_application

os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'config.settings')

application = get_asgi_application()

config>settings.py

"""
Django settings for config project.

Generated by 'django-admin startproject' using Django 6.0.2.

For more information on this file, see
https://docs.djangoproject.com/en/6.0/topics/settings/

For the full list of settings and their values, see

Settings | Django documentation
The web framework for perfectionists with deadlines.
""" from pathlib import Path # Build paths inside the project like this: BASE_DIR / 'subdir'. BASE_DIR = Path(__file__).resolve().parent.parent # Quick-start development settings - unsuitable for production # See https://docs.djangoproject.com/en/6.0/howto/deployment/checklist/ # SECURITY WARNING: keep the secret key used in production secret! SECRET_KEY = '秘密' # SECURITY WARNING: don't run with debug turned on in production! DEBUG = True ALLOWED_HOSTS = [] # Application definition INSTALLED_APPS = [ 'django.contrib.admin', 'django.contrib.auth', 'django.contrib.contenttypes', 'django.contrib.sessions', 'django.contrib.messages', 'django.contrib.staticfiles', 'error_analyze', ] MIDDLEWARE = [ 'django.middleware.security.SecurityMiddleware', 'django.contrib.sessions.middleware.SessionMiddleware', 'django.middleware.common.CommonMiddleware', 'django.middleware.csrf.CsrfViewMiddleware', 'django.contrib.auth.middleware.AuthenticationMiddleware', 'django.contrib.messages.middleware.MessageMiddleware', 'django.middleware.clickjacking.XFrameOptionsMiddleware', ] ROOT_URLCONF = 'config.urls' TEMPLATES = [ { 'BACKEND': 'django.template.backends.django.DjangoTemplates', 'DIRS': [], 'APP_DIRS': True, 'OPTIONS': { 'context_processors': [ 'django.template.context_processors.request', 'django.contrib.auth.context_processors.auth', 'django.contrib.messages.context_processors.messages', ], }, }, ] WSGI_APPLICATION = 'config.wsgi.application' # Database # https://docs.djangoproject.com/en/6.0/ref/settings/#databases DATABASES = { 'default': { 'ENGINE': 'django.db.backends.sqlite3', 'NAME': BASE_DIR / 'db.sqlite3', } } # Password validation # https://docs.djangoproject.com/en/6.0/ref/settings/#auth-password-validators AUTH_PASSWORD_VALIDATORS = [ { 'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator', }, { 'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator', }, { 'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator', }, { 'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator', }, ] # Internationalization # https://docs.djangoproject.com/en/6.0/topics/i18n/ LANGUAGE_CODE = 'en-us' TIME_ZONE = 'UTC' USE_I18N = True USE_TZ = True # Static files (CSS, JavaScript, Images) # https://docs.djangoproject.com/en/6.0/howto/static-files/ STATIC_URL = 'static/'

urls.py

"""
URL configuration for config project.

The `urlpatterns` list routes URLs to views. For more information please see:
    https://docs.djangoproject.com/en/6.0/topics/http/urls/
Examples:
Function views
    1. Add an import:  from my_app import views
    2. Add a URL to urlpatterns:  path('', views.home, name='home')
Class-based views
    1. Add an import:  from other_app.views import Home
    2. Add a URL to urlpatterns:  path('', Home.as_view(), name='home')
Including another URLconf
    1. Import the include() function: from django.urls import include, path
    2. Add a URL to urlpatterns:  path('blog/', include('blog.urls'))
"""
from django.contrib import admin
from django.urls import path, include

urlpatterns = [
    path('admin/', admin.site.urls),
    path('', include('error_analyze.urls')),
]

wsgi.py

"""
WSGI config for config project.

It exposes the WSGI callable as a module-level variable named ``application``.

For more information on this file, see
https://docs.djangoproject.com/en/6.0/howto/deployment/wsgi/
"""

import os

from django.core.wsgi import get_wsgi_application

os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'config.settings')

application = get_wsgi_application()

error_analyze>templates>index.html

<!DOCTYPE html>
<html lang="ja">
<head>
    <meta charset="UTF-8">
    <title>AUDIO_ANALYZER // MNML-GREY_v3</title>
    <style>
        @font-face { font-family: 'MisakiGothic'; src: url('https://cdn.leafscape.be/misaki/misaki_gothic_web.woff2') format('woff2'); }
        
        :root {
            --base: #1E1E20; --input-bg: #2A2A2E; --pink: #FF88BB; --text: #AAAAAA;
            --code-bg: #141416; --track-focus: #3A3A40;
            --btn-main: #FF88BB; --btn-shadow: #CC6699; --btn-light: #FFBBDD;
        }

        body { font-family: 'MisakiGothic', sans-serif; background: #0D0D0F; color: var(--text); margin: 0; padding: 40px; image-rendering: pixelated; }
        
        .container { 
            max-width: 800px; margin: 0 auto; background: var(--base); padding: 40px; 
            border-radius: 20px; border: 1px solid #333; position: relative; 
            box-shadow: 0 40px 100px rgba(0,0,0,0.8); 
            display: flex; flex-direction: column;
        }

        .header-text { font-size: 0.8rem; color: #555; letter-spacing: 3px; margin-bottom: 25px; }

        .status-msg { 
            font-size: 0.7rem; color: var(--pink); margin-bottom: 10px; 
            letter-spacing: 1px; border-bottom: 1px solid #333; display: inline-block; padding-bottom: 2px;
        }

        .lcd-panel { 
            background: var(--input-bg); border: 4px solid #111; padding: 20px; 
            border-radius: 8px; margin-bottom: 15px; 
            box-shadow: inset 0 2px 10px rgba(0,0,0,0.5); 
        }
        
        textarea { 
            width: 100%; height: 140px; background: transparent; border: none; 
            font-family: 'MisakiGothic'; font-size: 1.2rem; color: #EEE; 
            outline: none; resize: none; line-height: 1.6; 
        }

        .btn-wrapper { display: flex; justify-content: flex-end; width: 100%; margin-bottom: 20px; }

        .analyze-btn {
            width: 105px; height: 36px;
            background: var(--btn-main); border: none; cursor: pointer;
            display: flex; align-items: center; justify-content: center; gap: 6px;
            box-shadow: 0 0 0 2px #000, inset 2px 2px 0 var(--btn-light), inset -4px -4px 0 var(--btn-shadow), 4px 4px 0 rgba(0,0,0,0.3);
            transition: all 0.05s; outline: none;
        }

        .analyze-btn:active {
            transform: translate(2px, 2px);
            box-shadow: 0 0 0 2px #000, inset 4px 4px 0 var(--btn-shadow), inset -2px -2px 0 var(--btn-light), 0 0 0 rgba(0,0,0,0);
        }

        .heart {
            width: 12px; height: 12px; background: #FFF;
            clip-path: polygon(0 20%, 20% 20%, 20% 0, 40% 0, 40% 20%, 60% 20%, 60% 0, 80% 0, 80% 20%, 100% 20%, 100% 60%, 80% 60%, 80% 80%, 60% 80%, 60% 100%, 40% 100%, 40% 80%, 20% 80%, 20% 60%, 0 60%);
        }

        .btn-label { font-size: 0.8rem; color: #000; font-weight: bold; line-height: 1; margin-top: 2px; }

        .track-list-title { font-size: 0.7rem; color: var(--pink); border-left: 3px solid var(--pink); padding-left: 10px; margin: 25px 0 15px; letter-spacing: 2px; }
        
        .track-item { 
            background: var(--input-bg); padding: 12px 20px; margin-bottom: 8px; border-radius: 4px; 
            cursor: pointer; font-size: 1rem; width: fit-content; outline: none; border: 1px solid transparent;
            user-select: none;
        }
        .track-item:focus { background: var(--track-focus); border: 1px solid var(--pink); }

        .track-detail { background: var(--code-bg); border: 1px solid #333; margin: 10px 0 20px 20px; padding: 15px; display: none; }
        
        .viewport { height: 100px; overflow: hidden; position: relative; background: #000; border: 1px solid #222; }
        .code-container { position: absolute; width: 100%; transition: transform 0.1s ease-out; }
        .code-line { height: 20px; line-height: 20px; display: flex; font-family: monospace; font-size: 0.9rem; padding: 0 10px; }
        .line-num { color: #444; width: 40px; text-align: right; margin-right: 15px; user-select: none; }
        .line-text { color: #888; white-space: pre; }
        .target-line { background: rgba(255, 136, 187, 0.15); color: #fff; }
        .target-line .line-text { color: #fff; }

        /* SQL解析用テーブル */
        .sql-table-container { display: none; margin: 10px 0 20px 20px; }
        .sql-table { width: 100%; border-collapse: collapse; background: var(--code-bg); font-size: 0.85rem; }
        .sql-table th, .sql-table td { border: 1px solid #333; padding: 8px; text-align: left; }
        .sql-table th { color: var(--pink); background: #1A1A1E; font-size: 0.7rem; }
        .sql-table td { color: #EEE; }
    </style>
</head>
<body>
    <div class="container">
        <div class="header-text">DIGITAL_AUDIO_ANALYZER // MNML-GREY_v3</div>
        
        <form method="post">
            {% csrf_token %}
            {% if status_message %}<div class="status-msg">STATUS: {{ status_message }}</div>{% endif %}
            
            <div class="lcd-panel">
                <textarea name="error_log" placeholder="READY_">{{ raw_error }}</textarea>
            </div>
            
            <div class="btn-wrapper">
                <button type="submit" class="analyze-btn">
                    <div class="heart"></div>
                    <div class="btn-label">ケッテイ</div>
                </button>
            </div>
        </form>

        {% if viz_result %}
        <div class="track-list-title">TRACE_ANALYSIS // {{ viz_result.name }}</div>
        {% for step in viz_result.steps %}
        <div style="margin-left: {{ step.indent }}px;">
            <div class="track-item" tabindex="0" onclick="toggleTrace(this, {{ forloop.counter }}, {{ step.target_line }})">
                <strong>[{{ step.file }}] L{{ step.line }}</strong>
            </div>
            <div class="track-detail" id="trace-{{ forloop.counter }}">
                <div style="font-size: 0.7rem; color: var(--pink); margin-bottom: 8px;">FUNC: {{ step.func }} // J/K TO SCROLL</div>
                <div class="viewport">
                    <div class="code-container" id="container-{{ forloop.counter }}" data-offset="0">
                        {% for line in step.code_all %}
                        <div class="code-line {% if line.is_target %}target-line{% endif %}">
                            <span class="line-num">{{ line.num }}</span><span class="line-text">{{ line.text }}</span>
                        </div>
                        {% endfor %}
                    </div>
                </div>
            </div>
        </div>
        {% endfor %}
        {% endif %}

        {% if sql_result %}
        <div class="track-list-title">SQL_LINEAGE_MAP // DBL_CLICK TO EXPAND</div>
        {% if sql_result.error %}
            <div class="status-msg">ERROR: {{ sql_result.error }}</div>
        {% else %}
            {% for item in sql_result.lineage %}
            <div style="margin-left: {{ item.indent }}px;">
                <div class="track-item" tabindex="0" ondblclick="toggleSql(this, {{ forloop.counter }})">
                    <strong>[{{ item.type }}] {{ item.name }}</strong>
                </div>
                <div class="sql-table-container" id="sql-{{ forloop.counter }}">
                    <table class="sql-table">
                        <thead><tr>{% for col in item.columns %}<th>{{ col }}</th>{% endfor %}</tr></thead>
                        <tbody>
                            {% for row in item.data %}
                            <tr>{% for val in row %}<td>{{ val }}</td>{% endfor %}</tr>
                            {% empty %}
                            <tr><td colspan="{{ item.columns|length }}" style="text-align:center; color:#555;">NO_DATA</td></tr>
                            {% endfor %}
                        </tbody>
                    </table>
                </div>
            </div>
            {% endfor %}
        {% endif %}
        {% endif %}
    </div>

    <script>
        const LINE_H = 20; let lastActive = null;
        
        // Pythonトレースの表示切替 (シングルクリック)
        function toggleTrace(el, id, targetLine) {
            const detail = document.getElementById('trace-' + id), container = document.getElementById('container-' + id);
            const isOpening = (detail.style.display === 'none' || detail.style.display === '');
            detail.style.display = isOpening ? 'block' : 'none';
            if (isOpening) { 
                lastActive = el; el.focus(); 
                updateScroll(container, -((targetLine - 3) * LINE_H)); 
            }
        }
        
        function toggleSql(el, id) {
            const container = document.getElementById('sql-' + id);
            container.style.display = (container.style.display === 'none' || container.style.display === '') ? 'block' : 'none';
        }

        window.addEventListener('keydown', function(e) {
            if (e.target.tagName === 'TEXTAREA' || !lastActive) return;
            const detail = lastActive.parentElement.querySelector('.track-detail');
            if (!detail || detail.style.display === 'none') return;
            const container = detail.querySelector('.code-container');
            let current = parseInt(container.getAttribute('data-offset') || 0);
            if (e.key === 'j' || e.key === 'ArrowDown') { updateScroll(container, current - LINE_H); e.preventDefault(); } 
            else if (e.key === 'k' || e.key === 'ArrowUp') { updateScroll(container, current + LINE_H); e.preventDefault(); }
        }, true);

        function updateScroll(c, o) { c.style.transform = `translateY(${o}px)`; c.setAttribute('data-offset', o); }
    </script>
</body>
</html>

admin.py

from django.contrib import admin

# Register your models here.

apps.py

from django.apps import AppConfig

class ErrorAnalyzeConfig(AppConfig):
    default_auto_field = 'django.db.models.BigAutoField'
    name = 'error_analyze'

models.py

from django.db import models

# Create your models here.

test.py

from django.test import TestCase

# Create your tests here.

urls.py

from django.urls import path
from . import views

urlpatterns = [
    path('', views.index, name='index'),    
]

views.py

import re
import sqlite3
import os
from django.shortcuts import render

def get_lineage(cur, table_name, depth=0):
    lineage_data = []
    indent = depth * 30
    cur.execute("SELECT type, sql FROM sqlite_master WHERE LOWER(name)=LOWER(?)", (table_name,))
    info = cur.fetchone()
    columns, sample_data = [], []
    
    try:
        cur.execute(f"PRAGMA table_info({table_name})")
        columns = [col[1] for col in cur.fetchall()]
        cur.execute(f"SELECT * FROM {table_name} LIMIT 5")
        sample_data = [list(row) for row in cur.fetchall()]
    except: pass

    node_data = {'name': table_name, 'type': 'SOURCE', 'indent': indent, 'columns': columns, 'data': sample_data}
    
    if info:
        if info[0].upper() == 'VIEW':
            node_data['type'] = 'VIEW'
            lineage_data.append(node_data)
            view_sql = info[1]
            # 正規表現を強化: FROMやJOINの後の単語を抽出
            # カンマ区切りやサブクエリ、エイリアスに対応
            found_tables = re.findall(r"(?:FROM|JOIN)\s+([a-zA-Z0-9_]+)", view_sql, re.IGNORECASE)
            # 重複排除と予約語の除外
            for p in sorted(set(found_tables), key=found_tables.index):
                if p.upper() not in ["SELECT", "AS", "WHERE", "GROUP", "ORDER", "LEFT", "RIGHT", "INNER", "OUTER"]:
                    lineage_data.extend(get_lineage(cur, p, depth + 1))
        else:
            lineage_data.append(node_data)
    return lineage_data

def index(request):
    viz_result, sql_result, status_message = None, None, None
    raw_input = request.POST.get("error_log", "") if request.method == "POST" else ""
    db_path = os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))), 'kakeibo.db')

    if request.method == "POST" and raw_input:
        clean_input = raw_input.strip()
        upper_input = clean_input.upper()

        if any(upper_input.startswith(cmd) for cmd in ["CREATE", "DROP", "INSERT", "DELETE", "UPDATE"]):
            try:
                with sqlite3.connect(db_path) as conn:
                    conn.executescript(raw_input)
                status_message = "DISK_WRITE_SUCCESS"
            except Exception as e:
                status_message = f"WRITE_ERROR: {str(e)}"
        
        elif "SELECT" in upper_input:
            try:
                with sqlite3.connect(db_path) as conn:
                    cur = conn.cursor()
                    target_tables = re.findall(r"FROM\s+([a-zA-Z0-9_]+)", clean_input, re.IGNORECASE)
                    lineage_tree = []
                    for name in set(target_tables):
                        lineage_tree.extend(get_lineage(cur, name))
                    sql_result = {'lineage': lineage_tree}
            except Exception as e:
                sql_result = {'error': str(e)}
        else:
            # トレースバック解析(変更なし)
            trace_steps = re.findall(r'File\s+"(.+?)",\s*line\s*(\d+),\s*in\s*(.+)', raw_input)
            if trace_steps:
                steps = []
                for i, (path, line, func) in enumerate(trace_steps):
                    line_num = int(line)
                    code_all = []
                    try:
                        if os.path.exists(path):
                            with open(path, 'r', encoding='utf-8') as f:
                                for idx, text in enumerate(f.readlines()):
                                    curr = idx + 1
                                    code_all.append({'num': curr, 'text': text.rstrip(), 'is_target': (curr == line_num)})
                        else: 
                            code_all = [{'num': line_num, 'text': "/* FILE_NOT_FOUND */", 'is_target': True}]
                    except: 
                        code_all = [{'num': line_num, 'text': "/* LOAD_ERROR */", 'is_target': True}]
                    steps.append({'file': os.path.basename(path), 'line': line, 'func': func, 'indent': i * 20, 'code_all': code_all, 'target_line': line_num})
                viz_result = {'name': clean_input.split('\n')[-1], 'steps': steps}

    return render(request, 'analyzer/index.html', {
        'viz_result': viz_result, 'sql_result': sql_result,
        'status_message': status_message, 'raw_error': raw_input
    })

tools>test_broken.py

エラーコードを作り出すためのコード

import os

def calculate_ratio(total, count):
    return total / count

def process_transaction(data):
    user_total = data.get('total', 100)
    user_count = data.get('count', 0)
    return calculate_ratio(user_total, user_count)

def main():
    transaction_data = {'id': 101, 'total': 5000}
    print(f"DEBUG: Processing {transaction_data['id']}...")
    result = process_transaction(transaction_data)
    print(f"Result: {result}")

if __name__ == "__main__":
    main()

view_sql>admin.py

from django.contrib import admin

# Register your models here.

view_sql>asgi.py

"""
ASGI config for viz_site project.

It exposes the ASGI callable as a module-level variable named ``application``.

For more information on this file, see
https://docs.djangoproject.com/en/6.0/howto/deployment/asgi/
"""

import os

from django.core.asgi import get_asgi_application

os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'view_sql.settings')

application = get_asgi_application()

view_sql>models.py

from django.db import models

# Create your models here.

view_sql>settings.py

"""
Django settings for view_sql project.

Generated by 'django-admin startproject' using Django 6.0.2.

For more information on this file, see
https://docs.djangoproject.com/en/6.0/topics/settings/

For the full list of settings and their values, see

Settings | Django documentation
The web framework for perfectionists with deadlines.
""" from pathlib import Path # Build paths inside the project like this: BASE_DIR / 'subdir'. BASE_DIR = Path(__file__).resolve().parent.parent # Quick-start development settings - unsuitable for production # See https://docs.djangoproject.com/en/6.0/howto/deployment/checklist/ # SECURITY WARNING: keep the secret key used in production secret! SECRET_KEY = '秘密' # SECURITY WARNING: don't run with debug turned on in production! DEBUG = True ALLOWED_HOSTS = [] # Application definition INSTALLED_APPS = [ 'django.contrib.admin', 'django.contrib.auth', 'django.contrib.contenttypes', 'django.contrib.sessions', 'django.contrib.messages', 'django.contrib.staticfiles', 'error_analyze', ] MIDDLEWARE = [ 'django.middleware.security.SecurityMiddleware', 'django.contrib.sessions.middleware.SessionMiddleware', 'django.middleware.common.CommonMiddleware', 'django.middleware.csrf.CsrfViewMiddleware', 'django.contrib.auth.middleware.AuthenticationMiddleware', 'django.contrib.messages.middleware.MessageMiddleware', 'django.middleware.clickjacking.XFrameOptionsMiddleware', ] ROOT_URLCONF = 'view_sql.urls' TEMPLATES = [ { 'BACKEND': 'django.template.backends.django.DjangoTemplates', 'DIRS': [BASE_DIR / 'templates'], 'APP_DIRS': True, 'OPTIONS': { 'context_processors': [ 'django.template.context_processors.request', 'django.contrib.auth.context_processors.auth', 'django.contrib.messages.context_processors.messages', ], }, }, ] WSGI_APPLICATION = 'view_sql.wsgi.application' # Database # https://docs.djangoproject.com/en/6.0/ref/settings/#databases DATABASES = { 'default': { 'ENGINE': 'django.db.backends.sqlite3', 'NAME': BASE_DIR / 'db.sqlite3', } } # Password validation # https://docs.djangoproject.com/en/6.0/ref/settings/#auth-password-validators AUTH_PASSWORD_VALIDATORS = [ { 'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator', }, { 'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator', }, { 'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator', }, { 'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator', }, ] # Internationalization # https://docs.djangoproject.com/en/6.0/topics/i18n/ LANGUAGE_CODE = 'en-us' TIME_ZONE = 'UTC' USE_I18N = True USE_TZ = True # Static files (CSS, JavaScript, Images) # https://docs.djangoproject.com/en/6.0/howto/static-files/ STATIC_URL = 'static/'

view_sql>test.py

from django.test import TestCase

# Create your tests here.

view_sql>urls.py

from django.contrib import admin
from django.urls import include, path

urlpatterns = [
    path('admin/', admin.site.urls),
    path('', include('error_analyze.urls')),
]

view_sql>view.py

from django.shortcuts import render

# Create your views here.

view_sql>wsgi.py

"""
WSGI config for view_sql project.

It exposes the WSGI callable as a module-level variable named ``application``.

For more information on this file, see
https://docs.djangoproject.com/en/6.0/howto/deployment/wsgi/
"""

import os

from django.core.wsgi import get_wsgi_application

os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'view_sql.settings')

application = get_wsgi_application()

manage.py

#!/usr/bin/env python
"""Django's command-line utility for administrative tasks."""
import os
import sys


def main():
    """Run administrative tasks."""
    os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'config.settings')
    try:
        from django.core.management import execute_from_command_line
    except ImportError as exc:
        raise ImportError(
            "Couldn't import Django. Are you sure it's installed and "
            "available on your PYTHONPATH environment variable? Did you "
            "forget to activate a virtual environment?"
        ) from exc
    execute_from_command_line(sys.argv)


if __name__ == '__main__':
    main()

venvや__init__.pyなどは、何もいじってないので割愛します。

(いじってないけど載せたコードもあるので矛盾してますが笑)

タイトルとURLをコピーしました