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
environment:
- PYTHONPATH=/app
command: /bin/bash
command: python ./testdata/sample.py
tty: true

View File

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

View File

@ -2,4 +2,4 @@ openpyxl>=3.0.0
pandas>=1.0.0
pillow>=8.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 typing import Optional, Dict, Any, Union, Tuple
from pathlib import Path
# psycopg2のインポートを追加
import psycopg2
import psycopg2.extras
from .config_handler import ConfigHandler # ini file のロード
from .styles import StyleManager
from .merge import MergeManager
from .image import ImageManager
from .conditional import ConditionalFormatManager
from .page import PageManager, PaperSizes
import logging
class SumasenExcel:
"""Enhanced Excel handling class with extended functionality"""
def __init__(self, debug: bool = False):
self.debug = debug
def __init__(self, document: str, variables: Dict[str, Any],
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.template_filepath = None
self.output_filepath = 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._merge_manager = None
self._image_manager = None
self._conditional_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:
self.username = username
self.project_id = project_id
self.language = lang
# Setup directory structure
# document base を設定
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
for path in [docbase, self.docpath2]:
if not os.path.exists(path):
os.makedirs(path, mode=0o755)
# basic section をロード
basic = self.conf.get_section("basic")
if not basic:
logging.error(f"Basic section not found in INI file: {self.ini_file_path}")
# Load template
inifile = f"{document}.ini"
self.inifilepath = f"{self.docpath2}/{inifile}"
self.basic = basic
if not os.path.exists(self.inifilepath):
return {"status": "NCK", "message": f"INI file not found: {self.inifilepath}"}
# basicセクションから必要なパラメータを取得
template_file = basic.get("template_file")
if not template_file:
logging.error("template_file not found in basic section")
# Load template workbook
template_file = self._get_ini_param("basic", f"templatefile_{lang}")
self.template_filepath = f"{self.docpath2}/{template_file}"
doc_file = basic.get("doc_file")
if not doc_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):
# Copy from default if not exists
default_template = f"{self.docpath}/{template_file}"
shutil.copy2(default_template, self.template_filepath)
logging.error(f"Template file not found: {self.template_filepath}")
self.workbook = openpyxl.load_workbook(self.template_filepath)
return {"status": "ACK"}
self.template_workbook = openpyxl.load_workbook(self.template_filepath)
self.template_sheet = self.template_workbook.active
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:
section: INI file section
param: Parameter name
except Exception as e:
return {"status": False, "message": f"Exception in make_report: Error generating report: {str(e)}"}
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:
# Use configparser to handle INI files
import configparser
config = configparser.ConfigParser()
config.read(self.inifilepath)
return config[section][param]
except:
return None
# セクションの設定を取得
section_config = self.conf.get_section(section)
# セクションが存在しない場合はスキップ
if not section_config:
return {"status": False, "message": f"Error no section found: {section}"}
def make_report(self, db, data_rec: Dict[str, Any],
out_filename: Optional[str] = None,
screen_index: int = 0) -> None:
"""Generate Excel report from template
# テンプレートシートをコピー
template_sheet_name = section_config.get("template_sheet")
if not template_sheet_name or template_sheet_name not in self.template_workbook.sheetnames:
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:
db: Database connection
data_rec: Data records to populate report
out_filename: Optional output filename
screen_index: Screen index for multi-screen reports
"""
# Get output filename
if out_filename:
outfile = f"{out_filename}_{self._get_ini_param('basic', 'doc_file')}"
else:
outfile = self._get_ini_param('basic', 'doc_file')
table: テーブル名
where: WHERE句
group_range: 処理対象範囲
variables: DB接続情報を含む変数辞書
"""
logging.info(f"make_report.proceed_one_record step-1:table={table},where={where},group_range={group_range}")
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
sections = self._get_ini_param('basic', 'sections')
if not sections:
return
for section in sections.split(','):
self._process_section(section, db, data_rec, screen_index)
# Save workbook
self.workbook.save(self.output_filepath)
def _process_section(self, section: str, db, data_rec: Dict[str, Any],
screen_index: int) -> None:
"""Process individual section of report
cursor = self.conn.cursor(cursor_factory=psycopg2.extras.DictCursor)
# SQLクエリを実行
query = f"SELECT * FROM {table} WHERE {where} LIMIT 1"
cursor.execute(query)
record = cursor.fetchone()
if record:
# group_rangeの範囲内のセルを走査
for row in self.current_worksheet:
for cell in 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
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:
section: Section name
db: Database connection
data_rec: Data records
screen_index: Screen index
"""
# Get template sheet
sheet_orig = self._get_ini_param(section, 'sheet')
sheet_name = self._get_ini_param(section, f"sheetname_{self.language}")
table: テーブル名
where: WHERE句
sort: ORDER BY句
group_range: 処理対象範囲
variables: DB接続情報を含む変数辞書
"""
logging.info(f"make_report.proceed_all_record step-1:table={table},where={where},group_range={group_range}")
if not sheet_orig or not sheet_name:
return
try:
# グループ範囲の行数を取得
start_row, end_row = map(int, group_range.split(':')[0].split(','))
template_rows = end_row - start_row + 1
# Copy template sheet
template_sheet = self.workbook[sheet_orig]
new_sheet = self.workbook.copy_worksheet(template_sheet)
new_sheet.title = sheet_name
cursor = self.conn.cursor(cursor_factory=psycopg2.extras.DictCursor)
# SQLクエリを実行
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
groups = self._get_ini_param(section, 'groups')
if groups:
for group in groups.split(','):
self._process_group(new_sheet, section, group, db, data_rec, screen_index)
def _process_group(self, sheet, section: str, group: str,
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 copy_template_to_current(self,range,target_range):
# テンプレートシートから現在のシートにデータをコピー
source_range = self.template_sheet.range(range) # コピーする範囲を指定
target_range = self.current_sheet.range(target_range) # 貼り付け先の範囲を指定
# 値、数式、フォーマットをコピー
source_range.copy(target_range)
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
def apply_style(
self,

Binary file not shown.

View File

@ -1,55 +1,19 @@
from sumaexcel import SumasenExcel
import logging
# 初期化
excel = SumasenExcel()
excel.init("username", "project_id", "document")
variables = {"zekken_number":"5033","event_code":"FC岐阜"}
excel = SumasenExcel(document="test", variables=variables, docbase="./testdata")
logging.info("Excelファイル作成 step-1")
# シート初期化
excel.init_sheet("Sheet1")
# スタイル適用
excel.apply_style(
"A1:D10",
font={"name": "Arial", "size": 12, "bold": True},
fill={"start_color": "FFFF00"},
alignment={"horizontal": "center"}
)
# セルのマージ
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)
ret = excel.make_report(variables=variables)
logging.info(f"Excelファイル作成 step-2 : ret={ret}")
if ret["status"]==True:
filepath=ret["filepath"]
logging.info(f"Excelファイル作成 : ret.filepath={filepath}")
else:
message = ret.get("message", "No message provided")
logging.error(f"Excelファイル作成失敗 : ret.message={message}")

View File

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