feat: Add major feature updates - integrations, services, and utilities

- Add Google Calendar integration with OAuth 2.0 support
- Implement integration service and workflow engine
- Add new routes: auth, clients, custom_reports, integrations, invoices, team_chat
- Add utility modules: config_manager, email, excel_export, file_upload, permissions_seed
- Add integration view template
- Add Docker permission fixes and enhanced start scripts
- Add migration management utilities and legacy schema migration
- Add validation and version management scripts
- Update setup.py version to 4.9.16

This release significantly expands the application's integration capabilities,
adds new business logic services, and improves infrastructure tooling.
This commit is contained in:
Dries Peeters
2026-01-09 22:42:53 +01:00
parent 98bc9af56b
commit 4a8607f400
26 changed files with 1207 additions and 172 deletions
+20 -5
View File
@@ -73,11 +73,26 @@ def check_python_package(package_name):
def run_command(command, description):
"""Run a command and check if it succeeds"""
"""Run a command and check if it succeeds
Args:
command: Command string or list of command arguments
description: Human-readable description
"""
import shlex
try:
# If command is a string, split it safely
if isinstance(command, str):
try:
cmd_list = shlex.split(command)
except ValueError:
# Fallback to simple split
cmd_list = command.split()
else:
cmd_list = command
result = subprocess.run(
command,
shell=True,
cmd_list,
capture_output=True,
text=True,
timeout=30
@@ -197,11 +212,11 @@ def main():
print_header("7. Quick Test Validation")
# Check if pytest can discover tests
if run_command('pytest --collect-only -q', 'Test discovery'):
if run_command(['pytest', '--collect-only', '-q'], 'Test discovery'):
print_info("Tests can be discovered successfully")
# Try to run smoke tests (if they exist)
if run_command('pytest -m smoke --co -q', 'Smoke test discovery'):
if run_command(['pytest', '-m', 'smoke', '--co', '-q'], 'Smoke test discovery'):
print_info("Smoke tests are properly marked")
# 8. Check Docker setup
+67 -29
View File
@@ -10,52 +10,75 @@ import subprocess
import argparse
from datetime import datetime
import re
import shlex
class VersionManager:
def __init__(self):
self.repo_path = os.getcwd()
def run_command(self, command, capture_output=True):
"""Run a shell command and return the result"""
"""Run a command and return the result
Args:
command: Command string or list of command arguments
capture_output: Whether to capture output
"""
try:
# If command is a string, split it safely
if isinstance(command, str):
# For git commands with complex quoting, use shlex
try:
cmd_list = shlex.split(command)
except ValueError:
# Fallback to simple split
cmd_list = command.split()
else:
cmd_list = command
result = subprocess.run(
command,
shell=True,
cmd_list,
capture_output=capture_output,
text=True,
cwd=self.repo_path
)
if result.returncode != 0:
print(f"Error running command: {command}")
print(f"Error: {result.stderr}")
print(f"Error running command: {' '.join(cmd_list)}")
if hasattr(result, 'stderr') and result.stderr:
print(f"Error: {result.stderr}")
return None
return result.stdout.strip() if capture_output else result
return result.stdout.strip() if capture_output and hasattr(result, 'stdout') else result
except Exception as e:
print(f"Exception running command: {e}")
return None
def get_current_branch(self):
"""Get the current git branch"""
return self.run_command("git branch --show-current")
return self.run_command(['git', 'branch', '--show-current'])
def get_latest_tag(self):
"""Get the latest git tag"""
return self.run_command("git describe --tags --abbrev=0 2>/dev/null || echo 'none'")
result = self.run_command(['git', 'describe', '--tags', '--abbrev=0'])
if result:
return result
# If command fails (no tags), return 'none'
return 'none'
def get_commit_count(self):
"""Get the number of commits since the last tag"""
latest_tag = self.get_latest_tag()
if latest_tag == 'none':
return self.run_command("git rev-list --count HEAD")
return self.run_command(['git', 'rev-list', '--count', 'HEAD'])
else:
return self.run_command(f"git rev-list --count {latest_tag}..HEAD")
# Sanitize tag name to prevent command injection
safe_tag = re.sub(r'[^a-zA-Z0-9._/-]', '', latest_tag)
return self.run_command(['git', 'rev-list', '--count', f'{safe_tag}..HEAD'])
def get_commit_hash(self, short=True):
"""Get the current commit hash"""
if short:
return self.run_command("git rev-parse --short HEAD")
return self.run_command(['git', 'rev-parse', '--short', 'HEAD'])
else:
return self.run_command("git rev-parse HEAD")
return self.run_command(['git', 'rev-parse', 'HEAD'])
def validate_version_format(self, version):
"""Validate version format"""
@@ -118,8 +141,10 @@ class VersionManager:
print(f"Creating tag: {version}")
print(f"Message: {message}")
# Create the tag
if not self.run_command(f'git tag -a "{version}" -m "{message}"', capture_output=False):
# Create the tag using list to avoid shell injection
# Version and message are already validated/sanitized
tag_cmd = ['git', 'tag', '-a', version, '-m', message]
if not self.run_command(tag_cmd, capture_output=False):
print("Failed to create tag")
return False
@@ -128,7 +153,8 @@ class VersionManager:
# Push tag if requested
if push:
print("Pushing tag to remote...")
if not self.run_command(f'git push origin "{version}"', capture_output=False):
push_cmd = ['git', 'push', 'origin', version]
if not self.run_command(push_cmd, capture_output=False):
print("Failed to push tag to remote")
return False
print(f"✓ Tag '{version}' pushed to remote")
@@ -149,7 +175,7 @@ class VersionManager:
def list_tags(self):
"""List all tags"""
tags = self.run_command("git tag --sort=-version:refname")
tags = self.run_command(['git', 'tag', '--sort=-version:refname'])
if tags:
print("Available tags:")
for tag in tags.split('\n'):
@@ -167,15 +193,18 @@ class VersionManager:
print("No tags found")
return
print(f"Tag: {tag}")
print(f"Commit: {self.run_command(f'git rev-parse {tag}')}")
print(f"Date: {self.run_command(f'git log -1 --format=%cd {tag}')}")
print(f"Message: {self.run_command(f'git log -1 --format=%s {tag}')}")
# Sanitize tag name to prevent command injection
safe_tag = re.sub(r'[^a-zA-Z0-9._/-]', '', tag)
print(f"Tag: {safe_tag}")
print(f"Commit: {self.run_command(['git', 'rev-parse', safe_tag])}")
print(f"Date: {self.run_command(['git', 'log', '-1', '--format=%cd', safe_tag])}")
print(f"Message: {self.run_command(['git', 'log', '-1', '--format=%s', safe_tag])}")
# Show commits since this tag
commits_since = self.run_command(f'git log --oneline {tag}..HEAD')
commits_since = self.run_command(['git', 'log', '--oneline', f'{safe_tag}..HEAD'])
if commits_since:
print(f"\nCommits since {tag}:")
print(f"\nCommits since {safe_tag}:")
for commit in commits_since.split('\n')[:10]: # Show last 10 commits
if commit.strip():
print(f" {commit}")
@@ -260,7 +289,9 @@ def main():
# Generate changelog if requested
if args.changelog:
print("📋 Generating changelog...")
changelog_cmd = f"python scripts/generate-changelog.py {args.version}"
# Sanitize version before use
safe_version = re.sub(r'[^a-zA-Z0-9._/-]', '', args.version)
changelog_cmd = ['python', 'scripts/generate-changelog.py', safe_version]
if vm.run_command(changelog_cmd, capture_output=False):
print("✅ Changelog generated successfully")
else:
@@ -269,13 +300,17 @@ def main():
# Create GitHub release if requested
if args.github_release:
print("🐙 Creating GitHub release...")
github_cmd = f"gh release create {args.version}"
# Sanitize version before use
safe_version = re.sub(r'[^a-zA-Z0-9._/-]', '', args.version)
github_cmd = ['gh', 'release', 'create', safe_version]
if args.pre_release:
github_cmd += " --prerelease"
github_cmd.append('--prerelease')
if args.changelog and os.path.exists("CHANGELOG.md"):
github_cmd += " --notes-file CHANGELOG.md"
github_cmd.extend(['--notes-file', 'CHANGELOG.md'])
elif args.message:
github_cmd += f" --notes '{args.message}'"
# Sanitize message to prevent command injection
safe_message = args.message.replace("'", "'\"'\"'")
github_cmd.extend(['--notes', safe_message])
if vm.run_command(github_cmd, capture_output=False):
print("✅ GitHub release created successfully")
@@ -289,8 +324,11 @@ def main():
current_tag = vm.get_latest_tag()
version = args.version or vm.suggest_next_version(current_tag)
print(f"📋 Generating changelog for {version}...")
changelog_cmd = f"python scripts/generate-changelog.py {version}"
# Sanitize version before use
safe_version = re.sub(r'[^a-zA-Z0-9._/-]', '', version)
print(f"📋 Generating changelog for {safe_version}...")
changelog_cmd = ['python', 'scripts/generate-changelog.py', safe_version]
if vm.run_command(changelog_cmd, capture_output=False):
print("✅ Changelog generated successfully")
else: