import os
from contextlib import contextmanager # contextmanager ์ํฌํธ
from typing import Generator
from dotenv import load_dotenv
from sqlalchemy.ext.asyncio import create_async_engine, async_sessionmaker, AsyncSession
from sqlalchemy.orm import sessionmaker, declarative_base, Session # Session ํ์
ํํธ ์ถ๊ฐ
load_dotenv()
# ํ๊ฒฝ ๋ณ์ ๋ก๋ฉ ์ ๋ณ์๋ช
์ถ์ฝ ๋์ ๋ช
์์ ์ธ ์ด๋ฆ ์ฌ์ฉ ๊ถ์ฅ
MYSQL_USER = os.environ.get("MYSQL_USER")
MYSQL_PASSWORD = os.environ.get("MYSQL_PASSWORD")
MYSQL_HOST = os.environ.get("MYSQL_ALEMBIC_HOST") # alembic ์ ์ฉ ํธ์คํธ๊ฐ ์๋๋ผ๋ฉด MYSQL_HOST๋ก ํต์ผ ๊ฐ๋ฅ
MYSQL_PORT = os.environ.get("MYSQL_ALEMBIC_PORT")
MYSQL_DB = os.environ.get("MYSQL_DATABASE")
# MySQL ์ฐ๊ฒฐ ์ค์ (์ฌ์ฉ์, ๋น๋ฐ๋ฒํธ, ํธ์คํธ, DB์ด๋ฆ ์์ )
# ์: mysql+aiomysql://user:password@localhost:3306/dbname
SQLALCHEMY_DATABASE_URL = f"mysql+aiomysql://{MYSQL_USER}:{MYSQL_PASSWORD}@{MYSQL_HOST}:{MYSQL_PORT}/{MYSQL_DB}"
# SQLite ์ฌ์ฉ ์: SQLALCHEMY_DATABASE_URL = "sqlite:///./myapi.db"
# create_engine ์ค์ : MySQL ์ฐ๊ฒฐ ์ pool_pre_ping์ ์ข์ ๊ด๋ก์
๋๋ค.
engine = create_async_engine(
SQLALCHEMY_DATABASE_URL,
echo=True,
pool_pre_ping=True, # ์ฐ๊ฒฐ ์ ํจ์ฑ ์ฒดํฌ (MySQL ์ฐ๊ฒฐ ๋๊น ๋ฐฉ์ง)
pool_recycle=3600, # ์ฐ๊ฒฐ ์ฌ์ฌ์ฉ ์๊ฐ ์ค์
# SQLite ์ฌ์ฉ ์: connect_args={"check_same_thread": False}
)
# ์ธ์
์์ฑ์ ์ํ ํฉํ ๋ฆฌ
async_session = async_sessionmaker(
bind=engine,
class_=AsyncSession,
expire_on_commit=False
)
# Base ๋ชจ๋ธ ํด๋์ค ์์ฑ
Base = declarative_base()
# ์ฐธ๊ณ : Alembic ์ฌ์ฉ ์ ํ์ํ ์ฃผ์๋ค์ ์ ๊ฑฐํ๊ฑฐ๋ ๋ณ๋์ alembic config ํ์ผ๋ก ์ฎ๊ธฐ๋ ๊ฒ์ด ๊น๋ํฉ๋๋ค.
๐ ️ Code Note & Usage
* Human + AI Collaboration: ์ฌ๊ธฐ ๋ฌธ์์ ์ฝ๋๋ ์ ๊ฐ ์ํฐ๋ฆฌ๋ก ๊ตฌ์กฐ๋ฅผ ์ก๊ณ , Google AI๋ฅผ ๋ฌ๋ฌ ๋ณถ์๊ฐ๋ฉฐ(?) ์์ฑํ ์ํฐ๋ฆฌ ์ฝ๋ ๋ญ์น๋ค ์
๋๋ค.
* Free to Use: ๊ฐ์ธ์ ์ธ ์ทจ๋ฏธ๋ก ์ ๋ฆฌํ ๋ฌธ์์ ์ํฐ๋ฆฌ ์ฝ๋ ๋ญ์น ์ง๋ง, ๊ณต๋ถ ํ์ค๋ ์กฐ๊ธ์ด๋๋ง ๋์์ด ๋์ ๋ค๋ฉด ์์ ๋กญ๊ฒ ๊ฐ์ ธ๊ฐ์ ์ฌ์ฉํ์ค ์ ์๋๋ก ๊ณต๊ฐํฉ๋๋ค!
* ⚠️ Warning: ์ฌ๊ธฐ ์ํฐ๋ฆฌ ์ฝ๋ ๋ญ์น๋ฅผ, ๊ฐ์ ธ๊ฐ์ ๊ธฐํ์ฝ ๊ผญ ์ฌ์ฉ ํ์ ๋ค๋ฉด ๊ผญ ๋ณธ์ธ์ ํ๊ฒฝ์ ๋ง๊ฒ ๋ฒ๊ทธ๋ ์๋ฌ๋ฅผ ๊ผญ ํ ๋ฒ ๋ ํ์ธ(์ฃผ์)ํ๊ณ ์ฌ์ฉํด ์ฃผ์ธ์.
※ ์ฝ๋๋ฅผ ์ฌ์ฉํ ๋๋ ํญ์ ์ฃผ์๊ฐ ํ์ํฉ๋๋ค. ※
2026๋ 7์ 21์ผ ํ์์ผ
database.py
Cargo.toml
# curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
# rustup update
# cargo update
[package]
name = "sheets"
version = "0.1.0"
edition = "2024"
default-run = "sheets"
[[bin]]
name = "sheets"
path = "src/main.rs"
[profile.dev.package.backtrace]
opt-level = 3
[dependencies]
tokio = { version = "1", features = ["full"] }
futures = "0.3"
itertools = "0.12"
serde = "1"
serde_json = "1"
rustls = { version = "0.23", default-features = false, features = ["aws-lc-rs", "logging", "tls12"] }
google-sheets4 = "*"
yup-oauth2 = "12"
Inflector = "0.11.4"
src/main.rs
use std::path::Path;
use tokio::fs::File;
use tokio::io::AsyncWriteExt; // ๋น๋๊ธฐ ์ฐ๊ธฐ๋ฅผ ์ํ ํธ๋ ์ดํธ
mod gspread;
use crate::gspread::{SpreadsheetExporter, create_hub};
/// ์์ฑ๋ SQL ๋ฌธ์ ๋น๋๊ธฐ์ ์ผ๋ก .sql ํ์ผ์ ์ ์ฅ
async fn save_sql_to_file_async(sql: &str, file_name: &str) -> tokio::io::Result<()> {
// 1. ํ์ผ ๊ฒฝ๋ก ์ค์
let path = Path::new(file_name);
// 2. ๋น๋๊ธฐ ํ์ผ ์์ฑ (Tokio fs ์ฌ์ฉ)
let mut file = File::create(path).await?;
// 3. ๋น๋๊ธฐ ๋ฐ์ดํฐ ์ฐ๊ธฐ (์ ์ฒด ๋ฐ์ดํฐ๋ฅผ ์์ ํ๊ฒ ๊ธฐ๋ก)
file.write_all(sql.as_bytes()).await?;
// 4. ๋ฒํผ ๋น์ฐ๊ธฐ ๋ฐ ๋๊ธฐํ (๊ถ์ฅ ๊ด๋ก)
file.flush().await?;
println!("✅ [Async] SQL ํ์ผ ์ ์ฅ ์๋ฃ: {:?}", path.display());
Ok(())
}
#[tokio::main]
async fn main() -> tokio::io::Result<()> {
rustls::crypto::aws_lc_rs::default_provider()
.install_default()
.expect("Failed to install crypto provider");
let hub = create_hub("./sheets-xxxxxx-xxxx.json").await;
let exporter =
SpreadsheetExporter::new(hub, "xxxxxx-xx-xxxxxx".into());
let schema = exporter.export_schema().await;
save_sql_to_file_async(&schema, "output_schema.sql").await?;
let data = exporter.export_data().await;
save_sql_to_file_async(&data, "output_data.sql").await?;
println!("{}\n{}", schema, data);
Ok(())
}
src/gspread.rs
//extern crate hyper;
//extern crate hyper_rustls;
//extern crate hyper_util;
extern crate google_sheets4 as sheets4;
use sheets4::{Sheets, hyper_rustls, hyper_util, yup_oauth2};
use serde_json::Value;
use std::collections::HashMap;
use futures::future::join_all;
use hyper_util::client::legacy::connect::HttpConnector;
use hyper_rustls::HttpsConnector;
use inflector::Inflector; // ํจํค์ง: id = "0.11" (๋ค์ด๋ฐ ๋ณํ์ฉ)
pub type SheetsHub = Sheets>;
//#[derive(Default, Debug, Clone, PartialEq)]
pub struct SpreadsheetExporter {
hub: SheetsHub,
spreadsheet_id: String,
}
impl SpreadsheetExporter {
pub fn new(hub: SheetsHub, spreadsheet_id: String) -> Self {
Self { hub, spreadsheet_id }
}
// Value ํ์
์ String์ผ๋ก ์์ ํ๊ฒ ๋ณํํ๋ ํฌํผ ํจ์
fn val_to_string(val: &Value) -> String {
match val {
Value::String(s) => s.clone(),
Value::Number(n) => n.to_string(),
Value::Bool(b) => b.to_string(),
_ => val.to_string().replace('"', ""),
}
}
// ์ํธ์์ ๊ฐ์ ธ์จ Vec>๋ฅผ ์ ์ ๋ HashMap ๋ฆฌ์คํธ๋ก ๋ณํ
fn clean_rows(&self, values: &Vec>) -> Vec> {
if values.is_empty() { return vec![]; }
let headers: Vec = values[0].iter().map(Self::val_to_string).collect();
let mut result = Vec::new();
for row in values.iter().skip(1) {
if row.is_empty() || Self::val_to_string(&row[0]).trim().is_empty() { break; }
let mut row_map = HashMap::new();
for (i, header) in headers.iter().enumerate() {
let val = row.get(i).map(Self::val_to_string).unwrap_or_default();
row_map.insert(header.clone(), val);
}
result.push(row_map);
}
result
}
fn split_column(&self, raw_text: &str) -> String {
raw_text.split(',').map(|s| format!("`{}`", s.trim())).collect::>().join(", ")
}
fn format_sql_value(&self, val: &str) -> String {
let t = val.trim();
if t.is_empty() || t.to_uppercase() == "NULL" { return "NULL".to_string(); }
if t.parse::().is_ok() { t.to_string() } else { format!("'{}'", t.replace("'", "''")) }
}
async fn export_schema_unit(&self, row: HashMap) -> String {
let schema = row.get("SCHEMA").cloned().unwrap_or_default();
let table_name = row.get("TABLE_NAME").cloned().unwrap_or_default();
let engine = row.get("ENGINE").cloned().unwrap_or_default();
let auto_increment = row.get("AUTO_INCREMENT").cloned().unwrap_or_default();
let charset = row.get("CHARSET").cloned().unwrap_or_default();
let collate = row.get("COLLATE").cloned().unwrap_or_default();
let ss_id = match row.get("SPREADSHEET_ID") {
Some(id) => id,
None => return format!("-- Error: Missing ID for {}", table_name),
};
let result = self.hub.spreadsheets().values_batch_get(ss_id)
.add_ranges("COLUMN!A:Z")
.add_ranges("INDEX!A:Z")
.add_ranges("FOREIGN_KEY!A:Z")
.doit().await;
let (_, response) = match result {
Ok(res) => res,
Err(e) => return format!("-- API Error in {}: {}\n", table_name, e),
};
let v_ranges = response.value_ranges.unwrap_or_default();
let mut all_defs = Vec::new();
// 1. COLUMN ์ฒ๋ฆฌ
if let Some(vals) = v_ranges.get(0).and_then(|r| r.values.as_ref()) {
for r in self.clean_rows(vals) {
let f = r.get("FIELD").cloned().unwrap_or_default();
let t = r.get("TYPE").cloned().unwrap_or_default();
let s = r.get("SIGNED").cloned().unwrap_or_default();
let l = r.get("LENGTH").filter(|s| !s.is_empty()).map_or("".to_string(), |v| format!("({})", v));
let n = if ["NOT", "NO", "FALSE", "0"].contains(&r.get("NULL").unwrap_or(&"".into()).to_uppercase().as_str()) { "NOT NULL" } else { "" };
let d = r.get("DEFAULT").filter(|s| !s.is_empty()).map_or("".to_string(), |v| format!("DEFAULT '{}'", v.replace("'", "''")));
let c = r.get("COMMENT").filter(|s| !s.is_empty()).map_or("".to_string(), |v| format!("COMMENT '{}'", v.replace("'", "''")));
all_defs.push(format!(" `{}` {} {}{} {} {} {} {}", f.to_snake_case(), t, s, l, n, d, r.get("EXTRA").unwrap_or(&"".into()), c).trim().to_string());
}
}
// 2. INDEX ์ฒ๋ฆฌ
if let Some(vals) = v_ranges.get(1).and_then(|r| r.values.as_ref()) {
for r in self.clean_rows(vals) {
let itype = r.get("TYPE").unwrap_or(&"".into()).to_uppercase();
let cols = self.split_column(r.get("COLUMN").unwrap_or(&"".into()));
if itype == "PRIMARY" { all_defs.push(format!("PRIMARY KEY ({})", cols.to_snake_case())); }
else {
let prefix = if itype == "KEY" { "".into() } else { format!("{} ", itype) };
all_defs.push(format!("{}KEY `{}` ({})", prefix, r.get("NAME").unwrap_or(&"".into()).to_snake_case(), cols));
}
}
}
// 3. FOREIGN KEY ์ฒ๋ฆฌ
if let Some(vals) = v_ranges.get(2).and_then(|r| r.values.as_ref()) {
for r in self.clean_rows(vals) {
let mut fk = format!("CONSTRAINT `{}` FOREIGN KEY (`{}`) REFERENCES `{}` (`{}`)",
r.get("NAME").unwrap_or(&"".to_string()).to_snake_case(),
r.get("COLUMN").unwrap_or(&"".to_string()).to_snake_case(),
r.get("REF_TABLE").unwrap_or(&"".to_string()).to_plural().to_snake_case(),
r.get("REF_COLUMN").unwrap_or(&"".to_string()).to_snake_case());
if let Some(u) = r.get("ON_UPDATE").filter(|s| !s.is_empty()) { fk.push_str(&format!(" ON UPDATE {}", u)); }
if let Some(d) = r.get("ON_DELETE").filter(|s| !s.is_empty()) { fk.push_str(&format!(" ON DELETE {}", d)); }
all_defs.push(fk);
}
}
let schema_name = schema.to_snake_case();
let table_name = table_name.to_plural().to_snake_case();
format!("CREATE DATABASE IF NOT EXISTS {};\nUSE {};\nDROP TABLE IF EXISTS `{}`;\nCREATE TABLE `{}` (\n{}\n) ENGINE={} AUTO_INCREMENT={} DEFAULT CHARSET={} COLLATE={};\n",
schema_name, schema_name, table_name, table_name, all_defs.join(",\n"), engine, auto_increment, charset, collate)
}
async fn export_data_unit(&self, row: HashMap) -> String {
let table_name = row.get("TABLE_NAME").cloned().unwrap_or_default();
let ss_id = row.get("SPREADSHEET_ID").expect("No ID");
let result = self.hub.spreadsheets().values_get(ss_id, "DATA!A:Z").doit().await;
let (_, response) = match result {
Ok(res) => res,
Err(_) => return "".into(),
};
let values = response.values.unwrap_or_default();
if values.len() <= 1 { return "".into(); }
let headers: Vec = values[0].iter().map(Self::val_to_string).collect();
let mut insert_rows = Vec::new();
for r in self.clean_rows(&values) {
let row_vals: Vec = headers.iter()
.map(|h| self.format_sql_value(r.get(h).unwrap_or(&"".into())))
.collect();
insert_rows.push(format!("({})", row_vals.join(", ")));
}
let h_sql = headers.iter().map(|h| format!("`{}`", h.to_snake_case())).collect::>().join(", ");
format!("INSERT INTO `{}` ({})\nVALUES\n{};\n", table_name.to_plural(), h_sql, insert_rows.join(",\n"))
}
async fn execute_parallel(&self, unit_func: F) -> String
where
F: Fn(HashMap) -> Fut,
Fut: std::future::Future
main.py
from database import async_session
from models.answer import Answer
from models import Question
from sqlalchemy import select, insert
from sqlalchemy.exc import SQLAlchemyError
import asyncio
import SpreadsheetExporter
async def create_user_with_transaction(username: str):
# 1. ์ธ์
์์ฑ
async with async_session() as session:
# 2. ํธ๋์ญ์
์์ (๋น๋๊ธฐ ์ปจํ
์คํธ ๋งค๋์ )
async with session.begin():
try:
# ๋ฐ์ดํฐ ์์ฑ ์์
new_user = Answer(username=username)
session.add(new_user)
# ์ถ๊ฐ ์์
(์: ๋ก๊ทธ ๊ธฐ๋ก ๋ฑ)
# ์ด ๋ธ๋ก ์์์ ์๋ฌ๊ฐ ๋ฐ์ํ๋ฉด ์ ์ฒด ์์
์ด ๋กค๋ฐฑ๋ฉ๋๋ค.
except Exception as e:
# session.begin()์ ์ฌ์ฉํ๋ฉด ์๋ฌ ๋ฐ์ ์ ์๋ ๋กค๋ฐฑ๋์ง๋ง,
# ์ถ๊ฐ์ ์ธ ๋ก๊น
์ด ํ์ํ๋ฉด ์ฌ๊ธฐ์ ์ฒ๋ฆฌํฉ๋๋ค.
print(f"์๋ฌ ๋ฐ์: {e}")
raise
# ๋ธ๋ก์ ๋๊ฐ๋ฉด ์๋์ผ๋ก commit ์๋ฃ
print("ํธ๋์ญ์
์ด ์ฑ๊ณต์ ์ผ๋ก ์ปค๋ฐ๋์์ต๋๋ค.")
async def manual_transaction_example(username: str):
session = async_session()
try:
# ์์
์ํ
new_user = Answer(username=username)
session.add(new_user)
# ๋ช
์์ ์ปค๋ฐ
await session.commit()
print("์ปค๋ฐ ์ฑ๊ณต")
except SQLAlchemyError as e:
# ์๋ฌ ๋ฐ์ ์ ๋ช
์์ ๋กค๋ฐฑ
await session.rollback()
print(f"๋กค๋ฐฑ ์คํ: {e}")
finally:
# ์ธ์
๋ซ๊ธฐ
await session.close()
async def create_user_and_post(username: str, title: str):
async with async_session() as session:
async with session.begin():
# 1. ์ฌ์ฉ์ ์์ฑ
user = Answer(username=username)
session.add(user)
await session.flush() # DB์ ์์ ๋ฐ์ํ์ฌ user.id๋ฅผ ํ๋ณด
# 2. ํด๋น ์ฌ์ฉ์์ ํฌ์คํธ ์์ฑ
post = Question(title=title, owner_id=user.id)
session.add(post)
# ์ฌ๊ธฐ์ ์๋ฌ ๋ฐ์ ์ User ์์ฑ๋ ์ทจ์๋จ
async def fast_bulk_insert(user_data_list: list):
async with async_session() as session:
async with session.begin():
# Core์ insert ๊ตฌ๋ฌธ ์ฌ์ฉ
stmt = insert(Answer).values(user_data_list)
await session.execute(stmt)
import gspread_asyncio
# from google-auth package
from google.oauth2.service_account import Credentials
# First, set up a callback function that fetches our credentials off the disk.
# gspread_asyncio needs this to re-authenticate when credentials expire.
def get_creds():
# To obtain a service account JSON file, follow these steps:
# https://gspread.readthedocs.io/en/latest/oauth2.html#for-bots-using-service-account
#path = os.environ.get('GOOGLE_APPLICATION_CREDENTIALS')
path = "./sheets-xxxxxx-xxxxxxxxxxxx.json"
creds = Credentials.from_service_account_file(path)
scoped = creds.with_scopes([
"https://spreadsheets.google.com/feeds",
"https://www.googleapis.com/auth/spreadsheets",
"https://www.googleapis.com/auth/drive",
])
return scoped
if __name__ == "__main__":
# Execute when the module is not initialized from an import statement.
#main()
# --- ์ฌ์ฉ ์์ ---
async def main():
# Create an AsyncioGspreadClientManager object which
# will give us access to the Spreadsheet API.
agcm = gspread_asyncio.AsyncioGspreadClientManager(get_creds)
# Always authorize first.
# If you have a long-running program call authorize() repeatedly.
agc = await agcm.authorize()
spreadsheet_id = "xxxxxxxxx-xxx-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
exporter = SpreadsheetExporter.SpreadsheetExporter(agc, spreadsheet_id)
print("--- SQL ์คํค๋ง ์ถ์ถ ์์ (๋ณ๋ ฌ ์ฒ๋ฆฌ) ---")
# ๋ด๋ถ์ ์ผ๋ก _execute_parallel์ ํธ์ถํ์ฌ SCHEMA ์ํธ์ TRUE ํญ๋ชฉ์ ๋ณ๋ ฌ ์ฒ๋ฆฌํฉ๋๋ค.
schema_sql = await exporter.export_schema()
print(f"{schema_sql}")
with open("output_schema.sql", "w", encoding="utf-8") as f:
f.write(schema_sql)
print("✅ ์คํค๋ง ์ถ์ถ ์๋ฃ: output_schema.sql")
print("\n--- ๋ฐ์ดํฐ INSERT ๊ตฌ๋ฌธ ์ถ์ถ ์์ (๋ณ๋ ฌ ์ฒ๋ฆฌ) ---")
# ๋ด๋ถ์ ์ผ๋ก _execute_parallel์ ํธ์ถํ์ฌ DATA ์ํธ์ TRUE ํญ๋ชฉ์ ๋ณ๋ ฌ ์ฒ๋ฆฌํฉ๋๋ค.
data_sql = await exporter.export_data()
print(f"{data_sql}")
with open("output_data.sql", "w", encoding="utf-8") as f:
f.write(data_sql)
print("✅ ๋ฐ์ดํฐ ์ถ์ถ ์๋ฃ: output_data.sql")
# Turn on debugging if you're new to asyncio!
asyncio.run(main(), debug=True)
SpreadsheetExporter.py
import pandas as pd
import numpy as np
import asyncio
from gspread.exceptions import SpreadsheetNotFound, APIError, GSpreadException
class SpreadsheetExporter:
def __init__(self, agc, spreadsheet_id):
"""
:param agc: gspread_asyncio์ AsyncioGspreadClient
:param spreadsheet_id: ๋ฉ์ธ SCHEMA ์ํธ ID
"""
self.agc = agc
self.spreadsheet_id = spreadsheet_id
def _split_column(self, raw_text):
# 1. ์๋ณธ ๋ฌธ์์ด ์ ์
#raw_text = "reactable_type, reactable_id, id"
# 2. ์ผํ(,)๋ฅผ ๊ธฐ์ค์ผ๋ก ๋ถ๋ฆฌํ์ฌ ๋ฆฌ์คํธ์ ์ ์ฅ (๊ณต๋ฐฑ ์ ๊ฑฐ ํฌํจ)
column_list = [item.strip() for item in raw_text.split(',')]
# 3. ๊ฐ ์์๋ฅผ ๋ฐฑํฑ(`)์ผ๋ก ๊ฐ์ผ ํ ๋ค์ ํฉ์น๊ธฐ
result = ", ".join([f"`{item}`" for item in column_list])
# 4. ๊ฒฐ๊ณผ ์ถ๋ ฅ
#print(result)
return result
async def _export_schema_unit(self, row):
"""๊ฐ๋ณ ํ
์ด๋ธ์ ์คํค๋ง๋ฅผ ์์ฑํ๋ ๋น๋๊ธฐ ๋จ์ ์์
"""
try:
# ๊ตฌ์กฐ ๋ถํด ํ ๋น (์ํธ ์ปฌ๋ผ๋ช
์ ๋ง์ถฐ ์กฐ์ ํ์)
table_name = row['TABLE_NAME']
spreadsheet_id = row['SPREADSHEET_ID']
engine = row.get('ENGINE', 'InnoDB')
auto_inc = row.get('AUTO_INCREMENT', '1')
charset = row.get('CHARSET', 'utf8mb4')
collate = row.get('COLLATE', 'utf8mb4_unicode_ci')
ss = await self.agc.open_by_key(spreadsheet_id)
# --- COLUMN, INDEX, FK ์ํธ ๋ฐ์ดํฐ๋ฅผ ๋์์ ๊ฐ์ ธ์ค๊ธฐ (๋ณ๋ ฌ) ---
ws_names = ["COLUMN", "INDEX", "FOREIGN_KEY"]
worksheets = await asyncio.gather(*[ss.worksheet(name) for name in ws_names])
data_lists = await asyncio.gather(*[ws.get_all_values() for ws in worksheets])
col_data, idx_data, fk_data = data_lists
all_defs = []
# 1. COLUMN ์ฒ๋ฆฌ
if len(col_data) > 1:
df = pd.DataFrame(col_data[1:], columns=col_data[0])
#df = df[df.iloc[:, 0] != ""].copy()
"""
# 1. ์ฒซ ๋ฒ์งธ ์ด์ด ๊ณต๋ฐฑ์ธ์ง ํ์ธ (True/False)
is_empty = (df.iloc[:, 0] == "")
# 2. cummax()๋ฅผ ์ฌ์ฉํ์ฌ ํ ๋ฒ True๊ฐ ๋์ค๋ฉด ๊ทธ ์ดํ๋ ๊ณ์ True๊ฐ ๋๋๋ก ํจ
# 3. ๊ทธ ๋ฐ๋(~)์ธ ํ๋ค๋ง ๋จ๊น
df = df[~is_empty.cummax()].copy()
"""
# 2. ์ฒซ ๋ฒ์งธ ์ด์ ๊ธ์๊ฐ '์๋์ง' ํ์ธ (๊ธ์ ์์ผ๋ฉด True, ๋น์์ผ๋ฉด False)
is_not_empty = (df.iloc[:, 0] != "")
# 3. cumprod()๋ฅผ ์ฌ์ฉํ์ฌ ํ ๋ฒ False(๊ณต๋ฐฑ)๋ฅผ ๋ง๋๋ ์๊ฐ ๊ทธ ๋ค๋ ๋ฌด์กฐ๊ฑด False๋ก ๊ณ ์
# (์ค๊ฐ์ ์ฒซ ์นธ์ด ๋น๋ฉด ๋ค์ ํ์ ๋ฐ์ดํฐ๊ฐ ์์ด๋ ๋ฌด์ํ๋ ํต์ฌ ๋ก์ง)
df = df[is_not_empty.cumprod().astype(bool)].copy()
#for row in df.itertuples():
#print(f"์ด๋ฆ: {row.์ด๋ฆ}, ์ ์: {row.์ ์}")
type_sql = df.apply(lambda r: f"{r['TYPE']}({r['LENGTH']})" if r.get('LENGTH') else r['TYPE'], axis=1)
null_sql = df['NULL'].apply(lambda x: "NOT NULL" if str(x).upper() in ["NOT", "NO", "FALSE", "0"] else "")
default_sql = df['DEFAULT'].apply(lambda x: f"DEFAULT '{str(x).replace("'", "''")}'" if x else "")
comment_sql = df['COMMENT'].apply(lambda x: f"COMMENT '{str(x)[:1024].replace("'", "''")}'" if x else "")
lines = " `" + df['FIELD'] + "` " + type_sql + " " + null_sql + " " + default_sql + " " + df['EXTRA'] + " " + comment_sql
all_defs.extend(lines.str.strip().tolist())
# 2. INDEX ์ฒ๋ฆฌ
if len(idx_data) > 1:
df = pd.DataFrame(idx_data[1:], columns=idx_data[0])
#df = df[df.iloc[:, 0] != ""].copy()
# 1. ์ฒซ ๋ฒ์งธ ์ด์ด ๊ณต๋ฐฑ์ธ์ง ํ์ธ (True/False)
is_empty = (df.iloc[:, 0] == "")
# 2. cummax()๋ฅผ ์ฌ์ฉํ์ฌ ํ ๋ฒ True๊ฐ ๋์ค๋ฉด ๊ทธ ์ดํ๋ ๊ณ์ True๊ฐ ๋๋๋ก ํจ
# 3. ๊ทธ ๋ฐ๋(~)์ธ ํ๋ค๋ง ๋จ๊น
df = df[~is_empty.cummax()].copy()
idx_lines = df.apply(lambda r: f"PRIMARY KEY (`{r['COLUMN']}`)" if r['TYPE'].upper() == "PRIMARY"
#else f"{r['TYPE']+' ' if r['TYPE'].upper()!='KEY' else ''}KEY `{r['NAME']}` (`{r['COLUMN']}`)", axis=1)
else f"{r['TYPE']+' ' if r['TYPE'].upper()!='KEY' else ''}KEY `{r['NAME']}` ({self._split_column(r['COLUMN'])})", axis=1)
all_defs.extend(idx_lines.tolist())
# 3. FOREIGN KEY ์ฒ๋ฆฌ
if len(fk_data) > 1:
df = pd.DataFrame(fk_data[1:], columns=fk_data[0])
#df = df[df.iloc[:, 0] != ""].copy()
# 1. ์ฒซ ๋ฒ์งธ ์ด์ด ๊ณต๋ฐฑ์ธ์ง ํ์ธ (True/False)
is_empty = (df.iloc[:, 0] == "")
# 2. cummax()๋ฅผ ์ฌ์ฉํ์ฌ ํ ๋ฒ True๊ฐ ๋์ค๋ฉด ๊ทธ ์ดํ๋ ๊ณ์ True๊ฐ ๋๋๋ก ํจ
# 3. ๊ทธ ๋ฐ๋(~)์ธ ํ๋ค๋ง ๋จ๊น
df = df[~is_empty.cummax()].copy()
fk_lines = "CONSTRAINT `" + df['NAME'] + "` FOREIGN KEY (`" + df['COLUMN'] + "`) REFERENCES `" + df['REF_TABLE'] + "` (`" + df['REF_COLUMN'] + "`)"
fk_lines += df['ON_UPDATE'].apply(lambda x: f" ON UPDATE {x}" if x else "")
fk_lines += df['ON_DELETE'].apply(lambda x: f" ON DELETE {x}" if x else "")
all_defs.extend(fk_lines.tolist())
return (f"DROP TABLE IF EXISTS `{table_name}`;\n"
f"CREATE TABLE `{table_name}` (\n"
f"{',\n'.join(all_defs)}\n"
f") ENGINE={engine} AUTO_INCREMENT={auto_inc} DEFAULT CHARSET={charset} COLLATE={collate};\n")
except Exception as e:
return f"-- Error in {row.get('TABLE_NAME')}: {e}\n"
async def _export_data_unit(self, row):
"""๊ฐ๋ณ ํ
์ด๋ธ์ ๋ฐ์ดํฐ๋ฅผ INSERT ๋ฌธ์ผ๋ก ์์ฑํ๋ ๋น๋๊ธฐ ๋จ์ ์์
"""
try:
table_name = row['TABLE_NAME']
ss = await self.agc.open_by_key(row['SPREADSHEET_ID'])
ws = await ss.worksheet("DATA")
data = await ws.get_all_values()
if len(data) <= 1: return ""
df = pd.DataFrame(data[1:], columns=data[0])
#df = df[df.iloc[:, 0] != ""].copy()
# 1. ์ฒซ ๋ฒ์งธ ์ด์ด ๊ณต๋ฐฑ์ธ์ง ํ์ธ (True/False)
is_empty = (df.iloc[:, 0] == "")
# 2. cummax()๋ฅผ ์ฌ์ฉํ์ฌ ํ ๋ฒ True๊ฐ ๋์ค๋ฉด ๊ทธ ์ดํ๋ ๊ณ์ True๊ฐ ๋๋๋ก ํจ
# 3. ๊ทธ ๋ฐ๋(~)์ธ ํ๋ค๋ง ๋จ๊น
df = df[~is_empty.cummax()].copy()
# ๋ฐ์ดํฐ ๋ฒกํฐํ ์ฒ๋ฆฌ (Pandas)
for col in df.columns:
df[col] = df[col].astype(str).str.replace("'", "''")
mask = ~df[col].str.match(r'^-?\d+(\.\d+)?$') # ์ซ์๊ฐ ์๋๋ฉด ๋ฐ์ดํ
df.loc[mask, col] = "'" + df[col].loc[mask] + "'"
df.loc[df[col] == "''", col] = "NULL"
values_sql = "(" + df.agg(', '.join, axis=1) + ")"
header = ", ".join([f"`{h}`" for h in data[0]])
return f"INSERT INTO `{table_name}` ({header})\nVALUES\n{',\n'.join(values_sql)};\n"
except Exception as e:
return f"-- Data Error in {row.get('TABLE_NAME')}: {e}\n"
async def export_schema(self):
"""๋ฉ์ธ ์คํ ํจ์: ๋ชจ๋ ํ
์ด๋ธ ์คํค๋ง ๋ณ๋ ฌ ์ฒ๋ฆฌ"""
return await self._execute_parallel(self._export_schema_unit)
async def export_data(self):
"""๋ฉ์ธ ์คํ ํจ์: ๋ชจ๋ ํ
์ด๋ธ ๋ฐ์ดํฐ ๋ณ๋ ฌ ์ฒ๋ฆฌ"""
return await self._execute_parallel(self._export_data_unit)
async def _execute_parallel(self, unit_func):
"""SCHEMA ์ํธ๋ฅผ ์ฝ๊ณ ๋ณ๋ ฌ๋ก ํ์คํฌ๋ฅผ ์คํํ๋ ๊ณตํต ๋ก์ง"""
try:
main_ss = await self.agc.open_by_key(self.spreadsheet_id)
schema_ws = await main_ss.worksheet("SCHEMA")
raw_data = await schema_ws.get_all_values()
if len(raw_data) < 2: return "No data found."
df_main = pd.DataFrame(raw_data[1:], columns=raw_data[0])
#df = df[df.iloc[:, 0] != ""].copy()
# 1. ์ฒซ ๋ฒ์งธ ์ด์ด ๊ณต๋ฐฑ์ธ์ง ํ์ธ (True/False)
is_empty = (df_main.iloc[:, 0] == "")
# 2. cummax()๋ฅผ ์ฌ์ฉํ์ฌ ํ ๋ฒ True๊ฐ ๋์ค๋ฉด ๊ทธ ์ดํ๋ ๊ณ์ True๊ฐ ๋๋๋ก ํจ
# 3. ๊ทธ ๋ฐ๋(~)์ธ ํ๋ค๋ง ๋จ๊น
df_main = df_main[~is_empty.cummax()].copy()
# CHK ์ปฌ๋ผ์ด TRUE์ธ ๋์๋ง ์ ๋ณ
active_targets = df_main[df_main.iloc[:, 0].str.upper().str.strip() == "TRUE"]
# --- ํต์ฌ: asyncio.gather๋ฅผ ํตํ ๋ณ๋ ฌ ์คํ ---
tasks = [unit_func(row) for _, row in active_targets.iterrows()]
#tasks = [unit_func(row) for row in active_targets.itertuples()]
results = await asyncio.gather(*tasks)
return "\n".join(results)
except Exception as e:
return f"Critical Error: {e}"
"""
⚡ ์ต์ ํ ํฌ์ธํธ ์ค๋ช
๊ณ์ธต์ ๋ณ๋ ฌํ (Double asyncio.gather):
์์ ๋ ๋ฒจ: _execute_parallel์์ ์ฌ๋ฌ ํ
์ด๋ธ(Table A, Table B...)์ ๋์์ ์ฒ๋ฆฌํฉ๋๋ค.
ํ์ ๋ ๋ฒจ: _export_schema_unit ๋ด๋ถ์์ ํ ํ
์ด๋ธ์ COLUMN, INDEX, FK ์ํธ 3๊ฐ๋ฅผ ๋์์ ์ฝ์ด์ต๋๋ค. (๊ธฐ์กด ๋๋น ์ํธ ๋ก๋ฉ ์๋ ์ฝ 3๋ฐฐ ํฅ์)
Pandas ๋ฒกํฐํ ์ฐ์ฐ: for ๋ฃจํ ์์ด ์์ฒ ์ค์ INSERT ๊ฐ์ด๋ DDL ๊ตฌ๋ฌธ์ ํ ๋ฒ์ ์์ฑํฉ๋๋ค.
๋ฉ๋ชจ๋ฆฌ ํจ์จ: df[df.iloc[:, 0] != ""] ์ฝ๋๋ฅผ ํตํด ๊ตฌ๊ธ ์ํธ ํ๋จ์ ๋ถํ์ํ ๋น ํ์ ์ฆ์ ์ ๊ฑฐํ์ฌ ๋ฉ๋ชจ๋ฆฌ ๋ญ๋น๋ฅผ ๋ฐฉ์งํฉ๋๋ค.
์์ ์ฑ: asyncio.gather ๋์ค ํ๋์ ์ํธ์์ ์๋ฌ๊ฐ ๋ฐ์ํด๋ ์ ์ฒด ํ๋ก์ธ์ค๊ฐ ์ฃฝ์ง ์๋๋ก ๊ฐ๋ณ ์ ๋ ํจ์์ try-except๋ฅผ ๋ฐฐ์นํ์ฌ ์๋ฌ ๋ฉ์์ง๋ฅผ SQL ์ฃผ์(-- Error...) ํํ๋ก ๋ฐํํ๊ฒ ํ์ต๋๋ค.
์ด์ ์ด ์ฝ๋๋ฅผ ์ฌ์ฉํ๋ฉด ๊ตฌ๊ธ API ํ ๋น๋(Quota) ๋ด์์ ๊ฐ์ฅ ๋น ๋ฅธ ์๋๋ก SQL ์ถ์ถ์ด ๊ฐ๋ฅํฉ๋๋ค. gspread-asyncio ๊ณต์ ๋ฌธ์๋ฅผ ์ฐธ๊ณ ํ์ฌ AsyncioGspreadClient๋ฅผ ๋๊ฒจ์ฃผ์๋ฉด ๋ฐ๋ก ์๋ํฉ๋๋ค.
"""
"""
"reactable_type, reactable_id, id" ๋ฌธ์์ด์ , ๋ก ๋ถ๋ฆฌํ ๋ฆฌ์คํธ์ ์ ์ฅํํ
"`reactable_type`, `reactable_id`, `id`" ํ์์ผ๋ก ํ๋ฉด์ ์ถ๋ ฅํด์ฃผ๋ ํ์ด์ฌ ์ฝ๋๋ฅผ ์์ฑํด์ค
# 1. ์๋ณธ ๋ฌธ์์ด ์ ์
raw_text = "reactable_type, reactable_id, id"
# 2. ์ผํ(,)๋ฅผ ๊ธฐ์ค์ผ๋ก ๋ถ๋ฆฌํ์ฌ ๋ฆฌ์คํธ์ ์ ์ฅ (๊ณต๋ฐฑ ์ ๊ฑฐ ํฌํจ)
column_list = [item.strip() for item in raw_text.split(',')]
# 3. ๊ฐ ์์๋ฅผ ๋ฐฑํฑ(`)์ผ๋ก ๊ฐ์ผ ํ ๋ค์ ํฉ์น๊ธฐ
result = ", ".join([f"`{item}`" for item in column_list])
# 4. ๊ฒฐ๊ณผ ์ถ๋ ฅ
print(result)
"""
app/domain/sheet/__init__.py
# domain/sheet/__init__.py
# ํ์ ํ์ผ๋ค(router, schemas, crud)์ ํจํค์ง ๋ ๋ฒจ๋ก ๋์ด์ฌ๋ฆฝ๋๋ค.
from . import crud, router, schemas
# ์ธ๋ถ์์ 'from domain.answer import *'๋ฅผ ํ๊ฑฐ๋ ๊ฐ์ ธ๊ฐ ์ ์๋ ๋ฒ์๋ฅผ ๋ช
์ํฉ๋๋ค.
__all__ = ["router", "schemas", "crud"]
ํ์ด์ฌ ๋ค์ด๋ฒ ๋ด์ค ํฌ๋กค๋ง ์ด๋ณด์๋ 5๋ถ ๋ง์ ๊ตฌํํ๋ ๋ฐฉ๋ฒ
์๋ ํ์ธ์! ์ค๋์ ํ์ด์ฌ(Python) ์ ํ์ฉํ์ฌ ๋ค์ด๋ฒ ๋ด์ค ๋ฐ์ดํฐ๋ฅผ ํฌ๋กค๋ง ํ๋ ๋ฐฉ๋ฒ์ ์์๋ณด๊ฒ ์ต๋๋ค. ๋น ๋ฐ์ดํฐ ๋ถ์์ด๋ ๋ง์ผํ ์กฐ์ฌ๋ฅผ ํ ๋ ํน์ ํค์๋์ ๋ด์ค ๊ธฐ์ฌ ์ ๋ชฉ๊ณผ ๋งํฌ๋ฅผ ์์งํ๋ ์์ ์ด ํ์์ ์ธ๋ฐ์. ์ด๋ฒ ...
-
bool atob(const char * string) { if (!strcmp(string, "true")) return true; return false; }
-
Environment Variables: MAXSDKPATH=C:\Program Files (x86)\Autodesk\3ds Max 2010 SDK Sample source http://download.autodesk.com/media/...
-
/// CXXXView.cpp void CXXXView::OnInitialUpdate() { CView::OnInitialUpdate(); // TODO: Add your specialized code here and/or call the ...