hotglue-singer-sdk 1.0.2__py3-none-any.whl

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 (53) hide show
  1. hotglue_singer_sdk/__init__.py +34 -0
  2. hotglue_singer_sdk/authenticators.py +554 -0
  3. hotglue_singer_sdk/cli/__init__.py +1 -0
  4. hotglue_singer_sdk/cli/common_options.py +37 -0
  5. hotglue_singer_sdk/configuration/__init__.py +1 -0
  6. hotglue_singer_sdk/configuration/_dict_config.py +101 -0
  7. hotglue_singer_sdk/exceptions.py +52 -0
  8. hotglue_singer_sdk/helpers/__init__.py +1 -0
  9. hotglue_singer_sdk/helpers/_catalog.py +122 -0
  10. hotglue_singer_sdk/helpers/_classproperty.py +18 -0
  11. hotglue_singer_sdk/helpers/_compat.py +15 -0
  12. hotglue_singer_sdk/helpers/_flattening.py +374 -0
  13. hotglue_singer_sdk/helpers/_schema.py +100 -0
  14. hotglue_singer_sdk/helpers/_secrets.py +41 -0
  15. hotglue_singer_sdk/helpers/_simpleeval.py +678 -0
  16. hotglue_singer_sdk/helpers/_singer.py +280 -0
  17. hotglue_singer_sdk/helpers/_state.py +282 -0
  18. hotglue_singer_sdk/helpers/_typing.py +231 -0
  19. hotglue_singer_sdk/helpers/_util.py +27 -0
  20. hotglue_singer_sdk/helpers/capabilities.py +240 -0
  21. hotglue_singer_sdk/helpers/jsonpath.py +39 -0
  22. hotglue_singer_sdk/io_base.py +134 -0
  23. hotglue_singer_sdk/mapper.py +691 -0
  24. hotglue_singer_sdk/mapper_base.py +156 -0
  25. hotglue_singer_sdk/plugin_base.py +415 -0
  26. hotglue_singer_sdk/py.typed +0 -0
  27. hotglue_singer_sdk/sinks/__init__.py +14 -0
  28. hotglue_singer_sdk/sinks/batch.py +90 -0
  29. hotglue_singer_sdk/sinks/core.py +412 -0
  30. hotglue_singer_sdk/sinks/record.py +66 -0
  31. hotglue_singer_sdk/sinks/sql.py +299 -0
  32. hotglue_singer_sdk/streams/__init__.py +14 -0
  33. hotglue_singer_sdk/streams/core.py +1294 -0
  34. hotglue_singer_sdk/streams/graphql.py +74 -0
  35. hotglue_singer_sdk/streams/rest.py +611 -0
  36. hotglue_singer_sdk/streams/sql.py +1023 -0
  37. hotglue_singer_sdk/tap_base.py +580 -0
  38. hotglue_singer_sdk/target_base.py +554 -0
  39. hotglue_singer_sdk/target_sdk/__init__.py +0 -0
  40. hotglue_singer_sdk/target_sdk/auth.py +124 -0
  41. hotglue_singer_sdk/target_sdk/client.py +286 -0
  42. hotglue_singer_sdk/target_sdk/common.py +13 -0
  43. hotglue_singer_sdk/target_sdk/lambda.py +121 -0
  44. hotglue_singer_sdk/target_sdk/rest.py +108 -0
  45. hotglue_singer_sdk/target_sdk/sinks.py +16 -0
  46. hotglue_singer_sdk/target_sdk/target.py +570 -0
  47. hotglue_singer_sdk/target_sdk/target_base.py +627 -0
  48. hotglue_singer_sdk/testing.py +198 -0
  49. hotglue_singer_sdk/typing.py +603 -0
  50. hotglue_singer_sdk-1.0.2.dist-info/METADATA +53 -0
  51. hotglue_singer_sdk-1.0.2.dist-info/RECORD +53 -0
  52. hotglue_singer_sdk-1.0.2.dist-info/WHEEL +4 -0
  53. hotglue_singer_sdk-1.0.2.dist-info/licenses/LICENSE +201 -0
@@ -0,0 +1,34 @@
1
+ """SDK for building singer-compliant Singer taps."""
2
+
3
+ from hotglue_singer_sdk import streams
4
+ from hotglue_singer_sdk.mapper_base import InlineMapper
5
+ from hotglue_singer_sdk.plugin_base import PluginBase
6
+ from hotglue_singer_sdk.sinks import BatchSink, RecordSink, Sink, SQLSink
7
+ from hotglue_singer_sdk.streams import (
8
+ GraphQLStream,
9
+ RESTStream,
10
+ SQLConnector,
11
+ SQLStream,
12
+ Stream,
13
+ )
14
+ from hotglue_singer_sdk.tap_base import SQLTap, Tap
15
+ from hotglue_singer_sdk.target_base import SQLTarget, Target
16
+
17
+ __all__ = [
18
+ "BatchSink",
19
+ "GraphQLStream",
20
+ "InlineMapper",
21
+ "PluginBase",
22
+ "RecordSink",
23
+ "RESTStream",
24
+ "Sink",
25
+ "SQLConnector",
26
+ "SQLSink",
27
+ "SQLStream",
28
+ "SQLTap",
29
+ "SQLTarget",
30
+ "Stream",
31
+ "streams",
32
+ "Tap",
33
+ "Target",
34
+ ]
@@ -0,0 +1,554 @@
1
+ """Classes to assist in authenticating to APIs."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import base64
6
+ import logging
7
+ import math
8
+ from datetime import datetime, timedelta
9
+ from types import MappingProxyType
10
+ from typing import Any, Mapping
11
+ import json
12
+
13
+ import jwt
14
+ import requests
15
+ from cryptography.hazmat.backends import default_backend
16
+ from cryptography.hazmat.primitives import serialization
17
+ from singer import utils
18
+
19
+ from hotglue_singer_sdk.helpers._util import utc_now
20
+ from hotglue_singer_sdk.streams import Stream as RESTStreamBase
21
+
22
+ import threading
23
+
24
+ _token_lock = threading.Lock()
25
+
26
+
27
+ class SingletonMeta(type):
28
+ """A general purpose singleton metaclass."""
29
+
30
+ def __init__(cls, name: str, bases: tuple[type], dic: dict) -> None:
31
+ """Init metaclass.
32
+
33
+ The single instance is saved as an attribute of the the metaclass.
34
+
35
+ Args:
36
+ name: Name of the derived class.
37
+ bases: Base types of the derived class.
38
+ dic: Class dictionary of the derived class.
39
+ """
40
+ cls.__single_instance = None
41
+ super().__init__(name, bases, dic)
42
+
43
+ def __call__(cls, *args: Any, **kwargs: Any) -> Any: # noqa: ANN401
44
+ """Create or reuse the singleton.
45
+
46
+ Args:
47
+ args: Class constructor positional arguments.
48
+ kwargs: Class constructor keyword arguments.
49
+
50
+ Returns:
51
+ A singleton instance of the derived class.
52
+ """
53
+ if cls.__single_instance:
54
+ return cls.__single_instance
55
+ single_obj = cls.__new__(cls, None) # type: ignore
56
+ single_obj.__init__(*args, **kwargs)
57
+ cls.__single_instance = single_obj
58
+ return single_obj
59
+
60
+
61
+ class APIAuthenticatorBase:
62
+ """Base class for offloading API auth."""
63
+
64
+ def __init__(self, stream: RESTStreamBase) -> None:
65
+ """Init authenticator.
66
+
67
+ Args:
68
+ stream: A stream for a RESTful endpoint.
69
+ """
70
+ self.tap_name: str = stream.tap_name
71
+ self._config: dict[str, Any] = dict(stream.config)
72
+ self._auth_headers: dict[str, Any] = {}
73
+ self._auth_params: dict[str, Any] = {}
74
+ self.logger: logging.Logger = stream.logger
75
+
76
+ @property
77
+ def config(self) -> Mapping[str, Any]:
78
+ """Get stream or tap config.
79
+
80
+ Returns:
81
+ A frozen (read-only) config dictionary map.
82
+ """
83
+ return MappingProxyType(self._config)
84
+
85
+ @property
86
+ def auth_headers(self) -> dict:
87
+ """Get headers.
88
+
89
+ Returns:
90
+ HTTP headers for authentication.
91
+ """
92
+ return self._auth_headers or {}
93
+
94
+ @property
95
+ def auth_params(self) -> dict:
96
+ """Get query parameters.
97
+
98
+ Returns:
99
+ URL query parameters for authentication.
100
+ """
101
+ return self._auth_params or {}
102
+
103
+ def authenticate_request(self, request: requests.Request) -> None:
104
+ """Authenticate a request.
105
+
106
+ Args:
107
+ request: A `request object`_.
108
+
109
+ .. _request object:
110
+ https://requests.readthedocs.io/en/latest/api/#requests.Request
111
+ """
112
+ request.headers.update(self.auth_headers)
113
+ request.params.update(self.auth_params)
114
+
115
+
116
+ class SimpleAuthenticator(APIAuthenticatorBase):
117
+ """DEPRECATED: Please use a more specific authenticator.
118
+
119
+ This authenticator will merge a key-value pair to the stream
120
+ in either the request headers or query parameters.
121
+ """
122
+
123
+ def __init__(
124
+ self,
125
+ stream: RESTStreamBase,
126
+ auth_headers: dict | None = None,
127
+ ) -> None:
128
+ """Create a new authenticator.
129
+
130
+ If auth_headers is provided, it will be merged with http_headers specified on
131
+ the stream.
132
+
133
+ Args:
134
+ stream: The stream instance to use with this authenticator.
135
+ auth_headers: Authentication headers.
136
+ """
137
+ super().__init__(stream=stream)
138
+ if self._auth_headers is None:
139
+ self._auth_headers = {}
140
+ if auth_headers:
141
+ self._auth_headers.update(auth_headers)
142
+
143
+
144
+ class APIKeyAuthenticator(APIAuthenticatorBase):
145
+ """Implements API key authentication for REST Streams.
146
+
147
+ This authenticator will merge a key-value pair with either the
148
+ HTTP headers or query parameters specified on the stream. Common
149
+ examples of key names are "x-api-key" and "Authorization" but
150
+ any key-value pair may be used for this authenticator.
151
+ """
152
+
153
+ def __init__(
154
+ self,
155
+ stream: RESTStreamBase,
156
+ key: str,
157
+ value: str,
158
+ location: str = "header",
159
+ ) -> None:
160
+ """Create a new authenticator.
161
+
162
+ Args:
163
+ stream: The stream instance to use with this authenticator.
164
+ key: API key parameter name.
165
+ value: API key value.
166
+ location: Where the API key is to be added. Either 'header' or 'params'.
167
+
168
+ Raises:
169
+ ValueError: If the location value is not 'header' or 'params'.
170
+ """
171
+ super().__init__(stream=stream)
172
+ auth_credentials = {key: value}
173
+
174
+ if location not in ["header", "params"]:
175
+ raise ValueError("`type` must be one of 'header' or 'params'.")
176
+
177
+ if location == "header":
178
+ if self._auth_headers is None:
179
+ self._auth_headers = {}
180
+ self._auth_headers.update(auth_credentials)
181
+ elif location == "params":
182
+ if self._auth_params is None:
183
+ self._auth_params = {}
184
+ self._auth_params.update(auth_credentials)
185
+
186
+ @classmethod
187
+ def create_for_stream(
188
+ cls: type[APIKeyAuthenticator],
189
+ stream: RESTStreamBase,
190
+ key: str,
191
+ value: str,
192
+ location: str,
193
+ ) -> APIKeyAuthenticator:
194
+ """Create an Authenticator object specific to the Stream class.
195
+
196
+ Args:
197
+ stream: The stream instance to use with this authenticator.
198
+ key: API key parameter name.
199
+ value: API key value.
200
+ location: Where the API key is to be added. Either 'header' or 'params'.
201
+
202
+ Returns:
203
+ APIKeyAuthenticator: A new
204
+ :class:`hotglue_singer_sdk.authenticators.APIKeyAuthenticator` instance.
205
+ """
206
+ return cls(stream=stream, key=key, value=value, location=location)
207
+
208
+
209
+ class BearerTokenAuthenticator(APIAuthenticatorBase):
210
+ """Implements bearer token authentication for REST Streams.
211
+
212
+ This Authenticator implements Bearer Token authentication. The token
213
+ is a text string, included in the request header and prefixed with
214
+ 'Bearer '. The token will be merged with HTTP headers on the stream.
215
+ """
216
+
217
+ def __init__(self, stream: RESTStreamBase, token: str) -> None:
218
+ """Create a new authenticator.
219
+
220
+ Args:
221
+ stream: The stream instance to use with this authenticator.
222
+ token: Authentication token.
223
+ """
224
+ super().__init__(stream=stream)
225
+ auth_credentials = {"Authorization": f"Bearer {token}"}
226
+
227
+ if self._auth_headers is None:
228
+ self._auth_headers = {}
229
+ self._auth_headers.update(auth_credentials)
230
+
231
+ @classmethod
232
+ def create_for_stream(
233
+ cls: type[BearerTokenAuthenticator], stream: RESTStreamBase, token: str
234
+ ) -> BearerTokenAuthenticator:
235
+ """Create an Authenticator object specific to the Stream class.
236
+
237
+ Args:
238
+ stream: The stream instance to use with this authenticator.
239
+ token: Authentication token.
240
+
241
+ Returns:
242
+ BearerTokenAuthenticator: A new
243
+ :class:`hotglue_singer_sdk.authenticators.BearerTokenAuthenticator` instance.
244
+ """
245
+ return cls(stream=stream, token=token)
246
+
247
+
248
+ class BasicAuthenticator(APIAuthenticatorBase):
249
+ """Implements basic authentication for REST Streams.
250
+
251
+ This Authenticator implements basic authentication by concatinating a
252
+ username and password then base64 encoding the string. The resulting
253
+ token will be merged with any HTTP headers specified on the stream.
254
+ """
255
+
256
+ def __init__(
257
+ self,
258
+ stream: RESTStreamBase,
259
+ username: str,
260
+ password: str,
261
+ ) -> None:
262
+ """Create a new authenticator.
263
+
264
+ Args:
265
+ stream: The stream instance to use with this authenticator.
266
+ username: API username.
267
+ password: API password.
268
+ """
269
+ super().__init__(stream=stream)
270
+ credentials = f"{username}:{password}".encode()
271
+ auth_token = base64.b64encode(credentials).decode("ascii")
272
+ auth_credentials = {"Authorization": f"Basic {auth_token}"}
273
+
274
+ if self._auth_headers is None:
275
+ self._auth_headers = {}
276
+ self._auth_headers.update(auth_credentials)
277
+
278
+ @classmethod
279
+ def create_for_stream(
280
+ cls: type[BasicAuthenticator],
281
+ stream: RESTStreamBase,
282
+ username: str,
283
+ password: str,
284
+ ) -> BasicAuthenticator:
285
+ """Create an Authenticator object specific to the Stream class.
286
+
287
+ Args:
288
+ stream: The stream instance to use with this authenticator.
289
+ username: API username.
290
+ password: API password.
291
+
292
+ Returns:
293
+ BasicAuthenticator: A new
294
+ :class:`hotglue_singer_sdk.authenticators.BasicAuthenticator` instance.
295
+ """
296
+ return cls(stream=stream, username=username, password=password)
297
+
298
+
299
+ class OAuthAuthenticator(APIAuthenticatorBase):
300
+ """API Authenticator for OAuth 2.0 flows."""
301
+
302
+ def __init__(
303
+ self,
304
+ stream: RESTStreamBase,
305
+ auth_endpoint: str | None = None,
306
+ oauth_scopes: str | None = None,
307
+ default_expiration: int | None = None,
308
+ config_file: str | None = None,
309
+ ) -> None:
310
+ """Create a new authenticator.
311
+
312
+ Args:
313
+ stream: The stream instance to use with this authenticator.
314
+ auth_endpoint: API username.
315
+ oauth_scopes: API password.
316
+ default_expiration: Default token expiry in seconds.
317
+ """
318
+ super().__init__(stream=stream)
319
+ self._auth_endpoint = auth_endpoint
320
+ self._default_expiration = default_expiration
321
+ self._oauth_scopes = oauth_scopes
322
+
323
+ # Initialize internal tracking attributes
324
+ self.access_token: str | None = None
325
+ self.refresh_token: str | None = None
326
+ self.last_refreshed: datetime | None = None
327
+ self.expires_in: int | None = None
328
+ self._config_file = config_file
329
+ self._tap = stream._tap
330
+
331
+ @property
332
+ def auth_headers(self) -> dict:
333
+ """Return a dictionary of auth headers to be applied.
334
+
335
+ These will be merged with any `http_headers` specified in the stream.
336
+
337
+ Returns:
338
+ HTTP headers for authentication.
339
+ """
340
+ if not self.is_token_valid():
341
+ with _token_lock:
342
+ self.logger.info(f"[{threading.current_thread.__name__}] Token expired, locking and attempting to refresh token.")
343
+ if not self.is_token_valid():
344
+ self.update_access_token()
345
+ result = super().auth_headers
346
+ result["Authorization"] = f"Bearer {self.access_token}"
347
+ return result
348
+
349
+ @property
350
+ def auth_endpoint(self) -> str:
351
+ """Get the authorization endpoint.
352
+
353
+ Returns:
354
+ The API authorization endpoint if it is set.
355
+
356
+ Raises:
357
+ ValueError: If the endpoint is not set.
358
+ """
359
+ if not self._auth_endpoint:
360
+ raise ValueError("Authorization endpoint not set.")
361
+ return self._auth_endpoint
362
+
363
+ @property
364
+ def oauth_scopes(self) -> str | None:
365
+ """Get OAuth scopes.
366
+
367
+ Returns:
368
+ String of OAuth scopes, or None if not set.
369
+ """
370
+ return self._oauth_scopes
371
+
372
+ @property
373
+ def oauth_request_payload(self) -> dict:
374
+ """Get request body.
375
+
376
+ Returns:
377
+ A plain (OAuth) or encrypted (JWT) request body.
378
+ """
379
+ return self.oauth_request_body
380
+
381
+ @property
382
+ def oauth_request_body(self) -> dict:
383
+ """Get formatted body of the OAuth authorization request.
384
+
385
+ Sample implementation:
386
+
387
+ .. highlight:: python
388
+ .. code-block:: python
389
+
390
+ @property
391
+ def oauth_request_body(self) -> dict:
392
+ return {
393
+ 'grant_type': 'password',
394
+ 'scope': 'https://api.powerbi.com',
395
+ 'resource': 'https://analysis.windows.net/powerbi/api',
396
+ 'client_id': self.config["client_id"],
397
+ 'username': self.config.get("username", self.config["client_id"]),
398
+ 'password': self.config["password"],
399
+ }
400
+
401
+ Raises:
402
+ NotImplementedError: If derived class does not override this method.
403
+ """
404
+ raise NotImplementedError(
405
+ "The `oauth_request_body` property was not defined in the subclass."
406
+ )
407
+
408
+ @property
409
+ def client_id(self) -> str | None:
410
+ """Get client ID string to be used in authentication.
411
+
412
+ Returns:
413
+ Optional client secret from stream config if it has been set.
414
+ """
415
+ if self.config:
416
+ return self.config.get("client_id")
417
+ return None
418
+
419
+ @property
420
+ def client_secret(self) -> str | None:
421
+ """Get client secret to be used in authentication.
422
+
423
+ Returns:
424
+ Optional client secret from stream config if it has been set.
425
+ """
426
+ if self.config:
427
+ return self.config.get("client_secret")
428
+ return None
429
+
430
+ def is_token_valid(self) -> bool:
431
+ """Check if token is valid.
432
+
433
+ Returns:
434
+ True if the token is valid (fresh).
435
+ """
436
+ # if expires_in is not set, try to get it from the tap config
437
+ if self.expires_in is None and self._tap.config.get("expires_in"):
438
+ self.expires_in = self._tap.config.get("expires_in")
439
+
440
+ if self.last_refreshed is None:
441
+ return False
442
+ if not self.expires_in:
443
+ return True
444
+ if self.expires_in - int(utils.now().timestamp()) > 120:
445
+ return True
446
+ return False
447
+
448
+ def request_auth(self) -> tuple[str, str]:
449
+ """Return the authentication credentials for the request."""
450
+ return None
451
+
452
+ def update_access_token(self) -> None:
453
+ """Update `access_token` along with: `last_refreshed` and `expires_in`.
454
+
455
+ Raises:
456
+ RuntimeError: When OAuth login fails.
457
+ """
458
+ request_time = utc_now()
459
+ auth_request_payload = self.oauth_request_payload
460
+ token_response = requests.post(self.auth_endpoint, data=auth_request_payload, auth=self.request_auth())
461
+ try:
462
+ token_response.raise_for_status()
463
+ self.logger.info("OAuth authorization attempt was successful.")
464
+ except Exception as ex:
465
+ raise RuntimeError(
466
+ f"Failed OAuth login, response was '{token_response.json()}'. {ex}"
467
+ )
468
+ token_json = token_response.json()
469
+ self.access_token = token_json["access_token"]
470
+ self.expires_in = token_json.get("expires_in", self._default_expiration) + int(request_time.timestamp())
471
+ if self.expires_in is None:
472
+ self.logger.debug(
473
+ "No expires_in receied in OAuth response and no "
474
+ "default_expiration set. Token will be treated as if it never "
475
+ "expires."
476
+ )
477
+ self.last_refreshed = request_time
478
+ # Update the tap config with the new access_token and refresh_token
479
+ self._tap._config["access_token"] = token_json["access_token"]
480
+ self._tap._config["expires_in"] = self.expires_in
481
+ if token_json.get("refresh_token"):
482
+ #Log the refresh_token
483
+ self._tap.logger.info(f"Latest refresh token: {token_json.get('refresh_token')}")
484
+ self._tap._config["refresh_token"] = token_json["refresh_token"]
485
+
486
+ # Write the updated config back to the file
487
+ with open(self._tap.config_file, "w") as outfile:
488
+ json.dump(self._tap._config, outfile, indent=4)
489
+
490
+ class OAuthJWTAuthenticator(OAuthAuthenticator):
491
+ """API Authenticator for OAuth 2.0 flows which utilize a JWT refresh token."""
492
+
493
+ @property
494
+ def private_key(self) -> str | None:
495
+ """Return the private key to use in encryption.
496
+
497
+ Returns:
498
+ Private key from stream config.
499
+ """
500
+ return self.config.get("private_key", None)
501
+
502
+ @property
503
+ def private_key_passphrase(self) -> str | None:
504
+ """Return the private key passphrase to use in encryption.
505
+
506
+ Returns:
507
+ Passphrase for private key from stream config.
508
+ """
509
+ return self.config.get("private_key_passphrase", None)
510
+
511
+ @property
512
+ def oauth_request_body(self) -> dict:
513
+ """Return request body for OAuth request.
514
+
515
+ Returns:
516
+ Request body mapping for OAuth.
517
+ """
518
+ request_time = utc_now()
519
+ return {
520
+ "iss": self.client_id,
521
+ "scope": self.oauth_scopes,
522
+ "aud": self.auth_endpoint,
523
+ "exp": math.floor((request_time + timedelta(hours=1)).timestamp()),
524
+ "iat": math.floor(request_time.timestamp()),
525
+ }
526
+
527
+ @property
528
+ def oauth_request_payload(self) -> dict:
529
+ """Return request paytload for OAuth request.
530
+
531
+ Returns:
532
+ Payload object for OAuth.
533
+
534
+ Raises:
535
+ ValueError: If the private key is not set.
536
+ """
537
+ if not self.private_key:
538
+ raise ValueError("Missing 'private_key' property for OAuth payload.")
539
+
540
+ private_key: bytes | Any = bytes(self.private_key, "UTF-8")
541
+ if self.private_key_passphrase:
542
+ passphrase = bytes(self.private_key_passphrase, "UTF-8")
543
+ private_key = serialization.load_pem_private_key(
544
+ private_key,
545
+ password=passphrase,
546
+ backend=default_backend(),
547
+ )
548
+ private_key_string: str | Any = private_key.decode("UTF-8")
549
+ return {
550
+ "grant_type": "urn:ietf:params:oauth:grant-type:jwt-bearer",
551
+ "assertion": jwt.encode(
552
+ self.oauth_request_body, private_key_string, "RS256"
553
+ ),
554
+ }
@@ -0,0 +1 @@
1
+ """Helpers for the tap, target and mapper CLIs."""
@@ -0,0 +1,37 @@
1
+ """Common CLI options for plugins."""
2
+
3
+ import click
4
+
5
+ PLUGIN_VERSION = click.option(
6
+ "--version",
7
+ is_flag=True,
8
+ help="Display the package version.",
9
+ )
10
+
11
+ PLUGIN_ABOUT = click.option(
12
+ "--about",
13
+ is_flag=True,
14
+ help="Display package metadata and settings.",
15
+ )
16
+
17
+ PLUGIN_ABOUT_FORMAT = click.option(
18
+ "--format",
19
+ help="Specify output style for --about",
20
+ type=click.Choice(["json", "markdown"], case_sensitive=False),
21
+ default=None,
22
+ )
23
+
24
+ PLUGIN_CONFIG = click.option(
25
+ "--config",
26
+ multiple=True,
27
+ help="Configuration file location or 'ENV' to use environment variables.",
28
+ type=click.STRING,
29
+ default=(),
30
+ )
31
+
32
+ PLUGIN_FILE_INPUT = click.option(
33
+ "--input",
34
+ "file_input",
35
+ help="A path to read messages from instead of from standard in.",
36
+ type=click.File("r"),
37
+ )
@@ -0,0 +1 @@
1
+ """Configuration parsing and handling."""