day2 0.2.2__tar.gz → 0.2.4__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.
- {day2-0.2.2 → day2-0.2.4}/PKG-INFO +1 -29
- {day2-0.2.2 → day2-0.2.4}/README.md +0 -28
- day2-0.2.4/day2/auth/credentials.py +398 -0
- {day2-0.2.2 → day2-0.2.4}/day2/client/base.py +11 -0
- {day2-0.2.2 → day2-0.2.4}/day2/client/config.py +4 -0
- day2-0.2.4/day2/client/constants.py +23 -0
- day2-0.2.4/day2/client/profile.py +624 -0
- day2-0.2.4/day2/client/user_context.py +158 -0
- {day2-0.2.2 → day2-0.2.4}/day2/models/__init__.py +16 -0
- {day2-0.2.2 → day2-0.2.4}/day2/models/assessment.py +126 -2
- day2-0.2.4/day2/models/report.py +73 -0
- {day2-0.2.2 → day2-0.2.4}/day2/resources/__init__.py +2 -1
- {day2-0.2.2 → day2-0.2.4}/day2/resources/assessment.py +114 -2
- day2-0.2.4/day2/resources/report.py +115 -0
- {day2-0.2.2 → day2-0.2.4}/day2/resources/tenant.py +1 -1
- day2-0.2.4/day2/resources/user.py +43 -0
- day2-0.2.4/day2/session.py +398 -0
- day2-0.2.4/day2/utils/__init__.py +0 -0
- day2-0.2.4/day2/utils/helpers.py +27 -0
- {day2-0.2.2 → day2-0.2.4}/day2.egg-info/PKG-INFO +1 -29
- {day2-0.2.2 → day2-0.2.4}/day2.egg-info/SOURCES.txt +13 -13
- {day2-0.2.2 → day2-0.2.4}/day2_cli/cli.py +7 -12
- day2-0.2.4/day2_cli/commands/__init__.py +10 -0
- {day2-0.2.2 → day2-0.2.4}/day2_cli/commands/assessment.py +476 -268
- day2-0.2.4/day2_cli/commands/auth.py +1042 -0
- {day2-0.2.2 → day2-0.2.4}/day2_cli/commands/cost.py +13 -46
- day2-0.2.4/day2_cli/commands/profile.py +941 -0
- day2-0.2.4/day2_cli/commands/report.py +208 -0
- {day2-0.2.2 → day2-0.2.4}/day2_cli/commands/tenant.py +48 -98
- day2-0.2.4/day2_cli/utils/context.py +208 -0
- day2-0.2.4/day2_cli/utils/exit_codes.py +76 -0
- {day2-0.2.2 → day2-0.2.4}/pyproject.toml +47 -1
- day2-0.2.4/tests/test_exceptions.py +243 -0
- day2-0.2.2/day2/auth/credentials.py +0 -118
- day2-0.2.2/day2/session.py +0 -187
- day2-0.2.2/day2_cli/commands/__init__.py +0 -7
- day2-0.2.2/day2_cli/commands/auth.py +0 -132
- day2-0.2.2/day2_cli/commands/config.py +0 -306
- day2-0.2.2/day2_cli/utils/config.py +0 -132
- day2-0.2.2/tests/test_cli_auth.py +0 -201
- day2-0.2.2/tests/test_cli_config.py +0 -234
- day2-0.2.2/tests/test_client_base.py +0 -231
- day2-0.2.2/tests/test_config.py +0 -300
- day2-0.2.2/tests/test_models_assessment.py +0 -418
- day2-0.2.2/tests/test_models_cost.py +0 -112
- day2-0.2.2/tests/test_models_tenant.py +0 -162
- day2-0.2.2/tests/test_resources_assessment.py +0 -661
- day2-0.2.2/tests/test_resources_cost.py +0 -90
- day2-0.2.2/tests/test_resources_tenant.py +0 -230
- day2-0.2.2/tests/test_session.py +0 -163
- {day2-0.2.2 → day2-0.2.4}/LICENSE +0 -0
- {day2-0.2.2 → day2-0.2.4}/day2/__init__.py +0 -0
- {day2-0.2.2 → day2-0.2.4}/day2/_version.py +0 -0
- {day2-0.2.2 → day2-0.2.4}/day2/auth/__init__.py +0 -0
- {day2-0.2.2 → day2-0.2.4}/day2/client/__init__.py +0 -0
- {day2-0.2.2 → day2-0.2.4}/day2/exceptions.py +0 -0
- {day2-0.2.2 → day2-0.2.4}/day2/models/cost.py +0 -0
- {day2-0.2.2 → day2-0.2.4}/day2/models/tenant.py +0 -0
- {day2-0.2.2 → day2-0.2.4}/day2/resources/cost.py +0 -0
- {day2-0.2.2 → day2-0.2.4}/day2.egg-info/dependency_links.txt +0 -0
- {day2-0.2.2 → day2-0.2.4}/day2.egg-info/entry_points.txt +0 -0
- {day2-0.2.2 → day2-0.2.4}/day2.egg-info/requires.txt +0 -0
- {day2-0.2.2 → day2-0.2.4}/day2.egg-info/top_level.txt +0 -0
- {day2-0.2.2 → day2-0.2.4}/day2_cli/__init__.py +0 -0
- {day2-0.2.2 → day2-0.2.4}/day2_cli/utils/__init__.py +0 -0
- {day2-0.2.2 → day2-0.2.4}/day2_cli/utils/formatters.py +0 -0
- {day2-0.2.2 → day2-0.2.4}/day2_cli/utils/output_formatter.py +0 -0
- {day2-0.2.2 → day2-0.2.4}/setup.cfg +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: day2
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.4
|
|
4
4
|
Summary: MontyCloud DAY2 SDK for Python
|
|
5
5
|
Author-email: MontyCloud <info@montycloud.com>
|
|
6
6
|
Requires-Python: >=3.11
|
|
@@ -220,34 +220,6 @@ default_session.set_tenant("org-123")
|
|
|
220
220
|
assessments = day2.assessment().list_assessments(tenant_id="tenant-123", status="PENDING")
|
|
221
221
|
```
|
|
222
222
|
|
|
223
|
-
## Running Examples
|
|
224
|
-
|
|
225
|
-
The SDK comes with example scripts in the `examples` directory that demonstrate various features. To run these examples:
|
|
226
|
-
|
|
227
|
-
1. Clone the repository:
|
|
228
|
-
```bash
|
|
229
|
-
git clone https://github.com/montycloud/day2-sdk.git
|
|
230
|
-
cd day2-sdk
|
|
231
|
-
```
|
|
232
|
-
|
|
233
|
-
2. Create a virtual environment:
|
|
234
|
-
```bash
|
|
235
|
-
python -m venv .venv
|
|
236
|
-
source .venv/bin/activate # On Windows: .venv\Scripts\activate
|
|
237
|
-
```
|
|
238
|
-
|
|
239
|
-
3. Install the package in development mode:
|
|
240
|
-
```bash
|
|
241
|
-
pip install -e .
|
|
242
|
-
```
|
|
243
|
-
|
|
244
|
-
4. Run an example:
|
|
245
|
-
```bash
|
|
246
|
-
python examples/basic_usage.py
|
|
247
|
-
```
|
|
248
|
-
|
|
249
|
-
Note: Some examples may require you to have valid API credentials. Make sure to set up your authentication as described in the [Authentication](#authentication) section before running the examples.
|
|
250
|
-
|
|
251
223
|
## Error Handling
|
|
252
224
|
|
|
253
225
|
The SDK provides comprehensive error handling with custom exceptions:
|
|
@@ -182,34 +182,6 @@ default_session.set_tenant("org-123")
|
|
|
182
182
|
assessments = day2.assessment().list_assessments(tenant_id="tenant-123", status="PENDING")
|
|
183
183
|
```
|
|
184
184
|
|
|
185
|
-
## Running Examples
|
|
186
|
-
|
|
187
|
-
The SDK comes with example scripts in the `examples` directory that demonstrate various features. To run these examples:
|
|
188
|
-
|
|
189
|
-
1. Clone the repository:
|
|
190
|
-
```bash
|
|
191
|
-
git clone https://github.com/montycloud/day2-sdk.git
|
|
192
|
-
cd day2-sdk
|
|
193
|
-
```
|
|
194
|
-
|
|
195
|
-
2. Create a virtual environment:
|
|
196
|
-
```bash
|
|
197
|
-
python -m venv .venv
|
|
198
|
-
source .venv/bin/activate # On Windows: .venv\Scripts\activate
|
|
199
|
-
```
|
|
200
|
-
|
|
201
|
-
3. Install the package in development mode:
|
|
202
|
-
```bash
|
|
203
|
-
pip install -e .
|
|
204
|
-
```
|
|
205
|
-
|
|
206
|
-
4. Run an example:
|
|
207
|
-
```bash
|
|
208
|
-
python examples/basic_usage.py
|
|
209
|
-
```
|
|
210
|
-
|
|
211
|
-
Note: Some examples may require you to have valid API credentials. Make sure to set up your authentication as described in the [Authentication](#authentication) section before running the examples.
|
|
212
|
-
|
|
213
185
|
## Error Handling
|
|
214
186
|
|
|
215
187
|
The SDK provides comprehensive error handling with custom exceptions:
|
|
@@ -0,0 +1,398 @@
|
|
|
1
|
+
"""Credential management for the MontyCloud SDK."""
|
|
2
|
+
|
|
3
|
+
import configparser
|
|
4
|
+
import os
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from typing import Optional
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class Credentials:
|
|
10
|
+
"""Manages API keys and other authentication credentials."""
|
|
11
|
+
|
|
12
|
+
def __init__(
|
|
13
|
+
self,
|
|
14
|
+
api_key: Optional[str] = None,
|
|
15
|
+
api_secret_key: Optional[str] = None,
|
|
16
|
+
profile_name: Optional[str] = None,
|
|
17
|
+
base_url: Optional[str] = None,
|
|
18
|
+
):
|
|
19
|
+
"""Initialize credentials.
|
|
20
|
+
|
|
21
|
+
Args:
|
|
22
|
+
api_key: API key for authentication. If not provided, will attempt to load
|
|
23
|
+
from environment variables or configuration file.
|
|
24
|
+
api_secret_key: API secret key for authentication. If not provided, will attempt to load
|
|
25
|
+
from environment variables or configuration file.
|
|
26
|
+
profile_name: Profile name for credential section determination
|
|
27
|
+
base_url: Base URL for credential section determination
|
|
28
|
+
"""
|
|
29
|
+
self.profile_name = profile_name
|
|
30
|
+
self.base_url = base_url
|
|
31
|
+
|
|
32
|
+
# Determine which credential section to use
|
|
33
|
+
self.credential_section = self._determine_credential_section(profile_name)
|
|
34
|
+
|
|
35
|
+
self.api_key = api_key or self._load_from_env() or self._load_from_config()
|
|
36
|
+
self.secret_key = (
|
|
37
|
+
api_secret_key
|
|
38
|
+
or self._load_api_secret_key_from_env()
|
|
39
|
+
or self._load_api_secret_key_from_config()
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
if not self.api_key:
|
|
43
|
+
raise ValueError(
|
|
44
|
+
"No API key provided. Please provide an API key via the constructor, "
|
|
45
|
+
"environment variable DAY2_API_KEY, or configuration file."
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
def _get_credentials_file_path(self) -> Path:
|
|
49
|
+
"""Get the path to the credentials file.
|
|
50
|
+
|
|
51
|
+
Uses the same directory resolution logic as ProfileManager:
|
|
52
|
+
1. Check DAY2_CONFIG_PATH environment variable directory
|
|
53
|
+
2. Fall back to ~/.day2/ directory
|
|
54
|
+
|
|
55
|
+
Returns:
|
|
56
|
+
Path to the credentials file.
|
|
57
|
+
"""
|
|
58
|
+
if os.environ.get("DAY2_CONFIG_PATH"):
|
|
59
|
+
config_file_path = Path(os.environ["DAY2_CONFIG_PATH"])
|
|
60
|
+
config_dir = config_file_path.parent
|
|
61
|
+
else:
|
|
62
|
+
config_dir = Path.home() / ".day2"
|
|
63
|
+
|
|
64
|
+
return config_dir / "credentials"
|
|
65
|
+
|
|
66
|
+
def _determine_credential_section(self, profile_name: Optional[str]) -> str:
|
|
67
|
+
"""Determine which credential section to use.
|
|
68
|
+
|
|
69
|
+
This now uses a simpler approach:
|
|
70
|
+
- If no profile name, use DEFAULT
|
|
71
|
+
- Otherwise, try the profile's own section first
|
|
72
|
+
- The actual loading logic will handle fallback to DEFAULT
|
|
73
|
+
|
|
74
|
+
Args:
|
|
75
|
+
profile_name: Profile name
|
|
76
|
+
|
|
77
|
+
Returns:
|
|
78
|
+
Credential section name to use (profile name or "DEFAULT")
|
|
79
|
+
"""
|
|
80
|
+
if not profile_name:
|
|
81
|
+
return "DEFAULT"
|
|
82
|
+
|
|
83
|
+
# Return the profile name - the loading logic will handle checking
|
|
84
|
+
# if credentials exist there and falling back to DEFAULT if needed
|
|
85
|
+
return profile_name
|
|
86
|
+
|
|
87
|
+
def _load_from_env(self) -> Optional[str]:
|
|
88
|
+
"""Load API key from environment variables.
|
|
89
|
+
|
|
90
|
+
Returns:
|
|
91
|
+
API key if found in environment variables, None otherwise.
|
|
92
|
+
"""
|
|
93
|
+
return os.environ.get("DAY2_API_KEY")
|
|
94
|
+
|
|
95
|
+
def _load_api_secret_key_from_env(self) -> Optional[str]:
|
|
96
|
+
"""Load API secret key from environment variables.
|
|
97
|
+
|
|
98
|
+
Returns:
|
|
99
|
+
API secret key if found in environment variables, None otherwise.
|
|
100
|
+
"""
|
|
101
|
+
return os.environ.get("DAY2_API_SECRET_KEY")
|
|
102
|
+
|
|
103
|
+
def _normalize_section_name(self, section_name: str) -> str:
|
|
104
|
+
"""Normalize section name for consistent storage.
|
|
105
|
+
|
|
106
|
+
Args:
|
|
107
|
+
section_name: The section name to normalize
|
|
108
|
+
|
|
109
|
+
Returns:
|
|
110
|
+
Normalized section name (DEFAULT for default, original case for others)
|
|
111
|
+
"""
|
|
112
|
+
if section_name.lower() == "default":
|
|
113
|
+
return "DEFAULT"
|
|
114
|
+
return section_name
|
|
115
|
+
|
|
116
|
+
def _find_section_case_insensitive(
|
|
117
|
+
self, config_parser: configparser.ConfigParser, section_name: str
|
|
118
|
+
) -> Optional[str]:
|
|
119
|
+
"""Find a section in the config parser, case-insensitively.
|
|
120
|
+
|
|
121
|
+
Args:
|
|
122
|
+
config_parser: The ConfigParser instance to search
|
|
123
|
+
section_name: The section name to find (case-insensitive)
|
|
124
|
+
|
|
125
|
+
Returns:
|
|
126
|
+
The actual section name if found, None otherwise.
|
|
127
|
+
"""
|
|
128
|
+
# Check exact match first
|
|
129
|
+
if config_parser.has_section(section_name):
|
|
130
|
+
return section_name
|
|
131
|
+
|
|
132
|
+
# Check case-insensitive match
|
|
133
|
+
for actual_section in config_parser.sections():
|
|
134
|
+
if actual_section.lower() == section_name.lower():
|
|
135
|
+
return actual_section
|
|
136
|
+
|
|
137
|
+
# Special handling for DEFAULT section (configparser treats it specially)
|
|
138
|
+
if section_name.upper() == "DEFAULT":
|
|
139
|
+
# Check if DEFAULT exists in any case
|
|
140
|
+
if "DEFAULT" in config_parser:
|
|
141
|
+
return "DEFAULT"
|
|
142
|
+
for actual_section in config_parser.sections():
|
|
143
|
+
if actual_section.lower() == "default":
|
|
144
|
+
return actual_section
|
|
145
|
+
|
|
146
|
+
return None
|
|
147
|
+
|
|
148
|
+
def _load_from_config(self) -> Optional[str]:
|
|
149
|
+
"""Load API key from credentials file.
|
|
150
|
+
|
|
151
|
+
Returns:
|
|
152
|
+
API key if found in credentials file, None otherwise.
|
|
153
|
+
"""
|
|
154
|
+
credentials_file = self._get_credentials_file_path()
|
|
155
|
+
|
|
156
|
+
if not credentials_file.exists():
|
|
157
|
+
return None
|
|
158
|
+
|
|
159
|
+
try:
|
|
160
|
+
config_parser = configparser.ConfigParser()
|
|
161
|
+
config_parser.read(credentials_file)
|
|
162
|
+
|
|
163
|
+
# First, always check if the profile has its own credentials
|
|
164
|
+
if self.profile_name:
|
|
165
|
+
profile_section = self._find_section_case_insensitive(
|
|
166
|
+
config_parser, self.profile_name
|
|
167
|
+
)
|
|
168
|
+
if profile_section:
|
|
169
|
+
api_key = config_parser.get(
|
|
170
|
+
profile_section, "api_key", fallback=None
|
|
171
|
+
)
|
|
172
|
+
if api_key:
|
|
173
|
+
# Update credential_section to reflect what we actually used
|
|
174
|
+
self.credential_section = profile_section
|
|
175
|
+
return api_key
|
|
176
|
+
|
|
177
|
+
# Then try the determined section (case-insensitive)
|
|
178
|
+
actual_section = self._find_section_case_insensitive(
|
|
179
|
+
config_parser, self.credential_section
|
|
180
|
+
)
|
|
181
|
+
if actual_section:
|
|
182
|
+
return config_parser.get(actual_section, "api_key", fallback=None)
|
|
183
|
+
|
|
184
|
+
# Fall back to DEFAULT section if profile section doesn't exist (case-insensitive)
|
|
185
|
+
default_section = self._find_section_case_insensitive(
|
|
186
|
+
config_parser, "DEFAULT"
|
|
187
|
+
)
|
|
188
|
+
if default_section:
|
|
189
|
+
return config_parser.get(default_section, "api_key", fallback=None)
|
|
190
|
+
return None
|
|
191
|
+
except (configparser.Error, IOError):
|
|
192
|
+
return None
|
|
193
|
+
|
|
194
|
+
def _load_api_secret_key_from_config(self) -> Optional[str]:
|
|
195
|
+
"""Load API secret key from credentials file.
|
|
196
|
+
|
|
197
|
+
Returns:
|
|
198
|
+
API secret key if found in credentials file, None otherwise.
|
|
199
|
+
"""
|
|
200
|
+
credentials_file = self._get_credentials_file_path()
|
|
201
|
+
|
|
202
|
+
if not credentials_file.exists():
|
|
203
|
+
return None
|
|
204
|
+
|
|
205
|
+
try:
|
|
206
|
+
config_parser = configparser.ConfigParser()
|
|
207
|
+
config_parser.read(credentials_file)
|
|
208
|
+
|
|
209
|
+
# First, always check if the profile has its own credentials
|
|
210
|
+
if self.profile_name:
|
|
211
|
+
profile_section = self._find_section_case_insensitive(
|
|
212
|
+
config_parser, self.profile_name
|
|
213
|
+
)
|
|
214
|
+
if profile_section:
|
|
215
|
+
secret_key = config_parser.get(
|
|
216
|
+
profile_section, "api_secret_key", fallback=None
|
|
217
|
+
)
|
|
218
|
+
if secret_key:
|
|
219
|
+
return secret_key
|
|
220
|
+
|
|
221
|
+
# Then try the determined section (case-insensitive)
|
|
222
|
+
actual_section = self._find_section_case_insensitive(
|
|
223
|
+
config_parser, self.credential_section
|
|
224
|
+
)
|
|
225
|
+
if actual_section:
|
|
226
|
+
return config_parser.get(
|
|
227
|
+
actual_section, "api_secret_key", fallback=None
|
|
228
|
+
)
|
|
229
|
+
|
|
230
|
+
# Fall back to DEFAULT section if profile section doesn't exist (case-insensitive)
|
|
231
|
+
default_section = self._find_section_case_insensitive(
|
|
232
|
+
config_parser, "DEFAULT"
|
|
233
|
+
)
|
|
234
|
+
if default_section:
|
|
235
|
+
return config_parser.get(
|
|
236
|
+
default_section, "api_secret_key", fallback=None
|
|
237
|
+
)
|
|
238
|
+
return None
|
|
239
|
+
except (configparser.Error, IOError):
|
|
240
|
+
return None
|
|
241
|
+
|
|
242
|
+
def save_to_config(self, section_name: Optional[str] = None) -> None:
|
|
243
|
+
"""Save credentials to credentials file.
|
|
244
|
+
|
|
245
|
+
Args:
|
|
246
|
+
section_name: Section name to save credentials to. If None, uses DEFAULT.
|
|
247
|
+
"""
|
|
248
|
+
if not self.api_key:
|
|
249
|
+
return
|
|
250
|
+
|
|
251
|
+
config_dir = Path.home() / ".day2"
|
|
252
|
+
config_dir.mkdir(exist_ok=True)
|
|
253
|
+
|
|
254
|
+
credentials_file = config_dir / "credentials"
|
|
255
|
+
|
|
256
|
+
# Use provided section name or default to DEFAULT
|
|
257
|
+
target_section = self._normalize_section_name(section_name or "DEFAULT")
|
|
258
|
+
|
|
259
|
+
# Load existing credentials if they exist
|
|
260
|
+
config_parser = configparser.ConfigParser()
|
|
261
|
+
if credentials_file.exists():
|
|
262
|
+
config_parser.read(credentials_file)
|
|
263
|
+
|
|
264
|
+
# Handle case-insensitive section merging
|
|
265
|
+
self._merge_section_case_insensitive(config_parser, target_section)
|
|
266
|
+
|
|
267
|
+
# Update config with API key and secret
|
|
268
|
+
config_parser[target_section]["api_key"] = self.api_key
|
|
269
|
+
if self.secret_key:
|
|
270
|
+
config_parser[target_section]["api_secret_key"] = self.secret_key
|
|
271
|
+
|
|
272
|
+
# Save credentials
|
|
273
|
+
with open(credentials_file, "w", encoding="utf-8") as f:
|
|
274
|
+
config_parser.write(f)
|
|
275
|
+
|
|
276
|
+
def _merge_section_case_insensitive(
|
|
277
|
+
self, config_parser: configparser.ConfigParser, target_section: str
|
|
278
|
+
) -> None:
|
|
279
|
+
"""Merge existing section with different case into target section.
|
|
280
|
+
|
|
281
|
+
Args:
|
|
282
|
+
config_parser: The ConfigParser instance to modify
|
|
283
|
+
target_section: The normalized target section name
|
|
284
|
+
"""
|
|
285
|
+
existing_section = self._find_section_case_insensitive(
|
|
286
|
+
config_parser, target_section
|
|
287
|
+
)
|
|
288
|
+
|
|
289
|
+
if existing_section and existing_section != target_section:
|
|
290
|
+
# Copy existing values to preserve them
|
|
291
|
+
existing_values = dict(config_parser[existing_section])
|
|
292
|
+
config_parser.remove_section(existing_section)
|
|
293
|
+
config_parser[target_section] = existing_values
|
|
294
|
+
elif not existing_section:
|
|
295
|
+
# Create new section
|
|
296
|
+
config_parser[target_section] = {}
|
|
297
|
+
|
|
298
|
+
@classmethod
|
|
299
|
+
def save_credentials_to_section(
|
|
300
|
+
cls,
|
|
301
|
+
api_key: str,
|
|
302
|
+
api_secret_key: Optional[str],
|
|
303
|
+
section_name: str,
|
|
304
|
+
credentials_file_path: Optional[Path] = None,
|
|
305
|
+
) -> None:
|
|
306
|
+
"""Save credentials to a specific section in the credentials file.
|
|
307
|
+
|
|
308
|
+
This is a utility method for auth.py to save credentials without creating a Credentials instance.
|
|
309
|
+
|
|
310
|
+
Args:
|
|
311
|
+
api_key: API key to save
|
|
312
|
+
api_secret_key: API secret key to save (optional)
|
|
313
|
+
section_name: Section name to save to
|
|
314
|
+
credentials_file_path: Optional custom path to credentials file
|
|
315
|
+
"""
|
|
316
|
+
if credentials_file_path:
|
|
317
|
+
credentials_file = credentials_file_path
|
|
318
|
+
credentials_file.parent.mkdir(exist_ok=True)
|
|
319
|
+
else:
|
|
320
|
+
config_dir = Path.home() / ".day2"
|
|
321
|
+
config_dir.mkdir(exist_ok=True)
|
|
322
|
+
credentials_file = config_dir / "credentials"
|
|
323
|
+
|
|
324
|
+
# Normalize section name
|
|
325
|
+
normalized_section = cls.normalize_section_name(section_name)
|
|
326
|
+
|
|
327
|
+
# Load existing credentials
|
|
328
|
+
config_parser = configparser.ConfigParser()
|
|
329
|
+
if credentials_file.exists():
|
|
330
|
+
config_parser.read(credentials_file)
|
|
331
|
+
|
|
332
|
+
# Handle case-insensitive section merging
|
|
333
|
+
cls._merge_section_case_insensitive_static(config_parser, normalized_section)
|
|
334
|
+
|
|
335
|
+
# Update credentials
|
|
336
|
+
config_parser[normalized_section]["api_key"] = api_key or ""
|
|
337
|
+
if api_secret_key:
|
|
338
|
+
config_parser[normalized_section]["api_secret_key"] = api_secret_key
|
|
339
|
+
|
|
340
|
+
# Save credentials
|
|
341
|
+
with open(credentials_file, "w", encoding="utf-8") as f:
|
|
342
|
+
config_parser.write(f)
|
|
343
|
+
|
|
344
|
+
@staticmethod
|
|
345
|
+
def normalize_section_name(section_name: str) -> str:
|
|
346
|
+
"""Normalize section name for consistent storage.
|
|
347
|
+
|
|
348
|
+
Args:
|
|
349
|
+
section_name: The section name to normalize
|
|
350
|
+
|
|
351
|
+
Returns:
|
|
352
|
+
Normalized section name (DEFAULT for default, original case for others)
|
|
353
|
+
"""
|
|
354
|
+
if section_name.lower() == "default":
|
|
355
|
+
return "DEFAULT"
|
|
356
|
+
return section_name
|
|
357
|
+
|
|
358
|
+
@staticmethod
|
|
359
|
+
def _find_section_case_insensitive_static(
|
|
360
|
+
config_parser: configparser.ConfigParser, section_name: str
|
|
361
|
+
) -> Optional[str]:
|
|
362
|
+
"""Static version of _find_section_case_insensitive for class methods."""
|
|
363
|
+
# Check exact match first
|
|
364
|
+
if config_parser.has_section(section_name):
|
|
365
|
+
return section_name
|
|
366
|
+
|
|
367
|
+
# Check case-insensitive match
|
|
368
|
+
for actual_section in config_parser.sections():
|
|
369
|
+
if actual_section.lower() == section_name.lower():
|
|
370
|
+
return actual_section
|
|
371
|
+
|
|
372
|
+
# Special handling for DEFAULT section
|
|
373
|
+
if section_name.upper() == "DEFAULT":
|
|
374
|
+
if "DEFAULT" in config_parser:
|
|
375
|
+
return "DEFAULT"
|
|
376
|
+
for actual_section in config_parser.sections():
|
|
377
|
+
if actual_section.lower() == "default":
|
|
378
|
+
return actual_section
|
|
379
|
+
|
|
380
|
+
return None
|
|
381
|
+
|
|
382
|
+
@staticmethod
|
|
383
|
+
def _merge_section_case_insensitive_static(
|
|
384
|
+
config_parser: configparser.ConfigParser, target_section: str
|
|
385
|
+
) -> None:
|
|
386
|
+
"""Static version of _merge_section_case_insensitive for class methods."""
|
|
387
|
+
existing_section = Credentials._find_section_case_insensitive_static(
|
|
388
|
+
config_parser, target_section
|
|
389
|
+
)
|
|
390
|
+
|
|
391
|
+
if existing_section and existing_section != target_section:
|
|
392
|
+
# Copy existing values to preserve them
|
|
393
|
+
existing_values = dict(config_parser[existing_section])
|
|
394
|
+
config_parser.remove_section(existing_section)
|
|
395
|
+
config_parser[target_section] = existing_values
|
|
396
|
+
elif not existing_section:
|
|
397
|
+
# Create new section
|
|
398
|
+
config_parser[target_section] = {}
|
|
@@ -73,6 +73,17 @@ class BaseClient:
|
|
|
73
73
|
),
|
|
74
74
|
}
|
|
75
75
|
|
|
76
|
+
# Add User-Agent header
|
|
77
|
+
from day2._version import __version__
|
|
78
|
+
|
|
79
|
+
user_agent = f"day2-sdk/{__version__}"
|
|
80
|
+
if (
|
|
81
|
+
hasattr(self.session, "user_agent_suffix")
|
|
82
|
+
and self.session.user_agent_suffix
|
|
83
|
+
):
|
|
84
|
+
user_agent += f" {self.session.user_agent_suffix}"
|
|
85
|
+
headers["User-Agent"] = user_agent
|
|
86
|
+
|
|
76
87
|
# Add Authorization header if available
|
|
77
88
|
# Use auth_token (which is actually the api_secret_key internally)
|
|
78
89
|
if (
|
|
@@ -49,6 +49,9 @@ class Config:
|
|
|
49
49
|
# CLI output format (table or json)
|
|
50
50
|
output_format: str = "table"
|
|
51
51
|
|
|
52
|
+
# Profile name
|
|
53
|
+
profile: str = ""
|
|
54
|
+
|
|
52
55
|
@property
|
|
53
56
|
def api_url(self) -> str:
|
|
54
57
|
"""Get the full API URL.
|
|
@@ -74,6 +77,7 @@ class Config:
|
|
|
74
77
|
"retry_min_delay": self.retry_min_delay,
|
|
75
78
|
"retry_max_delay": self.retry_max_delay,
|
|
76
79
|
"output_format": self.output_format,
|
|
80
|
+
"profile": self.profile,
|
|
77
81
|
}
|
|
78
82
|
|
|
79
83
|
@classmethod
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
"""Constants for the MontyCloud DAY2 SDK."""
|
|
2
|
+
|
|
3
|
+
# Configuration constants
|
|
4
|
+
DEFAULT_SECTION = "DEFAULT"
|
|
5
|
+
PROFILE_PREFIX = "profile_"
|
|
6
|
+
DEFAULT_PROFILE_KEY = "default_profile"
|
|
7
|
+
CONFIG_DIR = ".day2"
|
|
8
|
+
CONFIG_FILE = "config"
|
|
9
|
+
|
|
10
|
+
# Environment variable keys
|
|
11
|
+
ENV_PROFILE_KEY = "DAY2_PROFILE"
|
|
12
|
+
|
|
13
|
+
# Configuration keys
|
|
14
|
+
KEY_TENANT_ID = "tenant_id"
|
|
15
|
+
KEY_BASE_URL = "base_url"
|
|
16
|
+
KEY_API_VERSION = "api_version"
|
|
17
|
+
KEY_TIMEOUT = "timeout"
|
|
18
|
+
KEY_MAX_RETRIES = "max_retries"
|
|
19
|
+
KEY_RETRY_BACKOFF = "retry_backoff_factor"
|
|
20
|
+
KEY_RETRY_MIN_DELAY = "retry_min_delay"
|
|
21
|
+
KEY_RETRY_MAX_DELAY = "retry_max_delay"
|
|
22
|
+
KEY_OUTPUT_FORMAT = "output_format"
|
|
23
|
+
KEY_PROFILE = "profile"
|