Skip to content

Commit 40146a9

Browse files
committed
Migrate configuration from YAML to Pydantic-settings
Replace the YAML-based settings.yaml configuration with a modern Pydantic-settings approach using environment variables. Changes: - Add pydantic-settings>=2.0 dependency - Create gerritclient/settings.py with GerritSettings model - Update client.py to import get_settings from new module - Delete legacy settings.yaml file - Add .env.example template for configuration - Update .gitignore to exclude .env files - Add comprehensive test suite for settings (18 tests) - Update documentation with environment variable configuration Configuration is now loaded from: 1. Environment variables (GERRIT_URL, GERRIT_AUTH_TYPE, etc.) 2. .env file in current directory This is a breaking change - users must migrate from settings.yaml to environment variables or .env files.
1 parent b3944de commit 40146a9

10 files changed

Lines changed: 541 additions & 43 deletions

File tree

.coverage

0 Bytes
Binary file not shown.

.env.example

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
# Gerrit Client Configuration
2+
# Copy this file to .env and customize with your settings
3+
#
4+
# Configuration is loaded from environment variables with GERRIT_ prefix.
5+
# You can either:
6+
# 1. Set environment variables directly: export GERRIT_URL=https://...
7+
# 2. Create a .env file (copy this template)
8+
9+
# Gerrit server URL (required)
10+
# Example: https://review.openstack.org or http://localhost:8080
11+
GERRIT_URL=https://review.example.com
12+
13+
# Authentication type (optional)
14+
# Valid values: basic, digest
15+
# Omit or leave empty for anonymous access
16+
GERRIT_AUTH_TYPE=basic
17+
18+
# Credentials (required if GERRIT_AUTH_TYPE is set)
19+
# Username: Your Gerrit account username
20+
# Password: HTTP password from Gerrit (Settings -> HTTP Credentials)
21+
GERRIT_USERNAME=your-username
22+
GERRIT_PASSWORD=your-http-password

.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,3 +9,6 @@
99
docs/build
1010
/dist/
1111
/build/
12+
13+
# Environment configuration (contains secrets)
14+
.env

docs/source/index.rst

Lines changed: 33 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,33 @@ Using pip::
4646

4747
pip install python-gerritclient
4848

49+
Configuration
50+
~~~~~~~~~~~~~
51+
52+
Configuration is loaded from environment variables with the ``GERRIT_`` prefix.
53+
54+
**Using environment variables** (recommended for CI/CD)::
55+
56+
export GERRIT_URL=https://review.example.com
57+
export GERRIT_AUTH_TYPE=basic
58+
export GERRIT_USERNAME=your-username
59+
export GERRIT_PASSWORD=your-http-password
60+
61+
**Using a .env file** (recommended for local development)::
62+
63+
# Create a .env file in your project directory
64+
GERRIT_URL=https://review.example.com
65+
GERRIT_AUTH_TYPE=basic
66+
GERRIT_USERNAME=your-username
67+
GERRIT_PASSWORD=your-http-password
68+
69+
**Configuration options:**
70+
71+
* ``GERRIT_URL`` (required): Gerrit server URL (e.g., ``https://review.openstack.org``)
72+
* ``GERRIT_AUTH_TYPE`` (optional): Authentication type - ``basic`` or ``digest``. Omit for anonymous access.
73+
* ``GERRIT_USERNAME`` (required if auth_type set): Your Gerrit username
74+
* ``GERRIT_PASSWORD`` (required if auth_type set): HTTP password from Gerrit (Settings -> HTTP Credentials)
75+
4976
Basic Usage
5077
~~~~~~~~~~~
5178

@@ -59,13 +86,17 @@ Python API::
5986

6087
from gerritclient import client
6188

89+
# Option 1: Use environment variables (automatic)
90+
group_client = client.get_client('group')
91+
members = group_client.get_group_members('core-team')
92+
93+
# Option 2: Explicit connection
6294
connection = client.connect(
63-
"review.example.com",
95+
"https://review.example.com",
6496
auth_type="digest",
6597
username="user",
6698
password="pass"
6799
)
68-
69100
group_client = client.get_client('group', connection=connection)
70101
members = group_client.get_group_members('core-team')
71102

gerritclient/client.py

Lines changed: 1 addition & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -14,14 +14,14 @@
1414
# under the License.
1515

1616
import json
17-
import os
1817

1918
import requests
2019
from requests import auth
2120

2221
import gerritclient
2322
from gerritclient import error
2423
from gerritclient.common import utils
24+
from gerritclient.settings import get_settings
2525

2626

2727
class APIClient:
@@ -177,42 +177,6 @@ def _decode_content(response):
177177
return json.loads(response.text.strip(")]}'"))
178178

179179

180-
def get_settings(file_path=None):
181-
"""Gets gerritclient configuration from 'settings.yaml' file.
182-
183-
If path to configuration 'settings.yaml' file not specified (None), then
184-
first try to get it from local directory and then from user .config one
185-
186-
:param str file_path: string that contains path to configuration file
187-
:raises: error.ConfigNotFoundException if configuration not specified
188-
"""
189-
190-
config = None
191-
192-
user_config = os.path.join(
193-
os.path.expanduser("~"), ".config", "gerritclient", "settings.yaml"
194-
)
195-
local_config = os.path.join(os.path.dirname(__file__), "settings.yaml")
196-
197-
if file_path is not None:
198-
config = file_path
199-
else:
200-
if os.path.isfile(local_config):
201-
config = local_config
202-
elif os.path.isfile(user_config):
203-
config = user_config
204-
205-
if config is None:
206-
raise error.ConfigNotFoundException("Configuration not found.")
207-
208-
try:
209-
config_data = utils.read_from_file(config)
210-
except OSError:
211-
msg = f"Could not read settings from {file_path}"
212-
raise error.InvalidFileException(msg)
213-
return config_data
214-
215-
216180
def connect(url, auth_type=None, username=None, password=None):
217181
"""Creates API connection."""
218182

gerritclient/settings.py

Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
#
2+
# Copyright 2017 Vitalii Kulanov
3+
#
4+
# Licensed under the Apache License, Version 2.0 (the "License"); you may
5+
# not use this file except in compliance with the License. You may obtain
6+
# a copy of the License at
7+
#
8+
# http://www.apache.org/licenses/LICENSE-2.0
9+
#
10+
# Unless required by applicable law or agreed to in writing, software
11+
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
12+
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
13+
# License for the specific language governing permissions and limitations
14+
# under the License.
15+
16+
"""Pydantic-based settings management for gerritclient.
17+
18+
Configuration is loaded from environment variables with the GERRIT_ prefix.
19+
A .env file in the current directory is also supported.
20+
21+
Environment variables:
22+
GERRIT_URL: Gerrit server URL (required)
23+
GERRIT_AUTH_TYPE: Authentication type - 'basic' or 'digest' (optional)
24+
GERRIT_USERNAME: Username for authentication (required if auth_type set)
25+
GERRIT_PASSWORD: HTTP password for authentication (required if auth_type set)
26+
27+
Example .env file:
28+
GERRIT_URL=https://review.example.com
29+
GERRIT_AUTH_TYPE=basic
30+
GERRIT_USERNAME=admin
31+
GERRIT_PASSWORD=secret
32+
"""
33+
34+
from typing import Literal
35+
36+
from pydantic import Field, field_validator, model_validator
37+
from pydantic_settings import BaseSettings, SettingsConfigDict
38+
39+
40+
class GerritSettings(BaseSettings):
41+
"""Gerrit client configuration with validation.
42+
43+
Configuration sources (highest to lowest priority):
44+
1. Explicit keyword arguments
45+
2. Environment variables (GERRIT_URL, GERRIT_AUTH_TYPE, etc.)
46+
3. .env file in current directory
47+
"""
48+
49+
model_config = SettingsConfigDict(
50+
env_prefix="GERRIT_",
51+
env_file=".env",
52+
env_file_encoding="utf-8",
53+
extra="ignore",
54+
)
55+
56+
url: str = Field(
57+
..., description="Gerrit server URL (e.g., https://review.example.com)"
58+
)
59+
auth_type: Literal["basic", "digest"] | None = Field(
60+
default=None,
61+
description="HTTP authentication scheme. Omit for anonymous access.",
62+
)
63+
username: str | None = Field(default=None, description="Gerrit username")
64+
password: str | None = Field(default=None, description="Gerrit HTTP password")
65+
66+
@field_validator("url")
67+
@classmethod
68+
def validate_url(cls, v: str) -> str:
69+
"""Validate and normalize the Gerrit server URL."""
70+
if not v.startswith(("http://", "https://")):
71+
raise ValueError("URL must start with http:// or https://")
72+
return v.rstrip("/")
73+
74+
@model_validator(mode="after")
75+
def validate_auth_credentials(self) -> "GerritSettings":
76+
"""Ensure username and password are provided when auth_type is set."""
77+
if self.auth_type and not all([self.username, self.password]):
78+
raise ValueError("username and password are required when auth_type is set")
79+
return self
80+
81+
def to_dict(self) -> dict:
82+
"""Convert settings to dict compatible with client.connect()."""
83+
return {
84+
"url": self.url,
85+
"auth_type": self.auth_type,
86+
"username": self.username,
87+
"password": self.password,
88+
}
89+
90+
91+
def get_settings() -> dict:
92+
"""Load gerritclient configuration.
93+
94+
Configuration sources (highest to lowest priority):
95+
1. Environment variables (GERRIT_URL, GERRIT_AUTH_TYPE, etc.)
96+
2. .env file in current directory
97+
98+
Returns:
99+
Configuration dictionary compatible with connect()
100+
101+
Raises:
102+
pydantic.ValidationError: If configuration is invalid or missing
103+
"""
104+
settings = GerritSettings()
105+
return settings.to_dict()

gerritclient/settings.yaml

Lines changed: 0 additions & 4 deletions
This file was deleted.

0 commit comments

Comments
 (0)