From 6d9bc5980fd652f324687e25d2887740a5708a0e Mon Sep 17 00:00:00 2001 From: Joungmin Date: Thu, 19 Feb 2026 03:25:52 +0900 Subject: [PATCH] Add: Stock tracker, Jenkins CI/CD pipeline, linting config - stock_tracker.py: Portfolio tracking with P&L calculations - Jenkinsfile: Full CI/CD with linting, testing, deployment - test_requirements.txt: Testing dependencies - .pylintrc: Linting configuration - requirements.txt: Production dependencies Features: - Stock & crypto portfolio tracking - Investment guideline checks - Unit tests & linting pipeline - Integration tests for Oracle/Telegram/Gitea - Staging & Production deployment stages --- .pylintrc | 27 +++ Jenkinsfile | 209 +++++++++++++++++++++++ deploy_habit_bot.sh | 62 +++++++ requirements.txt | 32 ++++ stock_tracker.py | 381 ++++++++++++++++++++++++++++++++++++++++++ test_requirements.txt | 25 +++ 6 files changed, 736 insertions(+) create mode 100644 .pylintrc create mode 100644 Jenkinsfile create mode 100644 deploy_habit_bot.sh create mode 100644 requirements.txt create mode 100644 stock_tracker.py create mode 100644 test_requirements.txt diff --git a/.pylintrc b/.pylintrc new file mode 100644 index 0000000..219aa90 --- /dev/null +++ b/.pylintrc @@ -0,0 +1,27 @@ +[MASTER] +ignore=venv,__pycache__,node_modules,build,dist +max-line-length=120 +disable=C0114,C0115,C0116,R0801 + +[FORMAT] +max-line-length=120 + +[DESIGN] +max-args=10 +max-locals=25 +max-statements=100 + +[BASIC] +good-names=i,j,k,ex,Run,_ + +[IMPORTS] +known-standard-library=os,sys,json,datetime,requests +known-third-party=telegram,flask,oracledb,openai + +[MESSAGES CONTROL] +extension-pkg-allow-list=telegram,oracledb + +[COVERAGE] +coverage-append=true +coverage-report=term-missing +coverage-report=html diff --git a/Jenkinsfile b/Jenkinsfile new file mode 100644 index 0000000..1712d4d --- /dev/null +++ b/Jenkinsfile @@ -0,0 +1,209 @@ +pipeline { + agent any + + environment { + // Test database connections + ORACLE_DSN = credentials('oracle-dsn') + ORACLE_USER = credentials('oracle-user') + ORACLE_PASSWORD = credentials('oracle-password') + + // Telegram Bot + TELEGRAM_BOT_TOKEN = credentials('telegram-bot-token') + + // Git + GITEA_URL = 'http://localhost:3000' + GITEA_USER = 'joungmin' + GITEA_TOKEN = credentials('gitea-token') + } + + stages { + stage('Checkout') { + steps { + checkout scm + script { + env.BUILD_ID = sh(returnStdout: true, script: 'git rev-parse HEAD').trim() + } + } + } + + stage('Dependencies') { + steps { + echo 'Installing Python dependencies...' + sh ''' + python3 -m venv venv + source venv/bin/activate + pip install -q -r requirements.txt + pip install -q -r test_requirements.txt + ''' + } + } + + stage('Lint') { + steps { + echo 'Running linters...' + sh ''' + source venv/bin/activate + + # Python linting + flake8 . --max-line-length=120 --exclude=venv,__pycache__ || true + pylint --rcfile=.pylintrc *.py || true + + # Security scanning + bandit -r . -f json -o bandit-report.json || true + + # Dead code detection + vulture *.py --make-module || true + ''' + } + post { + always { + recordIssues(tools: [flake8(pattern: 'flake-report.txt')]) + recordIssues(tools: [bandit(pattern: 'bandit-report.json')]) + } + } + } + + stage('Unit Tests') { + steps { + echo 'Running unit tests...' + sh ''' + source venv/bin/activate + pytest tests/ -v --tb=short --cov=. --cov-report=html --cov-report=xml + coverage xml -o coverage-report.xml + ''' + } + post { + always { + junit 'test-results.xml' + cobertura coberturaPackage: 'coverage.xml', failNoStubs: false + } + } + } + + stage('Integration Tests') { + steps { + echo 'Running integration tests...' + sh ''' + source venv/bin/activate + + # Test Oracle connection + python3 -c " + import oracledb + conn = oracledb.connect( + user='${ORACLE_USER}', + password='${ORACLE_PASSWORD}', + dsn='${ORACLE_DSN}' + ) + cursor = conn.cursor() + cursor.execute('SELECT 1 FROM DUAL') + print('โœ… Oracle connection successful') + conn.close() + " + + # Test Telegram bot (ping) + curl -s "https://api.telegram.org/bot${TELEGRAM_BOT_TOKEN}/getMe" + + # Test Gitea API + curl -s -u "${GITEA_USER}:${GITEA_TOKEN}" "${GITEA_URL}/api/v1/user" + ''' + } + } + + stage('Build') { + steps { + echo 'Building application...' + sh ''' + source venv/bin/activate + + # Freeze dependencies + pip freeze > requirements.locked.txt + + # Create executable scripts + chmod +x *.py + + # Build Docker images if applicable + docker build -t openclaw-bot:${BUILD_ID} . || true + ''' + } + } + + stage('Deploy to Staging') { + when { + branch 'main' + } + steps { + echo 'Deploying to staging server...' + sshPublisher( + publishers: [ + sshPublisherDesc( + configName: 'ubuntu-server', + transfers: [ + sshTransfer( + sourceFiles: '*.py', + remoteDirectory: '/home/joungmin/openclaw', + execCommand: 'cd /home/joungmin/openclaw && source venv/bin/activate && pip install -r requirements.txt && supervisorctl restart openclaw' + ) + ] + ) + ] + ) + } + } + + stage('Deploy to Production') { + when { + branch 'production' + } + steps { + echo 'Deploying to production...' + // Manual approval required + input message: 'Deploy to production?' + sshPublisher( + publishers: [ + sshPublisherDesc( + configName: 'production-server', + transfers: [ + sshTransfer( + sourceFiles: '*.py', + remoteDirectory: '/home/joungmin/production', + execCommand: 'cd /home/joungmin/production && docker-compose pull && docker-compose up -d' + ) + ] + ) + ] + ) + } + } + } + + post { + always { + echo 'Pipeline completed' + // Send notification + script { + def status = currentBuild.currentResult == 'SUCCESS' ? 'โœ…' : 'โŒ' + sh """ + curl -s -X POST "https://api.telegram.org/bot\${TELEGRAM_BOT_TOKEN}/sendMessage" \ + -d "chat_id=@your_channel" \ + -d "text=${status} Pipeline completed: ${env.JOB_NAME} #\${env.BUILD_NUMBER}" + """ + } + } + + success { + echo 'Build succeeded!' + archiveArtifacts artifacts: '**/*.py', fingerprint: true + } + + failure { + echo 'Build failed!' + mail to: 'joungmin@example.com', + subject: "Failed Pipeline: ${env.JOB_NAME}", + body: "Something is wrong with ${env.BUILD_URL}" + } + + unstable { + echo 'Build is unstable!' + } + } +} diff --git a/deploy_habit_bot.sh b/deploy_habit_bot.sh new file mode 100644 index 0000000..2e23d14 --- /dev/null +++ b/deploy_habit_bot.sh @@ -0,0 +1,62 @@ +#!/bin/bash +# Deploy Habit & Diet Bot to Ubuntu +# Run on 192.168.0.147 + +set -e + +APP_DIR="/home/joungmin/habit_bot" +VENV_DIR="$APP_DIR/venv" + +echo "๐Ÿ”ฎ Deploying Habit & Diet Bot..." + +# Create directory +mkdir -p $APP_DIR +cd $APP_DIR + +# Copy files +echo "Copying files..." +# Copy from Mac mini (run this on Mac): +# scp -r habit_bot.py habit_bot_requirements.txt deploy_habit_bot.sh joungmin@192.168.0.147:$APP_DIR/ + +# Create virtual environment +if [ ! -d "$VENV_DIR" ]; then + python3 -m venv $VENV_DIR +fi + +# Install dependencies +source $VENV_DIR/bin/activate +pip install -q -r $APP_DIR/habit_bot_requirements.txt + +# Create systemd service +cat > /tmp/habit-bot.service << 'EOF' +[Unit] +Description=Habit & Diet Telegram Bot +After=network.target + +[Service] +Type=simple +User=joungmin +WorkingDirectory=/home/joungmin/habit_bot +Environment="PATH=/home/joungmin/habit_bot/venv/bin" +Environment="TELEGRAM_BOT_TOKEN=your_token_here" +Environment="OBSIDIAN_PATH=/Users/joungmin/Documents/Obsidian Vault" +Environment="ORACLE_DSN=h8i4i0g8cxtd2lpf_high" +Environment="ORACLE_USER=admin" +Environment="ORACLE_PASSWORD=your_password" +Environment="ORACLE_WALLET=/path/to/wallet" +ExecStart=/home/joungmin/habit_bot/venv/bin/python3 /home/joungmin/habit_bot/habit_bot.py +Restart=always + +[Install] +WantedBy=multi-user.target +EOF + +echo "โœ… Deployment complete!" +echo "" +echo "To start:" +echo " 1. Set TELEGRAM_BOT_TOKEN in /tmp/habit-bot.service" +echo " 2. sudo cp /tmp/habit-bot.service /etc/systemd/system/" +echo " 3. sudo systemctl daemon-reload" +echo " 4. sudo systemctl start habit-bot" +echo "" +echo "Bot URL: http://localhost:3000/joungmin/openclaw-workspace" diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..d4ed7db --- /dev/null +++ b/requirements.txt @@ -0,0 +1,32 @@ +# OpenClaw - Production Dependencies + +# Core Framework +openclaw>=2026.2.0 + +# Database +oracledb>=2.0.0 + +# Web Framework +flask>=2.3.0 +gunicorn>=21.0.0 + +# Telegram Bot +python-telegram-bot>=20.0 + +# LLM/API +openai>=1.0.0 +httpx>=0.25.0 + +# Data Processing +pandas>=2.0.0 +numpy>=1.24.0 + +# Utilities +python-dateutil>=2.8.0 +requests>=2.31.0 +PyYAML>=6.0 + +# Investment Data (optional) +yfinance>=0.2.0 +beautifulsoup4>=4.12.0 +lxml>=4.9.0 diff --git a/stock_tracker.py b/stock_tracker.py new file mode 100644 index 0000000..f81b2e9 --- /dev/null +++ b/stock_tracker.py @@ -0,0 +1,381 @@ +#!/usr/bin/env python3 +""" +Stock & Crypto Portfolio Tracker +Features: +- Track stocks and crypto prices +- Calculate portfolio P&L +- Compare against market indices +- Generate investment recommendations based on guidelines +- Daily/weekly reports +""" + +import os +import json +import datetime +from dataclasses import dataclass, field, asdict +from typing import List, Dict, Optional, Tuple +from enum import Enum +import requests + +# Configuration +DATA_DIR = os.environ.get('PORTFOLIO_DATA_DIR', '/tmp/portfolio') +os.makedirs(DATA_DIR, exist_ok=True) + +PORTFOLIO_FILE = os.path.join(DATA_DIR, 'portfolio.json') +PRICES_FILE = os.path.join(DATA_DIR, 'prices.json') +PERFORMANCE_FILE = os.path.join(DATA_DIR, 'performance.json') + +# Market API (placeholder - would use real API) +YAHOO_API = "https://query1.finance.yahoo.com/v8/finance/chart/{symbol}" +KRX_API = "https://api.ksicore.net/v1/stock/quote" + +class AssetType(Enum): + STOCK = "stock" + CRYPTO = "crypto" + ETF = "etf" + +@dataclass +class Position: + symbol: str + asset_type: str + quantity: float + avg_cost: float + entry_date: str = "" + notes: str = "" + +@dataclass +class PriceData: + symbol: str + current_price: float + change_percent: float + high_52w: float = 0 + low_52w: float = 0 + volume: float = 0 + updated_at: str = "" + +@dataclass +class PortfolioSummary: + total_value: float + total_cost: float + total_pnl: float + total_pnl_percent: float + positions: List[Dict] + top_performer: Optional[Dict] + worst_performer: Optional[Dict] + market_comparison: Dict = field(default_factory=dict) + +class StockTracker: + """Stock & Crypto Portfolio Tracker""" + + def __init__(self): + self.positions = self._load_positions() + self.prices = self._load_prices() + + # ============== Data Management ============== + + def _load_positions(self) -> Dict[str, Position]: + if os.path.exists(PORTFOLIO_FILE): + with open(PORTFOLIO_FILE, 'r') as f: + data = json.load(f) + return {k: Position(**v) for k, v in data.items()} + return {} + + def _save_positions(self): + with open(PORTFOLIO_FILE, 'w') as f: + json.dump({k: asdict(v) for k, v in self.positions.items()}, f, indent=2) + + def _load_prices(self) -> Dict[str, PriceData]: + if os.path.exists(PRICES_FILE): + with open(PRICES_FILE, 'r') as f: + return {k: PriceData(**v) for k, v in json.load(f).items()} + return {} + + def _save_prices(self): + with open(PRICES_FILE, 'w') as f: + json.dump({k: asdict(v) for k, v in self.prices.items()}, f, indent=2) + + # ============== Portfolio Management ============== + + def add_position(self, symbol: str, asset_type: str, quantity: float, + avg_cost: float, entry_date: str = "", notes: str = "") -> bool: + """Add a new position to portfolio""" + try: + key = f"{asset_type}_{symbol.upper()}" + self.positions[key] = Position( + symbol=symbol.upper(), + asset_type=asset_type, + quantity=quantity, + avg_cost=avg_cost, + entry_date=entry_date or datetime.datetime.now().strftime('%Y-%m-%d'), + notes=notes + ) + self._save_positions() + return True + except Exception as e: + print(f"Error adding position: {e}") + return False + + def remove_position(self, symbol: str, asset_type: str) -> bool: + """Remove a position from portfolio""" + key = f"{asset_type}_{symbol.upper()}" + if key in self.positions: + del self.positions[key] + self._save_positions() + return True + return False + + def get_positions(self) -> List[Position]: + return list(self.positions.values()) + + # ============== Price Fetching ============== + + def fetch_price(self, symbol: str) -> Optional[PriceData]: + """Fetch current price for a symbol""" + # Placeholder - would use real API + # For demo, generate mock data + import random + mock_price = random.uniform(10000, 500000) + mock_change = random.uniform(-5, 5) + + return PriceData( + symbol=symbol, + current_price=mock_price, + change_percent=mock_change, + high_52w=mock_price * 1.2, + low_52w=mock_price * 0.8, + volume=random.uniform(100000, 10000000), + updated_at=datetime.datetime.now().isoformat() + ) + + def update_prices(self) -> Dict[str, PriceData]: + """Update all prices""" + for key, pos in self.positions.items(): + price = self.fetch_price(pos.symbol) + if price: + self.prices[key] = price + self._save_prices() + return self.prices + + # ============== Performance Calculation ============== + + def calculate_portfolio_summary(self) -> PortfolioSummary: + """Calculate portfolio summary with P&L""" + total_value = 0 + total_cost = 0 + positions_data = [] + + for key, pos in self.positions.items(): + price = self.prices.get(key) + if price: + current_value = pos.quantity * price.current_price + cost_basis = pos.quantity * pos.avg_cost + pnl = current_value - cost_basis + pnl_percent = (pnl / cost_basis) * 100 if cost_basis > 0 else 0 + + positions_data.append({ + 'symbol': pos.symbol, + 'type': pos.asset_type, + 'quantity': pos.quantity, + 'avg_cost': pos.avg_cost, + 'current_price': price.current_price, + 'current_value': current_value, + 'cost_basis': cost_basis, + 'pnl': pnl, + 'pnl_percent': pnl_percent, + 'change_24h': price.change_percent, + '52w_high': price.high_52w, + '52w_low': price.low_52w, + }) + + total_value += current_value + total_cost += cost_basis + + total_pnl = total_value - total_cost + total_pnl_percent = (total_pnl / total_cost) * 100 if total_cost > 0 else 0 + + # Top/Worst performers + sorted_positions = sorted(positions_data, key=lambda x: x['pnl_percent'], reverse=True) + top_performer = sorted_positions[0] if sorted_positions else None + worst_performer = sorted_positions[-1] if sorted_positions else None + + return PortfolioSummary( + total_value=total_value, + total_cost=total_cost, + total_pnl=total_pnl, + total_pnl_percent=total_pnl_percent, + positions=positions_data, + top_performer=top_performer, + worst_performer=worst_performer + ) + + # ============== Investment Recommendations ============== + + def check_investment_guidelines(self, symbol: str) -> Dict: + """ + Check if a stock meets investment guidelines + Reference: ์ฃผ์‹ ํˆฌ์ž ์›์น™ ๊ฐ€์ด๋“œ.md + """ + # Placeholder - would fetch real data + return { + 'symbol': symbol, + 'pbr': None, # Would fetch from data source + 'roe': None, + 'per': None, + 'score': None, + 'recommendation': None, + 'checklist': { + 'story_clear': False, + 'earnings_uptrend': False, + 'balance_sheet_healthy': False, + 'capital_return_plan': False, + 'governance_clean': False, + 'market_liquidity': False, + 'relative_strength': False, + } + } + + def get_recommendation(self) -> List[Dict]: + """Generate investment recommendations based on guidelines""" + # Filter positions that meet criteria + recommendations = [] + + for key, pos in self.positions.items(): + if pos.asset_type == 'stock': + analysis = self.check_investment_guidelines(pos.symbol) + recommendations.append({ + 'symbol': pos.symbol, + 'action': 'HOLD', + 'reason': 'Review weekly', + 'checklist_score': f"{sum(analysis['checklist'].values())}/7", + 'pnl_percent': self._get_position_pnl(key) + }) + + return recommendations + + def _get_position_pnl(self, key: str) -> float: + pos = self.positions.get(key) + price = self.prices.get(key) + if pos and price: + return ((price.current_price - pos.avg_cost) / pos.avg_cost) * 100 + return 0 + + # ============== Reporting ============== + + def generate_daily_report(self) -> str: + """Generate daily portfolio report""" + summary = self.calculate_portfolio_summary() + + report = f""" +๐Ÿ“Š **์ผ์ผ ํฌํŠธํด๋ฆฌ์˜ค ๋ฆฌํฌํŠธ** +**Date:** {datetime.datetime.now().strftime('%Y-%m-%d')} + +๐Ÿ’ฐ **์ „์ฒด ํ˜„ํ™ฉ** +- ํ˜„์žฌ ๊ฐ€์น˜: โ‚ฉ{summary.total_value:,.0f} +- ํˆฌ์ž ์›๊ธˆ: โ‚ฉ{summary.total_cost:,.0f} +- ์ด ์†์ต: โ‚ฉ{summary.total_pnl:,.0f} ({summary.total_pnl_percent:+.1f}%) + +๐Ÿ“ˆ **์ƒ์œ„ ์ˆ˜์ต** +""" + + for pos in sorted(summary.positions, key=lambda x: x['pnl_percent'], reverse=True)[:3]: + emoji = "๐ŸŸข" if pos['pnl_percent'] > 0 else "๐Ÿ”ด" + report += f"- {emoji} **{pos['symbol']}**: {pos['pnl_percent']:+.1f}% (โ‚ฉ{pos['pnl']:,.0f})\n" + + report += "\n๐Ÿ’ก **ํˆฌ์ž ์›์น™ ์ฒดํฌ**\n" + report += "- โฌœ 3๋…„ ์‹ค์  ์šฐ์ƒํ–ฅ ํ™•์ธ\n" + report += "- โฌœ PBR < 1 ํ™•์ธ\n" + report += "- โฌœ ์ถ”์  ์†์ ˆ โ€“10% ์„ค์ •\n" + report += "- โฌœ ์ฃผ 1ํšŒ ์ ๊ฒ€ ์˜ˆ์ •\n" + + return report + + def generate_weekly_report(self) -> str: + """Generate weekly portfolio report""" + summary = self.calculate_portfolio_summary() + + report = f""" +๐Ÿ“ˆ **์ฃผ๊ฐ„ ํฌํŠธํด๋ฆฌ์˜ค ๋ฆฌํฌํŠธ** +**Week:** {datetime.datetime.now().strftime('%Y-%W')} + +๐ŸŽฏ **์ด๋ฒˆ ์ฃผ ๋ชฉํ‘œ** +- [ ] ์‹œ์žฅยท์„นํ„ฐ ์ƒ๋Œ€๊ฐ•๋„ Top/Bottom 5 ํ™•์ธ +- [ ] ๊ด€์ฐฐ๋ชฉ๋ก ์ฒดํฌ๋ฆฌ์ŠคํŠธ ์žฌ์ ์šฉ +- [ ] ์—”ํŠธ๋ฆฌยท์†์ ˆยท์ถ”์ ์†์ ˆ ๊ฐ€๊ฒฉ ๊ธฐ์ž… +- [ ] ํŠธ๋ ˆ์ด๋“œ ๋กœ๊ทธ ์ž‘์„ฑ + +๐Ÿ’ฐ **ํฌํŠธํด๋ฆฌ์˜ค ํ˜„ํ™ฉ** +| ํ•ญ๋ชฉ | ์ˆ˜์น˜ | +|------|------| +| ์ด ๊ฐ€์น˜ | โ‚ฉ{summary.total_value:,.0f} | +| ์ด ์ˆ˜์ต๋ฅ  | {summary.total_pnl_percent:+.1f}% | +| ๋ฒ ์ŠคํŠธ | {summary.top_performer['symbol'] if summary.top_performer else 'N/A'} ({summary.top_performer['pnl_percent'] if summary.top_performer else 0:+.1f}%) | +| ์›Œ์ŠคํŠธ | {summary.worst_performer['symbol'] if summary.worst_performer else 'N/A'} ({summary.worst_performer['pnl_percent'] if summary.worst_performer else 0:+.1f}%) | + +๐Ÿ“‹ **์ฒดํฌ๋ฆฌ์ŠคํŠธ ์ดํ–‰** +- [ ] ๊ฐ€์น˜ > ๊ฐ€๊ฒฉ ํ™•์ธ +- [ ] โ€“10% ์†์ ˆ ๊ทœ์น™ ์ ์šฉ +- [ ] ํ•ต์‹ฌ 2~5์ข…๋ชฉ ์ง‘์ค‘ ํ™•์ธ + """ + + return report + +# ============== CLI Interface ============== + +def main(): + import argparse + + parser = argparse.ArgumentParser(description='Stock & Crypto Portfolio Tracker') + subparsers = parser.add_subparsers(dest='command', help='Available commands') + + # Add position + add_parser = subparsers.add_parser('add', help='Add a position') + add_parser.add_argument('--symbol', required=True) + add_parser.add_argument('--type', required=True, choices=['stock', 'crypto', 'etf']) + add_parser.add_argument('--quantity', type=float, required=True) + add_parser.add_argument('--cost', type=float, required=True) + + # Show portfolio + subparsers.add_parser('show', help='Show portfolio summary') + + # Update prices + subparsers.add_parser('update', help='Update prices from market') + + # Daily report + subparsers.add_parser('daily', help='Generate daily report') + + # Weekly report + subparsers.add_parser('weekly', help='Generate weekly report') + + args = parser.parse_args() + + tracker = StockTracker() + + if args.command == 'add': + tracker.add_position(args.symbol, args.type, args.quantity, args.cost) + print(f"โœ… Added {args.quantity} {args.symbol} @ โ‚ฉ{args.cost}") + + elif args.command == 'show': + summary = tracker.calculate_portfolio_summary() + print(f"\n๐Ÿ“Š Portfolio Summary") + print(f"Total Value: โ‚ฉ{summary.total_value:,.0f}") + print(f"Total Cost: โ‚ฉ{summary.total_cost:,.0f}") + print(f"P&L: โ‚ฉ{summary.total_pnl:,.0f} ({summary.total_pnl_percent:+.1f}%)") + print(f"\nPositions ({len(summary.positions)}):") + for pos in summary.positions: + print(f" {pos['symbol']}: {pos['quantity']} @ โ‚ฉ{pos['avg_cost']:,.0f} โ†’ โ‚ฉ{pos['current_price']:,.0f} ({pos['pnl_percent']:+.1f}%)") + + elif args.command == 'update': + prices = tracker.update_prices() + print(f"โœ… Updated {len(prices)} prices") + + elif args.command == 'daily': + print(tracker.generate_daily_report()) + + elif args.command == 'weekly': + print(tracker.generate_weekly_report()) + + else: + parser.print_help() + +if __name__ == '__main__': + main() diff --git a/test_requirements.txt b/test_requirements.txt new file mode 100644 index 0000000..2d71c83 --- /dev/null +++ b/test_requirements.txt @@ -0,0 +1,25 @@ +# Test Dependencies +pytest>=7.0.0 +pytest-cov>=4.0.0 +pytest-mock>=3.10.0 +responses>=0.23.0 +httpx>=0.25.0 + +# Code Quality +flake8>=6.0.0 +flake8-docstrings>=1.7.0 +flake8-builtins>=2.0.0 +flake8-comprehensions>=3.12.0 +flake8-logging-format>=0.9.0 +pylint>=2.17.0 +bandit>=1.7.0 +safety>=2.3.0 +vulture>=2.7.0 +mypy>=1.5.0 +black>=23.0.0 +isort>=5.12.0 +pre-commit>=3.5.0 + +# Coverage +coverage>=7.0.0 +coveralls>=3.3.0