diff options
| author | alexv-smirnov <[email protected]> | 2023-12-01 12:02:50 +0300 |
|---|---|---|
| committer | alexv-smirnov <[email protected]> | 2023-12-01 13:28:10 +0300 |
| commit | 0e578a4c44d4abd539d9838347b9ebafaca41dfb (patch) | |
| tree | a0c1969c37f818c830ebeff9c077eacf30be6ef8 /contrib/python/pytest-localserver/py3/pytest_localserver | |
| parent | 84f2d3d4cc985e63217cff149bd2e6d67ae6fe22 (diff) | |
Change "ya.make"
Diffstat (limited to 'contrib/python/pytest-localserver/py3/pytest_localserver')
7 files changed, 700 insertions, 0 deletions
diff --git a/contrib/python/pytest-localserver/py3/pytest_localserver/__init__.py b/contrib/python/pytest-localserver/py3/pytest_localserver/__init__.py new file mode 100644 index 00000000000..482058b8229 --- /dev/null +++ b/contrib/python/pytest-localserver/py3/pytest_localserver/__init__.py @@ -0,0 +1 @@ +from pytest_localserver._version import version as VERSION # noqa diff --git a/contrib/python/pytest-localserver/py3/pytest_localserver/_version.py b/contrib/python/pytest-localserver/py3/pytest_localserver/_version.py new file mode 100644 index 00000000000..044524b1665 --- /dev/null +++ b/contrib/python/pytest-localserver/py3/pytest_localserver/_version.py @@ -0,0 +1,16 @@ +# file generated by setuptools_scm +# don't change, don't track in version control +TYPE_CHECKING = False +if TYPE_CHECKING: + from typing import Tuple, Union + VERSION_TUPLE = Tuple[Union[int, str], ...] +else: + VERSION_TUPLE = object + +version: str +__version__: str +__version_tuple__: VERSION_TUPLE +version_tuple: VERSION_TUPLE + +__version__ = version = '0.8.1' +__version_tuple__ = version_tuple = (0, 8, 1) diff --git a/contrib/python/pytest-localserver/py3/pytest_localserver/http.py b/contrib/python/pytest-localserver/py3/pytest_localserver/http.py new file mode 100644 index 00000000000..0899597f5e8 --- /dev/null +++ b/contrib/python/pytest-localserver/py3/pytest_localserver/http.py @@ -0,0 +1,183 @@ +# Copyright (C) 2010-2013 Sebastian Rahlf and others (see AUTHORS). +# +# This program is release under the MIT license. You can find the full text of +# the license in the LICENSE file. +import enum +import itertools +import json +import sys +import threading + +from werkzeug.datastructures import Headers +from werkzeug.serving import make_server +from werkzeug.wrappers import Request +from werkzeug.wrappers import Response + + +class WSGIServer(threading.Thread): + + """ + HTTP server running a WSGI application in its own thread. + """ + + def __init__(self, host="127.0.0.1", port=0, application=None, **kwargs): + self.app = application + self._server = make_server(host, port, self.app, **kwargs) + self.server_address = self._server.server_address + + super().__init__(name=self.__class__, target=self._server.serve_forever) + + def __del__(self): + self.stop() + + def stop(self): + try: + server = self._server + except AttributeError: + pass + else: + server.shutdown() + + @property + def url(self): + host, port = self.server_address + proto = "http" if self._server.ssl_context is None else "https" + return "%s://%s:%i" % (proto, host, port) + + +class Chunked(enum.Enum): + NO = False + YES = True + AUTO = None + + def __bool__(self): + return bool(self.value) + + +def _encode_chunk(chunk, charset): + if isinstance(chunk, str): + chunk = chunk.encode(charset) + return "{:x}".format(len(chunk)).encode(charset) + b"\r\n" + chunk + b"\r\n" + + +class ContentServer(WSGIServer): + + """ + Small test server which can be taught which content (i.e. string) to serve + with which response code. Try the following snippet for testing API calls:: + + server = ContentServer(port=8080) + server.start() + print 'Test server running at http://%s:%i' % server.server_address + + # any request to http://localhost:8080 will get a 503 response. + server.content = 'Hello World!' + server.code = 503 + + # ... + + # we're done + server.stop() + + """ + + def __init__(self, host="127.0.0.1", port=0, ssl_context=None): + super().__init__(host, port, self, ssl_context=ssl_context) + self.content, self.code = ("", 204) # HTTP 204: No Content + self.headers = {} + self.show_post_vars = False + self.compress = None + self.requests = [] + self.chunked = Chunked.NO + + def __call__(self, environ, start_response): + """ + This is the WSGI application. + """ + request = Request(environ) + self.requests.append(request) + if ( + request.content_type == "application/x-www-form-urlencoded" + and request.method == "POST" + and self.show_post_vars + ): + content = json.dumps(request.form) + else: + content = self.content + + if self.chunked == Chunked.YES or ( + self.chunked == Chunked.AUTO and "chunked" in self.headers.get("Transfer-encoding", "") + ): + # If the code below ever changes to allow setting the charset of + # the Response object, the charset used here should also be changed + # to match. But until that happens, use UTF-8 since it is Werkzeug's + # default. + charset = "utf-8" + if isinstance(content, (str, bytes)): + content = (_encode_chunk(content, charset), "0\r\n\r\n") + else: + content = itertools.chain((_encode_chunk(item, charset) for item in content), ["0\r\n\r\n"]) + + response = Response(response=content, status=self.code) + response.headers.clear() + response.headers.extend(self.headers) + + # FIXME get compression working! + # if self.compress == 'gzip': + # content = gzip.compress(content.encode('utf-8')) + # response.content_encoding = 'gzip' + + return response(environ, start_response) + + def serve_content(self, content, code=200, headers=None, chunked=Chunked.NO): + """ + Serves string content (with specified HTTP error code) as response to + all subsequent request. + + :param content: content to be displayed + :param code: HTTP status code + :param headers: HTTP headers to be returned + :param chunked: whether to apply chunked transfer encoding to the content + """ + if not isinstance(content, (str, bytes, list, tuple)): + # If content is an iterable which is not known to be a string, + # bytes, or sequence, it might be something that can only be iterated + # through once, in which case we need to cache it so it can be reused + # to handle multiple requests. + try: + content = tuple(iter(content)) + except TypeError: + # this probably means that content is not iterable, so just go + # ahead in case it's some type that Response knows how to handle + pass + self.content = content + self.code = code + self.chunked = chunked + if headers: + self.headers = Headers(headers) + + +if __name__ == "__main__": # pragma: no cover + import os.path + import time + + app = ContentServer() + server = WSGIServer(application=app) + server.start() + + print("HTTP server is running at %s" % server.url) + print("Type <Ctrl-C> to stop") + + try: + path = sys.argv[1] + except IndexError: + path = os.path.join(os.path.dirname(os.path.abspath(__file__)), "..", "README.rst") + + app.serve_content(open(path).read(), 302) + + try: + while True: + time.sleep(1) + except KeyboardInterrupt: + print("\rstopping...") + server.stop() diff --git a/contrib/python/pytest-localserver/py3/pytest_localserver/https.py b/contrib/python/pytest-localserver/py3/pytest_localserver/https.py new file mode 100644 index 00000000000..856e222ac5c --- /dev/null +++ b/contrib/python/pytest-localserver/py3/pytest_localserver/https.py @@ -0,0 +1,149 @@ +# Copyright (C) 2010-2013 Sebastian Rahlf and others (see AUTHORS). +# +# This program is release under the MIT license. You can find the full text of +# the license in the LICENSE file. +import os.path + +from pytest_localserver.http import ContentServer + +#: default server certificate +DEFAULT_CERTIFICATE = os.path.join(os.getcwd(), "server.pem") + + +class SecureContentServer(ContentServer): + + """ + Small test server which works just like :class:`http.Server` over HTTP:: + + server = SecureContentServer( + port=8080, key='/srv/my.key', cert='my.certificate') + server.start() + print 'Test server running at %s' % server.url + server.serve_content(open('/path/to/some.file').read()) + # any call to https://localhost:8080 will get the contents of + # /path/to/some.file as a response. + + To avoid *ssl handshake failures* you can import the `pytest-localserver + CA`_ into your browser of choice. + + How to create a self-signed certificate + --------------------------------------- + + If you want to create your own server certificate, you need `OpenSSL`_ + installed on your machine. A self-signed certificate consists of a + certificate and a private key for your server. It can be created with + a command like this, using OpenSSL 1.1.1:: + + openssl req \ + -x509 \ + -newkey rsa:4096 \ + -sha256 \ + -days 3650 \ + -nodes \ + -keyout server.pem \ + -out server.pem \ + -subj "/CN=127.0.0.1/O=pytest-localserver/OU=Testing Dept." \ + -addext "subjectAltName=DNS:localhost" + + Note that both key and certificate are in a single file now named + ``server.pem``. + + How to create your own Certificate Authority + -------------------------------------------- + + Generate a server key and request for signing (csr). Make sure that the + common name (CN) is your IP address/domain name (e.g. ``localhost``). :: + + openssl genpkey \ + -algorithm RSA \ + -pkeyopt rsa_keygen_bits:4096 \ + -out server.key + openssl req \ + -new \ + -addext "subjectAltName=DNS:localhost" \ + -key server.key \ + -out server.csr + + Generate your own CA. Make sure that this time the CN is *not* your IP + address/domain name (e.g. ``localhost CA``). :: + + openssl genpkey \ + -algorithm RSA \ + -pkeyopt rsa_keygen_bits:4096 \ + -aes256 \ + -out ca.key + openssl req \ + -new \ + -x509 \ + -key ca.key \ + -out ca.crt + + Sign the certificate signing request (csr) with the self-created CA that + you made earlier. Note that OpenSSL does not copy the subjectAltName field + from the request (csr), so you have to provide it again as a file. If you + issue subsequent certificates and your browser already knows about previous + ones simply increment the serial number. :: + + echo "subjectAltName=DNS:localhost" >server-extensions.txt + openssl x509 -req -in server.csr -CA ca.crt -CAkey ca.key \ + -set_serial 01 -extfile server-extensions.txt -out server.crt + + Create a single file for both key and certificate:: + + cat server.key server.crt > server.pem + + Now you only need to import ``ca.crt`` as a CA in your browser. + + Want to know more? + ------------------ + + This information was compiled from the following sources, which you might + find helpful if you want to dig deeper into `pyOpenSSH`_, certificates and + CAs: + + - http://code.activestate.com/recipes/442473/ + - http://www.tc.umn.edu/~brams006/selfsign.html + - + + A more advanced tutorial can be found `here`_. + + .. _pytest-localserver CA: https://raw.githubusercontent.com/pytest-dev/pytest-localserver/master/pytest_localserver/ca.crt # noqa: E501 + .. _pyOpenSSH: https://launchpad.net/pyopenssl + """ + + def __init__(self, host="localhost", port=0, key=DEFAULT_CERTIFICATE, cert=DEFAULT_CERTIFICATE): + """ + :param key: location of file containing the server private key. + :param cert: location of file containing server certificate. + """ + + super().__init__(host, port, ssl_context=(key, cert)) + + +if __name__ == "__main__": # pragma: no cover + + import sys + import time + + print("Using certificate %s." % DEFAULT_CERTIFICATE) + + server = SecureContentServer() + server.start() + server.logging = True + + print("HTTPS server is running at %s" % server.url) + print("Type <Ctrl-C> to stop") + + try: + path = sys.argv[1] + except IndexError: + path = os.path.join(os.path.dirname(os.path.abspath(__file__)), "..", "README.rst") + + server.serve_content(open(path).read(), 302) + + try: + while True: + time.sleep(1) + except KeyboardInterrupt: + print("\rstopping...") + server.stop() diff --git a/contrib/python/pytest-localserver/py3/pytest_localserver/plugin.py b/contrib/python/pytest-localserver/py3/pytest_localserver/plugin.py new file mode 100644 index 00000000000..1e6ad2f1720 --- /dev/null +++ b/contrib/python/pytest-localserver/py3/pytest_localserver/plugin.py @@ -0,0 +1,90 @@ +# Copyright (C) 2011 Sebastian Rahlf <basti at redtoad dot de> +# +# This program is release under the MIT license. You can find the full text of +# the license in the LICENSE file. +import os +import pkgutil + +import pytest + + +def httpserver(request): + """The returned ``httpserver`` provides a threaded HTTP server instance + running on a randomly assigned port on localhost. It can be taught which + content (i.e. string) to serve with which response code and comes with + following attributes: + + * ``code`` - HTTP response code (int) + * ``content`` - content of next response (str) + * ``headers`` - response headers (dict) + + Once these attribute are set, all subsequent requests will be answered with + these values until they are changed or the server is stopped. A more + convenient way to change these is :: + + httpserver.serve_content( + content='My content', code=200, + headers={'content-type': 'text/plain'}) + + The server address can be found in property + + * ``url`` + + which is the string representation of tuple ``server_address`` (host as + str, port as int). + + Example:: + + import requests + def scrape(url): + html = requests.get(url).text + # some parsing happens here + # ... + return result + + def test_retrieve_some_content(httpserver): + httpserver.serve_content(open('cached-content.html').read()) + assert scrape(httpserver.url) == 'Found it!' + + """ + from pytest_localserver import http + + server = http.ContentServer() + server.start() + request.addfinalizer(server.stop) + return server + + +def httpsserver(request): + """The returned ``httpsserver`` (note the additional S!) provides a + threaded HTTP server instance similar to funcarg ``httpserver`` but with + SSL encryption. + """ + from pytest_localserver import https + try: + with open(https.DEFAULT_CERTIFICATE, 'wb') as f: + f.write(pkgutil.get_data('pytest_localserver', 'server.pem')) + server = https.SecureContentServer() + server.start() + request.addfinalizer(server.stop) + yield server + finally: + os.remove(https.DEFAULT_CERTIFICATE) + + +def smtpserver(request): + """The returned ``smtpserver`` provides a threaded instance of + ``smtpd.SMTPServer`` running on localhost. It has the following + attributes: + + * ``addr`` - server address as tuple (host as str, port as int) + """ + from pytest_localserver import smtp + + server = smtp.Server() + server.start() + request.addfinalizer(server.stop) + return server diff --git a/contrib/python/pytest-localserver/py3/pytest_localserver/server.pem b/contrib/python/pytest-localserver/py3/pytest_localserver/server.pem new file mode 100644 index 00000000000..4f7f1ed3227 --- /dev/null +++ b/contrib/python/pytest-localserver/py3/pytest_localserver/server.pem @@ -0,0 +1,84 @@ +-----BEGIN PRIVATE KEY----- +MIIJRAIBADANBgkqhkiG9w0BAQEFAASCCS4wggkqAgEAAoICAQC31RlRDMN6eGpQ +zZaqtgk7/c5h98PoPNRpFFoUuuWGdf5PlHv4fMym7Zmz2ljx1DqutKhIIUKqS1vh +xd5zMFpheOUTVPVfQc5evgTIm1GF0rMSSaQSFPVOX3nNXGmF1/Jq9YWTc/rb2ns/ +s+Ip1zSKBqDsdRbrkvpSa7cyCkxYcuYtYo5jRa930Fbn4cNj+aA3dxGXd4bLLfnR +BpRA0V5SzBv93MtOK9kngQwQhjBJC/L/acHPO5dzQISBhM9NTSCAH4zm0SlTiExK +DhBdExSbdjAjnJ3k82hNFLUqY1JAm4yVlvwD3vNY4hkf/gWzuQeJIhzK8kE4A+dD +8BZzdHroK9xnnpmSlS7/P0raQd3VZPc8swEDyw9MrdA5UU95b07sUVs0LM0vWhi+ +rwNAJHfiQ77twc0bP7niyy/Kg+UYf7m0i/nyvJFKq75rHOfZvmsPNs+gOdJff+yy +4vv9pmImj2nulgOgrGrzc4ICnx3GpoKmGFDq/p+hqk99P92dFHmwd7c2bYHQNpC9 +BJh8VzrVuyndX2mL5P+/LfmEi8tI06Imykzqtk/UODLJks7ZIrJfYlYmm7aVdrvO +1U2s10AfloCX/ZVO7u3k4lH7Stj+/C8Ap+5Cm4Q46sZGO0Z5b808p4ETcoAI/AAl +OwpHAMi9ueLqJ7J0ykCDl/LrTyNqrQIDAQABAoICACObXRn71Okl5cHc8HAEbml2 +UcFcElAraCEqVgBp6wdOV4HmitSop6M1pm3VvyCoMO2iBG5kMtt1WUiz4NCC7x6u +IgDKlfRrdKOZPqf0nafEFfdW2DbAZHtXtun2GmJYX5YkFElpT4/CE9lU6FueWYja +m9TxIQ1kHKRWRNemcv820iq8SkQkPUaBzjN/4S6+LTBRGdEyz6MPNrIsCg87/n8f +FdToLWDo0Vj7f/C7bSLY86pRO77+Fem293N23AhnBgKLGemjXdPWNKCrdLPyfC1Y +iR58uYCdPPihKC4bqtTkzCg1ZH8DcjMnKCKwOz6CelkviFAu+D73UpYwLMkUKLH3 +p3meFBwa0oEzUUof+W9J5HPnVX6nGR2V4fXkejcJoOBHUaSsuRFiPS4XJMj++DI7 +uiMOt7QljqCKirmCp8tVQ5raT9zwFgNCsR3+gemD1KC3zlXixGs1DyI4x2YwTgKU +c16vnh9fGS9zq/drxqbeMvVbyVZF98LjJfgPxcmyEAXVH46Rs3/KSr6ve6MpRk9G +3vLd7BVfEXoGA1Sha7PRg9OaKBgODfkDRsyZJqqkqHurE4P+8NQZ3mhzdGa4Prj9 +er5BrE3gmvagtQUJf0n+E6HRHGCFoq4i+jOeBw8qiwxgWV0ITfhneQDJF8JvBrzJ +IByC9fVUYB4R4wESRoOBAoIBAQDgJz1etpf/47PwM5A5bdqJEEMaT6d0ycI7QtFQ +1L3PgdRmu1ag2pCYogPQx1zrkMUe4qX0K1h372toRUz0RvoQFYF6qVLJNK/hOd+O +GQ/Rw3XuCvCCs+6QbeQNqUjhLeYf3+TH4IEbIvukIACnDlHvAhtu+IQR7sVwCXda +Slu1zW0ya7Pa/pPnEOQpA9D758/GjcZpe44hCBq+EnrV40Q3jHXEVtsDexq1ubzz +BZEVLr4iwVrEjELZo4pbT+wQx2waRFqTVej5RaQadnSMCdRC0LCTa+t7hfuzN+KN +DBoSUeOlcQ88TyEGvcZXo0jAyDBdN5HC38ujZlkqHHCZVEGxAoIBAQDR81WeHMYW +/vtUhrP3BaJMj3RL/Vmpujac/i9IjdxrP2bi9mweunkZBH9UHNPcJp2b/+uAdSJO +aQRzghCM+DmuOIuu4rB9FU6qpXGhcag126iu328eSYS1sJg5CVGs9ZhaxKk5xbro +1cV0uUS6Gxl2z1Kpsb2dy/zhPTSwf6nrKXYwfrM65+EURz0fniKGfgxs6+p+uTVS +kkLMe2nusJ1KLGrXqfJfa25sQKo1zaRFHLDd0/pgchijvVkhXDY3A7913i+xbQZu +KIfbGp0pH4XFUJn1AR4XqPpE+wmHiLeqEmFJ5xcDl4q3j2dGnO3mHUYYNFOxh1nt +1MCDCCbKJVu9AoIBAQCOUtv4o19nrqC1x0ev7zxvAtBYiHL/CIw3LHnTJQFQHFNM +125tu9lL0LMzgSJSwB0pOye8HTmTDYXZMwdloxtr0vvfcluKPdXe3+w+QVN2EPF0 +L6X+l1jGg7/lnLMVpxsS6gpNjxLqtA+ralZ/u+vyIhhhIZJaAI2EUb5iqgwJJ2JK +PXB5gGNQt7zm/fFXwRyAKcztdPINryOrw/gSjrblvl2YSL3PO/79m+2JMOOp24AG +eVa0rYpUvi4/REPTc4wEMZqBKm8+tyU3WDcwI52Ovwsez8s5Jx1l8fn7LM/xCeXN +SjguRt/lc+HYC2lKXtG2nm4Cmi6mlXnP7zbfZExBAoIBAQC1xAkU+W5ajGjFllWK +gJsx02TpQS92bVxI8Ru4ofD5/QszZgrXU7Px/93I0ahuShRb8eZO8ZpA7lTHOAzi +Lymo9xWf1Gzd7iuMO+4zyrXJ4yGYPKL0QswdjQVNJA9NQdekhezIsrKOUD1CQAAL +a9jQ7s9vUQ2L5wZJbvcF85EFooDLnXXIguZv6vk1PXBApjJVvq3nBqvuj+g7JoHg +/5E9nVTm4CCRke4o1JdIO4CDwUIy2wpCo6VHZXAcHLxnRtxkzHbYEj7l8jska1cz +OjJTUOPppQ0LiOUcAYcPi0MPgBgwplxbZMDZCNNt5AFnH2MHI45t/XPTH0WIa+9B +RbS1AoIBAQCqZri7tm9ngZfvKNNvdVTgBcKukDFek4f7ar0bOkISALtNrn1xXIID +1ggELNy9afTmzPlttqMVQIxSTL3p7LIkZzTsuK0uthbsyzXxLHw2m+oHgaYut7he +j2v7qTmaw7rgpTiORTDg00+5HDtdMmp3Km4aurNasPA80i8Z2ElI20i50LlQ4K5Q +lIqpHR4fwrBr4SLStzvBo9UK1YYQ94FyKd7xou3uXLLTlY3G8rD6jjKJE2Gg8Ga/ +gGzbRCZWH6AOk1iO/CmOPH6AdFn5axXTx+uAML1Lr2VQ+azrYZCtIhKmW/kuQPQg +apeiobcSY1vsX7eM8mQkM8TxrDLyNjtl +-----END PRIVATE KEY----- +-----BEGIN CERTIFICATE----- +MIIFiTCCA3GgAwIBAgIUUpWEFJm0PzrYTkhLe05yIBhBMuowDQYJKoZIhvcNAQEL +BQAwSTESMBAGA1UEAwwJMTI3LjAuMC4xMRswGQYDVQQKDBJweXRlc3QtbG9jYWxz +ZXJ2ZXIxFjAUBgNVBAsMDVRlc3RpbmcgRGVwdC4wHhcNMjEwOTE0MDU1NzAxWhcN +MzEwOTEyMDU1NzAxWjBJMRIwEAYDVQQDDAkxMjcuMC4wLjExGzAZBgNVBAoMEnB5 +dGVzdC1sb2NhbHNlcnZlcjEWMBQGA1UECwwNVGVzdGluZyBEZXB0LjCCAiIwDQYJ +KoZIhvcNAQEBBQADggIPADCCAgoCggIBALfVGVEMw3p4alDNlqq2CTv9zmH3w+g8 +1GkUWhS65YZ1/k+Ue/h8zKbtmbPaWPHUOq60qEghQqpLW+HF3nMwWmF45RNU9V9B +zl6+BMibUYXSsxJJpBIU9U5fec1caYXX8mr1hZNz+tvaez+z4inXNIoGoOx1FuuS ++lJrtzIKTFhy5i1ijmNFr3fQVufhw2P5oDd3EZd3hsst+dEGlEDRXlLMG/3cy04r +2SeBDBCGMEkL8v9pwc87l3NAhIGEz01NIIAfjObRKVOITEoOEF0TFJt2MCOcneTz +aE0UtSpjUkCbjJWW/APe81jiGR/+BbO5B4kiHMryQTgD50PwFnN0eugr3GeemZKV +Lv8/StpB3dVk9zyzAQPLD0yt0DlRT3lvTuxRWzQszS9aGL6vA0Akd+JDvu3BzRs/ +ueLLL8qD5Rh/ubSL+fK8kUqrvmsc59m+aw82z6A50l9/7LLi+/2mYiaPae6WA6Cs +avNzggKfHcamgqYYUOr+n6GqT30/3Z0UebB3tzZtgdA2kL0EmHxXOtW7Kd1faYvk +/78t+YSLy0jToibKTOq2T9Q4MsmSztkisl9iViabtpV2u87VTazXQB+WgJf9lU7u +7eTiUftK2P78LwCn7kKbhDjqxkY7RnlvzTyngRNygAj8ACU7CkcAyL254uonsnTK +QIOX8utPI2qtAgMBAAGjaTBnMB0GA1UdDgQWBBRzl1iPBK4XZwChdNhdPHfjAb/z +BzAfBgNVHSMEGDAWgBRzl1iPBK4XZwChdNhdPHfjAb/zBzAPBgNVHRMBAf8EBTAD +AQH/MBQGA1UdEQQNMAuCCWxvY2FsaG9zdDANBgkqhkiG9w0BAQsFAAOCAgEATk+Q +t6psMrtGeFcZKYdmSFqW3SZUba4l76PzvHRf8nMcB1eFuZ4mCdiv0NgcQkE8c9T+ +i/J4wEmJ+mf1033MP1vQmrGqnaYBsVHNBTaTsP+gLg6Z7AGPvPaL2fwmWWNwTT0O +1352bdz9ORacKSXW3Pq0Vi1pTMho0kAya3VQpl2paqz8qSUG7ijyGQ46VXjgqNZ1 +P5lv+6CWa3AwEQo6Edv1x+HLesRWVqVAkxxhlaGOPQm1cDlpnI4rxuYIMlsb5cNZ +XTAIxw6Es1eqlPcZ96EoGXyIrG7Ej6Yb9447PrC1ulMnIu74cWLY25eu+oVr7Nvk +Gjp2I7qbVjz9Ful0o0M9Wps4RzCgrpO4WeirCK/jFIUpmXJdn7V4mX0h2ako+dal +vczg+bAd4ZedJWHTiqJs9lVMh4/YD7Ck6n+iAZ8Jusq6OhyTY43/Nyp2zQbwQmYv +y3V6JVX+vY4Cq8pR1i8x5FBHnOCMPoT4sbOjKuoFWVi9wH1d65Q1JOo6/0eYzfwJ +nuGUJza7+aCxYNlqxtqX0ItM670ClxB7fuWUpKh5WHrHD2dqBhYwtXOl9yBHrFOJ +O8toKk3PmtlMqVZ8QXmgSqEy7wkfxhjJLgi2AQsqeA6nDrCLtr2pWdqDWoUfxY8r +r5rc71nFLay/H2CbOYELI+20VFMp8GF3kOZbkRA= +-----END CERTIFICATE----- diff --git a/contrib/python/pytest-localserver/py3/pytest_localserver/smtp.py b/contrib/python/pytest-localserver/py3/pytest_localserver/smtp.py new file mode 100644 index 00000000000..82dbc394b98 --- /dev/null +++ b/contrib/python/pytest-localserver/py3/pytest_localserver/smtp.py @@ -0,0 +1,177 @@ +# Copyright (C) 2011 Sebastian Rahlf <basti at redtoad dot de> +# with some ideas from http://code.activestate.com/recipes/440690/ +# SmtpMailsink Copyright 2005 Aviarc Corporation +# Written by Adam Feuer, Matt Branthwaite, and Troy Frever +# which is Licensed under the PSF License +import email + +import aiosmtpd.controller + + +class MessageDetails: + def __init__(self, peer, mailfrom, rcpttos, *, mail_options=None, rcpt_options=None): + self.peer = peer + self.mailfrom = mailfrom + self.rcpttos = rcpttos + if mail_options: + self.mail_options = mail_options + if rcpt_options: + self.rcpt_options = rcpt_options + + +class Handler: + def __init__(self): + self.outbox = [] + + async def handle_DATA(self, server, session, envelope): + message = email.message_from_bytes(envelope.content) + message.details = MessageDetails(session.peer, envelope.mail_from, envelope.rcpt_tos) + self.outbox.append(message) + return "250 OK" + + +class Server(aiosmtpd.controller.Controller): + + """ + Small SMTP test server. + + This is little more than a wrapper around aiosmtpd.controller.Controller + which offers a slightly different interface for backward compatibility with + earlier versions of pytest-localserver. You can just as well use a standard + Controller and pass it a Handler instance. + + Here is how to use this class for sending an email, if you really need to:: + + server = Server(port=8080) + server.start() + print 'SMTP server is running on %s:%i' % server.addr + + # any e-mail sent to localhost:8080 will end up in server.outbox + # ... + + server.stop() + + """ + + def __init__(self, host="localhost", port=0): + try: + super().__init__(Handler(), hostname=host, port=port, server_hostname=host) + except TypeError: + # for aiosmtpd <1.3 + super().__init__(Handler(), hostname=host, port=port) + + @property + def outbox(self): + return self.handler.outbox + + def _set_server_socket_attributes(self): + """ + Set the addr and port attributes on this Server instance, if they're not + already set. + """ + + # I split this out into its own method to allow running this code in + # aiosmtpd <1.4, which doesn't have the _trigger_server() method on + # the Controller class. If I put it directly in _trigger_server(), it + # would fail when calling super()._trigger_server(). In the future, when + # we can safely require aiosmtpd >=1.4, this method can be inlined + # directly into _trigger_server(). + if hasattr(self, "addr"): + assert hasattr(self, "port") + return + + self.addr = self.server.sockets[0].getsockname()[:2] + + # Work around a bug/missing feature in aiosmtpd (https://github.com/aio-libs/aiosmtpd/issues/276) + if self.port == 0: + self.port = self.addr[1] + assert self.port != 0 + + def _trigger_server(self): + self._set_server_socket_attributes() + super()._trigger_server() + + def is_alive(self): + return self._thread is not None and self._thread.is_alive() + + @property + def accepting(self): + try: + return self.server.is_serving() + except AttributeError: + # asyncio.base_events.Server.is_serving() only exists in Python 3.6 + # and up. For Python 3.5, asyncio.base_events.BaseEventLoop.is_running() + # is a close approximation; it should mostly return the same value + # except for brief periods when the server is starting up or shutting + # down. Once we drop support for Python 3.5, this branch becomes + # unnecessary. + return self.loop.is_running() + + # for aiosmtpd <1.4 + if not hasattr(aiosmtpd.controller.Controller, "_trigger_server"): + + def start(self): + super().start() + self._set_server_socket_attributes() + + def stop(self, timeout=None): + """ + Stops test server. + :param timeout: When the timeout argument is present and not None, it + should be a floating point number specifying a timeout for the + operation in seconds (or fractions thereof). + """ + + # This mostly copies the implementation from Controller.stop(), with two + # differences: + # - It removes the assertion that the thread exists, allowing stop() to + # be called more than once safely + # - It passes the timeout argument to Thread.join() + if self.loop.is_running(): + try: + self.loop.call_soon_threadsafe(self.cancel_tasks) + except AttributeError: + # for aiosmtpd < 1.4.3 + self.loop.call_soon_threadsafe(self._stop) + if self._thread is not None: + self._thread.join(timeout) + self._thread = None + self._thread_exception = None + self._factory_invoked = None + self.server_coro = None + self.server = None + self.smtpd = None + + def __del__(self): + # This is just for backward compatibility, to preserve the behavior that + # the server is stopped when this object is finalized. But it seems + # sketchy to rely on this to stop the server. Typically, the server + # should be stopped "manually", before it gets deleted. + if self.is_alive(): + self.stop() + + def __repr__(self): # pragma: no cover + return "<smtp.Server %s:%s>" % self.addr + + +def main(): + import time + + server = Server() + server.start() + + print("SMTP server is running on %s:%i" % server.addr) + print("Type <Ctrl-C> to stop") + + try: + while True: + time.sleep(1) + except KeyboardInterrupt: + pass + finally: + print("\rstopping...") + server.stop() + + +if __name__ == "__main__": # pragma: no cover + main() |
