basic debugging step 1

This commit is contained in:
2024-11-05 07:46:21 +09:00
parent d6464c1369
commit 0c2dfec7dd
8 changed files with 499 additions and 194 deletions

View File

@ -9,6 +9,6 @@ services:
- ..:/app - ..:/app
environment: environment:
- PYTHONPATH=/app - PYTHONPATH=/app
command: /bin/bash command: python ./testdata/sample.py
tty: true tty: true

View File

@ -2,10 +2,10 @@ FROM python:3.9-slim
WORKDIR /app WORKDIR /app
# 必要なシステムパッケージのインストール # GPGキーの更新とパッケージのインストール
RUN apt-get update && apt-get install -y \ RUN apt-get update --allow-insecure-repositories && \
git \ apt-get install -y --allow-unauthenticated python3-dev libpq-dev && \
&& rm -rf /var/lib/apt/lists/* rm -rf /var/lib/apt/lists/*
# Pythonパッケージのインストール # Pythonパッケージのインストール
COPY requirements.txt . COPY requirements.txt .
@ -16,12 +16,11 @@ COPY . .
RUN pip install --no-cache-dir -r requirements.txt RUN pip install --no-cache-dir -r requirements.txt
# 開発用パッケージのインストール # 開発用パッケージのインストール
RUN pip install --no-cache-dir \ RUN pip install --no-cache-dir --upgrade pip \
pytest \ pytest \
pytest-cov \ pytest-cov \
flake8 flake8
# パッケージのインストール # パッケージのインストール
RUN pip install -e . RUN pip install -e .

View File

@ -2,4 +2,4 @@ openpyxl>=3.0.0
pandas>=1.0.0 pandas>=1.0.0
pillow>=8.0.0 pillow>=8.0.0
configparser>=5.0.0 configparser>=5.0.0
psycopg2-binary==2.9.9

View File

@ -0,0 +1,166 @@
# config_handler.py
#
import configparser
import os
from typing import Any, Dict, Optional
import configparser
import os
import re
from typing import Any, Dict, Optional
class ConfigHandler:
"""変数置換機能付きの設定ファイル管理クラス"""
def __init__(self, ini_file_path: str, variables: Dict[str, str] = None):
"""
Args:
ini_file_path (str): INIファイルのパス
variables (Dict[str, str], optional): 置換用の変数辞書
"""
self.ini_file_path = ini_file_path
self.variables = variables or {}
self.config = configparser.ConfigParser()
self.load_config()
def _substitute_variables(self, text: str) -> str:
"""
テキスト内の変数を置換する
Args:
text (str): 置換対象のテキスト
Returns:
str: 置換後のテキスト
"""
# ${var}形式の変数を置換
pattern1 = r'\${([^}]+)}'
# [var]形式の変数を置換
pattern2 = r'\[([^\]]+)\]'
def replace_var(match):
var_name = match.group(1)
return self.variables.get(var_name, match.group(0))
# 両方のパターンで置換を実行
text = re.sub(pattern1, replace_var, text)
text = re.sub(pattern2, replace_var, text)
return text
def load_config(self) -> None:
"""設定ファイルを読み込み、変数を置換する"""
if not os.path.exists(self.ini_file_path):
raise FileNotFoundError(f"設定ファイルが見つかりません: {self.ini_file_path}")
# まず生のテキストとして読み込む
with open(self.ini_file_path, 'r', encoding='utf-8') as f:
content = f.read()
# 変数を置換
substituted_content = self._substitute_variables(content)
# 置換済みの内容を StringIO 経由で configparser に読み込ませる
from io import StringIO
self.config.read_file(StringIO(substituted_content))
def get_value(self, section: str, key: str, default: Any = None) -> Optional[str]:
"""
指定されたセクションのキーの値を取得する
Args:
section (str): セクション名
key (str): キー名
default (Any): デフォルト値(オプション)
Returns:
Optional[str]: 設定値。存在しない場合はデフォルト値
"""
try:
return self.config[section][key]
except KeyError:
return default
def get_section(self, section: str) -> Dict[str, str]:
"""
指定されたセクションの全ての設定を取得する
Args:
section (str): セクション名
Returns:
Dict[str, str]: セクションの設定をディクショナリで返す
"""
try:
return dict(self.config[section])
except KeyError:
return {}
def get_all_sections(self) -> Dict[str, Dict[str, str]]:
"""
全てのセクションの設定を取得する
Returns:
Dict[str, Dict[str, str]]: 全セクションの設定をネストされたディクショナリで返す
"""
return {section: dict(self.config[section]) for section in self.config.sections()}
# 使用例
if __name__ == "__main__":
# サンプルのINIファイル作成
sample_ini = """
[Database]
host = localhost
port = 5432
database = mydb
user = admin
password = secret
[Application]
debug = true
log_level = INFO
max_connections = 100
[Paths]
data_dir = /var/data
log_file = /var/log/app.log
"""
# サンプルINIファイルを作成
with open('config.ini', 'w', encoding='utf-8') as f:
f.write(sample_ini)
# 設定を読み込んで使用
config = ConfigHandler('config.ini')
# 特定の値を取得
db_host = config.get_value('Database', 'host')
db_port = config.get_value('Database', 'port')
print(f"Database connection: {db_host}:{db_port}")
# セクション全体を取得
db_config = config.get_section('Database')
print("Database configuration:", db_config)
# 全ての設定を取得
all_config = config.get_all_sections()
print("All configurations:", all_config)
# サンプル:
# # 設定ファイルから値を取得
# config = ConfigHandler('config.ini')
#
# # データベース設定を取得
# db_host = config.get_value('Database', 'host')
# db_port = config.get_value('Database', 'port')
# db_name = config.get_value('Database', 'database')
#
# # アプリケーション設定を取得
# debug_mode = config.get_value('Application', 'debug')
# log_level = config.get_value('Application', 'log_level')
#

View File

@ -10,183 +10,360 @@ import shutil
from datetime import datetime from datetime import datetime
from typing import Optional, Dict, Any, Union, Tuple from typing import Optional, Dict, Any, Union, Tuple
from pathlib import Path from pathlib import Path
# psycopg2のインポートを追加
import psycopg2
import psycopg2.extras
from .config_handler import ConfigHandler # ini file のロード
from .styles import StyleManager from .styles import StyleManager
from .merge import MergeManager from .merge import MergeManager
from .image import ImageManager from .image import ImageManager
from .conditional import ConditionalFormatManager from .conditional import ConditionalFormatManager
from .page import PageManager, PaperSizes from .page import PageManager, PaperSizes
import logging
class SumasenExcel: class SumasenExcel:
"""Enhanced Excel handling class with extended functionality""" """Enhanced Excel handling class with extended functionality"""
def __init__(self, debug: bool = False): def __init__(self, document: str, variables: Dict[str, Any],
self.debug = debug docbase: str = "./docbase") -> Dict[str, str]:
"""Initialize Excel document with basic settings
Args:
document: Document name
variables: Variables for document
docbase: Base directory for documents (default: "./docbase")
Returns:
Dict with status (true/false) and optional error message
"""
self.debug = True
self.workbook = None self.workbook = None
self.template_filepath = None self.template_filepath = None
self.output_filepath = None self.output_filepath = None
self.current_sheet = None self.current_sheet = None
self.dbname = None
self.user = None
self.password = None
self.host = None
self.port = None
self._style_manager = None self._style_manager = None
self._merge_manager = None self._merge_manager = None
self._image_manager = None self._image_manager = None
self._conditional_manager = None self._conditional_manager = None
self._page_manager = None self._page_manager = None
def init(self, username: str, project_id: str, document: str,
lang: str = "jp", docbase: str = "./docbase") -> Dict[str, str]:
"""Initialize Excel document with basic settings
Args:
username: User name for file operations
project_id: Project identifier
document: Document name
lang: Language code (default: "jp")
docbase: Base directory for documents (default: "./docbase")
Returns:
Dict with status ("ACK"/"NCK") and optional error message
"""
try: try:
self.username = username # document base を設定
self.project_id = project_id
self.language = lang
# Setup directory structure
self.docpath = docbase self.docpath = docbase
self.docpath2 = f"{docbase}/{project_id}" if not os.path.exists(docbase):
logging.error(f"Document base directory not found: {docbase}")
# ini fileをロード
self.ini_file_path = f"{docbase}/{document}.ini"
self.conf = ConfigHandler(self.ini_file_path, variables)
if not os.path.exists(self.ini_file_path):
logging.error(f"INI file not found: {self.ini_file_path}")
# Create directories if they don't exist # basic section をロード
for path in [docbase, self.docpath2]: basic = self.conf.get_section("basic")
if not os.path.exists(path): if not basic:
os.makedirs(path, mode=0o755) logging.error(f"Basic section not found in INI file: {self.ini_file_path}")
# Load template self.basic = basic
inifile = f"{document}.ini"
self.inifilepath = f"{self.docpath2}/{inifile}"
if not os.path.exists(self.inifilepath): # basicセクションから必要なパラメータを取得
return {"status": "NCK", "message": f"INI file not found: {self.inifilepath}"} template_file = basic.get("template_file")
if not template_file:
logging.error("template_file not found in basic section")
# Load template workbook doc_file = basic.get("doc_file")
template_file = self._get_ini_param("basic", f"templatefile_{lang}") if not doc_file:
self.template_filepath = f"{self.docpath2}/{template_file}" logging.error("doc_file not found in basic section")
self.maxcol = basic.get("maxcol")
if not self.maxcol:
self.maxcol = 100 # デフォルト値を設定
logging.warning("maxcol not found in basic section. Defaulting to 100")
else:
self.maxcol = int(self.maxcol)
sections = basic.get("sections")
if not sections:
logging.error("sections not found in basic section")
# セクションをリストに変換
self.section_list = [s.strip() for s in sections.split(",")]
if not self.section_list:
logging.error("sections not found in basic section")
# 出力ファイルパスを設定
self.output_filepath = f"{self.docpath}/{doc_file}"
# 新規のExcelワークブックを作成
self.workbook = openpyxl.Workbook()
# デフォルトで作成されるシートを削除
self.workbook.remove(self.workbook.active)
# テンプレートワークブックをロード
self.template_filepath = f"{self.docpath}/{template_file}"
if not os.path.exists(self.template_filepath): if not os.path.exists(self.template_filepath):
# Copy from default if not exists logging.error(f"Template file not found: {self.template_filepath}")
default_template = f"{self.docpath}/{template_file}"
shutil.copy2(default_template, self.template_filepath)
self.workbook = openpyxl.load_workbook(self.template_filepath) self.template_workbook = openpyxl.load_workbook(self.template_filepath)
self.template_sheet = self.template_workbook.active
return {"status": "ACK"}
except Exception as e: except Exception as e:
return {"status": "NCK", "message": str(e)} logging.error(f"Error initializing Excel document: {str(e)}")
def make_report(self,variables: Dict[str, Any]):
"""レポートを生成する"""
logging.info("make_report step-1")
try:
ret_status = True
# セクションごとに処理を実行(ワークシート単位)
for section in self.section_list:
ret = self.proceed_section(section,variables)
if ret["status"] == False:
message = ret.get("message", "No message provided")
return {"status": False, "message": f"Fail generating section: {section}...{message}"}
def _get_ini_param(self, section: str, param: str) -> Optional[str]: # 生成したワークブックを保存
"""Get parameter from INI file self.workbook.save(self.output_filepath)
return {"status": True, "message": f"Report generated successfully : {self.output_filepath}", "filepath":self.output_filepath}
Args: except Exception as e:
section: INI file section return {"status": False, "message": f"Exception in make_report: Error generating report: {str(e)}"}
param: Parameter name
def proceed_section(self, section: str, variables: Dict[str, Any]):
logging.info(f"make_report.proceed_section step-1:section={section}")
Returns:
Parameter value or None if not found
"""
try: try:
# Use configparser to handle INI files # セクションの設定を取得
import configparser section_config = self.conf.get_section(section)
config = configparser.ConfigParser() # セクションが存在しない場合はスキップ
config.read(self.inifilepath) if not section_config:
return config[section][param] return {"status": False, "message": f"Error no section found: {section}"}
except:
return None
def make_report(self, db, data_rec: Dict[str, Any], # テンプレートシートをコピー
out_filename: Optional[str] = None, template_sheet_name = section_config.get("template_sheet")
screen_index: int = 0) -> None: if not template_sheet_name or template_sheet_name not in self.template_workbook.sheetnames:
"""Generate Excel report from template return {"status": False, "message": f"Error no template sheet found: {template_sheet_name}"}
# シートの名前を設定
new_sheet = self.workbook.create_sheet(title=section_config.get("sheet_name", section))
self.worksheet = new_sheet
self.dbname=variables.get('db'),
self.user=variables.get('username'),
self.password=variables.get('password'),
self.host=variables.get('host'),
self.port=variables.get('port')
if not self.dbname or not self.user or not self.password or not self.host or not self.port:
return {"status": False, "message": f"Error no database connection information"}
# PostgreSQLに接続
self.conn = psycopg2.connect(
self.dbname,
self.user,
self.password,
self.host,
self.port
)
self._style_manager = StyleManager()
self._merge_manager = MergeManager(self.current_sheet)
self._image_manager = ImageManager(self.current_sheet)
self._conditional_manager = ConditionalFormatManager(self.current_sheet)
self._page_manager = PageManager(self.current_sheet)
# シートの幅を設定
fit_to_width = section_config.get("fit_to_width")
if fit_to_width:
new_sheet.sheet_view.zoomScaleNormal = float(fit_to_width)
# シートの向きを設定
orientation = section_config.get("orientation")
new_sheet.sheet_view.orientation = orientation if orientation else "portrait"
self.current_worksheet = new_sheet
# グループ定義を取得
groups = section_config.get("groups")
if not groups:
return {"status": False, "message": f"Error no group definitions found: {section}"}
# グループをリストに変換
group_list = [g.strip() for g in groups.split(",")]
if not group_list:
return {"status": False, "message": f"Error invalid group definitions found: {section}"}
# 各グループの設定を取得
for group in group_list:
ret = self.proceed_group(group,variables)
if ret["status"] == False:
return ret
return {"status": True, "message": f"Success generating section: {section}"}
except Exception as e:
return {"status": False, "message": f"Exception in proceed_section : Error generating report: {str(e)}"}
def proceed_group(self,group:str,variables: Dict[str, Any]):
logging.info(f"make_report.proceed_group step-1:section={group}")
try:
group_config = self.conf.get_section(group)
if not group_config:
return {"status": False, "message": f"Error no group section found: {group}"}
# グループの処理パラメータを取得
group_range = group_config.get("group_range")
table = group_config.get("table")
where = group_config.get("where")
if not where or not table or not group_range:
return {"status": False, "message": f"Error invalid group parameters: {group_config}"}
sort = group_config.get("sort")
if not sort:
ret = self.proceed_one_record(table,where,group_range,variables)
if ret.status == True:
return {"status": True, "message": f"Success generating group: {group}"}
else:
ret = self.proceed_all_records(table,where,sort,group_range,variables)
if ret.status == True:
return {"status": True, "message": f"Success generating group: {group}"}
except Exception as e:
logging.error(f"Error in proceed_group: {str(e)}")
return {"status": False, "message": f"Exception in proceed_group : Error generating report: {str(e)}"}
def proceed_one_record(self,table:str,where:str,group_range:str,variables: Dict[str, Any]):
"""1レコードのデータを取得してシートの値を置き換える
Args: Args:
db: Database connection table: テーブル名
data_rec: Data records to populate report where: WHERE句
out_filename: Optional output filename group_range: 処理対象範囲
screen_index: Screen index for multi-screen reports variables: DB接続情報を含む変数辞書
""" """
# Get output filename logging.info(f"make_report.proceed_one_record step-1:table={table},where={where},group_range={group_range}")
if out_filename:
outfile = f"{out_filename}_{self._get_ini_param('basic', 'doc_file')}"
else:
outfile = self._get_ini_param('basic', 'doc_file')
self.output_filepath = f"{self.docpath2}/{outfile}" try:
# まずself.template_sheetの指定範囲のセルをself.current_sheetにコピーする。
self.copy_template_to_current(self.range,self.range)
# Process sections cursor = self.conn.cursor(cursor_factory=psycopg2.extras.DictCursor)
sections = self._get_ini_param('basic', 'sections')
if not sections: # SQLクエリを実行
return query = f"SELECT * FROM {table} WHERE {where} LIMIT 1"
cursor.execute(query)
for section in sections.split(','): record = cursor.fetchone()
self._process_section(section, db, data_rec, screen_index)
if record:
# Save workbook # group_rangeの範囲内のセルを走査
self.workbook.save(self.output_filepath) for row in self.current_worksheet:
for cell in row:
def _process_section(self, section: str, db, data_rec: Dict[str, Any], if cell.value and isinstance(cell.value, str):
screen_index: int) -> None: # [field_name]形式の文字列を検索
"""Process individual section of report import re
matches = re.findall(r'\[(.*?)\]', cell.value)
# マッチした場合、フィールド値で置換
if matches:
new_value = cell.value
for field_name in matches:
if field_name in record:
new_value = new_value.replace(
f'[{field_name}]',
str(record[field_name])
)
cell.value = new_value
cursor.close()
self.conn.close()
return {"status": True, "message": f"Success generating group: "}
except Exception as e:
logging.error(f"Error in proceed_one_record: {str(e)}")
return {"status": False, "message": f"Exception in proceed_one_record:Error generating report: {str(e)}"}
def proceed_all_record(self, table: str, where: str, sort: str, group_range: str, variables: Dict[str, Any]):
"""複数レコードを取得してシートの値を置き換える
Args: Args:
section: Section name table: テーブル名
db: Database connection where: WHERE句
data_rec: Data records sort: ORDER BY句
screen_index: Screen index group_range: 処理対象範囲
""" variables: DB接続情報を含む変数辞書
# Get template sheet """
sheet_orig = self._get_ini_param(section, 'sheet') logging.info(f"make_report.proceed_all_record step-1:table={table},where={where},group_range={group_range}")
sheet_name = self._get_ini_param(section, f"sheetname_{self.language}")
if not sheet_orig or not sheet_name: try:
return # グループ範囲の行数を取得
start_row, end_row = map(int, group_range.split(':')[0].split(','))
template_rows = end_row - start_row + 1
# Copy template sheet cursor = self.conn.cursor(cursor_factory=psycopg2.extras.DictCursor)
template_sheet = self.workbook[sheet_orig]
new_sheet = self.workbook.copy_worksheet(template_sheet) # SQLクエリを実行
new_sheet.title = sheet_name query = f"SELECT * FROM {table} WHERE {where} ORDER BY {sort}"
cursor.execute(query)
records = cursor.fetchall()
current_row = start_row
for record in records:
# テンプレート範囲をコピー
self.copy_template_to_current(
f"{start_row},{end_row}",
f"{current_row},{current_row + template_rows - 1}"
)
# コピーした範囲内のセルを走査して値を置換
for row in range(current_row, current_row + template_rows):
for cell in self.current_worksheet[row]:
if cell.value and isinstance(cell.value, str):
# [field_name]形式の文字列を検索
import re
matches = re.findall(r'\[(.*?)\]', cell.value)
# マッチした場合、フィールド値で置換
if matches:
new_value = cell.value
for field_name in matches:
if field_name in record:
new_value = new_value.replace(
f'[{field_name}]',
str(record[field_name])
)
cell.value = new_value
current_row += template_rows
cursor.close()
self.conn.close()
return {"status": True, "message": "Success processing all records"}
except Exception as e:
logging.error(f"Error in proceed_all_record: {str(e)}")
return {"status": False, "message": f"Exception in proceed_all_record:Error processing records: {str(e)}"}
# Process groups def copy_template_to_current(self,range,target_range):
groups = self._get_ini_param(section, 'groups') # テンプレートシートから現在のシートにデータをコピー
if groups: source_range = self.template_sheet.range(range) # コピーする範囲を指定
for group in groups.split(','): target_range = self.current_sheet.range(target_range) # 貼り付け先の範囲を指定
self._process_group(new_sheet, section, group, db, data_rec, screen_index)
# 値、数式、フォーマットをコピー
def _process_group(self, sheet, section: str, group: str, source_range.copy(target_range)
db, data_rec: Dict[str, Any], screen_index: int) -> None:
"""Process group within section
Args:
sheet: Worksheet to process
section: Section name
group: Group name
db: Database connection
data_rec: Data records
screen_index: Screen index
"""
pass # Implementation details will follow
def init_sheet(self, sheet_name: str) -> None:
"""Initialize worksheet and managers"""
self.current_sheet = self.workbook[sheet_name]
self._style_manager = StyleManager()
self._merge_manager = MergeManager(self.current_sheet)
self._image_manager = ImageManager(self.current_sheet)
self._conditional_manager = ConditionalFormatManager(self.current_sheet)
self._page_manager = PageManager(self.current_sheet)
# Style operations # Style operations
def apply_style( def apply_style(
self, self,

Binary file not shown.

View File

@ -1,55 +1,19 @@
from sumaexcel import SumasenExcel from sumaexcel import SumasenExcel
import logging
# 初期化 # 初期化
excel = SumasenExcel() variables = {"zekken_number":"5033","event_code":"FC岐阜"}
excel.init("username", "project_id", "document") excel = SumasenExcel(document="test", variables=variables, docbase="./testdata")
logging.info("Excelファイル作成 step-1")
# シート初期化 # シート初期化
excel.init_sheet("Sheet1") ret = excel.make_report(variables=variables)
logging.info(f"Excelファイル作成 step-2 : ret={ret}")
# スタイル適用 if ret["status"]==True:
excel.apply_style( filepath=ret["filepath"]
"A1:D10", logging.info(f"Excelファイル作成 : ret.filepath={filepath}")
font={"name": "Arial", "size": 12, "bold": True}, else:
fill={"start_color": "FFFF00"}, message = ret.get("message", "No message provided")
alignment={"horizontal": "center"} logging.error(f"Excelファイル作成失敗 : ret.message={message}")
)
# セルのマージ
excel.merge_range(1, 1, 1, 4)
# 画像追加
excel.add_image(
"logo.png",
position=(1, 1),
size=(100, 100)
)
# 条件付き書式
excel.add_conditional_format(
"B2:B10",
format_type="color_scale",
min_color="00FF0000",
max_color="0000FF00"
)
# ページ設定
excel.setup_page(
orientation="landscape",
paper_size=PaperSizes.A4,
margins={
"left": 1.0,
"right": 1.0,
"top": 1.0,
"bottom": 1.0
},
header_footer={
"odd_header": "&L&BPage &P of &N&C&BConfidential",
"odd_footer": "&RDraft"
}
)
# レポート生成
excel.make_report(db, data_rec)

View File

@ -1,25 +1,24 @@
[basic] [basic]
templatefile_jp="certificate_template.xlsx" template_file=certificate_template.xlsx
doc_file="certificate_[zekken_number].xlsx" doc_file=certificate_[zekken_number].xlsx
sections=section1,section2 sections=section1,section2
developer=Sumasen
maxcol=8 maxcol=8
[section1] [section1]
sheet="certificate" template_sheet=certificate
sheetname_jp="岐阜ロゲ通過証明書" groups=group1,group2
groups="group1,group2"
fit_to_width=1 fit_to_width=1
orientation=portrait orientation=portrait
[section1.group1] [section1.group1]
table_name=rog_entry table_name=rog_entry
where="zekken_number='[zekken_number]' and event_code='[event_code]'" where=zekken_number='[zekken_number]' and event_code='[event_code]'
group_range="0,0,8,11" group_range=0,0,8,11
[section1.group2] [section1.group2]
table_name=gps_checkins table_name=gps_checkins
where=""zekken_number='[zekken_number]' and event_code='[event_code]' where=zekken_number='[zekken_number]' and event_code='[event_code]'
sort=order sort=order
group_range=0,12,8,12 group_range=0,12,8,12