day2 0.2.2__tar.gz → 0.2.3__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.
Files changed (66) hide show
  1. {day2-0.2.2 → day2-0.2.3}/PKG-INFO +47 -1
  2. {day2-0.2.2 → day2-0.2.3}/README.md +46 -0
  3. day2-0.2.3/day2/auth/credentials.py +400 -0
  4. {day2-0.2.2 → day2-0.2.3}/day2/client/base.py +11 -0
  5. {day2-0.2.2 → day2-0.2.3}/day2/client/config.py +4 -0
  6. day2-0.2.3/day2/client/constants.py +23 -0
  7. day2-0.2.3/day2/client/profile.py +624 -0
  8. day2-0.2.3/day2/client/user_context.py +158 -0
  9. {day2-0.2.2 → day2-0.2.3}/day2/models/__init__.py +11 -0
  10. {day2-0.2.2 → day2-0.2.3}/day2/models/assessment.py +124 -2
  11. day2-0.2.3/day2/models/report.py +41 -0
  12. {day2-0.2.2 → day2-0.2.3}/day2/resources/__init__.py +2 -1
  13. {day2-0.2.2 → day2-0.2.3}/day2/resources/assessment.py +110 -0
  14. day2-0.2.3/day2/resources/report.py +80 -0
  15. day2-0.2.3/day2/resources/user.py +43 -0
  16. day2-0.2.3/day2/session.py +422 -0
  17. {day2-0.2.2 → day2-0.2.3}/day2.egg-info/PKG-INFO +47 -1
  18. {day2-0.2.2 → day2-0.2.3}/day2.egg-info/SOURCES.txt +11 -13
  19. {day2-0.2.2 → day2-0.2.3}/day2_cli/cli.py +7 -12
  20. day2-0.2.3/day2_cli/commands/__init__.py +10 -0
  21. {day2-0.2.2 → day2-0.2.3}/day2_cli/commands/assessment.py +474 -266
  22. day2-0.2.3/day2_cli/commands/auth.py +1020 -0
  23. {day2-0.2.2 → day2-0.2.3}/day2_cli/commands/cost.py +13 -46
  24. day2-0.2.3/day2_cli/commands/profile.py +901 -0
  25. day2-0.2.3/day2_cli/commands/report.py +137 -0
  26. {day2-0.2.2 → day2-0.2.3}/day2_cli/commands/tenant.py +48 -98
  27. day2-0.2.3/day2_cli/utils/context.py +208 -0
  28. day2-0.2.3/day2_cli/utils/exit_codes.py +76 -0
  29. {day2-0.2.2 → day2-0.2.3}/pyproject.toml +47 -1
  30. day2-0.2.3/tests/test_exceptions.py +243 -0
  31. day2-0.2.2/day2/auth/credentials.py +0 -118
  32. day2-0.2.2/day2/session.py +0 -187
  33. day2-0.2.2/day2_cli/commands/__init__.py +0 -7
  34. day2-0.2.2/day2_cli/commands/auth.py +0 -132
  35. day2-0.2.2/day2_cli/commands/config.py +0 -306
  36. day2-0.2.2/day2_cli/utils/config.py +0 -132
  37. day2-0.2.2/tests/test_cli_auth.py +0 -201
  38. day2-0.2.2/tests/test_cli_config.py +0 -234
  39. day2-0.2.2/tests/test_client_base.py +0 -231
  40. day2-0.2.2/tests/test_config.py +0 -300
  41. day2-0.2.2/tests/test_models_assessment.py +0 -418
  42. day2-0.2.2/tests/test_models_cost.py +0 -112
  43. day2-0.2.2/tests/test_models_tenant.py +0 -162
  44. day2-0.2.2/tests/test_resources_assessment.py +0 -661
  45. day2-0.2.2/tests/test_resources_cost.py +0 -90
  46. day2-0.2.2/tests/test_resources_tenant.py +0 -230
  47. day2-0.2.2/tests/test_session.py +0 -163
  48. {day2-0.2.2 → day2-0.2.3}/LICENSE +0 -0
  49. {day2-0.2.2 → day2-0.2.3}/day2/__init__.py +0 -0
  50. {day2-0.2.2 → day2-0.2.3}/day2/_version.py +0 -0
  51. {day2-0.2.2 → day2-0.2.3}/day2/auth/__init__.py +0 -0
  52. {day2-0.2.2 → day2-0.2.3}/day2/client/__init__.py +0 -0
  53. {day2-0.2.2 → day2-0.2.3}/day2/exceptions.py +0 -0
  54. {day2-0.2.2 → day2-0.2.3}/day2/models/cost.py +0 -0
  55. {day2-0.2.2 → day2-0.2.3}/day2/models/tenant.py +0 -0
  56. {day2-0.2.2 → day2-0.2.3}/day2/resources/cost.py +0 -0
  57. {day2-0.2.2 → day2-0.2.3}/day2/resources/tenant.py +0 -0
  58. {day2-0.2.2 → day2-0.2.3}/day2.egg-info/dependency_links.txt +0 -0
  59. {day2-0.2.2 → day2-0.2.3}/day2.egg-info/entry_points.txt +0 -0
  60. {day2-0.2.2 → day2-0.2.3}/day2.egg-info/requires.txt +0 -0
  61. {day2-0.2.2 → day2-0.2.3}/day2.egg-info/top_level.txt +0 -0
  62. {day2-0.2.2 → day2-0.2.3}/day2_cli/__init__.py +0 -0
  63. {day2-0.2.2 → day2-0.2.3}/day2_cli/utils/__init__.py +0 -0
  64. {day2-0.2.2 → day2-0.2.3}/day2_cli/utils/formatters.py +0 -0
  65. {day2-0.2.2 → day2-0.2.3}/day2_cli/utils/output_formatter.py +0 -0
  66. {day2-0.2.2 → day2-0.2.3}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: day2
3
- Version: 0.2.2
3
+ Version: 0.2.3
4
4
  Summary: MontyCloud DAY2 SDK for Python
5
5
  Author-email: MontyCloud <info@montycloud.com>
6
6
  Requires-Python: >=3.11
@@ -276,6 +276,52 @@ except Exception as e:
276
276
  print(f"Unexpected error: {e}")
277
277
  ```
278
278
 
279
+ ## Development
280
+
281
+ ### Documentation
282
+
283
+ The project uses MkDocs for documentation. Here are the available commands:
284
+
285
+ ```bash
286
+ # Build documentation locally
287
+ make docs
288
+
289
+ # Serve documentation for local development
290
+ make serve-docs
291
+
292
+ # Deploy documentation to GitHub Pages (from main branch)
293
+ make deploy-docs
294
+
295
+ # Deploy documentation from any branch to GitHub Pages
296
+ make deploy-branch-docs
297
+ ```
298
+
299
+ The `deploy-branch-docs` command is useful for:
300
+ - Testing documentation changes before merging to main
301
+ - Deploying documentation from feature branches
302
+ - Creating documentation previews for pull requests
303
+
304
+ **Note**: `deploy-branch-docs` will overwrite the current GitHub Pages content, so use with caution in production environments.
305
+
306
+ ### Other Development Commands
307
+
308
+ ```bash
309
+ # Run tests
310
+ make test
311
+
312
+ # Run linting
313
+ make lint
314
+
315
+ # Format code
316
+ make format
317
+
318
+ # Run type checking
319
+ make mypy
320
+
321
+ # Run all CI checks
322
+ make ci
323
+ ```
324
+
279
325
  ## License
280
326
 
281
327
  This project is licensed under the MIT License - see the LICENSE file for details.
@@ -238,6 +238,52 @@ except Exception as e:
238
238
  print(f"Unexpected error: {e}")
239
239
  ```
240
240
 
241
+ ## Development
242
+
243
+ ### Documentation
244
+
245
+ The project uses MkDocs for documentation. Here are the available commands:
246
+
247
+ ```bash
248
+ # Build documentation locally
249
+ make docs
250
+
251
+ # Serve documentation for local development
252
+ make serve-docs
253
+
254
+ # Deploy documentation to GitHub Pages (from main branch)
255
+ make deploy-docs
256
+
257
+ # Deploy documentation from any branch to GitHub Pages
258
+ make deploy-branch-docs
259
+ ```
260
+
261
+ The `deploy-branch-docs` command is useful for:
262
+ - Testing documentation changes before merging to main
263
+ - Deploying documentation from feature branches
264
+ - Creating documentation previews for pull requests
265
+
266
+ **Note**: `deploy-branch-docs` will overwrite the current GitHub Pages content, so use with caution in production environments.
267
+
268
+ ### Other Development Commands
269
+
270
+ ```bash
271
+ # Run tests
272
+ make test
273
+
274
+ # Run linting
275
+ make lint
276
+
277
+ # Format code
278
+ make format
279
+
280
+ # Run type checking
281
+ make mypy
282
+
283
+ # Run all CI checks
284
+ make ci
285
+ ```
286
+
241
287
  ## License
242
288
 
243
289
  This project is licensed under the MIT License - see the LICENSE file for details.
@@ -0,0 +1,400 @@
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(
34
+ profile_name, base_url
35
+ )
36
+
37
+ self.api_key = api_key or self._load_from_env() or self._load_from_config()
38
+ self.secret_key = (
39
+ api_secret_key
40
+ or self._load_api_secret_key_from_env()
41
+ or self._load_api_secret_key_from_config()
42
+ )
43
+
44
+ if not self.api_key:
45
+ raise ValueError(
46
+ "No API key provided. Please provide an API key via the constructor, "
47
+ "environment variable DAY2_API_KEY, or configuration file."
48
+ )
49
+
50
+ def _get_credentials_file_path(self) -> Path:
51
+ """Get the path to the credentials file.
52
+
53
+ Uses the same directory resolution logic as ProfileManager:
54
+ 1. Check DAY2_CONFIG_PATH environment variable directory
55
+ 2. Fall back to ~/.day2/ directory
56
+
57
+ Returns:
58
+ Path to the credentials file.
59
+ """
60
+ if os.environ.get("DAY2_CONFIG_PATH"):
61
+ config_file_path = Path(os.environ["DAY2_CONFIG_PATH"])
62
+ config_dir = config_file_path.parent
63
+ else:
64
+ config_dir = Path.home() / ".day2"
65
+
66
+ return config_dir / "credentials"
67
+
68
+ def _determine_credential_section(
69
+ self, profile_name: Optional[str], base_url: Optional[str]
70
+ ) -> str:
71
+ """Determine which credential section to use based on profile and base URL.
72
+
73
+ Args:
74
+ profile_name: Profile name
75
+ base_url: Base URL for the profile
76
+
77
+ Returns:
78
+ Credential section name to use ("DEFAULT" or profile name)
79
+ """
80
+ if not profile_name:
81
+ return "DEFAULT"
82
+
83
+ # Use smart detection to determine if we should use profile-specific credentials
84
+ if self._should_use_profile_credentials(base_url):
85
+ return profile_name
86
+ return "DEFAULT"
87
+
88
+ def _should_use_profile_credentials(self, base_url: Optional[str]) -> bool:
89
+ """Determine if a profile should use profile-specific credentials.
90
+
91
+ Logic:
92
+ - If base_url is different from the default base_url, use profile-specific credentials
93
+ - Otherwise, use shared credentials for the common MSP case
94
+
95
+ Args:
96
+ base_url: Base URL for this profile (or None to use default)
97
+
98
+ Returns:
99
+ True if profile should use profile-specific credentials, False for shared
100
+ """
101
+ # Default base URL
102
+ default_base_url = "https://api.montycloud.com/day2/api"
103
+
104
+ # If no base_url provided, use default
105
+ if not base_url:
106
+ base_url = default_base_url
107
+
108
+ # Check if this base_url differs from default (different region/environment)
109
+ if base_url != default_base_url:
110
+ return True
111
+
112
+ # Default to shared credentials for the common MSP case
113
+ return False
114
+
115
+ def _load_from_env(self) -> Optional[str]:
116
+ """Load API key from environment variables.
117
+
118
+ Returns:
119
+ API key if found in environment variables, None otherwise.
120
+ """
121
+ return os.environ.get("DAY2_API_KEY")
122
+
123
+ def _load_api_secret_key_from_env(self) -> Optional[str]:
124
+ """Load API secret key from environment variables.
125
+
126
+ Returns:
127
+ API secret key if found in environment variables, None otherwise.
128
+ """
129
+ return os.environ.get("DAY2_API_SECRET_KEY")
130
+
131
+ def _normalize_section_name(self, section_name: str) -> str:
132
+ """Normalize section name for consistent storage.
133
+
134
+ Args:
135
+ section_name: The section name to normalize
136
+
137
+ Returns:
138
+ Normalized section name (DEFAULT for default, original case for others)
139
+ """
140
+ if section_name.lower() == "default":
141
+ return "DEFAULT"
142
+ return section_name
143
+
144
+ def _find_section_case_insensitive(
145
+ self, config_parser: configparser.ConfigParser, section_name: str
146
+ ) -> Optional[str]:
147
+ """Find a section in the config parser, case-insensitively.
148
+
149
+ Args:
150
+ config_parser: The ConfigParser instance to search
151
+ section_name: The section name to find (case-insensitive)
152
+
153
+ Returns:
154
+ The actual section name if found, None otherwise.
155
+ """
156
+ # Check exact match first
157
+ if config_parser.has_section(section_name):
158
+ return section_name
159
+
160
+ # Check case-insensitive match
161
+ for actual_section in config_parser.sections():
162
+ if actual_section.lower() == section_name.lower():
163
+ return actual_section
164
+
165
+ # Special handling for DEFAULT section (configparser treats it specially)
166
+ if section_name.upper() == "DEFAULT":
167
+ # Check if DEFAULT exists in any case
168
+ if "DEFAULT" in config_parser:
169
+ return "DEFAULT"
170
+ for actual_section in config_parser.sections():
171
+ if actual_section.lower() == "default":
172
+ return actual_section
173
+
174
+ return None
175
+
176
+ def _load_from_config(self) -> Optional[str]:
177
+ """Load API key from credentials file.
178
+
179
+ Returns:
180
+ API key if found in credentials file, None otherwise.
181
+ """
182
+ credentials_file = self._get_credentials_file_path()
183
+
184
+ if not credentials_file.exists():
185
+ return None
186
+
187
+ try:
188
+ config_parser = configparser.ConfigParser()
189
+ config_parser.read(credentials_file)
190
+
191
+ # Try to get from the determined section first (case-insensitive)
192
+ actual_section = self._find_section_case_insensitive(
193
+ config_parser, self.credential_section
194
+ )
195
+ if actual_section:
196
+ return config_parser.get(actual_section, "api_key", fallback=None)
197
+
198
+ # Fall back to DEFAULT section if profile section doesn't exist (case-insensitive)
199
+ default_section = self._find_section_case_insensitive(
200
+ config_parser, "DEFAULT"
201
+ )
202
+ if default_section:
203
+ return config_parser.get(default_section, "api_key", fallback=None)
204
+ return None
205
+ except (configparser.Error, IOError):
206
+ return None
207
+
208
+ def _load_api_secret_key_from_config(self) -> Optional[str]:
209
+ """Load API secret key from credentials file.
210
+
211
+ Returns:
212
+ API secret key if found in credentials file, None otherwise.
213
+ """
214
+ credentials_file = self._get_credentials_file_path()
215
+
216
+ if not credentials_file.exists():
217
+ return None
218
+
219
+ try:
220
+ config_parser = configparser.ConfigParser()
221
+ config_parser.read(credentials_file)
222
+
223
+ # Try to get from the determined section first (case-insensitive)
224
+ actual_section = self._find_section_case_insensitive(
225
+ config_parser, self.credential_section
226
+ )
227
+ if actual_section:
228
+ return config_parser.get(
229
+ actual_section, "api_secret_key", fallback=None
230
+ )
231
+
232
+ # Fall back to DEFAULT section if profile section doesn't exist (case-insensitive)
233
+ default_section = self._find_section_case_insensitive(
234
+ config_parser, "DEFAULT"
235
+ )
236
+ if default_section:
237
+ return config_parser.get(
238
+ default_section, "api_secret_key", fallback=None
239
+ )
240
+ return None
241
+ except (configparser.Error, IOError):
242
+ return None
243
+
244
+ def save_to_config(self, section_name: Optional[str] = None) -> None:
245
+ """Save credentials to credentials file.
246
+
247
+ Args:
248
+ section_name: Section name to save credentials to. If None, uses DEFAULT.
249
+ """
250
+ if not self.api_key:
251
+ return
252
+
253
+ config_dir = Path.home() / ".day2"
254
+ config_dir.mkdir(exist_ok=True)
255
+
256
+ credentials_file = config_dir / "credentials"
257
+
258
+ # Use provided section name or default to DEFAULT
259
+ target_section = self._normalize_section_name(section_name or "DEFAULT")
260
+
261
+ # Load existing credentials if they exist
262
+ config_parser = configparser.ConfigParser()
263
+ if credentials_file.exists():
264
+ config_parser.read(credentials_file)
265
+
266
+ # Handle case-insensitive section merging
267
+ self._merge_section_case_insensitive(config_parser, target_section)
268
+
269
+ # Update config with API key and secret
270
+ config_parser[target_section]["api_key"] = self.api_key
271
+ if self.secret_key:
272
+ config_parser[target_section]["api_secret_key"] = self.secret_key
273
+
274
+ # Save credentials
275
+ with open(credentials_file, "w", encoding="utf-8") as f:
276
+ config_parser.write(f)
277
+
278
+ def _merge_section_case_insensitive(
279
+ self, config_parser: configparser.ConfigParser, target_section: str
280
+ ) -> None:
281
+ """Merge existing section with different case into target section.
282
+
283
+ Args:
284
+ config_parser: The ConfigParser instance to modify
285
+ target_section: The normalized target section name
286
+ """
287
+ existing_section = self._find_section_case_insensitive(
288
+ config_parser, target_section
289
+ )
290
+
291
+ if existing_section and existing_section != target_section:
292
+ # Copy existing values to preserve them
293
+ existing_values = dict(config_parser[existing_section])
294
+ config_parser.remove_section(existing_section)
295
+ config_parser[target_section] = existing_values
296
+ elif not existing_section:
297
+ # Create new section
298
+ config_parser[target_section] = {}
299
+
300
+ @classmethod
301
+ def save_credentials_to_section(
302
+ cls,
303
+ api_key: str,
304
+ api_secret_key: Optional[str],
305
+ section_name: str,
306
+ credentials_file_path: Optional[Path] = None,
307
+ ) -> None:
308
+ """Save credentials to a specific section in the credentials file.
309
+
310
+ This is a utility method for auth.py to save credentials without creating a Credentials instance.
311
+
312
+ Args:
313
+ api_key: API key to save
314
+ api_secret_key: API secret key to save (optional)
315
+ section_name: Section name to save to
316
+ credentials_file_path: Optional custom path to credentials file
317
+ """
318
+ if credentials_file_path:
319
+ credentials_file = credentials_file_path
320
+ credentials_file.parent.mkdir(exist_ok=True)
321
+ else:
322
+ config_dir = Path.home() / ".day2"
323
+ config_dir.mkdir(exist_ok=True)
324
+ credentials_file = config_dir / "credentials"
325
+
326
+ # Normalize section name
327
+ normalized_section = cls.normalize_section_name(section_name)
328
+
329
+ # Load existing credentials
330
+ config_parser = configparser.ConfigParser()
331
+ if credentials_file.exists():
332
+ config_parser.read(credentials_file)
333
+
334
+ # Handle case-insensitive section merging
335
+ cls._merge_section_case_insensitive_static(config_parser, normalized_section)
336
+
337
+ # Update credentials
338
+ config_parser[normalized_section]["api_key"] = api_key or ""
339
+ if api_secret_key:
340
+ config_parser[normalized_section]["api_secret_key"] = api_secret_key
341
+
342
+ # Save credentials
343
+ with open(credentials_file, "w", encoding="utf-8") as f:
344
+ config_parser.write(f)
345
+
346
+ @staticmethod
347
+ def normalize_section_name(section_name: str) -> str:
348
+ """Normalize section name for consistent storage.
349
+
350
+ Args:
351
+ section_name: The section name to normalize
352
+
353
+ Returns:
354
+ Normalized section name (DEFAULT for default, original case for others)
355
+ """
356
+ if section_name.lower() == "default":
357
+ return "DEFAULT"
358
+ return section_name
359
+
360
+ @staticmethod
361
+ def _find_section_case_insensitive_static(
362
+ config_parser: configparser.ConfigParser, section_name: str
363
+ ) -> Optional[str]:
364
+ """Static version of _find_section_case_insensitive for class methods."""
365
+ # Check exact match first
366
+ if config_parser.has_section(section_name):
367
+ return section_name
368
+
369
+ # Check case-insensitive match
370
+ for actual_section in config_parser.sections():
371
+ if actual_section.lower() == section_name.lower():
372
+ return actual_section
373
+
374
+ # Special handling for DEFAULT section
375
+ if section_name.upper() == "DEFAULT":
376
+ if "DEFAULT" in config_parser:
377
+ return "DEFAULT"
378
+ for actual_section in config_parser.sections():
379
+ if actual_section.lower() == "default":
380
+ return actual_section
381
+
382
+ return None
383
+
384
+ @staticmethod
385
+ def _merge_section_case_insensitive_static(
386
+ config_parser: configparser.ConfigParser, target_section: str
387
+ ) -> None:
388
+ """Static version of _merge_section_case_insensitive for class methods."""
389
+ existing_section = Credentials._find_section_case_insensitive_static(
390
+ config_parser, target_section
391
+ )
392
+
393
+ if existing_section and existing_section != target_section:
394
+ # Copy existing values to preserve them
395
+ existing_values = dict(config_parser[existing_section])
396
+ config_parser.remove_section(existing_section)
397
+ config_parser[target_section] = existing_values
398
+ elif not existing_section:
399
+ # Create new section
400
+ 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"