Metadata-Version: 2.4
Name: spring-ready-python
Version: 0.1.0
Summary: Make Python FastAPI apps work with Spring Boot ecosystem (Eureka, Config Server, Actuator)
Project-URL: Homepage, https://github.com/tcivie/spring-ready-python
Project-URL: Issues, https://github.com/tcivie/spring-ready-python/issues
Author-email: Gleb Tcivie <gleb@tcivie.com>
License-Expression: MIT
Keywords: actuator,config-server,eureka,fastapi,microservices,prometheus,spring-boot
Classifier: Development Status :: 4 - Beta
Classifier: Framework :: FastAPI
Classifier: Intended Audience :: Developers
Classifier: License :: OSI Approved :: MIT License
Classifier: Operating System :: OS Independent
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.9
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Topic :: Software Development :: Libraries :: Application Frameworks
Requires-Python: >=3.9
Requires-Dist: fastapi>=0.100.0
Requires-Dist: requests>=2.31.0
Provides-Extra: all
Requires-Dist: prometheus-client>=0.19.0; extra == 'all'
Requires-Dist: spring-config-client-python>=0.1.0; extra == 'all'
Provides-Extra: config
Requires-Dist: spring-config-client-python>=0.1.0; extra == 'config'
Provides-Extra: dev
Requires-Dist: httpx>=0.24.0; extra == 'dev'
Requires-Dist: pytest-asyncio>=0.21.0; extra == 'dev'
Requires-Dist: pytest-cov>=4.1.0; extra == 'dev'
Requires-Dist: pytest>=7.4.0; extra == 'dev'
Requires-Dist: uvicorn>=0.23.0; extra == 'dev'
Provides-Extra: metrics
Requires-Dist: prometheus-client>=0.19.0; extra == 'metrics'
Description-Content-Type: text/markdown

# Spring-Ready Python

Make your Python FastAPI app work seamlessly with Spring Boot ecosystem: Eureka, Config Server, Spring Boot Admin, and Prometheus.

## Why This Exists

You have a Spring Boot microservices architecture with Eureka, Config Server, Spring Boot Admin, and Prometheus monitoring. You want to add a Python service but keep everything working together. This library makes that dead simple.

## Features

✅ **Eureka Service Registration** - Automatic registration with heartbeat  
✅ **Config Server Integration** - Load config from Spring Cloud Config with Eureka discovery  
✅ **Actuator Endpoints** - `/actuator/health`, `/actuator/info`, `/actuator/prometheus`  
✅ **FastAPI Integration** - Drop-in support for FastAPI apps  
✅ **Fail-Fast Behavior** - Matches Spring Boot's startup failure handling  
✅ **Exponential Backoff Retry** - Retry logic for Eureka and Config Server  
✅ **Zero Magic** - Own implementation, no hidden dependencies

## Installation

```bash
pip install spring-ready-python
```

Optional dependencies:
```bash
# For Config Server support
pip install spring-config-client-python

# For Prometheus metrics
pip install prometheus-client
```

## Quick Start

```python
from spring_ready import SpringReadyApp
from fastapi import FastAPI

# Create FastAPI app
app = FastAPI()

# Add Spring integration
spring_app = SpringReadyApp(app)
spring_app.start()

# Your routes
@app.get("/")
def read_root():
    return {"message": "Hello from Spring-Ready Python!"}

# Run with uvicorn
if __name__ == "__main__":
    import uvicorn
    uvicorn.run(app, host="0.0.0.0", port=8080)
```

That's it. Your app now:
- Registers with Eureka
- Loads config from Config Server (discovered via Eureka)
- Exposes `/actuator/health`, `/actuator/info`, `/actuator/prometheus`
- Shows up in Spring Boot Admin

## Configuration

Set via environment variables (matching Spring Boot conventions):

```bash
# Application
SPRING_APPLICATION_NAME=my-python-service
APP_PORT=8080
SPRING_PROFILES_ACTIVE=production

# Eureka
EUREKA_SERVER_URL=http://eureka:8761/eureka/
EUREKA_INSTANCE_IP=192.168.1.100  # Optional: custom IP for registration
EUREKA_INSTANCE_HOSTNAME=my-service.example.com  # Optional: custom hostname

# Config Server (optional - will be discovered from Eureka)
CONFIG_SERVER_URI=http://config-server:8888
CONFIG_SERVER_USERNAME=admin
CONFIG_SERVER_PASSWORD=secret
```

### Environment Variables Reference

| Variable | Description | Default | Example |
|----------|-------------|---------|---------|
| `SPRING_APPLICATION_NAME` | Application name | `python-service` | `my-python-service` |
| `APP_PORT` | Application port | `8080` | `8080` |
| `SPRING_PROFILES_ACTIVE` | Active profile | `default` | `production` |
| `EUREKA_SERVER_URL` | Eureka server URL(s) | `http://localhost:8761/eureka/` | `http://eureka:8761/eureka/` |
| `EUREKA_INSTANCE_IP` | Custom IP for registration | Auto-detected | `192.168.1.100` |
| `EUREKA_INSTANCE_HOSTNAME` | Custom hostname | Auto-detected | `my-service.local` |
| `CONFIG_SERVER_URI` | Config Server URL | Discovered from Eureka | `http://config:8888` |
| `CONFIG_SERVER_SERVICE_ID` | Config Server service ID | `CONFIG-SERVER` | `CONFIG-SERVER` |
| `CONFIG_SERVER_USERNAME` | Config Server username | None | `admin` |
| `CONFIG_SERVER_PASSWORD` | Config Server password | None | `secret` |

## Docker Example

```dockerfile
FROM python:3.11-slim

WORKDIR /app
COPY requirements.txt .
RUN pip install -r requirements.txt

COPY . .

ENV SPRING_APPLICATION_NAME=my-service
ENV EUREKA_SERVER_URL=http://eureka:8761/eureka/
ENV SPRING_PROFILES_ACTIVE=production

CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8080"]
```

```yaml
# docker-compose.yml
services:
  my-service:
    build: .
    environment:
      - EUREKA_SERVER_URL=http://eureka:8761/eureka/
      - SPRING_PROFILES_ACTIVE=production
    ports:
      - "8080:8080"
```

## Advanced Usage

### Custom Health Checks

```python
from spring_ready import SpringReadyApp

spring_app = SpringReadyApp(app)
spring_app.start()

# Add custom health check
def check_database():
    # Your database check logic
    return database.is_connected()

spring_app.health_endpoint.add_check("database", check_database)
```

### Service Discovery

```python
# Find another service registered in Eureka
config_server_url = spring_app.service_discovery.get_service_url("CONFIG-SERVER")

# Get all instances of a service
instances = spring_app.service_discovery.get_instances("my-other-service")

for instance in instances:
    print(f"Instance: {instance.base_url}")
```

### Manual Configuration

```python
from spring_ready import SpringReadyApp

spring_app = SpringReadyApp(
    app=app,
    app_name="my-service",
    app_port=8080,
    eureka_servers=["http://eureka1:8761/eureka/", "http://eureka2:8761/eureka/"],
    profile="production",
    fail_fast=True,
    prefer_ip_address=True
)
spring_app.start()
```

### Custom IP Address Registration

```python
from spring_ready import SpringReadyApp

# Specify a custom IP address for Eureka registration
# Useful in Docker/Kubernetes when you need to advertise a specific IP
spring_app = SpringReadyApp(
    app=app,
    app_name="my-service",
    instance_ip="192.168.1.100",  # Custom IP address
    instance_hostname="my-service.example.com"  # Optional: custom hostname
)
spring_app.start()
```

Or via environment variables:
```bash
export EUREKA_INSTANCE_IP=192.168.1.100
export EUREKA_INSTANCE_HOSTNAME=my-service.example.com
```

## How It Works

### Startup Sequence

1. **Eureka Registration**: Registers with Eureka (with retry/backoff)
2. **Config Discovery**: Discovers Config Server from Eureka
3. **Config Loading**: Loads configuration into `os.environ`
4. **Actuator Setup**: Exposes health, info, and metrics endpoints
5. **Heartbeat**: Starts background thread for Eureka heartbeats (every 30s)

### Shutdown Sequence

1. Stops heartbeat thread
2. Deregisters from Eureka
3. Clean exit

### Behavior Matching Spring Boot

| Feature | Spring Boot | spring-ready-python |
|---------|-------------|---------------------|
| Fail-fast on startup | ✓ | ✓ |
| Exponential backoff retry | ✓ | ✓ |
| Eureka heartbeat (30s) | ✓ | ✓ |
| Config Server discovery | ✓ | ✓ |
| Actuator endpoints | ✓ | ✓ |
| Graceful shutdown | ✓ | ✓ |

## Actuator Endpoints

### GET /actuator/health

Returns application health status:

```json
{
  "status": "UP",
  "components": {
    "diskSpace": {"status": "UP"},
    "eureka": {"status": "UP"},
    "ping": {"status": "UP"}
  }
}
```

### GET /actuator/info

Returns application metadata:

```json
{
  "app": {
    "name": "my-python-service",
    "version": "1.0.0"
  },
  "python": {
    "version": "3.11.5",
    "runtime": {
      "name": "CPython",
      "version": "3.11.5"
    }
  }
}
```

### GET /actuator/prometheus

Returns Prometheus metrics in exposition format:

```
# HELP python_info Python platform information
# TYPE python_info gauge
python_info{implementation="CPython",version="3.11.5"} 1.0
...
```

## Troubleshooting

### No Registration Attempts to Eureka Server

**Symptom**: You don't see any connection attempts to your Eureka server in logs or network monitoring.

**Cause**: The application is likely using the default Eureka URL (`http://localhost:8761/eureka/`) instead of your server.

**Solution**:

1. **Set the environment variable** (Recommended):
   ```bash
   export EUREKA_SERVER_URL=http://10.10.0.1:8761/eureka/
   python your_app.py
   ```

2. **Or pass it explicitly in code**:
   ```python
   spring_app = SpringReadyApp(
       app,
       eureka_servers=["http://10.10.0.1:8761/eureka/"]
   )
   ```

3. **Verify your configuration**:
   ```python
   import os
   print(f"EUREKA_SERVER_URL: {os.getenv('EUREKA_SERVER_URL')}")
   ```

4. **Check the startup logs**: Look for this message:
   ```
   INFO:spring_ready.core:Configured Eureka server(s): http://10.10.0.1:8761/eureka/
   ```

   If you see:
   ```
   WARNING:spring_ready.core:Using default Eureka server URL (http://localhost:8761/eureka/)
   ```
   Then you need to set `EUREKA_SERVER_URL`.

5. **Enable debug logging** to see detailed registration attempts:
   ```python
   import logging
   logging.basicConfig(level=logging.DEBUG)
   ```

6. **Verify network connectivity**:
   ```bash
   curl http://10.10.0.1:8761/eureka/apps
   ```
   Should return XML/JSON with registered applications.

**Common mistakes**:
- Forgetting to set `EUREKA_SERVER_URL` in Docker/Kubernetes deployments
- Setting the variable in your IDE but not in the runtime environment
- Using `localhost` when running in containers (use service names or IPs instead)

### Eureka Registration Fails

Check Eureka server is reachable:
```bash
curl http://eureka:8761/eureka/apps
```

Set `fail_fast=False` to start anyway:
```python
spring_app = SpringReadyApp(app, fail_fast=False)
```

### Config Server Not Found

Ensure Config Server is registered in Eureka:
```bash
curl http://eureka:8761/eureka/apps/CONFIG-SERVER
```

Or provide direct URL:
```bash
export CONFIG_SERVER_URI=http://config-server:8888
```

### Port Already in Use

Change the port:
```python
spring_app = SpringReadyApp(app, app_port=8081)
```

Or via environment:
```bash
export APP_PORT=8081
```

## Differences from Spring Boot

| Aspect | Spring Boot | Python |
|--------|-------------|---------|
| Metrics names | `jvm.memory.used` | `process_virtual_memory_bytes` |
| Runtime info | Java/JVM | Python/CPython |
| Config refresh | `/actuator/refresh` | Not supported (restart required) |
| Auto-configuration | Annotations | Explicit initialization |

## Requirements

- Python 3.9+
- FastAPI
- requests

Optional:
- `spring-config-client-python` for Config Server support
- `prometheus-client` for metrics

## License

MIT

## Contributing

Contributions welcome! This library is intentionally simple and focused. See [CONTRIBUTING.md](CONTRIBUTING.md).

## Comparison with Spring Boot

```kotlin
// Spring Boot (Kotlin)
@SpringBootApplication
@EnableDiscoveryClient
class MyApplication

// Configuration in application.yml
```

```python
# Python equivalent
from spring_ready import SpringReadyApp
from fastapi import FastAPI

app = FastAPI()
spring_app = SpringReadyApp(app)
spring_app.start()

# Configuration from environment variables
```

---

**Note**: This library focuses on making Python apps work with existing Spring Boot infrastructure. It's not a replacement for Spring Boot itself.
