adc-streaming 2.6.0__tar.gz → 2.7.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.
Files changed (38) hide show
  1. {adc_streaming-2.6.0 → adc_streaming-2.7.0}/.github/workflows/build.yml +1 -1
  2. {adc_streaming-2.6.0 → adc_streaming-2.7.0}/Makefile +1 -1
  3. {adc_streaming-2.6.0 → adc_streaming-2.7.0}/PKG-INFO +1 -1
  4. adc_streaming-2.7.0/adc/auth.py +147 -0
  5. {adc_streaming-2.6.0 → adc_streaming-2.7.0}/adc/kafka.py +2 -2
  6. {adc_streaming-2.6.0 → adc_streaming-2.7.0}/adc_streaming.egg-info/PKG-INFO +1 -1
  7. {adc_streaming-2.6.0 → adc_streaming-2.7.0}/adc_streaming.egg-info/SOURCES.txt +2 -0
  8. adc_streaming-2.7.0/adc_streaming.egg-info/scm_file_list.json +32 -0
  9. adc_streaming-2.7.0/adc_streaming.egg-info/scm_version.json +8 -0
  10. {adc_streaming-2.6.0 → adc_streaming-2.7.0}/tests/test_auth.py +10 -1
  11. {adc_streaming-2.6.0 → adc_streaming-2.7.0}/tests/test_kafka_integration.py +6 -1
  12. adc_streaming-2.6.0/adc/auth.py +0 -87
  13. {adc_streaming-2.6.0 → adc_streaming-2.7.0}/.gitignore +0 -0
  14. {adc_streaming-2.6.0 → adc_streaming-2.7.0}/LICENSE +0 -0
  15. {adc_streaming-2.6.0 → adc_streaming-2.7.0}/README.md +0 -0
  16. {adc_streaming-2.6.0 → adc_streaming-2.7.0}/adc/__init__.py +0 -0
  17. {adc_streaming-2.6.0 → adc_streaming-2.7.0}/adc/consumer.py +0 -0
  18. {adc_streaming-2.6.0 → adc_streaming-2.7.0}/adc/errors.py +0 -0
  19. {adc_streaming-2.6.0 → adc_streaming-2.7.0}/adc/io.py +0 -0
  20. {adc_streaming-2.6.0 → adc_streaming-2.7.0}/adc/producer.py +0 -0
  21. {adc_streaming-2.6.0 → adc_streaming-2.7.0}/adc_streaming.egg-info/dependency_links.txt +0 -0
  22. {adc_streaming-2.6.0 → adc_streaming-2.7.0}/adc_streaming.egg-info/not-zip-safe +0 -0
  23. {adc_streaming-2.6.0 → adc_streaming-2.7.0}/adc_streaming.egg-info/requires.txt +0 -0
  24. {adc_streaming-2.6.0 → adc_streaming-2.7.0}/adc_streaming.egg-info/top_level.txt +0 -0
  25. {adc_streaming-2.6.0 → adc_streaming-2.7.0}/doc/Makefile +0 -0
  26. {adc_streaming-2.6.0 → adc_streaming-2.7.0}/doc/_static/css/my_theme.css +0 -0
  27. {adc_streaming-2.6.0 → adc_streaming-2.7.0}/doc/_templates/layout.html +0 -0
  28. {adc_streaming-2.6.0 → adc_streaming-2.7.0}/doc/api/api.rst +0 -0
  29. {adc_streaming-2.6.0 → adc_streaming-2.7.0}/doc/api/streaming.rst +0 -0
  30. {adc_streaming-2.6.0 → adc_streaming-2.7.0}/doc/conf.py +0 -0
  31. {adc_streaming-2.6.0 → adc_streaming-2.7.0}/doc/index.rst +0 -0
  32. {adc_streaming-2.6.0 → adc_streaming-2.7.0}/doc/user/installation.rst +0 -0
  33. {adc_streaming-2.6.0 → adc_streaming-2.7.0}/doc/user/quickstart.rst +0 -0
  34. {adc_streaming-2.6.0 → adc_streaming-2.7.0}/pyproject.toml +0 -0
  35. {adc_streaming-2.6.0 → adc_streaming-2.7.0}/recipe/meta.yaml +0 -0
  36. {adc_streaming-2.6.0 → adc_streaming-2.7.0}/setup.cfg +0 -0
  37. {adc_streaming-2.6.0 → adc_streaming-2.7.0}/setup.py +0 -0
  38. {adc_streaming-2.6.0 → adc_streaming-2.7.0}/tests/test_kafka.py +0 -0
@@ -7,7 +7,7 @@ jobs:
7
7
  runs-on: ubuntu-latest
8
8
  strategy:
9
9
  matrix:
10
- python-version: ["3.9", "3.10", "3.11", "3.12", "3.13"]
10
+ python-version: ["3.10", "3.11", "3.12", "3.13", "3.14"]
11
11
 
12
12
  steps:
13
13
  - name: Check out the code
@@ -32,7 +32,7 @@ lint :
32
32
  # stop the build if there are Python syntax errors or undefined names
33
33
  flake8 . --count --select=E9,F63,F7,F82 --show-source --statistics
34
34
  # exit-zero treats all errors as warnings
35
- flake8 . --count --exit-zero --max-complexity=10 --max-line-length=100 --statistics
35
+ flake8 . --count --exit-zero --max-complexity=20 --max-line-length=100 --statistics
36
36
 
37
37
  .PHONY: format
38
38
  format :
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: adc-streaming
3
- Version: 2.6.0
3
+ Version: 2.7.0
4
4
  Summary: Astronomy Data Commons streaming client libraries
5
5
  Home-page: https://github.com/astronomy-commons/adc-streaming
6
6
  Author: Astronomy Data Commons Team
@@ -0,0 +1,147 @@
1
+ #!/usr/bin/env python
2
+
3
+ import base64
4
+ from collections.abc import Mapping
5
+ from enum import Enum
6
+ import json
7
+ import logging
8
+ import subprocess
9
+
10
+ import certifi
11
+
12
+ logger = logging.getLogger("adc-streaming.auth")
13
+
14
+
15
+ class SASLMethod(Enum):
16
+ """SASL method to use for authentication.
17
+ """
18
+
19
+ PLAIN = 1
20
+ SCRAM_SHA_256 = 2
21
+ SCRAM_SHA_512 = 3
22
+ OAUTHBEARER = 4
23
+
24
+ def __str__(self):
25
+ return self.name.replace("_", "-")
26
+
27
+
28
+ class SASLAuth(object):
29
+ """Attach SASL-based authentication to a client.
30
+
31
+ Returns client-based auth options when called.
32
+
33
+ Parameters
34
+ ----------
35
+ user : `str`
36
+ Username to authenticate with.
37
+ password : `str`
38
+ Password to authenticate with.
39
+ ssl : `bool`, optional
40
+ Whether to enable SSL (enabled by default).
41
+ method : `SASLMethod`, optional
42
+ The SASL method to authenticate. The default is SASLMethod.OAUTHBEARER
43
+ if token_endpoint is provided, or SASLMethod.PLAIN otherwise.
44
+ See valid SASL methods in SASLMethod.
45
+ ssl_ca_location : `str`, optional
46
+ If using SSL via a self-signed cert, a path/location
47
+ to the certificate.
48
+ ssl_endpoint_identification_algorithm : `str`, optional
49
+ If using SSL, the algorithm used to verify that certificate is valid for the endpoint.
50
+ token_endpoint : `str`, optional
51
+ The OpenID Connect token endpoint URL.
52
+ Required for OAUTHBEARER / OpenID Connect, otherwise ignored.
53
+
54
+ """
55
+
56
+ def __init__(self, user, password, ssl=True, method=None, token_endpoint=None, **kwargs):
57
+ if method is None:
58
+ if token_endpoint is not None or "token_command" in kwargs:
59
+ method = SASLMethod.OAUTHBEARER
60
+ else:
61
+ method = SASLMethod.PLAIN
62
+
63
+ self._method = method
64
+
65
+ # set up SSL options
66
+ if ssl:
67
+ if "ssl_ca_location" in kwargs:
68
+ ssl_cert = kwargs["ssl_ca_location"]
69
+ else:
70
+ ssl_cert = certifi.where()
71
+
72
+ self._config = {
73
+ "security.protocol": "SASL_SSL",
74
+ "ssl.ca.location": ssl_cert,
75
+ "https.ca.location": ssl_cert,
76
+ }
77
+ if "ssl_endpoint_identification_algorithm" in kwargs:
78
+ self._config["ssl.endpoint.identification.algorithm"] = \
79
+ kwargs["ssl_endpoint_identification_algorithm"]
80
+ else:
81
+ self._config = {"security.protocol": "SASL_PLAINTEXT"}
82
+
83
+ # set up SASL options
84
+ self._config["sasl.mechanism"] = str(self._method)
85
+ if method == SASLMethod.OAUTHBEARER:
86
+ if token_endpoint is not None:
87
+ self._config["sasl.oauthbearer.client.id"] = user
88
+ self._config["sasl.oauthbearer.client.secret"] = password
89
+ self._config["sasl.oauthbearer.method"] = "oidc"
90
+ self._config["sasl.oauthbearer.token.endpoint.url"] = token_endpoint
91
+ elif "token_command" in kwargs:
92
+ token_command = kwargs["token_command"]
93
+ self._config["sasl.oauthbearer.method"] = "default"
94
+ self._config["oauth_cb"] = lambda c: SASLAuth.external_token_callback(token_command)
95
+ else:
96
+ self._config["sasl.username"] = user
97
+ self._config["sasl.password"] = password
98
+
99
+ @staticmethod
100
+ def external_token_callback(command):
101
+ try:
102
+ presult = subprocess.run(command, shell=True, capture_output=True)
103
+ if presult.returncode != 0:
104
+ raise RuntimeError("Token callback command failed: " + presult.stderr)
105
+ rawdata = presult.stdout
106
+ # need to parse the resulting JWT enough to extract the expiration time
107
+ # we do no other validation, since downstream systems should be responsible
108
+ # for that already
109
+ sections = rawdata.split(b'.')
110
+ if len(sections) != 3:
111
+ raise RuntimeError("Token callback output does not appear to be a valid JWT")
112
+ try:
113
+ # The JWT spec mandates that base 64 padding be omitted, but
114
+ # base64.urlsafe_b64decode requires it, so we must put it back.
115
+ m = len(sections[1]) % 4
116
+ if m == 2:
117
+ sections[1] += b"=="
118
+ elif m == 3:
119
+ sections[1] += b"="
120
+ elif m == 1:
121
+ raise RuntimeError("Token callback output is not valid base64 data")
122
+ claims = json.loads(base64.urlsafe_b64decode(sections[1]).decode("utf-8"))
123
+ except UnicodeDecodeError:
124
+ raise RuntimeError("Token callback output is not valid UTF-8 after base64 decoding")
125
+ except json.JSONDecodeError:
126
+ raise RuntimeError("Token callback output is not valid JSON after base64 and "
127
+ "UTF-8 decoding")
128
+ if not isinstance(claims, Mapping):
129
+ raise RuntimeError("Token callback output does not contain valid claims")
130
+ if "sub" not in claims:
131
+ raise RuntimeError("Token callback output does not contain an subject claim")
132
+ if "exp" not in claims:
133
+ raise RuntimeError("Token callback output does not contain an expiration claim")
134
+ exp_value = claims["exp"]
135
+ if not isinstance(exp_value, int) and not isinstance(exp_value, float):
136
+ raise RuntimeError("Token expiration value is not a number")
137
+ exp_value = float(exp_value)
138
+ try:
139
+ return (rawdata.decode("utf-8").strip(), exp_value, claims["sub"], {})
140
+ except UnicodeDecodeError:
141
+ raise RuntimeError("Token callback output is not valid UTF-8 data")
142
+ except Exception as ex:
143
+ logger.error(f"Token callback error: {ex}")
144
+ raise
145
+
146
+ def __call__(self):
147
+ return self._config
@@ -14,12 +14,12 @@ def parse_kafka_url(val):
14
14
  if parsed.scheme != "kafka":
15
15
  raise ValueError("invalid kafka URL: must start with 'kafka://'")
16
16
 
17
+ group_id = parsed.username
18
+ # intentionally ignore passwords embedded in URLs to discourage their use
17
19
  split_netloc = parsed.netloc.split("@", maxsplit=1)
18
20
  if len(split_netloc) == 2:
19
- group_id = split_netloc[0]
20
21
  broker_addresses = split_netloc[1].split(",")
21
22
  else:
22
- group_id = None
23
23
  broker_addresses = split_netloc[0].split(",")
24
24
 
25
25
  topics = parsed.path.lstrip("/")
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: adc-streaming
3
- Version: 2.6.0
3
+ Version: 2.7.0
4
4
  Summary: Astronomy Data Commons streaming client libraries
5
5
  Home-page: https://github.com/astronomy-commons/adc-streaming
6
6
  Author: Astronomy Data Commons Team
@@ -18,6 +18,8 @@ adc_streaming.egg-info/SOURCES.txt
18
18
  adc_streaming.egg-info/dependency_links.txt
19
19
  adc_streaming.egg-info/not-zip-safe
20
20
  adc_streaming.egg-info/requires.txt
21
+ adc_streaming.egg-info/scm_file_list.json
22
+ adc_streaming.egg-info/scm_version.json
21
23
  adc_streaming.egg-info/top_level.txt
22
24
  doc/Makefile
23
25
  doc/conf.py
@@ -0,0 +1,32 @@
1
+ {
2
+ "files": [
3
+ ".github/workflows/build.yml",
4
+ ".gitignore",
5
+ "LICENSE",
6
+ "Makefile",
7
+ "README.md",
8
+ "adc/__init__.py",
9
+ "adc/auth.py",
10
+ "adc/consumer.py",
11
+ "adc/errors.py",
12
+ "adc/io.py",
13
+ "adc/kafka.py",
14
+ "adc/producer.py",
15
+ "doc/Makefile",
16
+ "doc/_static/css/my_theme.css",
17
+ "doc/_templates/layout.html",
18
+ "doc/api/api.rst",
19
+ "doc/api/streaming.rst",
20
+ "doc/conf.py",
21
+ "doc/index.rst",
22
+ "doc/user/installation.rst",
23
+ "doc/user/quickstart.rst",
24
+ "pyproject.toml",
25
+ "recipe/meta.yaml",
26
+ "setup.cfg",
27
+ "setup.py",
28
+ "tests/test_auth.py",
29
+ "tests/test_kafka.py",
30
+ "tests/test_kafka_integration.py"
31
+ ]
32
+ }
@@ -0,0 +1,8 @@
1
+ {
2
+ "tag": "2.6.0",
3
+ "distance": 5,
4
+ "node": "ge0095abe7c2bd5a1dce1a913653bdc084fcd2496",
5
+ "dirty": false,
6
+ "branch": "external-token-support",
7
+ "node_date": "2026-08-28"
8
+ }
@@ -28,7 +28,16 @@ from adc.auth import SASLAuth
28
28
  'sasl.password': 'test-pass',
29
29
  'security.protocol': 'SASL_SSL'
30
30
  }
31
- ]
31
+ ],
32
+ [
33
+ SASLAuth(
34
+ 'test', '', token_command="generate_token"
35
+ ),
36
+ {
37
+ 'sasl.mechanism': 'OAUTHBEARER',
38
+ 'sasl.oauthbearer.method': 'default',
39
+ }
40
+ ],
32
41
  ])
33
42
  def test_auth(auth, expected_config):
34
43
  # Check that the key/value pairs in expected_config are a subset
@@ -5,7 +5,11 @@ import unittest
5
5
  from datetime import datetime, timedelta
6
6
  from typing import List, Optional
7
7
 
8
- import docker
8
+ try:
9
+ import docker
10
+ have_docker = True
11
+ except ImportError:
12
+ have_docker = False
9
13
  import pytest
10
14
 
11
15
  import adc.consumer
@@ -17,6 +21,7 @@ logging.getLogger("adc-streaming").setLevel(logging.DEBUG)
17
21
  logger = logging.getLogger("adc-streaming.tests")
18
22
 
19
23
 
24
+ @pytest.mark.skipif(not have_docker, reason="requires docker")
20
25
  @pytest.mark.integration_test
21
26
  class KafkaIntegrationTestCase(unittest.TestCase):
22
27
  """This test runs a Kafka broker in a Docker container, and makes sure that
@@ -1,87 +0,0 @@
1
- #!/usr/bin/env python
2
-
3
- from enum import Enum
4
-
5
- import certifi
6
-
7
-
8
- class SASLMethod(Enum):
9
- """SASL method to use for authentication.
10
- """
11
-
12
- PLAIN = 1
13
- SCRAM_SHA_256 = 2
14
- SCRAM_SHA_512 = 3
15
- OAUTHBEARER = 4
16
-
17
- def __str__(self):
18
- return self.name.replace("_", "-")
19
-
20
-
21
- class SASLAuth(object):
22
- """Attach SASL-based authentication to a client.
23
-
24
- Returns client-based auth options when called.
25
-
26
- Parameters
27
- ----------
28
- user : `str`
29
- Username to authenticate with.
30
- password : `str`
31
- Password to authenticate with.
32
- ssl : `bool`, optional
33
- Whether to enable SSL (enabled by default).
34
- method : `SASLMethod`, optional
35
- The SASL method to authenticate. The default is SASLMethod.OAUTHBEARER
36
- if token_endpoint is provided, or SASLMethod.PLAIN otherwise.
37
- See valid SASL methods in SASLMethod.
38
- ssl_ca_location : `str`, optional
39
- If using SSL via a self-signed cert, a path/location
40
- to the certificate.
41
- ssl_endpoint_identification_algorithm : `str`, optional
42
- If using SSL, the algorithm used to verify that certificate is valid for the endpoint.
43
- token_endpoint : `str`, optional
44
- The OpenID Connect token endpoint URL.
45
- Required for OAUTHBEARER / OpenID Connect, otherwise ignored.
46
-
47
- """
48
-
49
- def __init__(self, user, password, ssl=True, method=None, token_endpoint=None, **kwargs):
50
- if method is None:
51
- if token_endpoint is None:
52
- method = SASLMethod.PLAIN
53
- else:
54
- method = SASLMethod.OAUTHBEARER
55
- self._method = method
56
-
57
- # set up SSL options
58
- if ssl:
59
- if "ssl_ca_location" in kwargs:
60
- ssl_cert = kwargs["ssl_ca_location"]
61
- else:
62
- ssl_cert = certifi.where()
63
-
64
- self._config = {
65
- "security.protocol": "SASL_SSL",
66
- "ssl.ca.location": ssl_cert,
67
- "https.ca.location": ssl_cert,
68
- }
69
- if "ssl_endpoint_identification_algorithm" in kwargs:
70
- self._config["ssl.endpoint.identification.algorithm"] = \
71
- kwargs["ssl_endpoint_identification_algorithm"]
72
- else:
73
- self._config = {"security.protocol": "SASL_PLAINTEXT"}
74
-
75
- # set up SASL options
76
- self._config["sasl.mechanism"] = str(self._method)
77
- if token_endpoint:
78
- self._config["sasl.oauthbearer.client.id"] = user
79
- self._config["sasl.oauthbearer.client.secret"] = password
80
- self._config["sasl.oauthbearer.method"] = "oidc"
81
- self._config["sasl.oauthbearer.token.endpoint.url"] = token_endpoint
82
- else:
83
- self._config["sasl.username"] = user
84
- self._config["sasl.password"] = password
85
-
86
- def __call__(self):
87
- return self._config
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes