grasant commited on
Commit
a0debed
·
verified ·
1 Parent(s): 9965336

Upload 15 files

Browse files
Files changed (15) hide show
  1. .dockerignore +68 -0
  2. .env.example +28 -0
  3. Dockerfile +39 -0
  4. README.md +636 -0
  5. app.py +93 -0
  6. config.py +129 -0
  7. docs/client_examples.md +597 -0
  8. logo.png +0 -0
  9. logo_1024.png +0 -0
  10. models.py +96 -0
  11. proportion_server.py +361 -0
  12. pyproject.toml +63 -0
  13. requirements.txt +9 -0
  14. styles.css +148 -0
  15. tests/test_tools.py +428 -0
.dockerignore ADDED
@@ -0,0 +1,68 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Python
2
+ __pycache__/
3
+ *.py[cod]
4
+ *$py.class
5
+ *.so
6
+ .Python
7
+ build/
8
+ develop-eggs/
9
+ dist/
10
+ downloads/
11
+ eggs/
12
+ .eggs/
13
+ lib/
14
+ lib64/
15
+ parts/
16
+ sdist/
17
+ var/
18
+ wheels/
19
+ *.egg-info/
20
+ .installed.cfg
21
+ *.egg
22
+
23
+ # Virtual environments
24
+ .env
25
+ .venv
26
+ env/
27
+ venv/
28
+ ENV/
29
+ env.bak/
30
+ venv.bak/
31
+
32
+ # IDE
33
+ .vscode/
34
+ .idea/
35
+ *.swp
36
+ *.swo
37
+ *~
38
+
39
+ # Testing
40
+ .pytest_cache/
41
+ .coverage
42
+ htmlcov/
43
+ .tox/
44
+ .cache
45
+ nosetests.xml
46
+ coverage.xml
47
+ *.cover
48
+ .hypothesis/
49
+
50
+ # Documentation
51
+ docs/_build/
52
+
53
+ # OS
54
+ .DS_Store
55
+ .DS_Store?
56
+ ._*
57
+ .Spotlight-V100
58
+ .Trashes
59
+ ehthumbs.db
60
+ Thumbs.db
61
+
62
+ # Git
63
+ .git/
64
+ .gitignore
65
+
66
+ # Project specific
67
+ *.md
68
+ .env.example
.env.example ADDED
@@ -0,0 +1,28 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Proportion MCP Server Configuration
2
+ # Copy this file to .env and set your values
3
+
4
+ # Server Configuration
5
+ SERVER_NAME=0.0.0.0
6
+ SERVER_PORT=7860
7
+
8
+ # Security Configuration (REQUIRED for production)
9
+ # Generate a secure token: python -c "import secrets; print(secrets.token_urlsafe(32))"
10
+ MCP_TOKEN=your-secure-token-here
11
+
12
+ # Request Limits
13
+ MAX_REQUEST_SIZE=65536
14
+
15
+ # Development Settings
16
+ DEBUG=false
17
+ RELOAD=false
18
+ LOG_LEVEL=INFO
19
+
20
+ # Example production configuration:
21
+ # MCP_TOKEN=abc123xyz789secure_token_here
22
+ # DEBUG=false
23
+ # LOG_LEVEL=WARNING
24
+
25
+ # Example development configuration:
26
+ # DEBUG=true
27
+ # RELOAD=true
28
+ # LOG_LEVEL=DEBUG
Dockerfile ADDED
@@ -0,0 +1,39 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Simple Docker build for MCP server
2
+ FROM python:3.11-slim
3
+
4
+ # Set working directory
5
+ WORKDIR /app
6
+
7
+ # Create non-root user for security
8
+ RUN groupadd -r appuser && useradd -r -g appuser appuser
9
+
10
+ # Copy requirements first for better caching
11
+ COPY requirements.txt .
12
+
13
+ # Install Python dependencies
14
+ RUN pip install --no-cache-dir --upgrade pip && \
15
+ pip install --no-cache-dir -r requirements.txt
16
+
17
+ # Copy application code
18
+ COPY --chown=appuser:appuser . .
19
+
20
+ # Set environment variables
21
+ ENV PYTHONPATH=/app
22
+ ENV PYTHONUNBUFFERED=1
23
+ ENV PYTHONDONTWRITEBYTECODE=1
24
+
25
+ # Create pytest cache directory with proper permissions
26
+ RUN mkdir -p /app/.pytest_cache && chown -R appuser:appuser /app/.pytest_cache
27
+
28
+ # Security: run as non-root user
29
+ USER appuser
30
+
31
+ # Expose port
32
+ EXPOSE 7860
33
+
34
+ # Add healthcheck
35
+ HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 \
36
+ CMD python -c "import requests; requests.get('http://localhost:7860')" || exit 1
37
+
38
+ # Run the application with proper signal handling
39
+ CMD ["python", "-u", "app.py"]
README.md ADDED
@@ -0,0 +1,636 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ---
2
+ title: Proportio – Precision Proportion Calculator
3
+ emoji: 🧮
4
+ colorFrom: red
5
+ colorTo: gray
6
+ sdk: gradio
7
+ app_file: app.py
8
+ pinned: false
9
+ license: apache-2.0
10
+ tags:
11
+ - mcp-server
12
+ - proportion-calculator
13
+ - gradio
14
+ - python
15
+ - mathematics
16
+ - llm-tools
17
+ ---
18
+
19
+ <div align="center">
20
+ <img src="logo_1024.png" alt="Proportio Logo" width="200" height="200">
21
+ </div>
22
+
23
+ [![License](https://img.shields.io/badge/License-Apache%202.0-blue.svg)](LICENSE)
24
+ [![Python](https://img.shields.io/badge/python-3.11+-blue.svg)](https://python.org)
25
+ [![Docker](https://img.shields.io/badge/docker-ready-blue.svg)](Dockerfile)
26
+ [![Tests](https://img.shields.io/badge/tests-58%20passing-green.svg)](tests/)
27
+ [![MCP Server](https://img.shields.io/badge/MCP-compatible-purple.svg)](https://modelcontextprotocol.io)
28
+
29
+ **Professional mathematical calculations for proportions, percentages, and scaling operations with assertion-based validation and MCP server integration.**
30
+
31
+ ---
32
+
33
+ ## 🎯 Overview
34
+
35
+ Proportio is a specialized mathematical calculation server designed for LLM agents and applications requiring precise proportion calculations. Built with **assertion-based validation** and **zero-tolerance error handling**, it provides reliable mathematical operations through both a web interface and Model Context Protocol (MCP) integration.
36
+
37
+ ### Key Use Cases
38
+
39
+ - **Recipe Scaling**: Scale ingredient quantities for different serving sizes
40
+ - **Financial Calculations**: Calculate percentages, ratios, and proportional growth
41
+ - **Engineering**: Resize dimensions, scale measurements, and maintain proportional relationships
42
+ - **Data Analysis**: Compute percentages, ratios, and proportional transformations
43
+ - **LLM Integration**: Provide reliable mathematical operations through MCP protocol
44
+
45
+ ---
46
+
47
+
48
+ https://github.com/user-attachments/assets/96d30b20-1bf0-4b2b-a1ea-d0a5776f547c
49
+
50
+ ---
51
+ ## ✨ Features
52
+
53
+ ### 🔢 Mathematical Functions
54
+ - **Percentage Calculations** - Convert parts to percentages with precision
55
+ - **Proportion Solving** - Solve missing terms in a/b = c/d relationships
56
+ - **Ratio Scaling** - Scale values by precise ratios
57
+ - **Proportionality Constants** - Find k in y = kx relationships
58
+ - **Dimension Resizing** - Uniform scaling of width/height pairs
59
+
60
+ ### 🛡️ Validation Architecture
61
+ - **Assertion-Based Validation** - Explicit mathematical preconditions
62
+ - **Zero Exception Handling** - No try-catch blocks, fast failure detection
63
+ - **Precise Error Messages** - Clear, actionable error descriptions
64
+ - **Type Safety** - Robust input validation and type checking
65
+
66
+ ### 🌐 Integration Options
67
+ - **Web Interface** - Professional Gradio-based UI with custom branding
68
+ - **MCP Server** - Native Model Context Protocol support for LLM agents
69
+ - **Docker Ready** - Containerized deployment with security best practices
70
+ - **API Access** - Direct function calls with comprehensive documentation
71
+
72
+ ### 🎨 Professional Design
73
+ - **Custom Branding** - Red-black-white theme with geometric logo
74
+ - **Responsive Layout** - Optimized for desktop and mobile devices
75
+ - **Split Results** - Clear separation of input/output sections
76
+ - **Error Handling** - User-friendly error messages and validation
77
+
78
+
79
+ ---
80
+
81
+ ## 📋 Table of Contents
82
+
83
+ - [🎯 Overview](#-overview)
84
+ - [✨ Features](#-features)
85
+ - [🚀 Quick Start](#-quick-start)
86
+ - [🔧 Core Functions](#-core-functions)
87
+ - [🏗️ Architecture](#️-architecture)
88
+ - [📦 Installation](#-installation)
89
+ - [🐳 Docker Deployment](#-docker-deployment)
90
+ - [🧪 Testing](#-testing)
91
+ - [🔌 MCP Integration](#-mcp-integration)
92
+ - [📖 API Reference](#-api-reference)
93
+ - [🛠️ Development](#️-development)
94
+ - [📝 License](#-license)
95
+
96
+ ---
97
+
98
+ ## 🚀 Quick Start
99
+
100
+ ### Using Docker (Recommended)
101
+
102
+ ```bash
103
+ # Clone the repository
104
+ git clone https://github.com/leksval/proportio.git
105
+ cd proportio
106
+
107
+ # Build and run with Docker
108
+ docker build -t proportio-server .
109
+ docker run -p 7860:7860 proportio-server
110
+
111
+ # Access the web interface
112
+ open http://localhost:7860
113
+ ```
114
+
115
+ ### Local Development
116
+
117
+ ```bash
118
+ # Install dependencies
119
+ pip install -r requirements.txt
120
+
121
+ # Run the server
122
+ python proportion_server.py
123
+
124
+ # Access the web interface
125
+ open http://localhost:7860
126
+ ```
127
+
128
+ ### Quick Function Examples
129
+
130
+ ```python
131
+ from proportion_server import percent_of, solve_proportion, resize_dimensions
132
+
133
+ # Calculate percentage
134
+ result = percent_of(25, 100) # Returns: 25.0
135
+
136
+ # Solve proportion: 3/4 = 6/?
137
+ result = solve_proportion(3, 4, 6, None) # Returns: 8.0
138
+
139
+ # Resize dimensions by 2x
140
+ width, height = resize_dimensions(100, 50, 2.0) # Returns: (200.0, 100.0)
141
+ ```
142
+
143
+ ---
144
+
145
+ ## 🔧 Core Functions
146
+
147
+ ### 1. **`percent_of(part, whole)`**
148
+ Calculate what percentage the part is of the whole.
149
+
150
+ ```python
151
+ percent_of(25, 100) # → 25.0%
152
+ percent_of(3, 4) # → 75.0%
153
+ percent_of(150, 100) # → 150.0%
154
+ ```
155
+
156
+ **Mathematical Preconditions:**
157
+ - `whole != 0` (division by zero protection)
158
+
159
+ **Real-world Examples:**
160
+ - Sales conversion rates
161
+ - Test score percentages
162
+ - Growth rate calculations
163
+
164
+ ### 2. **`solve_proportion(a, b, c, d)`**
165
+ Solve missing term in proportion a/b = c/d (exactly one parameter must be None).
166
+
167
+ ```python
168
+ solve_proportion(3, 4, 6, None) # → 8.0 (3/4 = 6/8)
169
+ solve_proportion(None, 4, 6, 8) # → 3.0 (?/4 = 6/8)
170
+ solve_proportion(2, None, 6, 9) # → 3.0 (2/? = 6/9)
171
+ ```
172
+
173
+ **Mathematical Preconditions:**
174
+ - Exactly one value must be None (missing)
175
+ - Division denominators != 0 (varies by missing value)
176
+
177
+ **Real-world Examples:**
178
+ - Recipe scaling (4 servings : 2 cups = 6 servings : ? cups)
179
+ - Currency exchange rates
180
+ - Map scale calculations
181
+
182
+ ### 3. **`scale_by_ratio(value, ratio)`**
183
+ Scale a value by a given ratio.
184
+
185
+ ```python
186
+ scale_by_ratio(100, 1.5) # → 150.0
187
+ scale_by_ratio(200, 0.5) # → 100.0
188
+ scale_by_ratio(50, 2.0) # → 100.0
189
+ ```
190
+
191
+ **Use Cases:**
192
+ - Applying discount percentages
193
+ - Scaling measurements
194
+ - Financial calculations
195
+
196
+ ### 4. **`direct_k(x, y)`**
197
+ Find proportionality constant k in direct variation y = kx.
198
+
199
+ ```python
200
+ direct_k(5, 15) # → 3.0 (15 = 3 × 5)
201
+ direct_k(4, 12) # → 3.0 (12 = 3 × 4)
202
+ direct_k(2, 7) # → 3.5 (7 = 3.5 × 2)
203
+ ```
204
+
205
+ **Mathematical Preconditions:**
206
+ - `x != 0` (division by zero protection)
207
+
208
+ **Applications:**
209
+ - Physics calculations (force = k × displacement)
210
+ - Economics (cost = k × quantity)
211
+ - Engineering (stress = k × strain)
212
+
213
+ ### 5. **`resize_dimensions(width, height, scale)`**
214
+ Resize dimensions with uniform scale factor.
215
+
216
+ ```python
217
+ resize_dimensions(100, 50, 2.0) # → (200.0, 100.0)
218
+ resize_dimensions(200, 100, 0.5) # → (100.0, 50.0)
219
+ resize_dimensions(150, 75, 1.5) # → (225.0, 112.5)
220
+ ```
221
+
222
+ **Mathematical Preconditions:**
223
+ - `width >= 0` (dimensions must be non-negative)
224
+ - `height >= 0` (dimensions must be non-negative)
225
+ - `scale > 0` (scale factor must be positive)
226
+
227
+ **Applications:**
228
+ - Image resizing
229
+ - Screen resolution scaling
230
+ - Architectural drawings
231
+
232
+ ---
233
+
234
+ ## 🏗️ Architecture
235
+
236
+ ### Assertion-Based Validation
237
+
238
+ Proportio uses **assertion-based validation** throughout, providing several key advantages:
239
+
240
+ ```python
241
+ def percent_of(part: float, whole: float) -> float:
242
+ # Mathematical preconditions
243
+ assert whole != 0, "Division by zero: whole cannot be zero"
244
+
245
+ # Direct calculation
246
+ percentage = (part / whole) * 100
247
+ return percentage
248
+ ```
249
+
250
+ **Benefits:**
251
+ - **Fast Failure**: Immediate error detection with precise messages
252
+ - **No Exception Overhead**: Zero try-catch complexity
253
+ - **Clear Preconditions**: Mathematical requirements explicitly documented
254
+ - **Predictable Behavior**: Consistent error handling across all functions
255
+
256
+ ### Project Structure
257
+
258
+ ```
259
+ proportio/
260
+ ├── proportion_server.py # Core mathematical functions + Gradio server
261
+ ├── models.py # Pydantic data models (simplified)
262
+ ├── config.py # Configuration and logging setup
263
+ ├── styles.css # Custom branding and responsive design
264
+ ├── tests/
265
+ │ └── test_tools.py # Comprehensive test suite (58 tests)
266
+ ├── requirements.txt # Minimal dependencies (3 packages)
267
+ ├── Dockerfile # Single-stage containerization
268
+ └── README.md # This documentation
269
+ ```
270
+
271
+ ### Dependency Architecture
272
+
273
+ **Streamlined Dependencies** (only 3 required):
274
+ - **`gradio[mcp]>=5.0.0`** - Web framework with MCP server capabilities
275
+ - **`pydantic>=2.8.0`** - Data validation and parsing
276
+ - **`pytest>=8.0.0`** - Testing framework
277
+
278
+ ### Error Handling Philosophy
279
+
280
+ **No Try-Catch Blocks** - All validation done through assertions:
281
+
282
+ ```python
283
+ # ❌ Old approach (complex exception handling)
284
+ try:
285
+ if whole == 0:
286
+ raise ValueError("Division by zero")
287
+ result = part / whole
288
+ except ValueError as e:
289
+ # Handle error...
290
+
291
+ # ✅ New approach (assertion-based)
292
+ assert whole != 0, "Division by zero: whole cannot be zero"
293
+ result = part / whole
294
+ ```
295
+
296
+ ---
297
+
298
+ ## 📦 Installation
299
+
300
+ ### System Requirements
301
+
302
+ - **Python 3.11+**
303
+ - **pip** package manager
304
+ - **Docker** (optional, for containerized deployment)
305
+
306
+ ### Local Installation
307
+
308
+ ```bash
309
+ # Clone repository
310
+ git clone https://github.com/leksval/proportio.git
311
+ cd proportio
312
+
313
+ # Create virtual environment (recommended)
314
+ python -m venv venv
315
+ source venv/bin/activate # On Windows: venv\Scripts\activate
316
+
317
+ # Install dependencies
318
+ pip install -r requirements.txt
319
+
320
+ # Verify installation
321
+ python -c "from proportion_server import percent_of; print(percent_of(25, 100))"
322
+ ```
323
+
324
+ ### Development Installation
325
+
326
+ ```bash
327
+ # Install with development dependencies
328
+ pip install -r requirements.txt
329
+
330
+ # Run tests to verify setup
331
+ python -m pytest tests/test_tools.py -v
332
+
333
+ # Start development server
334
+ python proportion_server.py
335
+ ```
336
+
337
+ ---
338
+
339
+ ## 🐳 Docker Deployment
340
+
341
+ ### Building the Container
342
+
343
+ ```bash
344
+ # Build image
345
+ docker build -t proportio-server .
346
+
347
+ # Run container
348
+ docker run -p 7860:7860 proportio-server
349
+
350
+ # Run with custom configuration
351
+ docker run -p 8080:7860 -e PORT=7860 proportio-server
352
+ ```
353
+
354
+ ### Container Features
355
+
356
+ - **Security**: Non-root user execution
357
+ - **Optimization**: Single-stage build for minimal image size
358
+ - **Flexibility**: Configurable port and environment settings
359
+ - **Health**: Automatic process management
360
+
361
+ ### Production Deployment
362
+
363
+ ```bash
364
+ # Run detached with restart policy
365
+ docker run -d \
366
+ --name proportio \
367
+ --restart unless-stopped \
368
+ -p 7860:7860 \
369
+ proportio-server
370
+
371
+ # View logs
372
+ docker logs proportio
373
+
374
+ # Stop container
375
+ docker stop proportio
376
+ ```
377
+
378
+ ---
379
+
380
+ ## 🧪 Testing
381
+
382
+ ### Test Suite Coverage
383
+
384
+ **58 comprehensive tests** covering:
385
+ - ✅ Basic functionality for all 5 core functions
386
+ - ✅ Edge cases and boundary conditions
387
+ - ✅ Error handling and assertion validation
388
+ - ✅ Integration workflows and chained calculations
389
+ - ✅ Floating-point precision and mathematical accuracy
390
+ - ✅ Type validation and input sanitization
391
+
392
+ ### Running Tests
393
+
394
+ ```bash
395
+ # Run all tests
396
+ python -m pytest tests/test_tools.py -v
397
+
398
+ # Run specific test class
399
+ python -m pytest tests/test_tools.py::TestPercentOf -v
400
+
401
+ # Run with coverage (if pytest-cov installed)
402
+ python -m pytest tests/test_tools.py --cov=proportion_server
403
+
404
+ # Run tests in Docker
405
+ docker run --rm proportio-server python -m pytest tests/test_tools.py -v
406
+ ```
407
+
408
+ ### Test Categories
409
+
410
+ #### **Unit Tests**
411
+ - Individual function validation
412
+ - Mathematical accuracy verification
413
+ - Error condition testing
414
+
415
+ #### **Integration Tests**
416
+ - Chained calculation workflows
417
+ - Real-world scenario testing
418
+ - Cross-function compatibility
419
+
420
+ #### **Edge Case Tests**
421
+ - Floating-point precision limits
422
+ - Very large and very small numbers
423
+ - Boundary condition validation
424
+
425
+ ### Sample Test Output
426
+
427
+ ```
428
+ ==================== test session starts ====================
429
+ collected 58 items
430
+
431
+ tests/test_tools.py::TestPercentOf::test_basic_percentage PASSED
432
+ tests/test_tools.py::TestPercentOf::test_zero_part PASSED
433
+ tests/test_tools.py::TestPercentOf::test_negative_values PASSED
434
+ ...
435
+ tests/test_tools.py::TestIntegration::test_real_world_recipe_scaling PASSED
436
+ tests/test_tools.py::TestIntegration::test_financial_calculation_workflow PASSED
437
+
438
+ ==================== 58 passed in 0.45s ====================
439
+ ```
440
+
441
+ ---
442
+
443
+ ## 🔌 MCP Integration
444
+
445
+ ### Model Context Protocol Support
446
+
447
+ Proportio provides native **MCP server capabilities** for seamless LLM integration:
448
+
449
+ ```python
450
+ # Launch with MCP support
451
+ demo.launch(
452
+ server_name="0.0.0.0",
453
+ server_port=7860,
454
+ mcp_server=True, # Enable MCP functionality
455
+ show_error=True
456
+ )
457
+ ```
458
+
459
+ ### Using with LLM Agents
460
+
461
+ The MCP server exposes all mathematical functions as tools that LLMs can call directly:
462
+
463
+ **Available MCP Tools:**
464
+ - `percent_of` - Calculate percentage relationships
465
+ - `solve_proportion` - Solve missing proportion terms
466
+ - `scale_by_ratio` - Apply scaling ratios
467
+ - `direct_k` - Find proportionality constants
468
+ - `resize_dimensions` - Scale dimensional pairs
469
+
470
+ ### MCP Connection Example
471
+
472
+ ```json
473
+ {
474
+ "name": "proportio",
475
+ "type": "sse",
476
+ "url": "http://localhost:7860/mcp"
477
+ }
478
+ ```
479
+
480
+ ### Integration Benefits
481
+
482
+ - **Reliable Math**: LLMs can delegate complex calculations
483
+ - **Error Handling**: Clear error messages for invalid inputs
484
+ - **Type Safety**: Automatic input validation and conversion
485
+ - **Performance**: Fast, direct mathematical operations
486
+
487
+ ---
488
+
489
+ ## 📖 API Reference
490
+
491
+ ### Function Signatures
492
+
493
+ ```python
494
+ def percent_of(part: float, whole: float) -> float:
495
+ """Calculate percentage that part is of whole."""
496
+
497
+ def solve_proportion(
498
+ a: Optional[float] = None,
499
+ b: Optional[float] = None,
500
+ c: Optional[float] = None,
501
+ d: Optional[float] = None
502
+ ) -> float:
503
+ """Solve missing term in proportion a/b = c/d."""
504
+
505
+ def scale_by_ratio(value: float, ratio: float) -> float:
506
+ """Scale value by given ratio."""
507
+
508
+ def direct_k(x: float, y: float) -> float:
509
+ """Find proportionality constant k where y = kx."""
510
+
511
+ def resize_dimensions(width: float, height: float, scale: float) -> Tuple[float, float]:
512
+ """Resize dimensions with uniform scale factor."""
513
+ ```
514
+
515
+ ### Error Messages
516
+
517
+ All functions provide clear, actionable error messages:
518
+
519
+ ```python
520
+ # Division by zero errors
521
+ "Division by zero: whole cannot be zero"
522
+ "Division by zero: x cannot be zero"
523
+ "Division by zero: denominator"
524
+
525
+ # Validation errors
526
+ "Exactly one value must be None"
527
+ "Width must be non-negative"
528
+ "Height must be non-negative"
529
+ "Scale factor must be positive"
530
+ ```
531
+
532
+ ### Return Types
533
+
534
+ - **Single Values**: `float` for mathematical results
535
+ - **Dimension Pairs**: `Tuple[float, float]` for width/height
536
+ - **Errors**: `AssertionError` with descriptive messages
537
+
538
+ ---
539
+
540
+ ## 🛠️ Development
541
+
542
+ ### Project Philosophy
543
+
544
+ 1. **Assertion-Based Validation** - No try-catch complexity
545
+ 2. **Mathematical Precision** - Accurate calculations with clear preconditions
546
+ 3. **Minimal Dependencies** - Only essential packages
547
+ 4. **Comprehensive Testing** - High test coverage with edge cases
548
+ 5. **Professional Design** - Clean, branded user interface
549
+
550
+ ### Code Style
551
+
552
+ ```python
553
+ # Clear function signatures with type hints
554
+ def function_name(param: Type) -> ReturnType:
555
+ """
556
+ Brief description.
557
+
558
+ Args:
559
+ param: Parameter description
560
+
561
+ Returns:
562
+ Return value description
563
+
564
+ Mathematical preconditions:
565
+ - Explicit constraint documentation
566
+ """
567
+ # Assertion-based validation
568
+ assert condition, "Clear error message"
569
+
570
+ # Direct calculation
571
+ result = calculation
572
+
573
+ # Optional logging
574
+ logger.debug(f"Operation completed: {result}")
575
+
576
+ return result
577
+ ```
578
+
579
+ ### Adding New Functions
580
+
581
+ 1. **Implement Core Logic** - Add function to `proportion_server.py`
582
+ 2. **Add Mathematical Preconditions** - Document constraints explicitly
583
+ 3. **Create Demo Function** - Add Gradio interface wrapper
584
+ 4. **Write Comprehensive Tests** - Cover all edge cases
585
+ 5. **Update Documentation** - Add examples and use cases
586
+
587
+ ### Contributing Guidelines
588
+
589
+ 1. **Fork the Repository** - Create your feature branch
590
+ 2. **Follow Code Style** - Use assertion-based validation
591
+ 3. **Add Tests** - Ensure comprehensive test coverage
592
+ 4. **Update Documentation** - Keep README current
593
+ 5. **Submit Pull Request** - Include description of changes
594
+
595
+ ---
596
+
597
+ ## 📝 License
598
+
599
+ This project is licensed under the **Apache License 2.0** - see the [LICENSE](LICENSE) file for details.
600
+
601
+ ### Key License Points
602
+
603
+ - ✅ **Commercial Use** - Use in commercial applications
604
+ - ✅ **Modification** - Modify and distribute changes
605
+ - ✅ **Distribution** - Distribute original or modified versions
606
+ - ✅ **Patent Use** - Grant of patent rights from contributors
607
+ - ⚠️ **Attribution** - Must include license and copyright notice
608
+ - ⚠️ **State Changes** - Must document modifications
609
+
610
+ ---
611
+
612
+ ## 🤝 Support
613
+
614
+ ### Getting Help
615
+
616
+ - **Issues**: [GitHub Issues](https://github.com/leksval/proportio/issues)
617
+ - **Documentation**: This README and inline code documentation
618
+ - **Examples**: See `tests/test_tools.py` for usage examples
619
+
620
+ ### Contributing
621
+
622
+ We welcome contributions! Please see the [Development](#️-development) section for guidelines.
623
+
624
+ ### Reporting Bugs
625
+
626
+ When reporting bugs, please include:
627
+ 1. **Environment Details** (Python version, OS, Docker version)
628
+ 2. **Reproduction Steps** (minimal code example)
629
+ 3. **Expected vs Actual Behavior**
630
+ 4. **Error Messages** (full stack trace if applicable)
631
+
632
+ ---
633
+
634
+ **Built with ❤️ for mathematical precision and LLM integration**
635
+
636
+ *Proportio - Where Mathematics Meets Reliability*
app.py ADDED
@@ -0,0 +1,93 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Main application entry point for Proportion MCP Server.
3
+ Handles both local development and cloud deployment.
4
+ """
5
+
6
+ import logging
7
+ import os
8
+ import signal
9
+ import sys
10
+
11
+ from proportion_server import create_gradio_app
12
+ from config import get_settings, setup_logging, is_huggingface_space
13
+
14
+ # Initialize logging and settings
15
+ setup_logging()
16
+ logger = logging.getLogger(__name__)
17
+ settings = get_settings()
18
+
19
+
20
+ def signal_handler(signum, frame):
21
+ """Handle shutdown signals gracefully."""
22
+ logger.info(f"📡 Received signal {signum}, shutting down gracefully...")
23
+ sys.exit(0)
24
+
25
+
26
+ def main() -> None:
27
+ """Main application entry point."""
28
+
29
+ # Set up signal handlers for graceful shutdown
30
+ signal.signal(signal.SIGINT, signal_handler)
31
+ signal.signal(signal.SIGTERM, signal_handler)
32
+
33
+ logger.info("🚀 Starting Proportion MCP Server")
34
+ logger.info(f"Settings: {settings.model_dump_json(indent=2)}")
35
+
36
+ # Create Gradio app
37
+ demo = create_gradio_app()
38
+ assert demo is not None, "Failed to create Gradio application"
39
+ logger.info("✅ Gradio application created successfully")
40
+
41
+ # Launch configuration
42
+ launch_kwargs = {
43
+ "server_name": settings.server_name,
44
+ "server_port": settings.server_port,
45
+ "mcp_server": True, # Enable MCP server functionality
46
+ "show_error": settings.debug,
47
+ "quiet": not settings.debug,
48
+ "prevent_thread_lock": False, # Keep main thread alive
49
+ }
50
+
51
+ # Development-specific settings
52
+ if not settings.is_production():
53
+ launch_kwargs.update({
54
+ "debug": settings.debug,
55
+ })
56
+
57
+ # Hugging Face Spaces specific settings
58
+ if is_huggingface_space():
59
+ launch_kwargs.update({
60
+ "server_name": "0.0.0.0", # Required for HF Spaces
61
+ "share": False, # Don't create gradio.live links on HF
62
+ "quiet": True, # Reduce logging noise
63
+ })
64
+ logger.info("🤗 Configured for Hugging Face Spaces deployment")
65
+
66
+ # Log the MCP endpoint information
67
+ if is_huggingface_space():
68
+ space_url = os.environ.get("SPACE_URL", "https://your-space.hf.space")
69
+ logger.info(f"🔗 MCP Server will be available at: {space_url}")
70
+ else:
71
+ logger.info(f"🔗 MCP Server will be available at: http://{settings.server_name}:{settings.server_port}")
72
+
73
+ logger.info("🔧 MCP Tools exposed: percent_of, solve_proportion, scale_by_ratio, direct_k, resize_dimensions")
74
+ logger.info("📖 Functions with proper type hints and docstrings are automatically exposed as MCP tools")
75
+
76
+ # Launch the server
77
+ logger.info("🌐 Starting server...")
78
+ demo.launch(**launch_kwargs)
79
+
80
+ # Keep the main thread alive
81
+ logger.info("✅ Server is running, press Ctrl+C to stop")
82
+ if hasattr(signal, 'pause'):
83
+ # Unix systems
84
+ signal.pause()
85
+ else:
86
+ # Windows systems
87
+ import time
88
+ while True:
89
+ time.sleep(1)
90
+
91
+
92
+ if __name__ == "__main__":
93
+ main()
config.py ADDED
@@ -0,0 +1,129 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Configuration management using Pydantic Settings.
3
+ Handles environment variables with validation and defaults.
4
+ """
5
+
6
+ import os
7
+ from typing import Optional
8
+ from pydantic import Field
9
+ from pydantic_settings import BaseSettings, SettingsConfigDict
10
+
11
+
12
+ class Settings(BaseSettings):
13
+ """Application settings with environment variable support."""
14
+
15
+ model_config = SettingsConfigDict(
16
+ env_file=".env",
17
+ env_file_encoding="utf-8",
18
+ case_sensitive=False,
19
+ extra="ignore"
20
+ )
21
+
22
+ # Server configuration
23
+ server_name: str = Field(
24
+ default="0.0.0.0",
25
+ description="Server bind address"
26
+ )
27
+
28
+ server_port: int = Field(
29
+ default=7860,
30
+ ge=1024,
31
+ le=65535,
32
+ description="Server port (1024-65535)"
33
+ )
34
+
35
+ # Security configuration
36
+ mcp_token: Optional[str] = Field(
37
+ default=None,
38
+ description="MCP authentication token (required for production)"
39
+ )
40
+
41
+ # Request limits
42
+ max_request_size: int = Field(
43
+ default=65536, # 64KB
44
+ ge=1024,
45
+ le=1048576, # 1MB max
46
+ description="Maximum request body size in bytes"
47
+ )
48
+
49
+ # Development settings
50
+ debug: bool = Field(
51
+ default=False,
52
+ description="Enable debug mode"
53
+ )
54
+
55
+ reload: bool = Field(
56
+ default=False,
57
+ description="Enable hot reload in development"
58
+ )
59
+
60
+ # Logging configuration
61
+ log_level: str = Field(
62
+ default="INFO",
63
+ description="Logging level (DEBUG, INFO, WARNING, ERROR, CRITICAL)"
64
+ )
65
+
66
+ def is_production(self) -> bool:
67
+ """Check if running in production mode."""
68
+ return not self.debug and self.mcp_token is not None
69
+
70
+ def validate_production_settings(self) -> None:
71
+ """Validate settings for production deployment."""
72
+ if self.is_production():
73
+ assert self.mcp_token, "MCP_TOKEN environment variable is required for production deployment"
74
+
75
+
76
+ # Global settings instance
77
+ settings = Settings()
78
+
79
+
80
+ def get_settings() -> Settings:
81
+ """Get the current settings instance."""
82
+ return settings
83
+
84
+
85
+ def setup_logging() -> None:
86
+ """Configure logging based on settings."""
87
+ import logging
88
+
89
+ logging.basicConfig(
90
+ level=getattr(logging, settings.log_level.upper()),
91
+ format="%(asctime)s - %(name)s - %(levelname)s - %(message)s",
92
+ datefmt="%Y-%m-%d %H:%M:%S"
93
+ )
94
+
95
+ # Set Gradio logging level
96
+ if not settings.debug:
97
+ logging.getLogger("gradio").setLevel(logging.WARNING)
98
+ logging.getLogger("uvicorn").setLevel(logging.WARNING)
99
+
100
+
101
+ # Environment detection helpers
102
+ def is_huggingface_space() -> bool:
103
+ """Check if running on Hugging Face Spaces."""
104
+ return "SPACE_ID" in os.environ
105
+
106
+
107
+ def is_docker() -> bool:
108
+ """Check if running in Docker container."""
109
+ return os.path.exists("/.dockerenv") or "DOCKER_CONTAINER" in os.environ
110
+
111
+
112
+ def get_deployment_info() -> dict[str, str]:
113
+ """Get deployment environment information."""
114
+ info = {
115
+ "environment": "unknown",
116
+ "platform": "local"
117
+ }
118
+
119
+ if is_huggingface_space():
120
+ info["platform"] = "huggingface_spaces"
121
+ info["environment"] = "production"
122
+ elif is_docker():
123
+ info["platform"] = "docker"
124
+ info["environment"] = "production" if settings.is_production() else "development"
125
+ else:
126
+ info["platform"] = "local"
127
+ info["environment"] = "development"
128
+
129
+ return info
docs/client_examples.md ADDED
@@ -0,0 +1,597 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # MCP Client Configuration Examples
2
+
3
+ This document provides configuration examples for various MCP clients to connect to the Proportio server.
4
+
5
+ ## Cursor IDE
6
+
7
+ ### Method 1: Settings JSON
8
+
9
+ Add to your Cursor settings (`Cmd/Ctrl + ,` → Open Settings JSON):
10
+
11
+ ```json
12
+ {
13
+ "mcpServers": {
14
+ "proportio": {
15
+ "url": "http://localhost:7860/gradio_api/mcp/sse",
16
+ "headers": {
17
+ "X-Api-Key": "your-secure-token-here"
18
+ }
19
+ }
20
+ }
21
+ }
22
+ ```
23
+
24
+ ### Method 2: Workspace Configuration
25
+
26
+ Create `.cursor/mcp.json` in your project root:
27
+
28
+ ```json
29
+ {
30
+ "proportio": {
31
+ "url": "http://localhost:7860/gradio_api/mcp/sse",
32
+ "headers": {
33
+ "X-Api-Key": "your-secure-token-here"
34
+ }
35
+ }
36
+ }
37
+ ```
38
+
39
+ ## Claude Desktop
40
+
41
+ ### Configuration File Location
42
+
43
+ - **macOS**: `~/Library/Application Support/Claude/claude_desktop_config.json`
44
+ - **Windows**: `%APPDATA%/Claude/claude_desktop_config.json`
45
+
46
+ ### Local Server Configuration
47
+
48
+ ```json
49
+ {
50
+ "mcpServers": {
51
+ "proportio": {
52
+ "command": "python",
53
+ "args": ["/path/to/proportio/app.py"],
54
+ "env": {
55
+ "MCP_TOKEN": "your-secure-token-here"
56
+ }
57
+ }
58
+ }
59
+ }
60
+ ```
61
+
62
+ ### Remote Server Configuration
63
+
64
+ ```json
65
+ {
66
+ "mcpServers": {
67
+ "proportio": {
68
+ "url": "https://your-server.com/gradio_api/mcp/sse",
69
+ "headers": {
70
+ "X-Api-Key": "your-secure-token-here"
71
+ }
72
+ }
73
+ }
74
+ }
75
+ ```
76
+
77
+ ## CrewAI
78
+
79
+ ### Basic Setup
80
+
81
+ ```python
82
+ from crewai import Agent, Task, Crew
83
+ from crewai.tools import MCPTool
84
+
85
+ # Configure the MCP tool
86
+ proportio_tool = MCPTool(
87
+ name="proportio_calculator",
88
+ url="http://localhost:7860/gradio_api/mcp/sse",
89
+ headers={"X-Api-Key": "your-secure-token-here"}
90
+ )
91
+
92
+ # Create an agent with the tool
93
+ math_agent = Agent(
94
+ role="Mathematical Calculator",
95
+ goal="Perform accurate proportion and percentage calculations",
96
+ backstory="You are an expert mathematician who uses reliable tools for calculations.",
97
+ tools=[proportio_tool],
98
+ verbose=True
99
+ )
100
+
101
+ # Example task
102
+ calculation_task = Task(
103
+ description="Calculate what percentage 75 is of 300, then scale that result by 1.5",
104
+ agent=math_agent,
105
+ expected_output="The percentage and scaled result with explanations"
106
+ )
107
+
108
+ # Run the crew
109
+ crew = Crew(
110
+ agents=[math_agent],
111
+ tasks=[calculation_task]
112
+ )
113
+
114
+ result = crew.kickoff()
115
+ print(result)
116
+ ```
117
+
118
+ ### Advanced CrewAI Setup with Multiple Tools
119
+
120
+ ```python
121
+ from crewai import Agent, Task, Crew
122
+ from crewai.tools import MCPTool
123
+
124
+ class ProportioTools:
125
+ def __init__(self, base_url="http://localhost:7860", api_key="your-token"):
126
+ self.base_url = f"{base_url}/gradio_api/mcp/sse"
127
+ self.headers = {"X-Api-Key": api_key}
128
+
129
+ def get_tools(self):
130
+ return [
131
+ MCPTool(
132
+ name="percent_calculator",
133
+ url=self.base_url,
134
+ headers=self.headers,
135
+ description="Calculate percentages accurately"
136
+ ),
137
+ MCPTool(
138
+ name="proportion_solver",
139
+ url=self.base_url,
140
+ headers=self.headers,
141
+ description="Solve proportion equations"
142
+ ),
143
+ MCPTool(
144
+ name="dimension_resizer",
145
+ url=self.base_url,
146
+ headers=self.headers,
147
+ description="Resize dimensions with scaling"
148
+ )
149
+ ]
150
+
151
+ # Usage
152
+ proportio = ProportioTools(api_key="your-secure-token-here")
153
+ tools = proportio.get_tools()
154
+
155
+ agent = Agent(
156
+ role="Design Calculator",
157
+ goal="Help with design calculations and proportions",
158
+ tools=tools
159
+ )
160
+ ```
161
+
162
+ ## OpenAI Agents SDK
163
+
164
+ ### Basic Configuration
165
+
166
+ ```python
167
+ from openai_agents import Agent
168
+ from openai_agents.mcp import MCPConnection
169
+
170
+ # Setup MCP connection
171
+ mcp_conn = MCPConnection(
172
+ url="http://localhost:7860/gradio_api/mcp/sse",
173
+ headers={"X-Api-Key": "your-secure-token-here"}
174
+ )
175
+
176
+ # Create agent with MCP tools
177
+ agent = Agent(
178
+ name="Math Assistant",
179
+ model="gpt-4",
180
+ mcp_connections=[mcp_conn]
181
+ )
182
+
183
+ # Use the agent
184
+ response = agent.run("What percentage is 45 out of 180?")
185
+ print(response)
186
+ ```
187
+
188
+ ## Generic HTTP Client (for testing)
189
+
190
+ ### Using curl
191
+
192
+ ```bash
193
+ # Test percent_of tool
194
+ curl -X POST http://localhost:7860/gradio_api/mcp/sse \
195
+ -H "X-Api-Key: your-secure-token-here" \
196
+ -H "Content-Type: application/json" \
197
+ -d '{
198
+ "jsonrpc": "2.0",
199
+ "id": 1,
200
+ "method": "tools/call",
201
+ "params": {
202
+ "name": "percent_of",
203
+ "arguments": {
204
+ "part": 25,
205
+ "whole": 100
206
+ }
207
+ }
208
+ }'
209
+ ```
210
+
211
+ ### Using Python requests
212
+
213
+ ```python
214
+ import requests
215
+ import json
216
+
217
+ def call_proportio_tool(tool_name, arguments, api_key="your-token"):
218
+ url = "http://localhost:7860/gradio_api/mcp/sse"
219
+ headers = {
220
+ "X-Api-Key": api_key,
221
+ "Content-Type": "application/json"
222
+ }
223
+
224
+ payload = {
225
+ "jsonrpc": "2.0",
226
+ "id": 1,
227
+ "method": "tools/call",
228
+ "params": {
229
+ "name": tool_name,
230
+ "arguments": arguments
231
+ }
232
+ }
233
+
234
+ response = requests.post(url, headers=headers, json=payload)
235
+ return response.json()
236
+
237
+ # Examples
238
+ result1 = call_proportio_tool("percent_of", {"part": 75, "whole": 300})
239
+ print(f"Percentage: {result1}")
240
+
241
+ result2 = call_proportio_tool("solve_proportion", {"a": 3, "b": 4, "c": 6, "d": None})
242
+ print(f"Missing value: {result2}")
243
+
244
+ result3 = call_proportio_tool("resize_dimensions", {
245
+ "width": 1920,
246
+ "height": 1080,
247
+ "scale": 0.5
248
+ })
249
+ print(f"New dimensions: {result3}")
250
+ ```
251
+
252
+ ## Environment-Specific Configurations
253
+
254
+ ### Development Environment
255
+
256
+ ```bash
257
+ # .env file
258
+ MCP_TOKEN=dev-token-123
259
+ DEBUG=true
260
+ LOG_LEVEL=DEBUG
261
+ RELOAD=true
262
+ ```
263
+
264
+ ### Production Environment
265
+
266
+ ```bash
267
+ # .env file
268
+ MCP_TOKEN=prod-secure-token-xyz789
269
+ DEBUG=false
270
+ LOG_LEVEL=WARNING
271
+ MAX_REQUEST_SIZE=32768
272
+ ```
273
+
274
+ ### Hugging Face Spaces
275
+
276
+ Set these as Space secrets:
277
+
278
+ - `MCP_TOKEN`: Your secure authentication token
279
+ - `LOG_LEVEL`: `INFO` or `WARNING`
280
+
281
+ Access URL: `https://your-username-proportio.hf.space/gradio_api/mcp/sse`
282
+
283
+ ## Security Best Practices
284
+
285
+ ### Token Generation
286
+
287
+ Generate secure tokens:
288
+
289
+ ```bash
290
+ # Python
291
+ python -c "import secrets; print(secrets.token_urlsafe(32))"
292
+
293
+ # OpenSSL
294
+ openssl rand -base64 32
295
+
296
+ # Node.js
297
+ node -e "console.log(require('crypto').randomBytes(32).toString('base64url'))"
298
+ ```
299
+
300
+ ### Token Rotation
301
+
302
+ 1. Generate new token
303
+ 2. Update all client configurations
304
+ 3. Update server environment variable
305
+ 4. Restart server
306
+ 5. Verify all clients work with new token
307
+
308
+ ### Network Security
309
+
310
+ - Use HTTPS in production
311
+ - Implement rate limiting at reverse proxy level
312
+ - Monitor for unusual usage patterns
313
+ - Keep logs for security auditing
314
+
315
+ ## Troubleshooting
316
+
317
+ ### Common Connection Issues
318
+
319
+ 1. **"Connection refused"**
320
+ - Check if server is running: `curl http://localhost:7860`
321
+ - Verify port is not blocked by firewall
322
+
323
+ 2. **"Invalid API key"**
324
+ - Ensure `X-Api-Key` header is included
325
+ - Check token matches `MCP_TOKEN` environment variable
326
+ - Verify no extra spaces or characters in token
327
+
328
+ 3. **"Request timeout"**
329
+ - Check server logs for errors
330
+ - Verify network connectivity
331
+ - Increase client timeout settings
332
+
333
+ ### Debug Mode
334
+
335
+ Enable debug logging to troubleshoot issues:
336
+
337
+ ```bash
338
+ export DEBUG=true
339
+ export LOG_LEVEL=DEBUG
340
+ python app.py
341
+ ```
342
+
343
+ This will show detailed logs of all MCP requests and responses.
344
+
345
+ ## Roo Code Assistant
346
+
347
+ ### Configuration
348
+
349
+ Roo uses MCP servers automatically when they're running locally. Simply start the Proportio server:
350
+
351
+ ```bash
352
+ python app.py
353
+ ```
354
+
355
+ Roo will detect the running MCP server at `http://localhost:7860/gradio_api/mcp/sse` and make the tools available.
356
+
357
+ ### Usage Examples
358
+
359
+ ```
360
+ Human: Calculate what percentage 45 is of 180
361
+
362
+ Roo: I'll use the proportion calculator to find what percentage 45 is of 180.
363
+
364
+ [Using percent_of tool with part=45, whole=180]
365
+
366
+ 45 is 25% of 180.
367
+ ```
368
+
369
+ ```
370
+ Human: I need to resize an image from 1920x1080 to half the size
371
+
372
+ Roo: I'll help you calculate the new dimensions using the resize tool.
373
+
374
+ [Using resize_dimensions tool with width=1920, height=1080, scale=0.5]
375
+
376
+ The new dimensions would be 960 × 540 pixels.
377
+ ```
378
+
379
+ ## OpenWebUI
380
+
381
+ ### Configuration File
382
+
383
+ Add to your OpenWebUI configuration (`config.yaml` or environment):
384
+
385
+ ```yaml
386
+ mcp:
387
+ servers:
388
+ proportio:
389
+ url: "http://localhost:7860/gradio_api/mcp/sse"
390
+ headers:
391
+ X-Api-Key: "your-secure-token-here"
392
+ tools:
393
+ - percent_of
394
+ - solve_proportion
395
+ - scale_by_ratio
396
+ - direct_k
397
+ - resize_dimensions
398
+ ```
399
+
400
+ ### Environment Variables
401
+
402
+ ```bash
403
+ # OpenWebUI MCP configuration
404
+ OPENWEBUI_MCP_SERVERS='[
405
+ {
406
+ "name": "proportio",
407
+ "url": "http://localhost:7860/gradio_api/mcp/sse",
408
+ "headers": {"X-Api-Key": "your-secure-token-here"}
409
+ }
410
+ ]'
411
+ ```
412
+
413
+ ### Docker Compose with OpenWebUI
414
+
415
+ ```yaml
416
+ version: '3.8'
417
+ services:
418
+ proportio:
419
+ build: .
420
+ ports:
421
+ - "7860:7860"
422
+ environment:
423
+ - MCP_TOKEN=your-secure-token-here
424
+ networks:
425
+ - webui_network
426
+
427
+ openwebui:
428
+ image: ghcr.io/open-webui/open-webui:main
429
+ ports:
430
+ - "3000:8080"
431
+ environment:
432
+ - OPENWEBUI_MCP_SERVERS=[{"name":"proportio","url":"http://proportio:7860/gradio_api/mcp/sse","headers":{"X-Api-Key":"your-secure-token-here"}}]
433
+ depends_on:
434
+ - proportio
435
+ networks:
436
+ - webui_network
437
+
438
+ networks:
439
+ webui_network:
440
+ driver: bridge
441
+ ```
442
+
443
+ ## Msty
444
+
445
+ ### Configuration
446
+
447
+ In Msty's settings, add the MCP server:
448
+
449
+ ```json
450
+ {
451
+ "mcp_servers": {
452
+ "proportio": {
453
+ "type": "sse",
454
+ "url": "http://localhost:7860/gradio_api/mcp/sse",
455
+ "auth": {
456
+ "type": "header",
457
+ "header_name": "X-Api-Key",
458
+ "header_value": "your-secure-token-here"
459
+ },
460
+ "description": "Mathematical proportion calculations"
461
+ }
462
+ }
463
+ }
464
+ ```
465
+
466
+ ### Using with Msty
467
+
468
+ 1. Start the Proportio server: `python app.py`
469
+ 2. Open Msty and go to Settings → MCP Servers
470
+ 3. Add a new server with the configuration above
471
+ 4. The tools will be available in chat sessions
472
+
473
+ ### Example Msty Usage
474
+
475
+ ```
476
+ You: I need to solve this proportion: 3/4 = x/12. What is x?
477
+
478
+ Msty: I'll use the proportion solver to find the missing value.
479
+
480
+ [Using solve_proportion tool]
481
+
482
+ The missing value x is 9. So 3/4 = 9/12.
483
+ ```
484
+
485
+ ## Local Development Setup Script
486
+
487
+ Create a setup script for easy local development:
488
+
489
+ ```bash
490
+ #!/bin/bash
491
+ # setup_local_mcp.sh
492
+
493
+ # Generate secure token
494
+ TOKEN=$(python -c "import secrets; print(secrets.token_urlsafe(32))")
495
+
496
+ # Create .env file
497
+ cat > .env << EOF
498
+ MCP_TOKEN=$TOKEN
499
+ DEBUG=true
500
+ LOG_LEVEL=DEBUG
501
+ RELOAD=true
502
+ EOF
503
+
504
+ echo "Generated secure token: $TOKEN"
505
+ echo "Add this to your MCP clients' X-Api-Key header"
506
+
507
+ # Start the server
508
+ python app.py
509
+ ```
510
+
511
+ Make it executable: `chmod +x setup_local_mcp.sh`
512
+
513
+ ## Testing Your MCP Connection
514
+
515
+ ### Quick Test Script
516
+
517
+ ```python
518
+ #!/usr/bin/env python3
519
+ """Test script for Proportio MCP server connection"""
520
+
521
+ import requests
522
+ import json
523
+ import sys
524
+
525
+ def test_mcp_connection(base_url="http://localhost:7860", api_key="your-token"):
526
+ """Test MCP server connection and tools."""
527
+
528
+ url = f"{base_url}/gradio_api/mcp/sse"
529
+ headers = {
530
+ "X-Api-Key": api_key,
531
+ "Content-Type": "application/json"
532
+ }
533
+
534
+ # Test cases
535
+ tests = [
536
+ {
537
+ "name": "Percentage Calculation",
538
+ "tool": "percent_of",
539
+ "args": {"part": 25, "whole": 100},
540
+ "expected": 25.0
541
+ },
542
+ {
543
+ "name": "Proportion Solving",
544
+ "tool": "solve_proportion",
545
+ "args": {"a": 3, "b": 4, "c": 6, "d": None},
546
+ "expected": 8.0
547
+ },
548
+ {
549
+ "name": "Dimension Resizing",
550
+ "tool": "resize_dimensions",
551
+ "args": {"width": 100, "height": 50, "scale": 2.0},
552
+ "expected": {"new_width": 200.0, "new_height": 100.0}
553
+ }
554
+ ]
555
+
556
+ print(f"Testing MCP server at {url}")
557
+ print(f"Using API key: {api_key[:8]}...")
558
+ print("-" * 50)
559
+
560
+ for test in tests:
561
+ try:
562
+ payload = {
563
+ "jsonrpc": "2.0",
564
+ "id": 1,
565
+ "method": "tools/call",
566
+ "params": {
567
+ "name": test["tool"],
568
+ "arguments": test["args"]
569
+ }
570
+ }
571
+
572
+ response = requests.post(url, headers=headers, json=payload, timeout=10)
573
+
574
+ if response.status_code == 200:
575
+ result = response.json()
576
+ print(f"✅ {test['name']}: PASSED")
577
+ print(f" Result: {result}")
578
+ else:
579
+ print(f"❌ {test['name']}: FAILED (HTTP {response.status_code})")
580
+ print(f" Response: {response.text}")
581
+
582
+ except Exception as e:
583
+ print(f"❌ {test['name']}: ERROR - {str(e)}")
584
+
585
+ print()
586
+
587
+ if __name__ == "__main__":
588
+ api_key = input("Enter your API key (or press Enter for 'test-token'): ").strip()
589
+ if not api_key:
590
+ api_key = "test-token"
591
+
592
+ test_mcp_connection(api_key=api_key)
593
+ ```
594
+
595
+ Save as `test_mcp.py` and run: `python test_mcp.py`
596
+
597
+ This comprehensive guide covers all major MCP clients including Roo, OpenWebUI, and Msty, providing practical examples for integrating the Proportio server into your development workflow.
logo.png ADDED
logo_1024.png ADDED
models.py ADDED
@@ -0,0 +1,96 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Pydantic models for proportion calculation inputs and outputs.
3
+ Provides robust validation and clear error messages.
4
+ """
5
+
6
+ from typing import Optional
7
+ from pydantic import BaseModel, Field, model_validator
8
+
9
+
10
+ class PercentOfInput(BaseModel):
11
+ """Input for calculating what percentage part is of whole."""
12
+
13
+ part: float = Field(description="The part value")
14
+ whole: float = Field(description="The whole value (must not be zero)")
15
+
16
+
17
+
18
+ class PercentOfOutput(BaseModel):
19
+ """Output for percentage calculation."""
20
+
21
+ percentage: float = Field(description="The calculated percentage")
22
+
23
+
24
+ class ProportionInput(BaseModel):
25
+ """Input for solving proportion a/b = c/d with exactly one missing value."""
26
+
27
+ a: Optional[float] = Field(None, description="First numerator")
28
+ b: Optional[float] = Field(None, description="First denominator")
29
+ c: Optional[float] = Field(None, description="Second numerator")
30
+ d: Optional[float] = Field(None, description="Second denominator")
31
+
32
+ @model_validator(mode='after')
33
+ def validate_exactly_one_none(self) -> 'ProportionInput':
34
+ """Ensure exactly one value is None (missing)."""
35
+ values = [self.a, self.b, self.c, self.d]
36
+ none_count = sum(1 for v in values if v is None)
37
+
38
+ assert none_count == 1, "Exactly one value must be None (missing) to solve proportion"
39
+
40
+ return self
41
+
42
+
43
+ class ProportionOutput(BaseModel):
44
+ """Output for proportion calculation."""
45
+
46
+ missing: float = Field(description="The calculated missing value")
47
+
48
+
49
+ class ScaleByRatioInput(BaseModel):
50
+ """Input for scaling a value by a ratio."""
51
+
52
+ value: float = Field(description="The value to scale")
53
+ ratio: float = Field(description="The scaling ratio")
54
+
55
+
56
+ class ScaleByRatioOutput(BaseModel):
57
+ """Output for ratio scaling."""
58
+
59
+ result: float = Field(description="The scaled result")
60
+
61
+
62
+ class DirectKInput(BaseModel):
63
+ """Input for finding constant of proportionality k in y = kx."""
64
+
65
+ x: float = Field(description="The x value (cannot be zero)")
66
+ y: float = Field(description="The y value")
67
+
68
+
69
+
70
+ class DirectKOutput(BaseModel):
71
+ """Output for proportionality constant calculation."""
72
+
73
+ k: float = Field(description="The proportionality constant")
74
+
75
+
76
+ class ResizeDimensionsInput(BaseModel):
77
+ """Input for resizing dimensions with uniform scale factor."""
78
+
79
+ width: float = Field(description="Original width (must be non-negative)")
80
+ height: float = Field(description="Original height (must be non-negative)")
81
+ scale: float = Field(description="Scale factor (must be positive)")
82
+
83
+
84
+ class ResizeDimensionsOutput(BaseModel):
85
+ """Output for dimension resizing."""
86
+
87
+ new_width: float = Field(description="The new width after scaling")
88
+ new_height: float = Field(description="The new height after scaling")
89
+
90
+
91
+ # Error response model for consistent error handling
92
+ class ErrorResponse(BaseModel):
93
+ """Standard error response format."""
94
+
95
+ error: str = Field(description="Error message")
96
+ detail: Optional[str] = Field(None, description="Additional error details")
proportion_server.py ADDED
@@ -0,0 +1,361 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Core proportion calculation tools with Gradio MCP server capabilities.
3
+ Provides reliable mathematical operations for LLM agents.
4
+ """
5
+
6
+ import logging
7
+ from typing import Optional, Tuple
8
+ import gradio as gr
9
+
10
+ from config import get_settings, setup_logging, get_deployment_info
11
+
12
+ # Setup logging
13
+ setup_logging()
14
+ logger = logging.getLogger(__name__)
15
+ settings = get_settings()
16
+
17
+
18
+ def percent_of(part: float, whole: float) -> float:
19
+ """
20
+ Calculate what percentage 'part' is of 'whole'.
21
+
22
+ Args:
23
+ part: The part value
24
+ whole: The whole value (must not be zero)
25
+
26
+ Returns:
27
+ The percentage value
28
+
29
+ Mathematical preconditions:
30
+ - whole != 0 (to avoid division by zero)
31
+ """
32
+ # Mathematical preconditions
33
+ assert whole != 0, "Division by zero: whole cannot be zero"
34
+
35
+ # Calculate percentage
36
+ percentage = (part / whole) * 100
37
+
38
+ logger.debug(f"Calculated {part} is {percentage:.2f}% of {whole}")
39
+
40
+ return percentage
41
+
42
+
43
+ def solve_proportion(
44
+ a: Optional[float] = None,
45
+ b: Optional[float] = None,
46
+ c: Optional[float] = None,
47
+ d: Optional[float] = None
48
+ ) -> float:
49
+ """
50
+ Solve missing term in proportion a/b = c/d.
51
+ Exactly one parameter must be None (missing).
52
+
53
+ Args:
54
+ a: First numerator (optional)
55
+ b: First denominator (optional, cannot be zero)
56
+ c: Second numerator (optional)
57
+ d: Second denominator (optional, cannot be zero)
58
+
59
+ Returns:
60
+ The missing value
61
+
62
+ Mathematical preconditions:
63
+ - Exactly one value must be None (missing)
64
+ - Division denominators != 0 (varies by missing value)
65
+ """
66
+ # Mathematical preconditions - exactly one value must be None
67
+ values = [a, b, c, d]
68
+ none_count = sum(1 for v in values if v is None)
69
+ assert none_count == 1, "Exactly one value must be None"
70
+
71
+ # Solve for missing value using cross multiplication: a*d = b*c
72
+ if a is None:
73
+ assert d != 0, "Division by zero: denominator"
74
+ missing = (b * c) / d
75
+ elif b is None:
76
+ assert c != 0, "Division by zero: denominator"
77
+ missing = (a * d) / c
78
+ elif c is None:
79
+ assert b != 0, "Division by zero: denominator"
80
+ missing = (a * d) / b
81
+ else: # d is None
82
+ assert a != 0, "Division by zero: denominator"
83
+ missing = (b * c) / a
84
+
85
+ logger.debug(f"Solved proportion: missing value = {missing}")
86
+
87
+ return missing
88
+
89
+
90
+ def scale_by_ratio(value: float, ratio: float) -> float:
91
+ """
92
+ Scale a value by a given ratio.
93
+
94
+ Args:
95
+ value: The value to scale
96
+ ratio: The scaling ratio
97
+
98
+ Returns:
99
+ The scaled value
100
+ """
101
+ # Calculate scaled result
102
+ scaled_result = value * ratio
103
+
104
+ logger.debug(f"Scaled {value} by ratio {ratio} = {scaled_result}")
105
+
106
+ return scaled_result
107
+
108
+
109
+ def direct_k(x: float, y: float) -> float:
110
+ """
111
+ Find constant of proportionality k in direct variation y = kx.
112
+
113
+ Args:
114
+ x: The x value (cannot be zero)
115
+ y: The y value
116
+
117
+ Returns:
118
+ The proportionality constant k
119
+
120
+ Mathematical preconditions:
121
+ - x != 0 (to avoid division by zero)
122
+ """
123
+ # Mathematical preconditions
124
+ assert x != 0, "Division by zero: x cannot be zero"
125
+
126
+ # Calculate proportionality constant
127
+ k = y / x
128
+
129
+ logger.debug(f"Found proportionality constant k = {k} for y = {y}, x = {x}")
130
+
131
+ return k
132
+
133
+
134
+ def resize_dimensions(width: float, height: float, scale: float) -> Tuple[float, float]:
135
+ """
136
+ Resize dimensions with uniform scale factor.
137
+
138
+ Args:
139
+ width: Original width (must be non-negative)
140
+ height: Original height (must be non-negative)
141
+ scale: Scale factor (must be positive)
142
+
143
+ Returns:
144
+ Tuple of (new_width, new_height)
145
+
146
+ Mathematical preconditions:
147
+ - width >= 0 (dimensions must be non-negative)
148
+ - height >= 0 (dimensions must be non-negative)
149
+ - scale > 0 (scale factor must be positive)
150
+ """
151
+ # Mathematical preconditions
152
+ assert width >= 0, "Width must be non-negative"
153
+ assert height >= 0, "Height must be non-negative"
154
+ assert scale > 0, "Scale factor must be positive"
155
+
156
+ # Calculate new dimensions
157
+ new_width = width * scale
158
+ new_height = height * scale
159
+
160
+ logger.debug(f"Resized {width}x{height} by {scale} = {new_width}x{new_height}")
161
+
162
+ return (new_width, new_height)
163
+
164
+
165
+ # Demo functions for Gradio interface
166
+ def demo_percent_of(part: float, whole: float) -> str:
167
+ """Demo function for percent_of calculation."""
168
+ result = percent_of(part, whole)
169
+ return f"{part} is {result:.2f}% of {whole}"
170
+
171
+
172
+ def demo_solve_proportion(a: Optional[str], b: Optional[str], c: Optional[str], d: Optional[str]) -> str:
173
+ """Demo function for proportion solving."""
174
+ # Convert string inputs to float or None
175
+ a_val = None if a == "" or a is None else float(a)
176
+ b_val = None if b == "" or b is None else float(b)
177
+ c_val = None if c == "" or c is None else float(c)
178
+ d_val = None if d == "" or d is None else float(d)
179
+
180
+ result = solve_proportion(a_val, b_val, c_val, d_val)
181
+ return f"Missing value: {result:.4f}"
182
+
183
+
184
+ def demo_scale_by_ratio(value: float, ratio: float) -> str:
185
+ """Demo function for scaling by ratio."""
186
+ result = scale_by_ratio(value, ratio)
187
+ return f"{value} × {ratio} = {result:.4f}"
188
+
189
+
190
+ def demo_direct_k(x: float, y: float) -> str:
191
+ """Demo function for finding proportionality constant."""
192
+ result = direct_k(x, y)
193
+ return f"k = {result:.4f} (where y = kx)"
194
+
195
+
196
+ def demo_resize_dimensions(width: float, height: float, scale: float) -> str:
197
+ """Demo function for resizing dimensions."""
198
+ new_width, new_height = resize_dimensions(width, height, scale)
199
+ return f"New dimensions: {new_width:.2f} × {new_height:.2f}"
200
+
201
+
202
+ def create_gradio_app():
203
+ """Create and return the Gradio interface."""
204
+ with gr.Blocks(
205
+ title="PROPORTIO",
206
+ css_paths=["styles.css"],
207
+ theme=gr.themes.Base(),
208
+ head='<link rel="icon" type="image/png" href="/file=logo.png"><link rel="shortcut icon" type="image/png" href="/file=logo.png">'
209
+ ) as demo:
210
+ with gr.Row():
211
+ with gr.Column(scale=1, min_width=72):
212
+ gr.Image("logo.png", height=72, width=72, show_label=False, show_download_button=False, show_fullscreen_button=False, container=False, interactive=False)
213
+ with gr.Column(scale=10):
214
+ gr.HTML("""
215
+ <div class="p-brand">
216
+ <h1 class="p-title">PROPORTIO</h1>
217
+ <div class="p-tagline">Mathematical Precision & Proportion Calculator</div>
218
+ </div>
219
+ """)
220
+
221
+ with gr.Tabs():
222
+ with gr.TabItem("📊 Percentage Calculator"):
223
+ gr.HTML('<div class="tab-title">Calculate Percentage</div>')
224
+ gr.HTML('<div class="tab-desc">Find what percentage the part is of the whole</div>')
225
+
226
+ with gr.Row():
227
+ with gr.Column(scale=1):
228
+ part_input = gr.Number(label="Part", value=25, info="The part value")
229
+ whole_input = gr.Number(label="Whole", value=100, info="The whole value (cannot be zero)")
230
+ percent_btn = gr.Button("Calculate Percentage", variant="secondary", size="lg")
231
+
232
+ with gr.Column(scale=1):
233
+ percent_output = gr.Textbox(label="Result", interactive=False, show_label=True)
234
+ gr.HTML('<div class="formula-box"><strong>Formula:</strong> (Part ÷ Whole) × 100</div>')
235
+
236
+ with gr.TabItem("⚖️ Proportion Solver"):
237
+ gr.HTML('<div class="tab-title">Solve Proportion (a/b = c/d)</div>')
238
+ gr.HTML('<div class="tab-desc">Leave exactly one field empty to solve for the missing value</div>')
239
+
240
+ with gr.Row():
241
+ with gr.Column(scale=1):
242
+ with gr.Row():
243
+ a_input = gr.Textbox(label="a", value="3", info="First numerator")
244
+ b_input = gr.Textbox(label="b", value="4", info="First denominator")
245
+ with gr.Row():
246
+ c_input = gr.Textbox(label="c", value="6", info="Second numerator")
247
+ d_input = gr.Textbox(label="d", value="", info="Second denominator")
248
+ proportion_btn = gr.Button("Solve Proportion", variant="secondary", size="lg")
249
+
250
+ with gr.Column(scale=1):
251
+ proportion_output = gr.Textbox(label="Result", interactive=False, show_label=True)
252
+ gr.HTML('<div class="formula-box"><strong>Formula:</strong> a × d = b × c</div>')
253
+
254
+ with gr.TabItem("📏 Scale by Ratio"):
255
+ gr.HTML('<div class="tab-title">Scale Value by Ratio</div>')
256
+ gr.HTML('<div class="tab-desc">Multiply a value by a scaling ratio</div>')
257
+
258
+ with gr.Row():
259
+ with gr.Column(scale=1):
260
+ value_input = gr.Number(label="Value", value=10, info="The value to scale")
261
+ ratio_input = gr.Number(label="Ratio", value=1.5, info="The scaling factor")
262
+ scale_btn = gr.Button("Scale Value", variant="secondary", size="lg")
263
+
264
+ with gr.Column(scale=1):
265
+ scale_output = gr.Textbox(label="Result", interactive=False, show_label=True)
266
+ gr.HTML('<div class="formula-box"><strong>Formula:</strong> Value × Ratio</div>')
267
+
268
+ with gr.TabItem("🔢 Find Constant k"):
269
+ gr.HTML('<div class="tab-title">Find Proportionality Constant (y = kx)</div>')
270
+ gr.HTML('<div class="tab-desc">Calculate the constant k in direct variation</div>')
271
+
272
+ with gr.Row():
273
+ with gr.Column(scale=1):
274
+ x_input = gr.Number(label="x", value=5, info="The x value (cannot be zero)")
275
+ y_input = gr.Number(label="y", value=15, info="The y value")
276
+ direct_btn = gr.Button("Find k", variant="secondary", size="lg")
277
+
278
+ with gr.Column(scale=1):
279
+ direct_output = gr.Textbox(label="Result", interactive=False, show_label=True)
280
+ gr.HTML('<div class="formula-box"><strong>Formula:</strong> k = y ÷ x</div>')
281
+
282
+ with gr.TabItem("📐 Resize Dimensions"):
283
+ gr.HTML('<div class="tab-title">Resize Dimensions</div>')
284
+ gr.HTML('<div class="tab-desc">Scale width and height by a uniform factor</div>')
285
+
286
+ with gr.Row():
287
+ with gr.Column(scale=1):
288
+ width_input = gr.Number(label="Width", value=100, info="Original width (≥ 0)")
289
+ height_input = gr.Number(label="Height", value=50, info="Original height (≥ 0)")
290
+ scale_dim_input = gr.Number(label="Scale Factor", value=2.0, info="Scaling factor (> 0)")
291
+ resize_btn = gr.Button("Resize", variant="secondary", size="lg")
292
+
293
+ with gr.Column(scale=1):
294
+ resize_output = gr.Textbox(label="Result", interactive=False, show_label=True)
295
+ gr.HTML('<div class="formula-box"><strong>Formula:</strong> New = Original × Scale</div>')
296
+
297
+ # Event handlers
298
+ percent_btn.click(
299
+ demo_percent_of,
300
+ inputs=[part_input, whole_input],
301
+ outputs=[percent_output]
302
+ )
303
+
304
+ proportion_btn.click(
305
+ demo_solve_proportion,
306
+ inputs=[a_input, b_input, c_input, d_input],
307
+ outputs=[proportion_output]
308
+ )
309
+
310
+ scale_btn.click(
311
+ demo_scale_by_ratio,
312
+ inputs=[value_input, ratio_input],
313
+ outputs=[scale_output]
314
+ )
315
+
316
+ direct_btn.click(
317
+ demo_direct_k,
318
+ inputs=[x_input, y_input],
319
+ outputs=[direct_output]
320
+ )
321
+
322
+ resize_btn.click(
323
+ demo_resize_dimensions,
324
+ inputs=[width_input, height_input, scale_dim_input],
325
+ outputs=[resize_output]
326
+ )
327
+
328
+ gr.HTML("""
329
+ <div class="footer">
330
+ <div class="footer-content">
331
+ <div class="footer-section">
332
+ <div class="footer-title">About</div>
333
+ <div class="footer-text">Professional mathematical calculations for proportions, percentages, and scaling operations.</div>
334
+ </div>
335
+ <div class="footer-section">
336
+ <div class="footer-title">Features</div>
337
+ <div class="footer-text">• Assertion-based validation<br>• Precise error handling<br>• MCP server integration</div>
338
+ </div>
339
+ </div>
340
+ </div>
341
+ """)
342
+
343
+ return demo
344
+
345
+
346
+ if __name__ == "__main__":
347
+ # Get deployment info
348
+ deployment_info = get_deployment_info()
349
+ logger.info(f"Starting Proportio Calculator - {deployment_info}")
350
+
351
+ # Create and launch the interface
352
+ demo = create_gradio_app()
353
+
354
+ # Launch with MCP server capabilities
355
+ demo.launch(
356
+ server_name="0.0.0.0",
357
+ server_port=settings.PORT,
358
+ share=settings.SHARE,
359
+ mcp_server=True, # Enable MCP server functionality
360
+ show_error=True
361
+ )
pyproject.toml ADDED
@@ -0,0 +1,63 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ [build-system]
2
+ requires = ["setuptools>=68.0"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "proportio-mcp-server"
7
+ version = "1.0.0"
8
+ description = "Gradio-hosted MCP server for reliable proportion calculations"
9
+ authors = [{name = "Proportio Team"}]
10
+ readme = "README.md"
11
+ requires-python = ">=3.11"
12
+ dependencies = [
13
+ "gradio[mcp]>=5.0.0",
14
+ "pydantic>=2.8.0",
15
+ "pytest>=8.0.0",
16
+ ]
17
+
18
+ [tool.black]
19
+ line-length = 88
20
+ target-version = ['py311']
21
+ include = '\.pyi?$'
22
+
23
+ [tool.ruff]
24
+ target-version = "py311"
25
+ line-length = 88
26
+ select = [
27
+ "E", # pycodestyle errors
28
+ "W", # pycodestyle warnings
29
+ "F", # pyflakes
30
+ "I", # isort
31
+ "B", # flake8-bugbear
32
+ "C4", # flake8-comprehensions
33
+ "UP", # pyupgrade
34
+ ]
35
+ ignore = []
36
+
37
+ [tool.ruff.per-file-ignores]
38
+ "tests/*" = ["S101"]
39
+
40
+ [tool.mypy]
41
+ python_version = "3.11"
42
+ warn_return_any = true
43
+ warn_unused_configs = true
44
+ disallow_untyped_defs = true
45
+ disallow_incomplete_defs = true
46
+ check_untyped_defs = true
47
+ disallow_untyped_decorators = true
48
+ no_implicit_optional = true
49
+ warn_redundant_casts = true
50
+ warn_unused_ignores = true
51
+ warn_no_return = true
52
+ warn_unreachable = true
53
+ strict_equality = true
54
+
55
+ [tool.pytest.ini_options]
56
+ testpaths = ["tests"]
57
+ python_files = ["test_*.py"]
58
+ python_classes = ["Test*"]
59
+ python_functions = ["test_*"]
60
+ addopts = [
61
+ "--strict-markers",
62
+ "--strict-config",
63
+ ]
requirements.txt ADDED
@@ -0,0 +1,9 @@
 
 
 
 
 
 
 
 
 
 
1
+ # Core dependencies
2
+ gradio[mcp]>=5.0.0
3
+ pydantic>=2.8.0
4
+ pydantic-settings>=2.0.0
5
+ websockets>=13.0,<14.0
6
+ requests>=2.31.0
7
+
8
+ # Testing
9
+ pytest>=8.0.0
styles.css ADDED
@@ -0,0 +1,148 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /* ---------- Proportio Minimal Theme ---------- */
2
+ :root{
3
+ --primary: #7c1212;
4
+ --secondary: #a10907;
5
+ --accent: #8f9190;
6
+ }
7
+
8
+ /* Logo visibility and alignment fix */
9
+ .gradio-row {
10
+ display: flex !important;
11
+ align-items: center !important;
12
+ gap: 16px !important;
13
+ }
14
+
15
+ .gradio-column {
16
+ display: flex !important;
17
+ align-items: center !important;
18
+ height: auto !important;
19
+ }
20
+
21
+ /* Target the first column (logo) */
22
+ .gradio-column:first-child {
23
+ justify-content: center !important;
24
+ align-items: center !important;
25
+ }
26
+
27
+ /* Target the second column (text) */
28
+ .gradio-column:last-child {
29
+ justify-content: flex-start !important;
30
+ align-items: center !important;
31
+ }
32
+
33
+ .gradio-image {
34
+ display: flex !important;
35
+ align-items: center !important;
36
+ justify-content: center !important;
37
+ margin: 0 !important;
38
+ padding: 0 !important;
39
+ }
40
+
41
+ .gradio-image img {
42
+ display: block !important;
43
+ opacity: 1 !important;
44
+ visibility: visible !important;
45
+ }
46
+
47
+ /* Ensure the brand section is vertically centered */
48
+ .p-brand {
49
+ display: flex !important;
50
+ flex-direction: column !important;
51
+ justify-content: center !important;
52
+ margin: 0 !important;
53
+ padding: 0 !important;
54
+ }
55
+
56
+ .p-title {
57
+ margin: 0 !important;
58
+ line-height: 1.2 !important;
59
+ }
60
+
61
+ .p-tagline {
62
+ margin: 0 !important;
63
+ line-height: 1.2 !important;
64
+ }
65
+
66
+ /* Override blue colors with primary red */
67
+ .gradio-tabs button {
68
+ background-color: var(--primary) !important;
69
+ border-color: var(--primary) !important;
70
+ }
71
+
72
+ .gradio-tabs button:hover {
73
+ background-color: var(--secondary) !important;
74
+ border-color: var(--secondary) !important;
75
+ }
76
+
77
+ .gradio-tabs button[aria-selected="true"] {
78
+ background-color: var(--primary) !important;
79
+ border-color: var(--primary) !important;
80
+ color: var(--accent) !important;
81
+ }
82
+
83
+ .gradio-tabs button[aria-selected="true"] span {
84
+ color: var(--accent) !important;
85
+ }
86
+
87
+ /* Override any blue accents */
88
+ .tab-nav button {
89
+ background-color: var(--primary) !important;
90
+ border-color: var(--primary) !important;
91
+ }
92
+
93
+ /* Target selected tab text colors */
94
+ button[role="tab"][aria-selected="true"] {
95
+ color: var(--accent) !important;
96
+ }
97
+
98
+ button[role="tab"][aria-selected="true"] span {
99
+ color: var(--accent) !important;
100
+ }
101
+
102
+ /* Override blue text in tabs */
103
+ .gradio-tabs button span {
104
+ color: var(--accent) !important;
105
+ }
106
+
107
+ /* Override blue underlines/borders on selected tabs */
108
+ .gradio-tabitem,
109
+ .gradio-tabs .tab-nav button[aria-selected="true"]::after,
110
+ .gradio-tabs button[aria-selected="true"]::after,
111
+ button[role="tab"][aria-selected="true"]::after {
112
+ border-bottom-color: var(--secondary) !important;
113
+ background-color: var(--secondary) !important;
114
+ }
115
+
116
+ /* Target the tab indicator line - more specific selectors */
117
+ .gradio-tabs .tab-nav .tab-indicator,
118
+ .gradio-tabs .selected-tab-indicator,
119
+ .gradio-tabs button[aria-selected="true"] + .tab-indicator,
120
+ .gradio-tabs button[aria-selected="true"]::before,
121
+ .gradio-tabs button[data-selected="true"]::before,
122
+ div[role="tablist"] button[aria-selected="true"]::before,
123
+ div[role="tablist"] button[aria-selected="true"]::after,
124
+ .gradio-tabs .tab-nav button::before,
125
+ .gradio-tabs .tab-nav button::after {
126
+ background-color: var(--secondary) !important;
127
+ border-color: var(--secondary) !important;
128
+ background: var(--secondary) !important;
129
+ }
130
+
131
+ /* Override any remaining blue accents - comprehensive approach */
132
+ .gradio-tabs button[aria-selected="true"] {
133
+ border-bottom: 2px solid var(--secondary) !important;
134
+ box-shadow: inset 0 -2px 0 var(--secondary) !important;
135
+ }
136
+
137
+ /* Target Gradio's internal tab styling */
138
+ .gradio-tabs button[data-selected="true"],
139
+ button[role="tab"][data-selected="true"] {
140
+ border-bottom-color: var(--secondary) !important;
141
+ box-shadow: inset 0 -2px 0 var(--secondary) !important;
142
+ }
143
+
144
+ /* Override blue focus states */
145
+ .gradio-tabs button:focus {
146
+ outline-color: var(--secondary) !important;
147
+ border-color: var(--secondary) !important;
148
+ }
tests/test_tools.py ADDED
@@ -0,0 +1,428 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Comprehensive test suite for proportion calculation tools.
3
+ Tests all functions with various inputs, edge cases, and error conditions.
4
+ """
5
+
6
+ import pytest
7
+ from proportion_server import (
8
+ percent_of, solve_proportion, scale_by_ratio,
9
+ direct_k, resize_dimensions
10
+ )
11
+
12
+
13
+ class TestPercentOf:
14
+ """Test the percent_of function."""
15
+
16
+ def test_basic_percentage(self):
17
+ """Test basic percentage calculations."""
18
+ assert percent_of(25, 100) == 25.0
19
+ assert percent_of(1, 4) == 25.0
20
+ assert percent_of(3, 4) == 75.0
21
+
22
+ def test_zero_part(self):
23
+ """Test with zero part."""
24
+ assert percent_of(0, 100) == 0.0
25
+ assert percent_of(0, 1) == 0.0
26
+
27
+ def test_negative_values(self):
28
+ """Test with negative values."""
29
+ assert percent_of(-25, 100) == -25.0
30
+ assert percent_of(25, -100) == -25.0
31
+ assert percent_of(-25, -100) == 25.0
32
+
33
+ def test_decimal_precision(self):
34
+ """Test decimal precision in calculations."""
35
+ result = percent_of(1, 3)
36
+ assert abs(result - 33.333333333333336) < 1e-10
37
+
38
+ def test_large_numbers(self):
39
+ """Test with large numbers."""
40
+ assert percent_of(1000000, 4000000) == 25.0
41
+
42
+ def test_very_small_numbers(self):
43
+ """Test with very small numbers."""
44
+ result = percent_of(1e-10, 1e-8)
45
+ assert abs(result - 1.0) < 1e-10
46
+
47
+ def test_zero_whole_error(self):
48
+ """Test assertion error when whole is zero."""
49
+ with pytest.raises(AssertionError, match="Division by zero: whole cannot be zero"):
50
+ percent_of(10, 0)
51
+
52
+ def test_negative_whole_edge_cases(self):
53
+ """Test edge cases with negative whole values."""
54
+ assert percent_of(10, -5) == -200.0
55
+ assert percent_of(-10, -5) == 200.0
56
+
57
+
58
+ class TestSolveProportion:
59
+ """Test the solve_proportion function."""
60
+
61
+ def test_solve_a(self):
62
+ """Test solving for 'a' in proportion a/b = c/d."""
63
+ result = solve_proportion(None, 4, 6, 8)
64
+ assert result == 3.0
65
+
66
+ def test_solve_b(self):
67
+ """Test solving for 'b' in proportion a/b = c/d."""
68
+ result = solve_proportion(3, None, 6, 8)
69
+ assert result == 4.0
70
+
71
+ def test_solve_c(self):
72
+ """Test solving for 'c' in proportion a/b = c/d."""
73
+ result = solve_proportion(3, 4, None, 8)
74
+ assert result == 6.0
75
+
76
+ def test_solve_d(self):
77
+ """Test solving for 'd' in proportion a/b = c/d."""
78
+ result = solve_proportion(3, 4, 6, None)
79
+ assert result == 8.0
80
+
81
+ def test_negative_values(self):
82
+ """Test with negative values."""
83
+ result = solve_proportion(-3, 4, 6, None)
84
+ assert result == -8.0
85
+
86
+ def test_decimal_results(self):
87
+ """Test with decimal results."""
88
+ result = solve_proportion(1, 3, 2, None)
89
+ assert abs(result - 6.0) < 1e-10
90
+
91
+ def test_multiple_none_error(self):
92
+ """Test assertion error when multiple values are None."""
93
+ with pytest.raises(AssertionError, match="Exactly one value must be None"):
94
+ solve_proportion(None, None, 6, 8)
95
+
96
+ with pytest.raises(AssertionError, match="Exactly one value must be None"):
97
+ solve_proportion(None, 4, None, 8)
98
+
99
+ with pytest.raises(AssertionError, match="Exactly one value must be None"):
100
+ solve_proportion(None, None, None, None)
101
+
102
+ def test_no_none_error(self):
103
+ """Test assertion error when no values are None."""
104
+ with pytest.raises(AssertionError, match="Exactly one value must be None"):
105
+ solve_proportion(3, 4, 6, 8)
106
+
107
+ def test_zero_denominator_error(self):
108
+ """Test assertion error when denominators are zero."""
109
+ with pytest.raises(AssertionError, match="Division by zero: denominator"):
110
+ solve_proportion(None, 2, 3, 0) # Solving for a, divides by d=0
111
+
112
+ with pytest.raises(AssertionError, match="Division by zero: denominator"):
113
+ solve_proportion(2, None, 0, 3) # Solving for b, divides by c=0
114
+
115
+ def test_division_by_zero_calculation(self):
116
+ """Test assertion error for division by zero during calculation."""
117
+ with pytest.raises(AssertionError, match="Division by zero: denominator"):
118
+ # This would require dividing by zero in calculation
119
+ solve_proportion(None, 4, 0, 0)
120
+
121
+ def test_large_numbers(self):
122
+ """Test with large numbers."""
123
+ result = solve_proportion(1000000, 2000000, 500000, None)
124
+ assert result == 1000000.0
125
+
126
+ def test_very_small_numbers(self):
127
+ """Test with very small numbers."""
128
+ result = solve_proportion(1e-10, 2e-10, 5e-11, None)
129
+ assert abs(result - 1e-10) < 1e-20
130
+
131
+
132
+ class TestScaleByRatio:
133
+ """Test the scale_by_ratio function."""
134
+
135
+ def test_positive_scaling(self):
136
+ """Test scaling with positive ratios."""
137
+ assert scale_by_ratio(10, 2.5) == 25.0
138
+ assert scale_by_ratio(100, 0.5) == 50.0
139
+
140
+ def test_negative_values(self):
141
+ """Test scaling with negative values."""
142
+ assert scale_by_ratio(-10, 2.0) == -20.0
143
+ assert scale_by_ratio(10, -2.0) == -20.0
144
+ assert scale_by_ratio(-10, -2.0) == 20.0
145
+
146
+ def test_zero_scaling(self):
147
+ """Test scaling with zero ratio."""
148
+ assert scale_by_ratio(100, 0) == 0.0
149
+
150
+ def test_zero_value(self):
151
+ """Test scaling zero value."""
152
+ assert scale_by_ratio(0, 5.0) == 0.0
153
+
154
+ def test_identity_scaling(self):
155
+ """Test scaling with ratio of 1."""
156
+ assert scale_by_ratio(42, 1.0) == 42.0
157
+
158
+ def test_fractional_scaling(self):
159
+ """Test scaling with fractional ratios."""
160
+ assert scale_by_ratio(9, 1/3) == 3.0
161
+
162
+ def test_large_numbers(self):
163
+ """Test scaling with large numbers."""
164
+ result = scale_by_ratio(1e6, 1e-3)
165
+ assert result == 1000.0
166
+
167
+ def test_very_small_numbers(self):
168
+ """Test scaling with very small numbers."""
169
+ result = scale_by_ratio(1e-10, 1e10)
170
+ assert result == 1.0
171
+
172
+ def test_precision_edge_cases(self):
173
+ """Test precision with edge cases."""
174
+ result = scale_by_ratio(1/3, 3)
175
+ assert abs(result - 1.0) < 1e-15
176
+
177
+
178
+ class TestDirectK:
179
+ """Test the direct_k function."""
180
+
181
+ def test_positive_values(self):
182
+ """Test with positive values."""
183
+ assert direct_k(5, 15) == 3.0
184
+ assert direct_k(2, 10) == 5.0
185
+
186
+ def test_negative_values(self):
187
+ """Test with negative values."""
188
+ assert direct_k(-4, 12) == -3.0
189
+ assert direct_k(4, -12) == -3.0
190
+ assert direct_k(-4, -12) == 3.0
191
+
192
+ def test_zero_y(self):
193
+ """Test with zero y value."""
194
+ assert direct_k(5, 0) == 0.0
195
+
196
+ def test_zero_x_error(self):
197
+ """Test assertion error when x is zero."""
198
+ with pytest.raises(AssertionError, match="Division by zero: x cannot be zero"):
199
+ direct_k(0, 10)
200
+
201
+ def test_decimal_precision(self):
202
+ """Test decimal precision."""
203
+ result = direct_k(3, 1)
204
+ assert abs(result - 0.3333333333333333) < 1e-15
205
+
206
+ def test_large_numbers(self):
207
+ """Test with large numbers."""
208
+ assert direct_k(1e6, 2e6) == 2.0
209
+
210
+ def test_very_small_numbers(self):
211
+ """Test with very small numbers."""
212
+ result = direct_k(1e-10, 5e-10)
213
+ assert abs(result - 5.0) < 1e-10
214
+
215
+ def test_fractional_results(self):
216
+ """Test fractional results."""
217
+ result = direct_k(7, 2)
218
+ assert abs(result - 2/7) < 1e-15
219
+
220
+
221
+ class TestResizeDimensions:
222
+ """Test the resize_dimensions function."""
223
+
224
+ def test_uniform_scaling(self):
225
+ """Test uniform scaling of dimensions."""
226
+ result = resize_dimensions(100, 50, 2.0)
227
+ assert result == (200.0, 100.0)
228
+
229
+ def test_zero_dimensions(self):
230
+ """Test with zero dimensions."""
231
+ result = resize_dimensions(0, 0, 3.0)
232
+ assert result == (0.0, 0.0)
233
+
234
+ result = resize_dimensions(100, 0, 2.0)
235
+ assert result == (200.0, 0.0)
236
+
237
+ def test_identity_scaling(self):
238
+ """Test scaling with factor of 1."""
239
+ result = resize_dimensions(100, 50, 1.0)
240
+ assert result == (100.0, 50.0)
241
+
242
+ def test_large_scaling(self):
243
+ """Test with large scale factors."""
244
+ result = resize_dimensions(10, 20, 100.0)
245
+ assert result == (1000.0, 2000.0)
246
+
247
+ def test_small_scaling(self):
248
+ """Test with small scale factors."""
249
+ result = resize_dimensions(100, 200, 0.1)
250
+ assert result == (10.0, 20.0)
251
+
252
+ def test_decimal_dimensions(self):
253
+ """Test with decimal dimensions."""
254
+ result = resize_dimensions(10.5, 20.7, 2.0)
255
+ assert result == (21.0, 41.4)
256
+
257
+ def test_negative_dimensions_error(self):
258
+ """Test assertion error with negative dimensions."""
259
+ with pytest.raises(AssertionError, match="Width must be non-negative"):
260
+ resize_dimensions(-100, 50, 2.0)
261
+
262
+ with pytest.raises(AssertionError, match="Height must be non-negative"):
263
+ resize_dimensions(100, -50, 2.0)
264
+
265
+ with pytest.raises(AssertionError, match="Width must be non-negative"):
266
+ resize_dimensions(-100, -50, 2.0)
267
+
268
+ def test_zero_scale_error(self):
269
+ """Test assertion error with zero scale factor."""
270
+ with pytest.raises(AssertionError, match="Scale factor must be positive"):
271
+ resize_dimensions(100, 50, 0)
272
+
273
+ def test_negative_scale_error(self):
274
+ """Test assertion error with negative scale factor."""
275
+ with pytest.raises(AssertionError, match="Scale factor must be positive"):
276
+ resize_dimensions(100, 50, -1.5)
277
+
278
+ with pytest.raises(AssertionError, match="Scale factor must be positive"):
279
+ resize_dimensions(100, 50, -0.1)
280
+
281
+ def test_very_large_dimensions(self):
282
+ """Test with very large dimensions."""
283
+ result = resize_dimensions(1e6, 1e7, 0.001)
284
+ assert result == (1000.0, 10000.0)
285
+
286
+ def test_precision_edge_cases(self):
287
+ """Test precision edge cases."""
288
+ result = resize_dimensions(1/3, 2/3, 3.0)
289
+ expected_width = (1/3) * 3.0
290
+ expected_height = (2/3) * 3.0
291
+ assert abs(result[0] - expected_width) < 1e-15
292
+ assert abs(result[1] - expected_height) < 1e-15
293
+
294
+
295
+ class TestIntegration:
296
+ """Integration tests combining multiple functions."""
297
+
298
+ def test_chained_calculations(self):
299
+ """Test chaining multiple calculations."""
300
+ # Start with a percentage
301
+ percent = percent_of(25, 100) # 25%
302
+
303
+ # Use result in proportion
304
+ result = solve_proportion(percent, 100, None, 200) # 25/100 = x/200
305
+ assert result == 50.0
306
+
307
+ def test_proportion_verification(self):
308
+ """Test proportion verification using cross multiplication."""
309
+ a, b, c = 3, 4, 6
310
+ d = solve_proportion(a, b, c, None)
311
+
312
+ # Verify: a/b should equal c/d
313
+ ratio1 = a / b
314
+ ratio2 = c / d
315
+ assert abs(ratio1 - ratio2) < 1e-15
316
+
317
+ def test_resize_and_scale_workflow(self):
318
+ """Test resizing and scaling workflow."""
319
+ # Original dimensions
320
+ width, height = 100, 50
321
+
322
+ # Resize by factor of 2
323
+ new_width, new_height = resize_dimensions(width, height, 2.0)
324
+ assert new_width == 200.0
325
+ assert new_height == 100.0
326
+
327
+ # Scale the area by ratio
328
+ original_area = width * height
329
+ new_area = new_width * new_height
330
+ area_ratio = new_area / original_area
331
+ assert area_ratio == 4.0 # 2^2 = 4
332
+
333
+ def test_percentage_and_proportion_workflow(self):
334
+ """Test workflow combining percentages and proportions."""
335
+ # What percentage is 15 of 60?
336
+ percent = percent_of(15, 60) # 25%
337
+
338
+ # If 25% of some number is 30, what's the number?
339
+ result = solve_proportion(percent, 100, 30, None)
340
+ assert result == 120.0
341
+
342
+ def test_scaling_chain(self):
343
+ """Test chain of scaling operations."""
344
+ value = 100
345
+
346
+ # Scale by 1.5
347
+ value = scale_by_ratio(value, 1.5) # 150
348
+
349
+ # Scale by 2/3
350
+ value = scale_by_ratio(value, 2/3) # 100
351
+
352
+ # Should be back to original
353
+ assert abs(value - 100.0) < 1e-15
354
+
355
+ def test_real_world_recipe_scaling(self):
356
+ """Test real-world recipe scaling scenario."""
357
+ # Original recipe serves 4, we want to serve 6
358
+ # Original calls for 2 cups flour
359
+ original_servings = 4
360
+ new_servings = 6
361
+ original_flour = 2
362
+
363
+ # Find scaling ratio
364
+ ratio = new_servings / original_servings # 1.5
365
+
366
+ # Scale flour amount
367
+ new_flour = scale_by_ratio(original_flour, ratio)
368
+ assert new_flour == 3.0
369
+
370
+ # Verify using proportion
371
+ flour_check = solve_proportion(original_flour, original_servings, None, new_servings)
372
+ assert flour_check == 3.0
373
+
374
+ def test_financial_calculation_workflow(self):
375
+ """Test financial calculation workflow."""
376
+ # Investment grows from $1000 to $1250
377
+ initial = 1000
378
+ final = 1250
379
+
380
+ # What's the growth percentage?
381
+ growth_amount = final - initial # 250
382
+ growth_percent = percent_of(growth_amount, initial) # 25%
383
+
384
+ # Find the growth factor
385
+ growth_factor = direct_k(initial, final) # 1.25
386
+
387
+ # Verify by scaling
388
+ verification = scale_by_ratio(initial, growth_factor)
389
+ assert verification == final
390
+
391
+
392
+ class TestErrorHandling:
393
+ """Test error handling and edge cases."""
394
+
395
+ def test_type_validation_errors(self):
396
+ """Test that functions handle type validation properly."""
397
+ # These should work with int inputs (converted to float)
398
+ assert percent_of(1, 4) == 25.0
399
+ assert solve_proportion(1, 2, 3, None) == 6.0
400
+ assert scale_by_ratio(10, 2) == 20.0
401
+ assert direct_k(2, 6) == 3.0
402
+ assert resize_dimensions(10, 20, 2) == (20.0, 40.0)
403
+
404
+ def test_boundary_conditions(self):
405
+ """Test mathematical boundary conditions."""
406
+ # Actual zero denominators should raise error
407
+ with pytest.raises(AssertionError):
408
+ percent_of(10, 0)
409
+
410
+ # Very small but non-zero numbers should work
411
+ result = percent_of(10, 1e-100)
412
+ assert result == 1e103
413
+
414
+ # Large numbers should work
415
+ result = percent_of(1e50, 1e60)
416
+ assert abs(result - 1e-8) < 1e-15
417
+
418
+ def test_floating_point_precision_limits(self):
419
+ """Test floating point precision limits."""
420
+ # Very small numbers
421
+ result = scale_by_ratio(1e-300, 1e-300)
422
+ # Should not raise error, might be 0.0 due to underflow
423
+ assert isinstance(result, float)
424
+
425
+ # Very large numbers
426
+ result = scale_by_ratio(1e100, 1e100)
427
+ # Should not raise error, might be inf due to overflow
428
+ assert isinstance(result, float)