Spaces:
Running
Running
dboa9 commited on
Commit ·
134c04b
1
Parent(s): 3e21203
updated doc corrections/edits & hugin face updates
Browse files- Dockerfile +0 -70
- shared/jira_adapter.py +0 -295
Dockerfile
DELETED
|
@@ -1,70 +0,0 @@
|
|
| 1 |
-
# Moltbot Hybrid Engine - Multi-service Dockerfile
|
| 2 |
-
# Runs: FastAPI (port 7860) + Ollama (port 11434, background)
|
| 3 |
-
# Build: 2026-02-06 v5.0
|
| 4 |
-
# Dev Mode compatible per HF docs
|
| 5 |
-
# FIX: Install Ollama as root BEFORE switching to user
|
| 6 |
-
|
| 7 |
-
FROM python:3.11-slim
|
| 8 |
-
|
| 9 |
-
# Install ALL packages required for HF Spaces Dev Mode + our needs
|
| 10 |
-
RUN apt-get update && apt-get install -y --no-install-recommends \
|
| 11 |
-
bash \
|
| 12 |
-
curl \
|
| 13 |
-
wget \
|
| 14 |
-
procps \
|
| 15 |
-
git \
|
| 16 |
-
git-lfs \
|
| 17 |
-
&& apt-get clean \
|
| 18 |
-
&& rm -rf /var/lib/apt/lists/*
|
| 19 |
-
|
| 20 |
-
# Install Ollama AS ROOT - detect architecture and download correct binary
|
| 21 |
-
# The install.sh may fail in Docker, so we do direct download with arch detection
|
| 22 |
-
RUN ARCH=$(uname -m) && \
|
| 23 |
-
echo "Detected architecture: $ARCH" && \
|
| 24 |
-
if [ "$ARCH" = "x86_64" ] || [ "$ARCH" = "amd64" ]; then \
|
| 25 |
-
OLLAMA_URL="https://ollama.com/download/ollama-linux-amd64"; \
|
| 26 |
-
elif [ "$ARCH" = "aarch64" ] || [ "$ARCH" = "arm64" ]; then \
|
| 27 |
-
OLLAMA_URL="https://ollama.com/download/ollama-linux-arm64"; \
|
| 28 |
-
else \
|
| 29 |
-
echo "Unknown architecture: $ARCH, trying amd64"; \
|
| 30 |
-
OLLAMA_URL="https://ollama.com/download/ollama-linux-amd64"; \
|
| 31 |
-
fi && \
|
| 32 |
-
echo "Downloading Ollama from: $OLLAMA_URL" && \
|
| 33 |
-
curl -fSL "$OLLAMA_URL" -o /usr/local/bin/ollama && \
|
| 34 |
-
chmod +x /usr/local/bin/ollama && \
|
| 35 |
-
echo "Ollama binary size: $(du -h /usr/local/bin/ollama | cut -f1)" && \
|
| 36 |
-
ls -la /usr/local/bin/ollama
|
| 37 |
-
|
| 38 |
-
# Create HF-required user (uid 1000)
|
| 39 |
-
RUN useradd -m -u 1000 user
|
| 40 |
-
|
| 41 |
-
# Create Ollama model storage directory owned by user
|
| 42 |
-
RUN mkdir -p /home/user/ollama_models && chown -R user:user /home/user/ollama_models
|
| 43 |
-
|
| 44 |
-
# Switch to user
|
| 45 |
-
USER user
|
| 46 |
-
ENV HOME=/home/user \
|
| 47 |
-
PATH=/home/user/.local/bin:/usr/local/bin:$PATH \
|
| 48 |
-
OLLAMA_MODELS=/home/user/ollama_models \
|
| 49 |
-
OLLAMA_HOST=0.0.0.0
|
| 50 |
-
|
| 51 |
-
# Set working directory to /app (required for dev mode)
|
| 52 |
-
WORKDIR /app
|
| 53 |
-
|
| 54 |
-
# Upgrade pip
|
| 55 |
-
RUN pip install --no-cache-dir --upgrade pip
|
| 56 |
-
|
| 57 |
-
# Copy all files with correct ownership
|
| 58 |
-
COPY --chown=user . /app
|
| 59 |
-
|
| 60 |
-
# Install Python dependencies
|
| 61 |
-
RUN pip install --no-cache-dir -r requirements.txt
|
| 62 |
-
|
| 63 |
-
# Make start script executable
|
| 64 |
-
RUN chmod +x start.sh
|
| 65 |
-
|
| 66 |
-
# Expose HF Spaces port
|
| 67 |
-
EXPOSE 7860
|
| 68 |
-
|
| 69 |
-
# CMD required (not ENTRYPOINT) for dev mode compatibility
|
| 70 |
-
CMD ["./start.sh"]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
shared/jira_adapter.py
DELETED
|
@@ -1,295 +0,0 @@
|
|
| 1 |
-
"""
|
| 2 |
-
JIRA & CONFLUENCE ADAPTER
|
| 3 |
-
=========================
|
| 4 |
-
Zero-risk adapter for Jira and Confluence integration.
|
| 5 |
-
Loads credentials from environment variables or config files.
|
| 6 |
-
|
| 7 |
-
Usage:
|
| 8 |
-
from shared import JiraAdapter, ConfluenceAdapter
|
| 9 |
-
|
| 10 |
-
jira = JiraAdapter()
|
| 11 |
-
jira.add_comment("COURT-123", "Bundle generated successfully")
|
| 12 |
-
|
| 13 |
-
confluence = ConfluenceAdapter()
|
| 14 |
-
confluence.update_page("123456", "New content")
|
| 15 |
-
|
| 16 |
-
Created: 2026-02-04
|
| 17 |
-
"""
|
| 18 |
-
|
| 19 |
-
import os
|
| 20 |
-
import re
|
| 21 |
-
from pathlib import Path
|
| 22 |
-
from typing import Optional, Dict, Any
|
| 23 |
-
|
| 24 |
-
# Try to import jira library
|
| 25 |
-
try:
|
| 26 |
-
from jira import JIRA
|
| 27 |
-
JIRA_AVAILABLE = True
|
| 28 |
-
except ImportError:
|
| 29 |
-
JIRA = None
|
| 30 |
-
JIRA_AVAILABLE = False
|
| 31 |
-
|
| 32 |
-
# Try to import atlassian library for Confluence
|
| 33 |
-
try:
|
| 34 |
-
from atlassian import Confluence
|
| 35 |
-
CONFLUENCE_AVAILABLE = True
|
| 36 |
-
except ImportError:
|
| 37 |
-
Confluence = None
|
| 38 |
-
CONFLUENCE_AVAILABLE = False
|
| 39 |
-
|
| 40 |
-
|
| 41 |
-
def _load_config_from_files() -> Dict[str, str]:
|
| 42 |
-
"""
|
| 43 |
-
Load configuration from ~/.bashrc or ~/.secure/api_keys.
|
| 44 |
-
Returns dict with JIRA_URL, JIRA_EMAIL, JIRA_TOKEN, CONFLUENCE_URL.
|
| 45 |
-
"""
|
| 46 |
-
config = {}
|
| 47 |
-
|
| 48 |
-
# Files to search for credentials
|
| 49 |
-
config_files = [
|
| 50 |
-
Path.home() / ".bashrc",
|
| 51 |
-
Path.home() / ".secure" / "api_keys",
|
| 52 |
-
Path.home() / ".env",
|
| 53 |
-
]
|
| 54 |
-
|
| 55 |
-
patterns = {
|
| 56 |
-
'JIRA_URL': r'export\s+JIRA_URL\s*=\s*["\']?([^"\';\n]+)',
|
| 57 |
-
'JIRA_EMAIL': r'export\s+JIRA_EMAIL\s*=\s*["\']?([^"\';\n]+)',
|
| 58 |
-
'JIRA_TOKEN': r'export\s+JIRA_TOKEN\s*=\s*["\']?([^"\';\n]+)',
|
| 59 |
-
'CONFLUENCE_URL': r'export\s+CONFLUENCE_URL\s*=\s*["\']?([^"\';\n]+)',
|
| 60 |
-
}
|
| 61 |
-
|
| 62 |
-
for config_file in config_files:
|
| 63 |
-
if config_file.exists():
|
| 64 |
-
try:
|
| 65 |
-
content = config_file.read_text()
|
| 66 |
-
for key, pattern in patterns.items():
|
| 67 |
-
if key not in config or not config[key]:
|
| 68 |
-
match = re.search(pattern, content)
|
| 69 |
-
if match:
|
| 70 |
-
config[key] = match.group(1).strip()
|
| 71 |
-
except Exception:
|
| 72 |
-
continue
|
| 73 |
-
|
| 74 |
-
return config
|
| 75 |
-
|
| 76 |
-
|
| 77 |
-
class JiraAdapter:
|
| 78 |
-
"""
|
| 79 |
-
Robust Jira adapter with automatic credential loading.
|
| 80 |
-
"""
|
| 81 |
-
|
| 82 |
-
def __init__(self, url: str = None, email: str = None, token: str = None):
|
| 83 |
-
"""
|
| 84 |
-
Initialize Jira connection.
|
| 85 |
-
Credentials loaded from: args > env vars > config files
|
| 86 |
-
"""
|
| 87 |
-
# Load from config files first as fallback
|
| 88 |
-
file_config = _load_config_from_files()
|
| 89 |
-
|
| 90 |
-
# Priority: args > env vars > config files
|
| 91 |
-
self.url = url or os.environ.get('JIRA_URL') or file_config.get('JIRA_URL')
|
| 92 |
-
self.email = email or os.environ.get('JIRA_EMAIL') or file_config.get('JIRA_EMAIL')
|
| 93 |
-
self.token = token or os.environ.get('JIRA_TOKEN') or file_config.get('JIRA_TOKEN')
|
| 94 |
-
|
| 95 |
-
self.client = None
|
| 96 |
-
self.connected = False
|
| 97 |
-
self.error_message = None
|
| 98 |
-
|
| 99 |
-
# Attempt connection
|
| 100 |
-
if self.url and self.email and self.token and JIRA_AVAILABLE:
|
| 101 |
-
try:
|
| 102 |
-
self.client = JIRA(
|
| 103 |
-
server=self.url,
|
| 104 |
-
basic_auth=(self.email, self.token)
|
| 105 |
-
)
|
| 106 |
-
self.connected = True
|
| 107 |
-
print(f"✅ [JiraAdapter] Connected to {self.url}")
|
| 108 |
-
except Exception as e:
|
| 109 |
-
self.error_message = str(e)
|
| 110 |
-
print(f"⚠️ [JiraAdapter] Connection failed: {e}")
|
| 111 |
-
else:
|
| 112 |
-
missing = []
|
| 113 |
-
if not self.url:
|
| 114 |
-
missing.append('JIRA_URL')
|
| 115 |
-
if not self.email:
|
| 116 |
-
missing.append('JIRA_EMAIL')
|
| 117 |
-
if not self.token:
|
| 118 |
-
missing.append('JIRA_TOKEN')
|
| 119 |
-
if not JIRA_AVAILABLE:
|
| 120 |
-
missing.append('jira library')
|
| 121 |
-
self.error_message = f"Missing: {', '.join(missing)}"
|
| 122 |
-
print(f"⚠️ [JiraAdapter] Not configured: {self.error_message}")
|
| 123 |
-
|
| 124 |
-
def add_comment(self, issue_key: str, comment: str) -> bool:
|
| 125 |
-
"""Add a comment to a Jira issue."""
|
| 126 |
-
if not self.connected:
|
| 127 |
-
print(f"⚠️ [JiraAdapter] Cannot add comment - not connected")
|
| 128 |
-
return False
|
| 129 |
-
|
| 130 |
-
try:
|
| 131 |
-
self.client.add_comment(issue_key, comment)
|
| 132 |
-
print(f"✅ [JiraAdapter] Comment added to {issue_key}")
|
| 133 |
-
return True
|
| 134 |
-
except Exception as e:
|
| 135 |
-
print(f"❌ [JiraAdapter] Failed to add comment to {issue_key}: {e}")
|
| 136 |
-
return False
|
| 137 |
-
|
| 138 |
-
def update_status(self, issue_key: str, status: str) -> bool:
|
| 139 |
-
"""Update the status of a Jira issue."""
|
| 140 |
-
if not self.connected:
|
| 141 |
-
return False
|
| 142 |
-
|
| 143 |
-
try:
|
| 144 |
-
transitions = self.client.transitions(issue_key)
|
| 145 |
-
for t in transitions:
|
| 146 |
-
if t['name'].lower() == status.lower():
|
| 147 |
-
self.client.transition_issue(issue_key, t['id'])
|
| 148 |
-
print(f"✅ [JiraAdapter] Status updated to '{status}' for {issue_key}")
|
| 149 |
-
return True
|
| 150 |
-
print(f"⚠️ [JiraAdapter] Status '{status}' not found for {issue_key}")
|
| 151 |
-
return False
|
| 152 |
-
except Exception as e:
|
| 153 |
-
print(f"❌ [JiraAdapter] Failed to update status: {e}")
|
| 154 |
-
return False
|
| 155 |
-
|
| 156 |
-
def create_issue(self, project: str, summary: str, description: str = "",
|
| 157 |
-
issue_type: str = "Task") -> Optional[str]:
|
| 158 |
-
"""Create a new Jira issue. Returns issue key or None."""
|
| 159 |
-
if not self.connected:
|
| 160 |
-
return None
|
| 161 |
-
|
| 162 |
-
try:
|
| 163 |
-
issue = self.client.create_issue(
|
| 164 |
-
project=project,
|
| 165 |
-
summary=summary,
|
| 166 |
-
description=description,
|
| 167 |
-
issuetype={'name': issue_type}
|
| 168 |
-
)
|
| 169 |
-
print(f"✅ [JiraAdapter] Created issue {issue.key}")
|
| 170 |
-
return issue.key
|
| 171 |
-
except Exception as e:
|
| 172 |
-
print(f"❌ [JiraAdapter] Failed to create issue: {e}")
|
| 173 |
-
return None
|
| 174 |
-
|
| 175 |
-
|
| 176 |
-
def is_connected(self) -> bool:
|
| 177 |
-
"""Check if JIRA connection is active (Added by Auto-Fix)."""
|
| 178 |
-
return self.connected and self.client is not None
|
| 179 |
-
|
| 180 |
-
def get_issue(self, issue_key: str) -> Optional[Dict[str, Any]]:
|
| 181 |
-
"""Get issue details."""
|
| 182 |
-
if not self.connected:
|
| 183 |
-
return None
|
| 184 |
-
|
| 185 |
-
try:
|
| 186 |
-
issue = self.client.issue(issue_key)
|
| 187 |
-
return {
|
| 188 |
-
'key': issue.key,
|
| 189 |
-
'summary': issue.fields.summary,
|
| 190 |
-
'status': issue.fields.status.name,
|
| 191 |
-
'description': issue.fields.description
|
| 192 |
-
}
|
| 193 |
-
except Exception as e:
|
| 194 |
-
print(f"❌ [JiraAdapter] Failed to get issue {issue_key}: {e}")
|
| 195 |
-
return None
|
| 196 |
-
|
| 197 |
-
|
| 198 |
-
class ConfluenceAdapter:
|
| 199 |
-
"""
|
| 200 |
-
Robust Confluence adapter with automatic credential loading.
|
| 201 |
-
"""
|
| 202 |
-
|
| 203 |
-
def __init__(self, url: str = None, email: str = None, token: str = None):
|
| 204 |
-
"""
|
| 205 |
-
Initialize Confluence connection.
|
| 206 |
-
"""
|
| 207 |
-
file_config = _load_config_from_files()
|
| 208 |
-
|
| 209 |
-
self.url = url or os.environ.get('CONFLUENCE_URL') or file_config.get('CONFLUENCE_URL')
|
| 210 |
-
self.email = email or os.environ.get('JIRA_EMAIL') or file_config.get('JIRA_EMAIL')
|
| 211 |
-
self.token = token or os.environ.get('JIRA_TOKEN') or file_config.get('JIRA_TOKEN')
|
| 212 |
-
|
| 213 |
-
self.client = None
|
| 214 |
-
self.connected = False
|
| 215 |
-
|
| 216 |
-
if self.url and self.email and self.token and CONFLUENCE_AVAILABLE:
|
| 217 |
-
try:
|
| 218 |
-
self.client = Confluence(
|
| 219 |
-
url=self.url,
|
| 220 |
-
username=self.email,
|
| 221 |
-
password=self.token,
|
| 222 |
-
cloud=True
|
| 223 |
-
)
|
| 224 |
-
self.connected = True
|
| 225 |
-
print(f"✅ [ConfluenceAdapter] Connected to {self.url}")
|
| 226 |
-
except Exception as e:
|
| 227 |
-
print(f"⚠️ [ConfluenceAdapter] Connection failed: {e}")
|
| 228 |
-
else:
|
| 229 |
-
print(f"⚠️ [ConfluenceAdapter] Not configured")
|
| 230 |
-
|
| 231 |
-
def is_connected(self) -> bool:
|
| 232 |
-
"""Check if Confluence connection is active."""
|
| 233 |
-
return self.connected and self.client is not None
|
| 234 |
-
|
| 235 |
-
def update_page(self, page_id: str, content: str, title: str = None) -> bool:
|
| 236 |
-
"""Update a Confluence page."""
|
| 237 |
-
if not self.connected:
|
| 238 |
-
return False
|
| 239 |
-
|
| 240 |
-
try:
|
| 241 |
-
page = self.client.get_page_by_id(page_id)
|
| 242 |
-
current_title = title or page['title']
|
| 243 |
-
|
| 244 |
-
self.client.update_page(
|
| 245 |
-
page_id=page_id,
|
| 246 |
-
title=current_title,
|
| 247 |
-
body=content
|
| 248 |
-
)
|
| 249 |
-
print(f"✅ [ConfluenceAdapter] Updated page {page_id}")
|
| 250 |
-
return True
|
| 251 |
-
except Exception as e:
|
| 252 |
-
print(f"❌ [ConfluenceAdapter] Failed to update page: {e}")
|
| 253 |
-
return False
|
| 254 |
-
|
| 255 |
-
def get_page_content(self, page_id: str) -> Optional[str]:
|
| 256 |
-
"""Get Confluence page content."""
|
| 257 |
-
if not self.connected:
|
| 258 |
-
return None
|
| 259 |
-
|
| 260 |
-
try:
|
| 261 |
-
page = self.client.get_page_by_id(page_id, expand='body.storage')
|
| 262 |
-
return page['body']['storage']['value']
|
| 263 |
-
except Exception as e:
|
| 264 |
-
print(f"❌ [ConfluenceAdapter] Failed to get page: {e}")
|
| 265 |
-
return None
|
| 266 |
-
|
| 267 |
-
|
| 268 |
-
def diagnose():
|
| 269 |
-
"""Run diagnostic check on Jira/Confluence connectivity."""
|
| 270 |
-
print("=" * 60)
|
| 271 |
-
print("JIRA/CONFLUENCE ADAPTER DIAGNOSTIC")
|
| 272 |
-
print("=" * 60)
|
| 273 |
-
|
| 274 |
-
config = _load_config_from_files()
|
| 275 |
-
print(f"\nConfig from files:")
|
| 276 |
-
for k, v in config.items():
|
| 277 |
-
masked = v[:10] + "..." if v and len(v) > 10 else v
|
| 278 |
-
print(f" {k}: {masked}")
|
| 279 |
-
|
| 280 |
-
print(f"\nJira library available: {JIRA_AVAILABLE}")
|
| 281 |
-
print(f"Confluence library available: {CONFLUENCE_AVAILABLE}")
|
| 282 |
-
|
| 283 |
-
print("\nTesting Jira connection...")
|
| 284 |
-
jira = JiraAdapter()
|
| 285 |
-
print(f" Connected: {jira.connected}")
|
| 286 |
-
|
| 287 |
-
print("\nTesting Confluence connection...")
|
| 288 |
-
confluence = ConfluenceAdapter()
|
| 289 |
-
print(f" Connected: {confluence.connected}")
|
| 290 |
-
|
| 291 |
-
print("=" * 60)
|
| 292 |
-
|
| 293 |
-
|
| 294 |
-
if __name__ == "__main__":
|
| 295 |
-
diagnose()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|