ServiceX_DID_Finder_lib 3.0.1__tar.gz → 3.2.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.
@@ -1,16 +1,18 @@
1
- Metadata-Version: 2.1
1
+ Metadata-Version: 2.4
2
2
  Name: ServiceX_DID_Finder_lib
3
- Version: 3.0.1
3
+ Version: 3.2.0
4
4
  Summary: ServiceX DID Library Routines
5
+ License-File: LICENSE
5
6
  Author: Gordon Watts
6
7
  Author-email: gwatts@uw.edu
7
- Requires-Python: >=3.8.1,<4.0.0
8
+ Requires-Python: >=3.10,<4.0
8
9
  Classifier: Programming Language :: Python :: 3
9
- Classifier: Programming Language :: Python :: 3.9
10
10
  Classifier: Programming Language :: Python :: 3.10
11
11
  Classifier: Programming Language :: Python :: 3.11
12
12
  Classifier: Programming Language :: Python :: 3.12
13
13
  Classifier: Programming Language :: Python :: 3.13
14
+ Classifier: Programming Language :: Python :: 3.14
14
15
  Requires-Dist: Celery (>=5.4,<6.0)
15
16
  Requires-Dist: make-it-sync (>=1.0.0,<2.0.0)
17
+ Requires-Dist: python-logstash (>=0.4.8,<0.5.0)
16
18
  Requires-Dist: requests (>=2.25.0,<3.0.0)
@@ -1,14 +1,16 @@
1
1
  [tool.poetry]
2
2
  name = "ServiceX_DID_Finder_lib"
3
- version = "3.0.1"
3
+ version = "3.2.0"
4
4
  description = "ServiceX DID Library Routines"
5
5
  authors = ["Gordon Watts <gwatts@uw.edu>"]
6
6
 
7
7
  [tool.poetry.dependencies]
8
- python = "^3.8.1"
8
+ python = "^3.10"
9
9
  make-it-sync = "^1.0.0"
10
10
  requests = "^2.25.0"
11
11
  Celery= "^5.4"
12
+ python-logstash = "^0.4.8"
13
+
12
14
 
13
15
 
14
16
  [tool.poetry.group.dev]
@@ -1,9 +1,37 @@
1
+ # Copyright (c) 2021-2025, IRIS-HEP
2
+ # All rights reserved.
3
+ #
4
+ # Redistribution and use in source and binary forms, with or without
5
+ # modification, are permitted provided that the following conditions are met:
6
+ #
7
+ # * Redistributions of source code must retain the above copyright notice, this
8
+ # list of conditions and the following disclaimer.
9
+ #
10
+ # * Redistributions in binary form must reproduce the above copyright notice,
11
+ # this list of conditions and the following disclaimer in the documentation
12
+ # and/or other materials provided with the distribution.
13
+ #
14
+ # * Neither the name of the copyright holder nor the names of its
15
+ # contributors may be used to endorse or promote products derived from
16
+ # this software without specific prior written permission.
17
+ #
18
+ # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
19
+ # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
20
+ # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
21
+ # DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
22
+ # FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
23
+ # DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
24
+ # SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
25
+ # CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
26
+ # OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
27
+ # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
28
+
1
29
  import argparse
2
30
  from datetime import datetime
3
31
  import json
4
32
  import logging
5
33
  import time
6
- from typing import Any, AsyncGenerator, Callable, Dict, List, Optional
34
+ from typing import Any, AsyncGenerator, Callable, Dict, List, Optional, Union
7
35
  import sys
8
36
 
9
37
  import pika
@@ -15,7 +43,8 @@ from servicex_did_finder_lib.util_uri import parse_did_uri
15
43
  from .servicex_adaptor import ServiceXAdapter
16
44
 
17
45
  # The type for the callback method to handle DID's, supplied by the user.
18
- UserDIDHandler = Callable[[str, Dict[str, Any]], AsyncGenerator[Dict[str, Any], None]]
46
+ UserDIDHandler = Callable[[str, Dict[str, Any]], AsyncGenerator[Union[Dict[str, Any],
47
+ List[Dict[str, Any]]], None]]
19
48
 
20
49
  # Given name, build the RabbitMQ queue name by appending this.
21
50
  # This is backed into how ServiceX works - do not change unless it
@@ -1,4 +1,4 @@
1
- # Copyright (c) 2022, IRIS-HEP
1
+ # Copyright (c) 2022-2025, IRIS-HEP
2
2
  # All rights reserved.
3
3
  #
4
4
  # Redistribution and use in source and binary forms, with or without
@@ -36,6 +36,7 @@ from servicex_did_finder_lib.did_logging import initialize_root_logger
36
36
  from servicex_did_finder_lib.did_summary import DIDSummary
37
37
  from servicex_did_finder_lib.servicex_adaptor import ServiceXAdapter
38
38
  from servicex_did_finder_lib.util_uri import parse_did_uri
39
+ from servicex_did_finder_lib import exceptions
39
40
 
40
41
  # The type for the callback method to handle DID's, supplied by the user.
41
42
  # Arguments are:
@@ -102,14 +103,7 @@ class DIDFinderTask(Task):
102
103
 
103
104
  if did_info.file_count > 0: # otherwise wait until all files arrive then limit results
104
105
  acc.send_on(did_info.file_count)
105
- except Exception:
106
- # noinspection PyTypeChecker
107
- self.logger.error(
108
- f"Error processing DID {did}",
109
- extra={"dataset_id": dataset_id},
110
- exc_info=1
111
- )
112
- finally:
106
+
113
107
  elapsed_time = int((datetime.now() - start_time).total_seconds())
114
108
  servicex.put_fileset_complete(
115
109
  {
@@ -120,6 +114,23 @@ class DIDFinderTask(Task):
120
114
  "elapsed-time": elapsed_time,
121
115
  }
122
116
  )
117
+ except Exception as e:
118
+ # noinspection PyTypeChecker
119
+ self.logger.error(
120
+ f"Error processing DID {did}",
121
+ extra={"dataset_id": dataset_id},
122
+ exc_info=1
123
+ )
124
+ elapsed_time = int((datetime.now() - start_time).total_seconds())
125
+ error_dict: dict[str, Any] = {"elapsed-time": elapsed_time,
126
+ "message": str(e)}
127
+ if isinstance(e, exceptions.BaseDIDFinderException):
128
+ error_dict["error-type"] = e.error_type
129
+ else:
130
+ error_dict["error-type"] = "internal_failure"
131
+ servicex.put_fileset_error(
132
+ error_dict
133
+ )
123
134
 
124
135
 
125
136
  class DIDFinderApp(Celery):
@@ -0,0 +1,52 @@
1
+ # Copyright (c) 2025, IRIS-HEP
2
+ # All rights reserved.
3
+ #
4
+ # Redistribution and use in source and binary forms, with or without
5
+ # modification, are permitted provided that the following conditions are met:
6
+ #
7
+ # * Redistributions of source code must retain the above copyright notice, this
8
+ # list of conditions and the following disclaimer.
9
+ #
10
+ # * Redistributions in binary form must reproduce the above copyright notice,
11
+ # this list of conditions and the following disclaimer in the documentation
12
+ # and/or other materials provided with the distribution.
13
+ #
14
+ # * Neither the name of the copyright holder nor the names of its
15
+ # contributors may be used to endorse or promote products derived from
16
+ # this software without specific prior written permission.
17
+ #
18
+ # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
19
+ # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
20
+ # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
21
+ # DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
22
+ # FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
23
+ # DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
24
+ # SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
25
+ # CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
26
+ # OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
27
+ # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
28
+
29
+ # Define some exceptions to indicate various problems in DID finding
30
+
31
+ class BaseDIDFinderException(Exception):
32
+ """ Base exception type """
33
+ transient: bool = True
34
+ error_type: str = ""
35
+
36
+
37
+ class NoSuchDatasetException(BaseDIDFinderException):
38
+ """ The dataset does not exist in the catalog """
39
+ transient = True
40
+ error_type = "does_not_exist"
41
+
42
+
43
+ class BadDatasetNameException(BaseDIDFinderException):
44
+ """ The specified dataset name is invalid """
45
+ transient = False
46
+ error_type = "bad_name"
47
+
48
+
49
+ class LookupFailureException(BaseDIDFinderException):
50
+ """ There was a failure when looking up the dataset in the catalog """
51
+ transient = True
52
+ error_type = "internal_failure"
@@ -0,0 +1,147 @@
1
+ # Copyright (c) 2019-25, IRIS-HEP
2
+ # All rights reserved.
3
+ #
4
+ # Redistribution and use in source and binary forms, with or without
5
+ # modification, are permitted provided that the following conditions are met:
6
+ #
7
+ # * Redistributions of source code must retain the above copyright notice, this
8
+ # list of conditions and the following disclaimer.
9
+ #
10
+ # * Redistributions in binary form must reproduce the above copyright notice,
11
+ # this list of conditions and the following disclaimer in the documentation
12
+ # and/or other materials provided with the distribution.
13
+ #
14
+ # * Neither the name of the copyright holder nor the names of its
15
+ # contributors may be used to endorse or promote products derived from
16
+ # this software without specific prior written permission.
17
+ #
18
+ # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
19
+ # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
20
+ # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
21
+ # DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
22
+ # FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
23
+ # DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
24
+ # SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
25
+ # CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
26
+ # OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
27
+ # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
28
+ import logging
29
+ import os
30
+
31
+ import logstash
32
+ import functools
33
+
34
+ instance = os.environ.get("INSTANCE_NAME", "Unknown")
35
+
36
+
37
+ class LogstashFormatter(logstash.formatter.LogstashFormatterBase):
38
+ def __init__(self, component_name=None, message_type='Logstash', tags=None, fqdn=False):
39
+ super().__init__(message_type, tags, fqdn)
40
+ self.component_name = component_name
41
+
42
+ def format(self, record):
43
+ message = {
44
+ "@timestamp": self.format_timestamp(record.created),
45
+ "@version": "1",
46
+ "message": record.getMessage(),
47
+ "path": record.pathname,
48
+ "tags": self.tags,
49
+ "type": self.message_type,
50
+ "instance": instance,
51
+ "component": self.component_name,
52
+ # Extra Fields
53
+ "level": record.levelname,
54
+ }
55
+
56
+ # Add extra fields
57
+ message.update(self.get_extra_fields(record))
58
+
59
+ # If exception, add debug info
60
+ if record.exc_info:
61
+ message.update(self.get_debug_fields(record))
62
+
63
+ return self.serialize(message)
64
+
65
+
66
+ class StreamFormatter(logging.Formatter):
67
+ """
68
+ A custom formatter that adds extras.
69
+ Normally log messages are "level instance component msg extra: {}"
70
+ """
71
+
72
+ def_keys = [
73
+ "name",
74
+ "msg",
75
+ "args",
76
+ "levelname",
77
+ "levelno",
78
+ "pathname",
79
+ "filename",
80
+ "module",
81
+ "exc_info",
82
+ "exc_text",
83
+ "stack_info",
84
+ "lineno",
85
+ "funcName",
86
+ "created",
87
+ "msecs",
88
+ "relativeCreated",
89
+ "thread",
90
+ "threadName",
91
+ "processName",
92
+ "process",
93
+ "message",
94
+ "taskName",
95
+ ]
96
+
97
+ def format(self, record: logging.LogRecord) -> str:
98
+ """
99
+ :param record: LogRecord
100
+ :return: formatted log message
101
+ """
102
+
103
+ string = super().format(record)
104
+ extra = {k: v for k, v in record.__dict__.items() if k not in self.def_keys}
105
+ if len(extra) > 0:
106
+ string += " extra: " + str(extra)
107
+ return string
108
+
109
+
110
+ @functools.lru_cache
111
+ def initialize_logging(log=None, component_name: str = None, **kwargs):
112
+ """
113
+ Get a logger and initialize it so that it outputs the correct format
114
+ :param component_name: Name of the component to insert into log messages
115
+ :param log: optional logger to initialize
116
+ :return: logger with the correct formatting that outputs to the console
117
+ """
118
+
119
+ if log is None:
120
+ log = logging.getLogger("servicex_did_finder_lib")
121
+
122
+ log.setLevel(logging.INFO)
123
+ log.propagate = False # keep our records out of the root logger Celery hijacks
124
+
125
+ stream_handler = logging.StreamHandler()
126
+ stream_formatter = StreamFormatter(
127
+ "%(levelname)s " + f"{instance} {component_name} " + "%(message)s"
128
+ )
129
+ stream_handler.setFormatter(stream_formatter)
130
+ stream_handler.setLevel(log.level)
131
+ log.addHandler(stream_handler)
132
+
133
+ logstash_host = os.environ.get("LOGSTASH_HOST")
134
+
135
+ if logstash_host:
136
+ logstash_port = int(os.environ.get("LOGSTASH_PORT", 5959))
137
+ logstash_handler = logstash.TCPLogstashHandler(
138
+ logstash_host, logstash_port, version=1
139
+ )
140
+ logstash_formatter = LogstashFormatter(component_name=component_name)
141
+ logstash_handler.setFormatter(logstash_formatter)
142
+ logstash_handler.setLevel(log.level)
143
+ log.addHandler(logstash_handler)
144
+
145
+ log.info("Initialized logging")
146
+
147
+ return log
@@ -1,4 +1,4 @@
1
- # Copyright (c) 2019, IRIS-HEP
1
+ # Copyright (c) 2019-2025, IRIS-HEP
2
2
  # All rights reserved.
3
3
  #
4
4
  # Redistribution and use in source and binary forms, with or without
@@ -74,6 +74,10 @@ class ServiceXAdapter:
74
74
  self.logger.error(f'After {attempts} tries, failed to send ServiceX App '
75
75
  f'a put_file_bulk message: {mesg} - Ignoring error.')
76
76
 
77
+ def put_file_add(self, file):
78
+ # add one file
79
+ self.put_file_add_bulk([file])
80
+
77
81
  def put_fileset_complete(self, summary):
78
82
  success = False
79
83
  attempts = 0
@@ -88,3 +92,18 @@ class ServiceXAdapter:
88
92
  if not success:
89
93
  self.logger.error(f'After {attempts} tries, failed to send ServiceX App a put_file '
90
94
  f'message: {str(summary)} - Ignoring error.')
95
+
96
+ def put_fileset_error(self, summary):
97
+ success = False
98
+ attempts = 0
99
+ while not success and attempts < MAX_RETRIES:
100
+ try:
101
+ requests.put(f"{self.endpoint}{self.dataset_id}/error", json=summary)
102
+ success = True
103
+ except requests.exceptions.ConnectionError:
104
+ self.logger.exception(f'Connection error to ServiceX App. Will retry '
105
+ f'(try {attempts} out of {MAX_RETRIES}')
106
+ attempts += 1
107
+ if not success:
108
+ self.logger.error(f'After {attempts} tries, failed to send ServiceX App a put_file '
109
+ f'message: {str(summary)} - Ignoring error.')