spring-ready-python 0.1.0__tar.gz → 1.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- spring_ready_python-1.1.0/.env.example +90 -0
- spring_ready_python-1.1.0/.github/FUNDING.yml +15 -0
- spring_ready_python-1.1.0/.github/ISSUE_TEMPLATE/bug_report.md +38 -0
- spring_ready_python-1.1.0/.github/ISSUE_TEMPLATE/custom.md +10 -0
- spring_ready_python-1.1.0/.github/ISSUE_TEMPLATE/feature_request.md +20 -0
- spring_ready_python-1.1.0/CODE_OF_CONDUCT.md +128 -0
- spring_ready_python-1.1.0/CONTRIBUTING.md +87 -0
- spring_ready_python-1.1.0/LICENSE +21 -0
- {spring_ready_python-0.1.0 → spring_ready_python-1.1.0}/PKG-INFO +38 -1
- {spring_ready_python-0.1.0 → spring_ready_python-1.1.0}/README.md +36 -0
- spring_ready_python-1.1.0/SECURITY.md +73 -0
- {spring_ready_python-0.1.0 → spring_ready_python-1.1.0}/spring_ready/__init__.py +1 -1
- {spring_ready_python-0.1.0 → spring_ready_python-1.1.0}/spring_ready/core.py +36 -5
- {spring_ready_python-0.1.0 → spring_ready_python-1.1.0}/spring_ready/eureka/instance.py +10 -4
- {spring_ready_python-0.1.0 → spring_ready_python-1.1.0}/.github/workflows/python-publish.yml +0 -0
- {spring_ready_python-0.1.0 → spring_ready_python-1.1.0}/.gitignore +0 -0
- {spring_ready_python-0.1.0 → spring_ready_python-1.1.0}/Dockerfile +0 -0
- {spring_ready_python-0.1.0 → spring_ready_python-1.1.0}/docker-compose.yml +0 -0
- {spring_ready_python-0.1.0 → spring_ready_python-1.1.0}/example.py +0 -0
- {spring_ready_python-0.1.0 → spring_ready_python-1.1.0}/pyproject.toml +0 -0
- {spring_ready_python-0.1.0 → spring_ready_python-1.1.0}/spring_ready/actuator/__init__.py +0 -0
- {spring_ready_python-0.1.0 → spring_ready_python-1.1.0}/spring_ready/actuator/auditevents.py +0 -0
- {spring_ready_python-0.1.0 → spring_ready_python-1.1.0}/spring_ready/actuator/beans.py +0 -0
- {spring_ready_python-0.1.0 → spring_ready_python-1.1.0}/spring_ready/actuator/caches.py +0 -0
- {spring_ready_python-0.1.0 → spring_ready_python-1.1.0}/spring_ready/actuator/configprops.py +0 -0
- {spring_ready_python-0.1.0 → spring_ready_python-1.1.0}/spring_ready/actuator/discovery.py +0 -0
- {spring_ready_python-0.1.0 → spring_ready_python-1.1.0}/spring_ready/actuator/env.py +0 -0
- {spring_ready_python-0.1.0 → spring_ready_python-1.1.0}/spring_ready/actuator/health.py +0 -0
- {spring_ready_python-0.1.0 → spring_ready_python-1.1.0}/spring_ready/actuator/heapdump.py +0 -0
- {spring_ready_python-0.1.0 → spring_ready_python-1.1.0}/spring_ready/actuator/httptrace.py +0 -0
- {spring_ready_python-0.1.0 → spring_ready_python-1.1.0}/spring_ready/actuator/info.py +0 -0
- {spring_ready_python-0.1.0 → spring_ready_python-1.1.0}/spring_ready/actuator/logfile.py +0 -0
- {spring_ready_python-0.1.0 → spring_ready_python-1.1.0}/spring_ready/actuator/loggers.py +0 -0
- {spring_ready_python-0.1.0 → spring_ready_python-1.1.0}/spring_ready/actuator/mappings.py +0 -0
- {spring_ready_python-0.1.0 → spring_ready_python-1.1.0}/spring_ready/actuator/metrics.py +0 -0
- {spring_ready_python-0.1.0 → spring_ready_python-1.1.0}/spring_ready/actuator/prometheus.py +0 -0
- {spring_ready_python-0.1.0 → spring_ready_python-1.1.0}/spring_ready/actuator/refresh.py +0 -0
- {spring_ready_python-0.1.0 → spring_ready_python-1.1.0}/spring_ready/actuator/scheduledtasks.py +0 -0
- {spring_ready_python-0.1.0 → spring_ready_python-1.1.0}/spring_ready/actuator/threaddump.py +0 -0
- {spring_ready_python-0.1.0 → spring_ready_python-1.1.0}/spring_ready/config/__init__.py +0 -0
- {spring_ready_python-0.1.0 → spring_ready_python-1.1.0}/spring_ready/config/loader.py +0 -0
- {spring_ready_python-0.1.0 → spring_ready_python-1.1.0}/spring_ready/eureka/__init__.py +0 -0
- {spring_ready_python-0.1.0 → spring_ready_python-1.1.0}/spring_ready/eureka/client.py +0 -0
- {spring_ready_python-0.1.0 → spring_ready_python-1.1.0}/spring_ready/eureka/discovery.py +0 -0
- {spring_ready_python-0.1.0 → spring_ready_python-1.1.0}/spring_ready/eureka/registry.py +0 -0
- {spring_ready_python-0.1.0 → spring_ready_python-1.1.0}/spring_ready/exceptions.py +0 -0
- {spring_ready_python-0.1.0 → spring_ready_python-1.1.0}/spring_ready/integrations/__init__.py +0 -0
- {spring_ready_python-0.1.0 → spring_ready_python-1.1.0}/spring_ready/integrations/fastapi.py +0 -0
- {spring_ready_python-0.1.0 → spring_ready_python-1.1.0}/spring_ready/retry.py +0 -0
- {spring_ready_python-0.1.0 → spring_ready_python-1.1.0}/tests/test_basic.py +0 -0
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
# Spring-Ready Python - Environment Configuration Example
|
|
2
|
+
# Copy this file to .env and update with your actual values
|
|
3
|
+
|
|
4
|
+
# =============================================================================
|
|
5
|
+
# Application Configuration
|
|
6
|
+
# =============================================================================
|
|
7
|
+
|
|
8
|
+
# Application name (must match Spring Boot naming conventions)
|
|
9
|
+
SPRING_APPLICATION_NAME=python-service
|
|
10
|
+
|
|
11
|
+
# Application port
|
|
12
|
+
APP_PORT=8080
|
|
13
|
+
|
|
14
|
+
# Active profile (dev, staging, production, etc.)
|
|
15
|
+
SPRING_PROFILES_ACTIVE=development
|
|
16
|
+
|
|
17
|
+
# Application version (shown in /actuator/info)
|
|
18
|
+
APP_VERSION=1.0.0
|
|
19
|
+
|
|
20
|
+
# =============================================================================
|
|
21
|
+
# Eureka Configuration
|
|
22
|
+
# =============================================================================
|
|
23
|
+
|
|
24
|
+
# IMPORTANT: Eureka server URL(s)
|
|
25
|
+
# - For single server: http://eureka-host:8761/eureka/
|
|
26
|
+
# - For multiple servers (failover): http://eureka1:8761/eureka/,http://eureka2:8761/eureka/
|
|
27
|
+
# - Default if not set: http://localhost:8761/eureka/
|
|
28
|
+
#
|
|
29
|
+
# COMMON MISTAKE: Forgetting to set this for non-localhost deployments!
|
|
30
|
+
# If you see "Using default Eureka server URL" warning, set this variable.
|
|
31
|
+
EUREKA_SERVER_URL=http://10.10.0.1:8761/eureka/
|
|
32
|
+
|
|
33
|
+
# Custom IP address for Eureka registration (optional)
|
|
34
|
+
# - If not set, will auto-detect from hostname
|
|
35
|
+
# - Useful when running in Docker/Kubernetes with specific networking requirements
|
|
36
|
+
# - Example: When container has internal IP but needs to advertise external IP
|
|
37
|
+
# EUREKA_INSTANCE_IP=192.168.1.100
|
|
38
|
+
|
|
39
|
+
# Custom hostname for Eureka registration (optional)
|
|
40
|
+
# - If not set, will auto-detect using socket.gethostname()
|
|
41
|
+
# - Ignored when prefer_ip_address=True (default behavior)
|
|
42
|
+
# EUREKA_INSTANCE_HOSTNAME=my-service.example.com
|
|
43
|
+
|
|
44
|
+
# =============================================================================
|
|
45
|
+
# Config Server Configuration (Optional)
|
|
46
|
+
# =============================================================================
|
|
47
|
+
|
|
48
|
+
# Config Server URI (if not using Eureka discovery)
|
|
49
|
+
# Leave commented to auto-discover via Eureka
|
|
50
|
+
# CONFIG_SERVER_URI=http://config-server:8888
|
|
51
|
+
|
|
52
|
+
# Config Server service ID in Eureka (default: CONFIG-SERVER)
|
|
53
|
+
# CONFIG_SERVER_SERVICE_ID=CONFIG-SERVER
|
|
54
|
+
|
|
55
|
+
# Config Server authentication (if required)
|
|
56
|
+
# CONFIG_SERVER_USERNAME=admin
|
|
57
|
+
# CONFIG_SERVER_PASSWORD=secret
|
|
58
|
+
|
|
59
|
+
# =============================================================================
|
|
60
|
+
# Logging Configuration
|
|
61
|
+
# =============================================================================
|
|
62
|
+
|
|
63
|
+
# Log level (DEBUG, INFO, WARNING, ERROR, CRITICAL)
|
|
64
|
+
# Use DEBUG to see detailed Eureka registration attempts
|
|
65
|
+
LOG_LEVEL=INFO
|
|
66
|
+
|
|
67
|
+
# =============================================================================
|
|
68
|
+
# Testing Configuration
|
|
69
|
+
# =============================================================================
|
|
70
|
+
|
|
71
|
+
# Testing mode - disables Eureka and Config Server connections
|
|
72
|
+
# Set to 1 to enable testing mode (useful for unit tests or local development)
|
|
73
|
+
# When enabled, the application will start with actuator endpoints only
|
|
74
|
+
# TESTING=1
|
|
75
|
+
|
|
76
|
+
# =============================================================================
|
|
77
|
+
# Quick Start Examples
|
|
78
|
+
# =============================================================================
|
|
79
|
+
|
|
80
|
+
# Example 1: Local development with local Eureka
|
|
81
|
+
# EUREKA_SERVER_URL=http://localhost:8761/eureka/
|
|
82
|
+
|
|
83
|
+
# Example 2: Docker Compose setup
|
|
84
|
+
# EUREKA_SERVER_URL=http://eureka:8761/eureka/
|
|
85
|
+
|
|
86
|
+
# Example 3: Production with specific IP
|
|
87
|
+
# EUREKA_SERVER_URL=http://10.10.0.1:8761/eureka/
|
|
88
|
+
|
|
89
|
+
# Example 4: Multiple Eureka servers for high availability
|
|
90
|
+
# EUREKA_SERVER_URL=http://eureka1.prod.example.com:8761/eureka/,http://eureka2.prod.example.com:8761/eureka/
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
# These are supported funding model platforms
|
|
2
|
+
|
|
3
|
+
github: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2]
|
|
4
|
+
patreon: # Replace with a single Patreon username
|
|
5
|
+
open_collective: # Replace with a single Open Collective username
|
|
6
|
+
ko_fi: tcivie
|
|
7
|
+
tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel
|
|
8
|
+
community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry
|
|
9
|
+
liberapay: # Replace with a single Liberapay username
|
|
10
|
+
issuehunt: # Replace with a single IssueHunt username
|
|
11
|
+
lfx_crowdfunding: # Replace with a single LFX Crowdfunding project-name e.g., cloud-foundry
|
|
12
|
+
polar: # Replace with a single Polar username
|
|
13
|
+
buy_me_a_coffee: # Replace with a single Buy Me a Coffee username
|
|
14
|
+
thanks_dev: # Replace with a single thanks.dev username
|
|
15
|
+
custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2']
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: Bug report
|
|
3
|
+
about: Create a report to help us improve
|
|
4
|
+
title: ''
|
|
5
|
+
labels: ''
|
|
6
|
+
assignees: ''
|
|
7
|
+
|
|
8
|
+
---
|
|
9
|
+
|
|
10
|
+
**Describe the bug**
|
|
11
|
+
A clear and concise description of what the bug is.
|
|
12
|
+
|
|
13
|
+
**To Reproduce**
|
|
14
|
+
Steps to reproduce the behavior:
|
|
15
|
+
1. Go to '...'
|
|
16
|
+
2. Click on '....'
|
|
17
|
+
3. Scroll down to '....'
|
|
18
|
+
4. See error
|
|
19
|
+
|
|
20
|
+
**Expected behavior**
|
|
21
|
+
A clear and concise description of what you expected to happen.
|
|
22
|
+
|
|
23
|
+
**Screenshots**
|
|
24
|
+
If applicable, add screenshots to help explain your problem.
|
|
25
|
+
|
|
26
|
+
**Desktop (please complete the following information):**
|
|
27
|
+
- OS: [e.g. iOS]
|
|
28
|
+
- Browser [e.g. chrome, safari]
|
|
29
|
+
- Version [e.g. 22]
|
|
30
|
+
|
|
31
|
+
**Smartphone (please complete the following information):**
|
|
32
|
+
- Device: [e.g. iPhone6]
|
|
33
|
+
- OS: [e.g. iOS8.1]
|
|
34
|
+
- Browser [e.g. stock browser, safari]
|
|
35
|
+
- Version [e.g. 22]
|
|
36
|
+
|
|
37
|
+
**Additional context**
|
|
38
|
+
Add any other context about the problem here.
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: Feature request
|
|
3
|
+
about: Suggest an idea for this project
|
|
4
|
+
title: ''
|
|
5
|
+
labels: ''
|
|
6
|
+
assignees: ''
|
|
7
|
+
|
|
8
|
+
---
|
|
9
|
+
|
|
10
|
+
**Is your feature request related to a problem? Please describe.**
|
|
11
|
+
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
|
|
12
|
+
|
|
13
|
+
**Describe the solution you'd like**
|
|
14
|
+
A clear and concise description of what you want to happen.
|
|
15
|
+
|
|
16
|
+
**Describe alternatives you've considered**
|
|
17
|
+
A clear and concise description of any alternative solutions or features you've considered.
|
|
18
|
+
|
|
19
|
+
**Additional context**
|
|
20
|
+
Add any other context or screenshots about the feature request here.
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
# Contributor Covenant Code of Conduct
|
|
2
|
+
|
|
3
|
+
## Our Pledge
|
|
4
|
+
|
|
5
|
+
We as members, contributors, and leaders pledge to make participation in our
|
|
6
|
+
community a harassment-free experience for everyone, regardless of age, body
|
|
7
|
+
size, visible or invisible disability, ethnicity, sex characteristics, gender
|
|
8
|
+
identity and expression, level of experience, education, socio-economic status,
|
|
9
|
+
nationality, personal appearance, race, religion, or sexual identity
|
|
10
|
+
and orientation.
|
|
11
|
+
|
|
12
|
+
We pledge to act and interact in ways that contribute to an open, welcoming,
|
|
13
|
+
diverse, inclusive, and healthy community.
|
|
14
|
+
|
|
15
|
+
## Our Standards
|
|
16
|
+
|
|
17
|
+
Examples of behavior that contributes to a positive environment for our
|
|
18
|
+
community include:
|
|
19
|
+
|
|
20
|
+
* Demonstrating empathy and kindness toward other people
|
|
21
|
+
* Being respectful of differing opinions, viewpoints, and experiences
|
|
22
|
+
* Giving and gracefully accepting constructive feedback
|
|
23
|
+
* Accepting responsibility and apologizing to those affected by our mistakes,
|
|
24
|
+
and learning from the experience
|
|
25
|
+
* Focusing on what is best not just for us as individuals, but for the
|
|
26
|
+
overall community
|
|
27
|
+
|
|
28
|
+
Examples of unacceptable behavior include:
|
|
29
|
+
|
|
30
|
+
* The use of sexualized language or imagery, and sexual attention or
|
|
31
|
+
advances of any kind
|
|
32
|
+
* Trolling, insulting or derogatory comments, and personal or political attacks
|
|
33
|
+
* Public or private harassment
|
|
34
|
+
* Publishing others' private information, such as a physical or email
|
|
35
|
+
address, without their explicit permission
|
|
36
|
+
* Other conduct which could reasonably be considered inappropriate in a
|
|
37
|
+
professional setting
|
|
38
|
+
|
|
39
|
+
## Enforcement Responsibilities
|
|
40
|
+
|
|
41
|
+
Community leaders are responsible for clarifying and enforcing our standards of
|
|
42
|
+
acceptable behavior and will take appropriate and fair corrective action in
|
|
43
|
+
response to any behavior that they deem inappropriate, threatening, offensive,
|
|
44
|
+
or harmful.
|
|
45
|
+
|
|
46
|
+
Community leaders have the right and responsibility to remove, edit, or reject
|
|
47
|
+
comments, commits, code, wiki edits, issues, and other contributions that are
|
|
48
|
+
not aligned to this Code of Conduct, and will communicate reasons for moderation
|
|
49
|
+
decisions when appropriate.
|
|
50
|
+
|
|
51
|
+
## Scope
|
|
52
|
+
|
|
53
|
+
This Code of Conduct applies within all community spaces, and also applies when
|
|
54
|
+
an individual is officially representing the community in public spaces.
|
|
55
|
+
Examples of representing our community include using an official e-mail address,
|
|
56
|
+
posting via an official social media account, or acting as an appointed
|
|
57
|
+
representative at an online or offline event.
|
|
58
|
+
|
|
59
|
+
## Enforcement
|
|
60
|
+
|
|
61
|
+
Instances of abusive, harassing, or otherwise unacceptable behavior may be
|
|
62
|
+
reported to the community leaders responsible for enforcement at
|
|
63
|
+
gleb@tcivie.com.
|
|
64
|
+
All complaints will be reviewed and investigated promptly and fairly.
|
|
65
|
+
|
|
66
|
+
All community leaders are obligated to respect the privacy and security of the
|
|
67
|
+
reporter of any incident.
|
|
68
|
+
|
|
69
|
+
## Enforcement Guidelines
|
|
70
|
+
|
|
71
|
+
Community leaders will follow these Community Impact Guidelines in determining
|
|
72
|
+
the consequences for any action they deem in violation of this Code of Conduct:
|
|
73
|
+
|
|
74
|
+
### 1. Correction
|
|
75
|
+
|
|
76
|
+
**Community Impact**: Use of inappropriate language or other behavior deemed
|
|
77
|
+
unprofessional or unwelcome in the community.
|
|
78
|
+
|
|
79
|
+
**Consequence**: A private, written warning from community leaders, providing
|
|
80
|
+
clarity around the nature of the violation and an explanation of why the
|
|
81
|
+
behavior was inappropriate. A public apology may be requested.
|
|
82
|
+
|
|
83
|
+
### 2. Warning
|
|
84
|
+
|
|
85
|
+
**Community Impact**: A violation through a single incident or series
|
|
86
|
+
of actions.
|
|
87
|
+
|
|
88
|
+
**Consequence**: A warning with consequences for continued behavior. No
|
|
89
|
+
interaction with the people involved, including unsolicited interaction with
|
|
90
|
+
those enforcing the Code of Conduct, for a specified period of time. This
|
|
91
|
+
includes avoiding interactions in community spaces as well as external channels
|
|
92
|
+
like social media. Violating these terms may lead to a temporary or
|
|
93
|
+
permanent ban.
|
|
94
|
+
|
|
95
|
+
### 3. Temporary Ban
|
|
96
|
+
|
|
97
|
+
**Community Impact**: A serious violation of community standards, including
|
|
98
|
+
sustained inappropriate behavior.
|
|
99
|
+
|
|
100
|
+
**Consequence**: A temporary ban from any sort of interaction or public
|
|
101
|
+
communication with the community for a specified period of time. No public or
|
|
102
|
+
private interaction with the people involved, including unsolicited interaction
|
|
103
|
+
with those enforcing the Code of Conduct, is allowed during this period.
|
|
104
|
+
Violating these terms may lead to a permanent ban.
|
|
105
|
+
|
|
106
|
+
### 4. Permanent Ban
|
|
107
|
+
|
|
108
|
+
**Community Impact**: Demonstrating a pattern of violation of community
|
|
109
|
+
standards, including sustained inappropriate behavior, harassment of an
|
|
110
|
+
individual, or aggression toward or disparagement of classes of individuals.
|
|
111
|
+
|
|
112
|
+
**Consequence**: A permanent ban from any sort of public interaction within
|
|
113
|
+
the community.
|
|
114
|
+
|
|
115
|
+
## Attribution
|
|
116
|
+
|
|
117
|
+
This Code of Conduct is adapted from the [Contributor Covenant][homepage],
|
|
118
|
+
version 2.0, available at
|
|
119
|
+
https://www.contributor-covenant.org/version/2/0/code_of_conduct.html.
|
|
120
|
+
|
|
121
|
+
Community Impact Guidelines were inspired by [Mozilla's code of conduct
|
|
122
|
+
enforcement ladder](https://github.com/mozilla/diversity).
|
|
123
|
+
|
|
124
|
+
[homepage]: https://www.contributor-covenant.org
|
|
125
|
+
|
|
126
|
+
For answers to common questions about this code of conduct, see the FAQ at
|
|
127
|
+
https://www.contributor-covenant.org/faq. Translations are available at
|
|
128
|
+
https://www.contributor-covenant.org/translations.
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
# Contributing to Spring-Ready Python
|
|
2
|
+
|
|
3
|
+
Thanks for considering contributing! This library is intentionally simple and focused.
|
|
4
|
+
|
|
5
|
+
## Philosophy
|
|
6
|
+
|
|
7
|
+
- **Keep it simple** - No over-engineering
|
|
8
|
+
- **Match Spring Boot behavior** - Fail-fast, retry logic, etc.
|
|
9
|
+
- **Own implementation** - No hidden dependencies
|
|
10
|
+
- **Well-tested** - Every feature needs tests
|
|
11
|
+
- **Clear documentation** - Examples for everything
|
|
12
|
+
|
|
13
|
+
## Development Setup
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
# Clone the repo
|
|
17
|
+
git clone https://github.com/tcivie/spring_ready_python.git
|
|
18
|
+
cd spring_ready_python
|
|
19
|
+
|
|
20
|
+
# Create virtual environment
|
|
21
|
+
python -m venv venv
|
|
22
|
+
source venv/bin/activate # On Windows: venv\Scripts\activate
|
|
23
|
+
|
|
24
|
+
# Install dependencies
|
|
25
|
+
pip install -e ".[dev,all]"
|
|
26
|
+
|
|
27
|
+
# Run tests
|
|
28
|
+
pytest tests/
|
|
29
|
+
|
|
30
|
+
# Run example
|
|
31
|
+
python example.py
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
## Making Changes
|
|
35
|
+
|
|
36
|
+
1. **Fork the repository**
|
|
37
|
+
2. **Create a branch** - `git checkout -b feature/my-feature`
|
|
38
|
+
3. **Write tests** - Tests go in `tests/`
|
|
39
|
+
4. **Make changes** - Keep it focused
|
|
40
|
+
5. **Run tests** - `pytest tests/`
|
|
41
|
+
6. **Update docs** - If adding features
|
|
42
|
+
7. **Submit PR** - Describe what and why
|
|
43
|
+
|
|
44
|
+
## Code Style
|
|
45
|
+
|
|
46
|
+
- Use type hints where it makes sense
|
|
47
|
+
- Follow PEP 8 (but don't be annoying about it)
|
|
48
|
+
- Keep functions small and focused
|
|
49
|
+
- Comment the "why", not the "what"
|
|
50
|
+
- Match Spring Boot naming conventions where possible
|
|
51
|
+
|
|
52
|
+
## Testing
|
|
53
|
+
|
|
54
|
+
```bash
|
|
55
|
+
# Run all tests
|
|
56
|
+
pytest tests/
|
|
57
|
+
|
|
58
|
+
# Run with coverage
|
|
59
|
+
pytest tests/ --cov=spring_ready --cov-report=html
|
|
60
|
+
|
|
61
|
+
# Run specific test
|
|
62
|
+
pytest tests/test_basic.py::TestRetry
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
## What We're Looking For
|
|
66
|
+
|
|
67
|
+
✅ **Bug fixes** - Always welcome
|
|
68
|
+
✅ **Better error messages** - Help users debug
|
|
69
|
+
✅ **Performance improvements** - If measurable
|
|
70
|
+
✅ **Documentation fixes** - Typos, clarity, examples
|
|
71
|
+
✅ **More tests** - Increase coverage
|
|
72
|
+
|
|
73
|
+
## What We're NOT Looking For
|
|
74
|
+
|
|
75
|
+
❌ **New frameworks** - FastAPI only for now
|
|
76
|
+
❌ **Complex features** - Keep it simple
|
|
77
|
+
❌ **Breaking changes** - Unless absolutely necessary
|
|
78
|
+
❌ **Dependencies** - Avoid adding new ones
|
|
79
|
+
❌ **Runtime config refresh** - Out of scope
|
|
80
|
+
|
|
81
|
+
## Questions?
|
|
82
|
+
|
|
83
|
+
Open an issue first if you're unsure whether a feature fits.
|
|
84
|
+
|
|
85
|
+
## License
|
|
86
|
+
|
|
87
|
+
By contributing, you agree that your contributions will be licensed under the MIT License.
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 Gleb Tcivie
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: spring-ready-python
|
|
3
|
-
Version:
|
|
3
|
+
Version: 1.1.0
|
|
4
4
|
Summary: Make Python FastAPI apps work with Spring Boot ecosystem (Eureka, Config Server, Actuator)
|
|
5
5
|
Project-URL: Homepage, https://github.com/tcivie/spring-ready-python
|
|
6
6
|
Project-URL: Issues, https://github.com/tcivie/spring-ready-python/issues
|
|
7
7
|
Author-email: Gleb Tcivie <gleb@tcivie.com>
|
|
8
8
|
License-Expression: MIT
|
|
9
|
+
License-File: LICENSE
|
|
9
10
|
Keywords: actuator,config-server,eureka,fastapi,microservices,prometheus,spring-boot
|
|
10
11
|
Classifier: Development Status :: 4 - Beta
|
|
11
12
|
Classifier: Framework :: FastAPI
|
|
@@ -130,6 +131,7 @@ CONFIG_SERVER_PASSWORD=secret
|
|
|
130
131
|
| `EUREKA_SERVER_URL` | Eureka server URL(s) | `http://localhost:8761/eureka/` | `http://eureka:8761/eureka/` |
|
|
131
132
|
| `EUREKA_INSTANCE_IP` | Custom IP for registration | Auto-detected | `192.168.1.100` |
|
|
132
133
|
| `EUREKA_INSTANCE_HOSTNAME` | Custom hostname | Auto-detected | `my-service.local` |
|
|
134
|
+
| `EUREKA_INSTANCE_SECURE` | Register with HTTPS URLs | `false` | `true` |
|
|
133
135
|
| `CONFIG_SERVER_URI` | Config Server URL | Discovered from Eureka | `http://config:8888` |
|
|
134
136
|
| `CONFIG_SERVER_SERVICE_ID` | Config Server service ID | `CONFIG-SERVER` | `CONFIG-SERVER` |
|
|
135
137
|
| `CONFIG_SERVER_USERNAME` | Config Server username | None | `admin` |
|
|
@@ -235,6 +237,41 @@ export EUREKA_INSTANCE_IP=192.168.1.100
|
|
|
235
237
|
export EUREKA_INSTANCE_HOSTNAME=my-service.example.com
|
|
236
238
|
```
|
|
237
239
|
|
|
240
|
+
### HTTPS Registration (Secure Mode)
|
|
241
|
+
|
|
242
|
+
When your application uses HTTPS, you need to register with HTTPS URLs in Eureka so that Spring Boot Admin and other services can connect to it correctly.
|
|
243
|
+
|
|
244
|
+
```python
|
|
245
|
+
from spring_ready import SpringReadyApp
|
|
246
|
+
|
|
247
|
+
# Register with HTTPS URLs in Eureka
|
|
248
|
+
spring_app = SpringReadyApp(
|
|
249
|
+
app=app,
|
|
250
|
+
app_name="my-service",
|
|
251
|
+
app_port=8443,
|
|
252
|
+
secure=True # Register with https:// URLs
|
|
253
|
+
)
|
|
254
|
+
spring_app.start()
|
|
255
|
+
```
|
|
256
|
+
|
|
257
|
+
Or via environment variable:
|
|
258
|
+
```bash
|
|
259
|
+
export EUREKA_INSTANCE_SECURE=true
|
|
260
|
+
```
|
|
261
|
+
|
|
262
|
+
When `secure=True`:
|
|
263
|
+
- The `homePageUrl`, `statusPageUrl`, and `healthCheckUrl` will use `https://`
|
|
264
|
+
- The `securePort` will be enabled and set to your `app_port`
|
|
265
|
+
- The regular `port` will be disabled
|
|
266
|
+
|
|
267
|
+
This is equivalent to Spring Boot's:
|
|
268
|
+
```yaml
|
|
269
|
+
eureka:
|
|
270
|
+
instance:
|
|
271
|
+
secure-port-enabled: true
|
|
272
|
+
non-secure-port-enabled: false
|
|
273
|
+
```
|
|
274
|
+
|
|
238
275
|
## How It Works
|
|
239
276
|
|
|
240
277
|
### Startup Sequence
|
|
@@ -92,6 +92,7 @@ CONFIG_SERVER_PASSWORD=secret
|
|
|
92
92
|
| `EUREKA_SERVER_URL` | Eureka server URL(s) | `http://localhost:8761/eureka/` | `http://eureka:8761/eureka/` |
|
|
93
93
|
| `EUREKA_INSTANCE_IP` | Custom IP for registration | Auto-detected | `192.168.1.100` |
|
|
94
94
|
| `EUREKA_INSTANCE_HOSTNAME` | Custom hostname | Auto-detected | `my-service.local` |
|
|
95
|
+
| `EUREKA_INSTANCE_SECURE` | Register with HTTPS URLs | `false` | `true` |
|
|
95
96
|
| `CONFIG_SERVER_URI` | Config Server URL | Discovered from Eureka | `http://config:8888` |
|
|
96
97
|
| `CONFIG_SERVER_SERVICE_ID` | Config Server service ID | `CONFIG-SERVER` | `CONFIG-SERVER` |
|
|
97
98
|
| `CONFIG_SERVER_USERNAME` | Config Server username | None | `admin` |
|
|
@@ -197,6 +198,41 @@ export EUREKA_INSTANCE_IP=192.168.1.100
|
|
|
197
198
|
export EUREKA_INSTANCE_HOSTNAME=my-service.example.com
|
|
198
199
|
```
|
|
199
200
|
|
|
201
|
+
### HTTPS Registration (Secure Mode)
|
|
202
|
+
|
|
203
|
+
When your application uses HTTPS, you need to register with HTTPS URLs in Eureka so that Spring Boot Admin and other services can connect to it correctly.
|
|
204
|
+
|
|
205
|
+
```python
|
|
206
|
+
from spring_ready import SpringReadyApp
|
|
207
|
+
|
|
208
|
+
# Register with HTTPS URLs in Eureka
|
|
209
|
+
spring_app = SpringReadyApp(
|
|
210
|
+
app=app,
|
|
211
|
+
app_name="my-service",
|
|
212
|
+
app_port=8443,
|
|
213
|
+
secure=True # Register with https:// URLs
|
|
214
|
+
)
|
|
215
|
+
spring_app.start()
|
|
216
|
+
```
|
|
217
|
+
|
|
218
|
+
Or via environment variable:
|
|
219
|
+
```bash
|
|
220
|
+
export EUREKA_INSTANCE_SECURE=true
|
|
221
|
+
```
|
|
222
|
+
|
|
223
|
+
When `secure=True`:
|
|
224
|
+
- The `homePageUrl`, `statusPageUrl`, and `healthCheckUrl` will use `https://`
|
|
225
|
+
- The `securePort` will be enabled and set to your `app_port`
|
|
226
|
+
- The regular `port` will be disabled
|
|
227
|
+
|
|
228
|
+
This is equivalent to Spring Boot's:
|
|
229
|
+
```yaml
|
|
230
|
+
eureka:
|
|
231
|
+
instance:
|
|
232
|
+
secure-port-enabled: true
|
|
233
|
+
non-secure-port-enabled: false
|
|
234
|
+
```
|
|
235
|
+
|
|
200
236
|
## How It Works
|
|
201
237
|
|
|
202
238
|
### Startup Sequence
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
# Security Policy
|
|
2
|
+
|
|
3
|
+
## Supported Versions
|
|
4
|
+
|
|
5
|
+
| Version | Supported |
|
|
6
|
+
| ------- | ------------------ |
|
|
7
|
+
| 1.0.x | :white_check_mark: |
|
|
8
|
+
| < 1.0 | :x: |
|
|
9
|
+
|
|
10
|
+
As this is a new project, only the latest 0.1.x release receives security updates.
|
|
11
|
+
|
|
12
|
+
## Reporting a Vulnerability
|
|
13
|
+
|
|
14
|
+
**Please do NOT open a public issue for security vulnerabilities.**
|
|
15
|
+
|
|
16
|
+
### How to Report
|
|
17
|
+
|
|
18
|
+
1. **Email**: Send details to the maintainer (check package metadata for contact)
|
|
19
|
+
2. **GitHub Security Advisory**: Use the "Security" tab → "Report a vulnerability"
|
|
20
|
+
|
|
21
|
+
### What to Include
|
|
22
|
+
|
|
23
|
+
- Description of the vulnerability
|
|
24
|
+
- Steps to reproduce
|
|
25
|
+
- Potential impact
|
|
26
|
+
- Affected versions
|
|
27
|
+
- Suggested fix (if you have one)
|
|
28
|
+
|
|
29
|
+
### What to Expect
|
|
30
|
+
|
|
31
|
+
- **Initial Response**: Within 7 days
|
|
32
|
+
- **Status Update**: Every 14 days until resolved
|
|
33
|
+
- **Fix Timeline**: Best effort, no guaranteed SLA
|
|
34
|
+
- **Credit**: You'll be credited in release notes (if desired)
|
|
35
|
+
|
|
36
|
+
### Important Notes
|
|
37
|
+
|
|
38
|
+
This is a small open-source project maintained by volunteers. Security issues will be addressed as quickly as possible, but response times may vary.
|
|
39
|
+
|
|
40
|
+
If you need an immediate fix, consider:
|
|
41
|
+
- Forking and applying your own patch
|
|
42
|
+
- Contributing a PR with the security fix
|
|
43
|
+
- Using dependency pinning to avoid affected versions
|
|
44
|
+
|
|
45
|
+
## Security Best Practices
|
|
46
|
+
|
|
47
|
+
When using spring-ready-python:
|
|
48
|
+
|
|
49
|
+
1. **Never hardcode credentials** - Use environment variables
|
|
50
|
+
2. **Use HTTPS for Eureka/Config Server** - Avoid plain HTTP in production
|
|
51
|
+
3. **Secure your Config Server** - Enable authentication
|
|
52
|
+
4. **Keep dependencies updated** - Run `pip list --outdated` regularly
|
|
53
|
+
5. **Review actuator endpoints** - Ensure they're not publicly exposed without authentication
|
|
54
|
+
6. **Use secrets management** - Kubernetes Secrets, AWS Secrets Manager, etc.
|
|
55
|
+
|
|
56
|
+
## Known Security Considerations
|
|
57
|
+
|
|
58
|
+
- **Actuator endpoints expose application info** - Secure them in production
|
|
59
|
+
- **Config Server credentials stored in environment** - Use proper secrets management
|
|
60
|
+
- **Eureka registration sends metadata** - Don't include sensitive data in metadata
|
|
61
|
+
- **Heartbeat thread runs in background** - Ensure proper shutdown handling
|
|
62
|
+
|
|
63
|
+
## Dependencies
|
|
64
|
+
|
|
65
|
+
This library has minimal dependencies:
|
|
66
|
+
- `fastapi` - Web framework (security is your responsibility)
|
|
67
|
+
- `requests` - HTTP client (ensure you're using latest version)
|
|
68
|
+
|
|
69
|
+
Optional dependencies:
|
|
70
|
+
- `spring-config-client-python` - Config Server client
|
|
71
|
+
- `prometheus-client` - Metrics client
|
|
72
|
+
|
|
73
|
+
Keep all dependencies updated to receive security patches.
|
|
@@ -59,7 +59,8 @@ class SpringReadyApp:
|
|
|
59
59
|
prefer_ip_address: bool = True,
|
|
60
60
|
instance_ip: Optional[str] = None,
|
|
61
61
|
instance_hostname: Optional[str] = None,
|
|
62
|
-
instance_metadata: Optional[dict] = None
|
|
62
|
+
instance_metadata: Optional[dict] = None,
|
|
63
|
+
secure: bool = False
|
|
63
64
|
):
|
|
64
65
|
"""
|
|
65
66
|
Args:
|
|
@@ -73,6 +74,7 @@ class SpringReadyApp:
|
|
|
73
74
|
instance_ip: Custom IP address to register with Eureka (auto-detected if None)
|
|
74
75
|
instance_hostname: Custom hostname to register with Eureka (auto-detected if None)
|
|
75
76
|
instance_metadata: Additional metadata for Eureka instance
|
|
77
|
+
secure: If True, register with HTTPS URLs (default from env EUREKA_INSTANCE_SECURE)
|
|
76
78
|
"""
|
|
77
79
|
# Configuration from environment variables or parameters
|
|
78
80
|
self.app_name = app_name or os.getenv("SPRING_APPLICATION_NAME") or os.getenv("APP_NAME", "python-service")
|
|
@@ -82,6 +84,12 @@ class SpringReadyApp:
|
|
|
82
84
|
self.prefer_ip_address = prefer_ip_address
|
|
83
85
|
self.instance_ip = instance_ip or os.getenv("EUREKA_INSTANCE_IP")
|
|
84
86
|
self.instance_hostname = instance_hostname or os.getenv("EUREKA_INSTANCE_HOSTNAME")
|
|
87
|
+
self.secure = secure or os.getenv("EUREKA_INSTANCE_SECURE", "false").lower() == "true"
|
|
88
|
+
|
|
89
|
+
# Testing mode - disables Eureka and Config Server connections
|
|
90
|
+
self.testing_mode = os.getenv("TESTING", "0") == "1"
|
|
91
|
+
if self.testing_mode:
|
|
92
|
+
logger.info("TESTING mode enabled - Eureka and Config Server connections will be disabled")
|
|
85
93
|
|
|
86
94
|
# Parse Eureka servers
|
|
87
95
|
if eureka_servers is None:
|
|
@@ -146,14 +154,29 @@ class SpringReadyApp:
|
|
|
146
154
|
logger.info(f"Starting Spring-Ready application: {self.app_name}")
|
|
147
155
|
|
|
148
156
|
try:
|
|
157
|
+
# If testing mode is enabled, skip Eureka and Config Server setup
|
|
158
|
+
if self.testing_mode:
|
|
159
|
+
logger.info("Running in TESTING mode - skipping Eureka and Config Server setup")
|
|
160
|
+
|
|
161
|
+
# Only set up actuator endpoints
|
|
162
|
+
logger.info("Setting up actuator endpoints...")
|
|
163
|
+
self._setup_actuator_endpoints()
|
|
164
|
+
|
|
165
|
+
self._started = True
|
|
166
|
+
logger.info(f"✓ Spring-Ready application started successfully in TESTING mode on port {self.app_port}")
|
|
167
|
+
logger.info(f" Actuator: http://localhost:{self.app_port}/actuator/health")
|
|
168
|
+
return
|
|
169
|
+
|
|
149
170
|
# Step 1: Create Eureka instance info
|
|
150
171
|
instance_info = InstanceInfo.create(
|
|
151
172
|
app_name=self.app_name,
|
|
152
173
|
port=self.app_port,
|
|
174
|
+
secure_port=self.app_port, # Use same port for HTTPS
|
|
153
175
|
prefer_ip_address=self.prefer_ip_address,
|
|
154
176
|
ip_addr=self.instance_ip,
|
|
155
177
|
host_name=self.instance_hostname,
|
|
156
|
-
metadata=self._get_instance_metadata()
|
|
178
|
+
metadata=self._get_instance_metadata(),
|
|
179
|
+
secure=self.secure
|
|
157
180
|
)
|
|
158
181
|
|
|
159
182
|
# Log instance registration details
|
|
@@ -163,9 +186,12 @@ class SpringReadyApp:
|
|
|
163
186
|
logger.info(f"Auto-detected IP address for Eureka registration: {instance_info.ip_addr}")
|
|
164
187
|
|
|
165
188
|
logger.info(f"Instance will register as: {instance_info.instance_id}")
|
|
189
|
+
if self.secure:
|
|
190
|
+
logger.info(f"Secure mode enabled - registering with HTTPS URLs")
|
|
166
191
|
|
|
167
192
|
# Configure FastAPI OpenAPI servers to use the correct service URL
|
|
168
|
-
|
|
193
|
+
protocol = "https" if self.secure else "http"
|
|
194
|
+
service_url = f"{protocol}://{instance_info.ip_addr}:{self.app_port}"
|
|
169
195
|
self.fastapi_app.servers = [{"url": service_url, "description": "Service instance"}]
|
|
170
196
|
logger.info(f"Configured OpenAPI server URL: {service_url}")
|
|
171
197
|
|
|
@@ -309,8 +335,13 @@ class SpringReadyApp:
|
|
|
309
335
|
|
|
310
336
|
# Build base URL for actuator discovery
|
|
311
337
|
# Try to use instance_ip if available, otherwise use localhost
|
|
312
|
-
|
|
313
|
-
|
|
338
|
+
# In testing mode, always use localhost since we don't connect to Eureka
|
|
339
|
+
if self.testing_mode:
|
|
340
|
+
host = "localhost"
|
|
341
|
+
else:
|
|
342
|
+
host = self.instance_ip if self.instance_ip else "localhost"
|
|
343
|
+
protocol = "https" if self.secure else "http"
|
|
344
|
+
base_url = f"{protocol}://{host}:{self.app_port}"
|
|
314
345
|
|
|
315
346
|
# Integrate with FastAPI (CORS and middleware already set up in __init__)
|
|
316
347
|
self.actuator_integration = FastAPIActuatorIntegration(
|
|
@@ -97,7 +97,8 @@ class InstanceInfo:
|
|
|
97
97
|
port: int = 8080,
|
|
98
98
|
secure_port: int = 443,
|
|
99
99
|
metadata: Optional[Dict[str, str]] = None,
|
|
100
|
-
prefer_ip_address: bool = True
|
|
100
|
+
prefer_ip_address: bool = True,
|
|
101
|
+
secure: bool = False
|
|
101
102
|
) -> "InstanceInfo":
|
|
102
103
|
"""
|
|
103
104
|
Factory method to create InstanceInfo with sensible defaults.
|
|
@@ -111,6 +112,7 @@ class InstanceInfo:
|
|
|
111
112
|
secure_port: HTTPS port
|
|
112
113
|
metadata: Additional metadata
|
|
113
114
|
prefer_ip_address: Use IP address instead of hostname
|
|
115
|
+
secure: If True, register with HTTPS URLs and enable secure port
|
|
114
116
|
"""
|
|
115
117
|
|
|
116
118
|
# Auto-detect hostname and IP
|
|
@@ -131,8 +133,10 @@ class InstanceInfo:
|
|
|
131
133
|
if not instance_id:
|
|
132
134
|
instance_id = f"{app_name.lower()}:{host_name}:{port}"
|
|
133
135
|
|
|
134
|
-
# Build URLs
|
|
135
|
-
|
|
136
|
+
# Build URLs based on secure flag
|
|
137
|
+
protocol = "https" if secure else "http"
|
|
138
|
+
actual_port = secure_port if secure else port
|
|
139
|
+
base_url = f"{protocol}://{ip_addr}:{actual_port}"
|
|
136
140
|
|
|
137
141
|
return cls(
|
|
138
142
|
app=app_name.upper(),
|
|
@@ -142,7 +146,9 @@ class InstanceInfo:
|
|
|
142
146
|
vip_address=app_name.lower(),
|
|
143
147
|
secure_vip_address=app_name.lower(),
|
|
144
148
|
port=port,
|
|
145
|
-
secure_port=secure_port,
|
|
149
|
+
secure_port=secure_port if secure else 443,
|
|
150
|
+
port_enabled=not secure,
|
|
151
|
+
secure_port_enabled=secure,
|
|
146
152
|
home_page_url=base_url,
|
|
147
153
|
status_page_url=f"{base_url}/actuator/info",
|
|
148
154
|
health_check_url=f"{base_url}/actuator/health",
|
{spring_ready_python-0.1.0 → spring_ready_python-1.1.0}/.github/workflows/python-publish.yml
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{spring_ready_python-0.1.0 → spring_ready_python-1.1.0}/spring_ready/actuator/auditevents.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{spring_ready_python-0.1.0 → spring_ready_python-1.1.0}/spring_ready/actuator/configprops.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{spring_ready_python-0.1.0 → spring_ready_python-1.1.0}/spring_ready/actuator/scheduledtasks.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{spring_ready_python-0.1.0 → spring_ready_python-1.1.0}/spring_ready/integrations/__init__.py
RENAMED
|
File without changes
|
{spring_ready_python-0.1.0 → spring_ready_python-1.1.0}/spring_ready/integrations/fastapi.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|