Skip to content

Commit 8f52eb1

Browse files
committed
# Projeto completo
1 parent 5afece4 commit 8f52eb1

30 files changed

+1123
-0
lines changed

.idea/.gitignore

+8
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

.idea/aws.xml

+18
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

.idea/misc.xml

+6
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

.idea/modules.xml

+8
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

.idea/sql-query-builder.iml

+17
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

.idea/vcs.xml

+6
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

LICENSE

+18
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
MIT License
2+
Copyright (c) 2024 Michael Bullet (michaelbullet.com)
3+
LinkedIn: https://www.linkedin.com/in/michael-bullet/
4+
Permission is hereby granted, free of charge, to any person obtaining a copy
5+
of this software and associated documentation files (the "Software"), to deal
6+
in the Software without restriction, including without limitation the rights
7+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
8+
copies of the Software, and to permit persons to whom the Software is
9+
furnished to do so, subject to the following conditions:
10+
The above copyright notice and this permission notice shall be included in all
11+
copies or substantial portions of the Software.
12+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
13+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
14+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
15+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
16+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
17+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
18+
SOFTWARE.

backend/config.yml

+52
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
# Database configurations
2+
databases:
3+
mysql:
4+
pool_size: 5
5+
max_overflow: 10
6+
pool_timeout: 30
7+
pool_recycle: 3600
8+
9+
oracle:
10+
pool_size: 5
11+
max_overflow: 10
12+
pool_timeout: 30
13+
pool_recycle: 3600
14+
15+
postgresql:
16+
pool_size: 5
17+
max_overflow: 10
18+
pool_timeout: 30
19+
pool_recycle: 3600
20+
21+
mssql:
22+
pool_size: 5
23+
max_overflow: 10
24+
pool_timeout: 30
25+
pool_recycle: 3600
26+
27+
# Logging configuration
28+
logging:
29+
version: 1
30+
formatters:
31+
default:
32+
format: '%(asctime)s - %(name)s - %(levelname)s - %(message)s'
33+
handlers:
34+
console:
35+
class: logging.StreamHandler
36+
formatter: default
37+
level: INFO
38+
file:
39+
class: logging.FileHandler
40+
formatter: default
41+
filename: 'logs/app.log'
42+
level: DEBUG
43+
root:
44+
level: INFO
45+
handlers: [console, file]
46+
47+
# API configuration
48+
api:
49+
host: '0.0.0.0'
50+
port: 5000
51+
debug: true
52+
secret_key: 'your-secret-key-here' # Mude isso em produção

backend/requirements.txt

+14
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
Flask==3.0.0
2+
SQLAlchemy==2.0.25
3+
PyMySQL==1.1.0
4+
psycopg2-binary==2.9.9
5+
cx-Oracle==8.3.0
6+
pyodbc==5.0.1
7+
python-dotenv==1.0.0
8+
PyYAML==6.0.1
9+
marshmallow==3.20.1
10+
marshmallow-dataclass==8.6.0
11+
pytest==7.4.4
12+
black==23.12.1
13+
flake8==7.0.0
14+
mypy==1.8.0

backend/src/__init__.py

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
'''Arquivo de inicialização do backend'''

backend/src/app.py

+78
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
from flask import Flask, request, jsonify
2+
from datetime import datetime
3+
import time
4+
import logging.config
5+
import yaml
6+
from database.manager import DatabaseManager
7+
from database.models import QueryConfig, QueryResult
8+
from query.builder import QueryBuilder
9+
10+
# Configuração do app
11+
app = Flask(__name__)
12+
db_manager = DatabaseManager('config.yml')
13+
14+
# Configuração de logging
15+
with open('config.yml', 'r') as f:
16+
config = yaml.safe_load(f)
17+
logging.config.dictConfig(config['logging'])
18+
19+
logger = logging.getLogger(__name__)
20+
21+
@app.route('/health', methods=['GET'])
22+
def health_check():
23+
"""Endpoint para verificação de saúde da API."""
24+
return jsonify({
25+
'status': 'healthy',
26+
'timestamp': datetime.now().isoformat()
27+
})
28+
29+
@app.route('/query', methods=['POST'])
30+
def execute_query():
31+
"""Endpoint principal para execução de queries."""
32+
try:
33+
start_time = time.time()
34+
35+
# Valida e cria configuração
36+
config = QueryConfig(**request.json)
37+
config.validate()
38+
39+
# Obtém conexão e executa query
40+
with db_manager.get_connection(
41+
config.db_type, config.user, config.password,
42+
config.host, config.db_name
43+
) as connection:
44+
# Obtém metadados e constrói query
45+
metadata = db_manager.get_metadata(connection)
46+
query_builder = QueryBuilder(metadata)
47+
query = query_builder.build_query(config)
48+
49+
# Executa query e processa resultados
50+
result = connection.execute(query)
51+
data = [dict(row) for row in result]
52+
53+
# Cria resultado
54+
execution_time = time.time() - start_time
55+
query_result = QueryResult(
56+
success=True,
57+
data=data,
58+
query=str(query),
59+
execution_time=execution_time,
60+
total_rows=len(data)
61+
)
62+
63+
return jsonify(query_result.__dict__)
64+
65+
except Exception as e:
66+
logger.error(f"Erro ao executar query: {str(e)}")
67+
return jsonify({
68+
'success': False,
69+
'error': str(e.__class__.__name__),
70+
'details': str(e)
71+
}), 500
72+
73+
if __name__ == '__main__':
74+
app.run(
75+
host=config['api']['host'],
76+
port=config['api']['port'],
77+
debug=config['api']['debug']
78+
)

backend/src/database/__init__.py

+35
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
from sqlalchemy import create_engine
2+
from sqlalchemy.ext.declarative import declarative_base
3+
from sqlalchemy.orm import sessionmaker
4+
import os
5+
6+
# Create base class for declarative models
7+
Base = declarative_base()
8+
9+
def get_database_url():
10+
"""Get database URL from environment variables or use default"""
11+
host = os.getenv('DB_HOST', 'localhost')
12+
port = os.getenv('DB_PORT', '5432')
13+
name = os.getenv('DB_NAME', 'myapp')
14+
user = os.getenv('DB_USER', 'user')
15+
password = os.getenv('DB_PASSWORD', '')
16+
17+
return f"postgresql://{user}:{password}@{host}:{port}/{name}"
18+
19+
# Create database engine
20+
engine = create_engine(get_database_url())
21+
22+
# Create sessionmaker
23+
SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)
24+
25+
def get_db():
26+
"""Dependency to get database session"""
27+
db = SessionLocal()
28+
try:
29+
yield db
30+
finally:
31+
db.close()
32+
33+
def init_db():
34+
"""Initialize database tables"""
35+
Base.metadata.create_all(bind=engine)

backend/src/database/manager.py

+78
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
from typing import Dict, Optional
2+
from sqlalchemy import create_engine, MetaData
3+
from sqlalchemy.engine import Engine
4+
from sqlalchemy.exc import SQLAlchemyError
5+
from contextlib import contextmanager
6+
import logging
7+
import yaml
8+
9+
logger = logging.getLogger(__name__)
10+
11+
class DatabaseManager:
12+
"""Gerencia conexões com diferentes tipos de bancos de dados."""
13+
14+
SUPPORTED_DBS = {
15+
'mysql': 'mysql+pymysql',
16+
'postgresql': 'postgresql',
17+
'oracle': 'oracle+cx_oracle',
18+
'mssql': 'mssql+pyodbc'
19+
}
20+
21+
def __init__(self, config_path: str = 'config.yml'):
22+
self.engines: Dict[str, Engine] = {}
23+
self.config = self._load_config(config_path)
24+
25+
def _load_config(self, config_path: str) -> dict:
26+
"""Carrega configurações do arquivo YAML."""
27+
try:
28+
with open(config_path, 'r') as f:
29+
return yaml.safe_load(f)
30+
except Exception as e:
31+
logger.error(f"Erro ao carregar configuração: {e}")
32+
raise
33+
34+
def get_engine(self, db_type: str, user: str, password: str,
35+
host: str, db_name: str) -> Engine:
36+
"""Obtém ou cria uma engine SQLAlchemy para o banco especificado."""
37+
if db_type not in self.SUPPORTED_DBS:
38+
raise ValueError(f"Banco de dados não suportado: {db_type}")
39+
40+
connection_key = f"{db_type}://{user}@{host}/{db_name}"
41+
42+
if connection_key not in self.engines:
43+
db_config = self.config['databases'].get(db_type, {})
44+
connection_string = (
45+
f"{self.SUPPORTED_DBS[db_type]}://"
46+
f"{user}:{password}@{host}/{db_name}"
47+
)
48+
49+
self.engines[connection_key] = create_engine(
50+
connection_string,
51+
pool_size=db_config.get('pool_size', 5),
52+
max_overflow=db_config.get('max_overflow', 10),
53+
pool_timeout=db_config.get('pool_timeout', 30),
54+
pool_recycle=db_config.get('pool_recycle', 3600)
55+
)
56+
57+
return self.engines[connection_key]
58+
59+
@contextmanager
60+
def get_connection(self, db_type: str, user: str, password: str,
61+
host: str, db_name: str):
62+
"""Context manager para obter uma conexão do pool."""
63+
engine = self.get_engine(db_type, user, password, host, db_name)
64+
try:
65+
connection = engine.connect()
66+
yield connection
67+
finally:
68+
connection.close()
69+
70+
def get_metadata(self, engine: Engine) -> MetaData:
71+
"""Obtém metadados das tabelas do banco de dados."""
72+
metadata = MetaData()
73+
try:
74+
metadata.reflect(bind=engine)
75+
return metadata
76+
except SQLAlchemyError as e:
77+
logger.error(f"Erro ao obter metadados: {e}")
78+
raise

0 commit comments

Comments
 (0)