scout-apm 3.3.0__cp38-cp38-musllinux_1_2_i686.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 (65) hide show
  1. scout_apm/__init__.py +0 -0
  2. scout_apm/api/__init__.py +197 -0
  3. scout_apm/async_/__init__.py +1 -0
  4. scout_apm/async_/api.py +41 -0
  5. scout_apm/async_/instruments/__init__.py +0 -0
  6. scout_apm/async_/instruments/jinja2.py +13 -0
  7. scout_apm/async_/starlette.py +101 -0
  8. scout_apm/bottle.py +86 -0
  9. scout_apm/celery.py +153 -0
  10. scout_apm/compat.py +104 -0
  11. scout_apm/core/__init__.py +99 -0
  12. scout_apm/core/_objtrace.cpython-38-i386-linux-gnu.so +0 -0
  13. scout_apm/core/agent/__init__.py +0 -0
  14. scout_apm/core/agent/commands.py +250 -0
  15. scout_apm/core/agent/manager.py +319 -0
  16. scout_apm/core/agent/socket.py +211 -0
  17. scout_apm/core/backtrace.py +116 -0
  18. scout_apm/core/cli/__init__.py +0 -0
  19. scout_apm/core/cli/core_agent_manager.py +32 -0
  20. scout_apm/core/config.py +404 -0
  21. scout_apm/core/context.py +140 -0
  22. scout_apm/core/error.py +95 -0
  23. scout_apm/core/error_service.py +167 -0
  24. scout_apm/core/metadata.py +66 -0
  25. scout_apm/core/n_plus_one_tracker.py +41 -0
  26. scout_apm/core/objtrace.py +24 -0
  27. scout_apm/core/platform_detection.py +66 -0
  28. scout_apm/core/queue_time.py +99 -0
  29. scout_apm/core/sampler.py +149 -0
  30. scout_apm/core/samplers/__init__.py +0 -0
  31. scout_apm/core/samplers/cpu.py +76 -0
  32. scout_apm/core/samplers/memory.py +23 -0
  33. scout_apm/core/samplers/thread.py +41 -0
  34. scout_apm/core/stacktracer.py +30 -0
  35. scout_apm/core/threading.py +56 -0
  36. scout_apm/core/tracked_request.py +328 -0
  37. scout_apm/core/web_requests.py +167 -0
  38. scout_apm/django/__init__.py +7 -0
  39. scout_apm/django/apps.py +137 -0
  40. scout_apm/django/instruments/__init__.py +0 -0
  41. scout_apm/django/instruments/huey.py +30 -0
  42. scout_apm/django/instruments/sql.py +140 -0
  43. scout_apm/django/instruments/template.py +35 -0
  44. scout_apm/django/middleware.py +211 -0
  45. scout_apm/django/request.py +144 -0
  46. scout_apm/dramatiq.py +42 -0
  47. scout_apm/falcon.py +142 -0
  48. scout_apm/flask/__init__.py +118 -0
  49. scout_apm/flask/sqlalchemy.py +28 -0
  50. scout_apm/huey.py +54 -0
  51. scout_apm/hug.py +40 -0
  52. scout_apm/instruments/__init__.py +21 -0
  53. scout_apm/instruments/elasticsearch.py +263 -0
  54. scout_apm/instruments/jinja2.py +127 -0
  55. scout_apm/instruments/pymongo.py +105 -0
  56. scout_apm/instruments/redis.py +77 -0
  57. scout_apm/instruments/urllib3.py +80 -0
  58. scout_apm/rq.py +85 -0
  59. scout_apm/sqlalchemy.py +38 -0
  60. scout_apm-3.3.0.dist-info/LICENSE +21 -0
  61. scout_apm-3.3.0.dist-info/METADATA +82 -0
  62. scout_apm-3.3.0.dist-info/RECORD +65 -0
  63. scout_apm-3.3.0.dist-info/WHEEL +5 -0
  64. scout_apm-3.3.0.dist-info/entry_points.txt +2 -0
  65. scout_apm-3.3.0.dist-info/top_level.txt +1 -0
scout_apm/__init__.py ADDED
File without changes
@@ -0,0 +1,197 @@
1
+ # coding=utf-8
2
+
3
+ import sys
4
+
5
+ import scout_apm.core
6
+ from scout_apm.compat import ContextDecorator, text
7
+ from scout_apm.core.config import ScoutConfig
8
+ from scout_apm.core.error import ErrorMonitor
9
+ from scout_apm.core.tracked_request import TrackedRequest
10
+
11
+ # The async_ module can only be shipped on Python 3.6+
12
+ try:
13
+ from scout_apm.async_.api import AsyncDecoratorMixin
14
+ except ImportError:
15
+
16
+ class AsyncDecoratorMixin(object):
17
+ pass
18
+
19
+
20
+ __all__ = [
21
+ "BackgroundTransaction",
22
+ "Config",
23
+ "Context",
24
+ "Error",
25
+ "WebTransaction",
26
+ "install",
27
+ "instrument",
28
+ ]
29
+
30
+
31
+ class Context(object):
32
+ @classmethod
33
+ def add(self, key, value):
34
+ """Adds context to the currently executing request.
35
+
36
+ :key: Any String identifying the request context.
37
+ Example: "user_ip", "plan", "alert_count"
38
+ :value: Any json-serializable type.
39
+ Example: "1.1.1.1", "free", 100
40
+ :returns: nothing.
41
+ """
42
+ TrackedRequest.instance().tag(key, value)
43
+
44
+
45
+ class Config(ScoutConfig):
46
+ pass
47
+
48
+
49
+ install = scout_apm.core.install
50
+
51
+
52
+ def ignore_transaction():
53
+ TrackedRequest.instance().tag("ignore_transaction", True)
54
+
55
+
56
+ class instrument(AsyncDecoratorMixin, ContextDecorator):
57
+ def __init__(self, operation, kind="Custom", tags=None):
58
+ self.operation = text(kind) + "/" + text(operation)
59
+ if tags is None:
60
+ self.tags = {}
61
+ else:
62
+ self.tags = tags
63
+
64
+ def __enter__(self):
65
+ tracked_request = TrackedRequest.instance()
66
+ self.span = tracked_request.start_span(operation=self.operation)
67
+ for key, value in self.tags.items():
68
+ self.tag(key, value)
69
+ return self
70
+
71
+ def __exit__(self, *exc):
72
+ tracked_request = TrackedRequest.instance()
73
+ tracked_request.stop_span()
74
+ return False
75
+
76
+ def tag(self, key, value):
77
+ if self.span is not None:
78
+ self.span.tag(key, value)
79
+
80
+
81
+ class Transaction(AsyncDecoratorMixin, ContextDecorator):
82
+ """
83
+ This Class is not meant to be used directly.
84
+ Use one of the subclasses
85
+ (WebTransaction or BackgroundTransaction)
86
+ """
87
+
88
+ def __init__(self, name, tags=None):
89
+ self.name = text(name)
90
+ if tags is None:
91
+ self.tags = {}
92
+ else:
93
+ self.tags = tags
94
+
95
+ @classmethod
96
+ def start(cls, kind, name, tags=None):
97
+ operation = text(kind) + "/" + text(name)
98
+
99
+ tracked_request = TrackedRequest.instance()
100
+ tracked_request.operation = operation
101
+ tracked_request.is_real_request = True
102
+ span = tracked_request.start_span(
103
+ operation=operation, should_capture_backtrace=False
104
+ )
105
+ if tags is not None:
106
+ for key, value in tags.items():
107
+ tracked_request.tag(key, value)
108
+ return span
109
+
110
+ @classmethod
111
+ def stop(cls):
112
+ tracked_request = TrackedRequest.instance()
113
+ tracked_request.stop_span()
114
+ return True
115
+
116
+ # __enter__ must be defined by child classes.
117
+
118
+ # *exc is any exception raised. Ignore that
119
+ def __exit__(self, *exc):
120
+ WebTransaction.stop()
121
+ return False
122
+
123
+ def tag(self, key, value):
124
+ if self.span is not None:
125
+ self.span.tag(key, value)
126
+
127
+
128
+ class WebTransaction(Transaction):
129
+ @classmethod
130
+ def start(cls, name, tags=None):
131
+ super(WebTransaction, cls).start("Controller", text(name), tags)
132
+
133
+ def __enter__(self):
134
+ super(WebTransaction, self).start("Controller", self.name, self.tags)
135
+
136
+
137
+ class BackgroundTransaction(Transaction):
138
+ @classmethod
139
+ def start(cls, name, tags=None):
140
+ super(BackgroundTransaction, cls).start("Job", text(name), tags)
141
+
142
+ def __enter__(self):
143
+ super(BackgroundTransaction, self).start("Job", self.name, self.tags)
144
+
145
+
146
+ def rename_transaction(name):
147
+ if name is not None:
148
+ tracked_request = TrackedRequest.instance()
149
+ tracked_request.tag("transaction.name", name)
150
+
151
+
152
+ class Error(object):
153
+ @classmethod
154
+ def capture(
155
+ cls,
156
+ exception,
157
+ request_path=None,
158
+ request_params=None,
159
+ session=None,
160
+ custom_controller=None,
161
+ custom_params=None,
162
+ ):
163
+ """
164
+ Capture the exception manually.
165
+
166
+ Utilizes sys.exc_info to gather the traceback. This has the side
167
+ effect that if another exception is raised before calling
168
+ ``Error.capture``, the traceback will match the most recently
169
+ raised exception.
170
+
171
+ Includes any context added for the TrackedRequest.
172
+
173
+ :exception: Any exception.
174
+ :request_path: Any String identifying the relative path of the request.
175
+ Example: "/hello-world/"
176
+ :request_params: Any json-serializable dict representing the
177
+ querystring parameters.
178
+ Example: {"page": 1}
179
+ :session: Any json-serializable dict representing the
180
+ request session.
181
+ Example: {"step": 0}
182
+ :custom_controller: Any String identifying the controller or job.
183
+ Example: "send_email"
184
+ :custom_params: Any json-serializable dict.
185
+ Example: {"to": "scout@test.com", "from": "no-reply@test.com"}
186
+ :returns: nothing.
187
+ """
188
+ if isinstance(exception, Exception):
189
+ exc_info = (exception.__class__, exception, sys.exc_info()[2])
190
+ ErrorMonitor.send(
191
+ exc_info,
192
+ request_path=request_path,
193
+ request_params=request_params,
194
+ session=session,
195
+ custom_controller=custom_controller,
196
+ custom_params=custom_params,
197
+ )
@@ -0,0 +1 @@
1
+ # coding=utf-8
@@ -0,0 +1,41 @@
1
+ # coding=utf-8
2
+
3
+ from functools import wraps
4
+
5
+
6
+ class AsyncDecoratorMixin(object):
7
+ """Provide the ability to decorate both sync and async functions."""
8
+
9
+ is_async = False
10
+
11
+ @classmethod
12
+ def async_(cls, operation, tags=None, **kwargs):
13
+ """
14
+ Instrument an async function via a decorator.
15
+
16
+ This will return an awaitable which must be awaited.
17
+ Using this on a synchronous function will raise a
18
+ RuntimeError.
19
+
20
+ ``
21
+ @instrument.async_("Foo")
22
+ async def foo():
23
+ ...
24
+ ``
25
+ """
26
+ instance = cls(operation, tags=tags, **kwargs)
27
+ instance.is_async = True
28
+ return instance
29
+
30
+ def __call__(self, func):
31
+ if self.is_async:
32
+ # Until https://bugs.python.org/issue37398 has a resolution,
33
+ # manually wrap the async function
34
+ @wraps(func)
35
+ async def decorated(*args, **kwds):
36
+ with self._recreate_cm():
37
+ return await func(*args, **kwds)
38
+
39
+ return decorated
40
+ else:
41
+ return super().__call__(func)
File without changes
@@ -0,0 +1,13 @@
1
+ # coding=utf-8
2
+
3
+ import wrapt
4
+
5
+ from scout_apm.core.tracked_request import TrackedRequest
6
+
7
+
8
+ @wrapt.decorator
9
+ async def wrapped_render_async(wrapped, instance, args, kwargs):
10
+ tracked_request = TrackedRequest.instance()
11
+ with tracked_request.span(operation="Template/Render") as span:
12
+ span.tag("name", instance.name)
13
+ return await wrapped(*args, **kwargs)
@@ -0,0 +1,101 @@
1
+ # coding=utf-8
2
+
3
+ import wrapt
4
+ from starlette.background import BackgroundTask
5
+
6
+ import scout_apm.core
7
+ from scout_apm.core.tracked_request import TrackedRequest
8
+ from scout_apm.core.web_requests import asgi_track_request_data
9
+
10
+
11
+ class ScoutMiddleware:
12
+ def __init__(self, app):
13
+ self.app = app
14
+ installed = scout_apm.core.install()
15
+ self._do_nothing = not installed
16
+ if installed:
17
+ install_background_instrumentation()
18
+
19
+ async def __call__(self, scope, receive, send):
20
+ if self._do_nothing or scope["type"] != "http":
21
+ return await self.app(scope, receive, send)
22
+
23
+ tracked_request = TrackedRequest.instance()
24
+ # Assume the request is real until we determine it's not. This is useful
25
+ # when the asyncio instrumentation is determining if a new Task should
26
+ # reuse the existing tracked request.
27
+ tracked_request.is_real_request = True
28
+ # Can't name controller until post-routing - see final clause
29
+ controller_span = tracked_request.start_span(operation="Controller/Unknown")
30
+
31
+ asgi_track_request_data(scope, tracked_request)
32
+
33
+ def grab_extra_data():
34
+ if "endpoint" in scope:
35
+ # Rename top span
36
+ endpoint = scope["endpoint"]
37
+ if not hasattr(endpoint, "__qualname__"):
38
+ endpoint = endpoint.__class__
39
+ controller_span.operation = "Controller/{}.{}".format(
40
+ endpoint.__module__,
41
+ endpoint.__qualname__,
42
+ )
43
+ tracked_request.operation = controller_span.operation
44
+ else:
45
+ # Mark the request as not real
46
+ tracked_request.is_real_request = False
47
+
48
+ # From AuthenticationMiddleware - bypass request.user because it
49
+ # throws AssertionError if 'user' is not in Scope, and we need a
50
+ # try/except already
51
+ try:
52
+ username = scope["user"].display_name
53
+ except (KeyError, AttributeError):
54
+ pass
55
+ else:
56
+ tracked_request.tag("username", username)
57
+
58
+ async def wrapped_send(data):
59
+ type_ = data.get("type", None)
60
+ if type_ == "http.response.start" and 500 <= data.get("status", 200) <= 599:
61
+ tracked_request.tag("error", "true")
62
+ elif type_ == "http.response.body" and not data.get("more_body", False):
63
+ # Finish HTTP span when body finishes sending, not later (e.g.
64
+ # after background tasks)
65
+ grab_extra_data()
66
+ tracked_request.stop_span()
67
+ return await send(data)
68
+
69
+ try:
70
+ await self.app(scope, receive, wrapped_send)
71
+ except Exception as exc:
72
+ tracked_request.tag("error", "true")
73
+ raise exc
74
+ finally:
75
+ if tracked_request.end_time is None:
76
+ grab_extra_data()
77
+ tracked_request.stop_span()
78
+
79
+
80
+ background_instrumentation_installed = False
81
+
82
+
83
+ def install_background_instrumentation():
84
+ global background_instrumentation_installed
85
+ if background_instrumentation_installed:
86
+ return
87
+ background_instrumentation_installed = True
88
+
89
+ @wrapt.decorator
90
+ async def wrapped_background_call(wrapped, instance, args, kwargs):
91
+ tracked_request = TrackedRequest.instance()
92
+ tracked_request.is_real_request = True
93
+
94
+ operation = "Job/{}.{}".format(
95
+ instance.func.__module__, instance.func.__qualname__
96
+ )
97
+ tracked_request.operation = operation
98
+ with tracked_request.span(operation=operation):
99
+ return await wrapped(*args, **kwargs)
100
+
101
+ BackgroundTask.__call__ = wrapped_background_call(BackgroundTask.__call__)
scout_apm/bottle.py ADDED
@@ -0,0 +1,86 @@
1
+ # coding=utf-8
2
+
3
+ import wrapt
4
+ from bottle import request, response
5
+
6
+ import scout_apm.core
7
+ from scout_apm.core.config import scout_config
8
+ from scout_apm.core.queue_time import track_request_queue_time
9
+ from scout_apm.core.tracked_request import TrackedRequest
10
+ from scout_apm.core.web_requests import create_filtered_path, ignore_path
11
+
12
+
13
+ class ScoutPlugin(object):
14
+ def __init__(self):
15
+ self.name = "scout"
16
+ self.api = 2
17
+
18
+ def set_config_from_bottle(self, app):
19
+ bottle_configs = {}
20
+ prefix = "scout."
21
+ prefix_len = len(prefix)
22
+ for key, value in app.config.items():
23
+ if key.startswith(prefix) and len(key) > prefix_len:
24
+ scout_key = key[prefix_len:]
25
+ bottle_configs[scout_key] = value
26
+ scout_config.set(**bottle_configs)
27
+
28
+ def setup(self, app):
29
+ self.set_config_from_bottle(app)
30
+ installed = scout_apm.core.install()
31
+ self._do_nothing = not installed
32
+
33
+ def apply(self, callback, context):
34
+ if self._do_nothing:
35
+ return callback
36
+ return wrap_callback(callback)
37
+
38
+
39
+ @wrapt.decorator
40
+ def wrap_callback(wrapped, instance, args, kwargs):
41
+ tracked_request = TrackedRequest.instance()
42
+ tracked_request.is_real_request = True
43
+
44
+ path = request.path
45
+ # allitems() is an undocumented bottle internal
46
+ tracked_request.tag("path", create_filtered_path(path, request.query.allitems()))
47
+ if ignore_path(path):
48
+ tracked_request.tag("ignore_transaction", True)
49
+
50
+ if request.route.name is not None:
51
+ controller_name = request.route.name
52
+ else:
53
+ controller_name = request.route.rule
54
+ if controller_name == "/":
55
+ controller_name = "/home"
56
+ if not controller_name.startswith("/"):
57
+ controller_name = "/" + controller_name
58
+
59
+ if scout_config.value("collect_remote_ip"):
60
+ # Determine a remote IP to associate with the request. The
61
+ # value is spoofable by the requester so this is not suitable
62
+ # to use in any security sensitive context.
63
+ user_ip = (
64
+ request.headers.get("x-forwarded-for", "").split(",")[0]
65
+ or request.headers.get("client-ip", "").split(",")[0]
66
+ or request.environ.get("REMOTE_ADDR")
67
+ )
68
+ tracked_request.tag("user_ip", user_ip)
69
+
70
+ queue_time = request.headers.get("x-queue-start", "") or request.headers.get(
71
+ "x-request-start", ""
72
+ )
73
+ track_request_queue_time(queue_time, tracked_request)
74
+ operation = "Controller{}".format(controller_name)
75
+
76
+ with tracked_request.span(operation=operation):
77
+ tracked_request.operation = operation
78
+ try:
79
+ value = wrapped(*args, **kwargs)
80
+ except Exception:
81
+ tracked_request.tag("error", "true")
82
+ raise
83
+ else:
84
+ if 500 <= response.status_code <= 599:
85
+ tracked_request.tag("error", "true")
86
+ return value
scout_apm/celery.py ADDED
@@ -0,0 +1,153 @@
1
+ # coding=utf-8
2
+
3
+ import datetime as dt
4
+ import logging
5
+
6
+ from celery.signals import before_task_publish, task_failure, task_postrun, task_prerun
7
+
8
+ from scout_apm.core.queue_time import track_job_queue_time
9
+
10
+ try:
11
+ import django
12
+ from django.views.debug import SafeExceptionReporterFilter
13
+
14
+ def get_safe_settings():
15
+ return SafeExceptionReporterFilter().get_safe_settings()
16
+
17
+ except ImportError:
18
+ # Django not installed
19
+ get_safe_settings = None
20
+
21
+ import scout_apm.core
22
+ from scout_apm.compat import datetime_to_timestamp
23
+ from scout_apm.core.config import scout_config
24
+ from scout_apm.core.error import ErrorMonitor
25
+ from scout_apm.core.tracked_request import TrackedRequest
26
+
27
+ logger = logging.getLogger(__name__)
28
+
29
+
30
+ def before_task_publish_callback(headers=None, properties=None, **kwargs):
31
+ if "scout_task_start" not in headers:
32
+ headers["scout_task_start"] = datetime_to_timestamp(
33
+ dt.datetime.now(dt.timezone.utc)
34
+ )
35
+
36
+
37
+ def task_prerun_callback(task=None, **kwargs):
38
+ tracked_request = TrackedRequest.instance()
39
+ tracked_request.is_real_request = True
40
+
41
+ start_time_header = getattr(task.request, "scout_task_start", None)
42
+ track_job_queue_time(start_time_header, tracked_request)
43
+
44
+ task_id = getattr(task.request, "id", None)
45
+ if task_id:
46
+ tracked_request.tag("task_id", task_id)
47
+ parent_task_id = getattr(task.request, "parent_id", None)
48
+ if parent_task_id:
49
+ tracked_request.tag("parent_task_id", parent_task_id)
50
+
51
+ delivery_info = getattr(task.request, "delivery_info", None)
52
+ if delivery_info:
53
+ tracked_request.tag("is_eager", delivery_info.get("is_eager", False))
54
+ tracked_request.tag("exchange", delivery_info.get("exchange", "unknown"))
55
+ tracked_request.tag("priority", delivery_info.get("priority", "unknown"))
56
+ tracked_request.tag("routing_key", delivery_info.get("routing_key", "unknown"))
57
+ tracked_request.tag("queue", delivery_info.get("queue", "unknown"))
58
+
59
+ operation = "Job/" + task.name
60
+ tracked_request.start_span(operation=operation)
61
+ tracked_request.operation = operation
62
+
63
+
64
+ def task_postrun_callback(task=None, **kwargs):
65
+ tracked_request = TrackedRequest.instance()
66
+ tracked_request.stop_span()
67
+
68
+
69
+ def task_failure_callback(
70
+ sender,
71
+ task_id=None,
72
+ exception=None,
73
+ args=None,
74
+ kwargs=None,
75
+ traceback=None,
76
+ einfo=None,
77
+ **remaining,
78
+ ):
79
+ tracked_request = TrackedRequest.instance()
80
+ tracked_request.tag("error", "true")
81
+
82
+ custom_controller = sender.name
83
+ custom_params = {
84
+ "celery": {
85
+ "task_id": task_id,
86
+ "args": args,
87
+ "kwargs": kwargs,
88
+ }
89
+ }
90
+
91
+ # Look up the django settings if populated.
92
+ environment = None
93
+ if get_safe_settings:
94
+ try:
95
+ environment = get_safe_settings()
96
+ except django.core.exceptions.ImproperlyConfigured as exc:
97
+ # Django not setup correctly
98
+ logger.debug(
99
+ "Celery integration does not have django configured properly: %r", exc
100
+ )
101
+ pass
102
+ except Exception as exc:
103
+ logger.debug(
104
+ "Celery task_failure callback exception: %r", exc, exc_info=exc
105
+ )
106
+ pass
107
+
108
+ # Celery occassionally will send the traceback as a string rather
109
+ # than a Stack trace object as the docs indicate. In that case,
110
+ # fall back to the billiard ExceptionInfo instance
111
+ traceback = traceback if traceback and not isinstance(traceback, str) else einfo.tb
112
+ exc_info = (exception.__class__, exception, traceback)
113
+ ErrorMonitor.send(
114
+ exc_info,
115
+ environment=environment,
116
+ custom_params=custom_params,
117
+ custom_controller=custom_controller,
118
+ )
119
+
120
+
121
+ def install(app=None):
122
+ if app is not None:
123
+ copy_configuration(app)
124
+
125
+ installed = scout_apm.core.install()
126
+ if not installed:
127
+ return
128
+
129
+ before_task_publish.connect(before_task_publish_callback)
130
+ task_prerun.connect(task_prerun_callback)
131
+ task_failure.connect(task_failure_callback)
132
+ task_postrun.connect(task_postrun_callback)
133
+
134
+
135
+ def copy_configuration(app):
136
+ prefix = "scout_"
137
+ prefix_len = len(prefix)
138
+
139
+ to_set = {}
140
+ for key, value in app.conf.items():
141
+ key_lower = key.lower()
142
+ if key_lower.startswith(prefix) and len(key_lower) > prefix_len:
143
+ scout_key = key_lower[prefix_len:]
144
+ to_set[scout_key] = value
145
+
146
+ scout_config.set(**to_set)
147
+
148
+
149
+ def uninstall():
150
+ before_task_publish.disconnect(before_task_publish_callback)
151
+ task_prerun.disconnect(task_prerun_callback)
152
+ task_postrun.disconnect(task_postrun_callback)
153
+ task_failure.disconnect(task_failure_callback)
scout_apm/compat.py ADDED
@@ -0,0 +1,104 @@
1
+ # coding=utf-8
2
+
3
+ import datetime as dt
4
+ import gzip
5
+ import inspect
6
+ import queue
7
+ from contextlib import ContextDecorator
8
+ from functools import wraps
9
+ from html import escape
10
+ from urllib.parse import parse_qsl, urlencode, urljoin
11
+
12
+ import certifi
13
+ import urllib3
14
+
15
+
16
+ def iteritems(dictionary):
17
+ return dictionary.items()
18
+
19
+
20
+ # datetime_to_timestamp converts a naive UTC datetime to a unix timestamp
21
+ def datetime_to_timestamp(datetime_obj):
22
+ return datetime_obj.replace(tzinfo=dt.timezone.utc).timestamp()
23
+
24
+
25
+ def text(value, encoding="utf-8", errors="strict"):
26
+ """
27
+ Convert a value to str on Python 3 and unicode on Python 2.
28
+ """
29
+ if isinstance(value, str):
30
+ return value
31
+ elif isinstance(value, bytes):
32
+ return str(value, encoding, errors)
33
+ else:
34
+ return str(value)
35
+
36
+
37
+ def get_pos_args(func):
38
+ return inspect.getfullargspec(func).args
39
+
40
+
41
+ def unwrap_decorators(func):
42
+ unwrapped = func
43
+ while True:
44
+ # N.B. only some decorators set __wrapped__ on Python 2.7
45
+ try:
46
+ unwrapped = unwrapped.__wrapped__
47
+ except AttributeError:
48
+ break
49
+ return unwrapped
50
+
51
+
52
+ def kwargs_only(func):
53
+ """
54
+ Source: https://pypi.org/project/kwargs-only/
55
+ Make a function only accept keyword arguments.
56
+ This can be dropped in Python 3 in lieu of:
57
+ def foo(*, bar=default):
58
+ Source: https://pypi.org/project/kwargs-only/
59
+ """
60
+ if hasattr(inspect, "signature"): # pragma: no cover
61
+ # Python 3
62
+ signature = inspect.signature(func)
63
+ arg_names = list(signature.parameters.keys())
64
+ else: # pragma: no cover
65
+ # Python 2
66
+ signature = inspect.getargspec(func)
67
+ arg_names = signature.args
68
+
69
+ if len(arg_names) > 0 and arg_names[0] in ("self", "cls"):
70
+ allowable_args = 1
71
+ else:
72
+ allowable_args = 0
73
+
74
+ @wraps(func)
75
+ def wrapper(*args, **kwargs):
76
+ if len(args) > allowable_args:
77
+ raise TypeError(
78
+ "{} should only be called with keyword args".format(func.__name__)
79
+ )
80
+ return func(*args, **kwargs)
81
+
82
+ return wrapper
83
+
84
+
85
+ def urllib3_cert_pool_manager(**kwargs):
86
+ return urllib3.PoolManager(cert_reqs="CERT_REQUIRED", ca_certs=certifi.where())
87
+
88
+
89
+ def gzip_compress(data):
90
+ return gzip.compress(data)
91
+
92
+
93
+ __all__ = [
94
+ "ContextDecorator",
95
+ "datetime_to_timestamp",
96
+ "escape",
97
+ "gzip_compress",
98
+ "kwargs_only",
99
+ "parse_qsl",
100
+ "queue",
101
+ "text",
102
+ "urlencode",
103
+ "urljoin",
104
+ ]