Hello community,

here is the log from the commit of package python-django-eremaea2 for 
openSUSE:Factory checked in at 2020-12-02 13:58:18
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Comparing /work/SRC/openSUSE:Factory/python-django-eremaea2 (Old)
 and      /work/SRC/openSUSE:Factory/.python-django-eremaea2.new.5913 (New)
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

Package is "python-django-eremaea2"

Wed Dec  2 13:58:18 2020 rev:2 rq:852354 version:2.0.16

Changes:
--------
--- 
/work/SRC/openSUSE:Factory/python-django-eremaea2/python-django-eremaea2.changes
    2020-09-01 20:11:13.328699211 +0200
+++ 
/work/SRC/openSUSE:Factory/.python-django-eremaea2.new.5913/python-django-eremaea2.changes
  2020-12-02 13:58:20.389808627 +0100
@@ -1,0 +2,6 @@
+Tue Dec  1 08:25:48 UTC 2020 - Matwey Kornilov <[email protected]>
+
+- Version 2.0.16
+  - Support HTTP Digest Auth in eremaeactl tool
+
+-------------------------------------------------------------------

Old:
----
  django-eremaea2-2.0.15.tar.gz

New:
----
  django-eremaea2-2.0.16.tar.gz

++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

Other differences:
------------------
++++++ python-django-eremaea2.spec ++++++
--- /var/tmp/diff_new_pack.rExDYu/_old  2020-12-02 13:58:20.937809205 +0100
+++ /var/tmp/diff_new_pack.rExDYu/_new  2020-12-02 13:58:20.937809205 +0100
@@ -20,7 +20,7 @@
 %{?!python_module:%define python_module() python-%{**} python3-%{**}}
 %define skip_python2 1
 Name:           python-django-eremaea2
-Version:        2.0.15
+Version:        2.0.16
 Release:        0
 Summary:        A simple Django application to store and show webcam snapshots
 License:        BSD-2-Clause
@@ -34,6 +34,8 @@
 BuildRequires:  %{python_module django-dj-inmemorystorage}
 BuildRequires:  %{python_module djangorestframework >= 3.7.0}
 BuildRequires:  %{python_module mock}
+BuildRequires:  %{python_module requests-mock}
+BuildRequires:  %{python_module requests-toolbelt}
 BuildRequires:  %{python_module requests}
 BuildRequires:  %{python_module setuptools}
 BuildRequires:  %{python_module six}
@@ -46,6 +48,7 @@
 Requires:       python-cmdln
 Requires:       python-djangorestframework >= 3.7.0
 Requires:       python-requests
+Requires:       python-requests-toolbelt
 Requires:       python-six
 Requires(post):   update-alternatives
 Requires(postun):  update-alternatives

++++++ django-eremaea2-2.0.15.tar.gz -> django-eremaea2-2.0.16.tar.gz ++++++
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/django-eremaea2-2.0.15/PKG-INFO 
new/django-eremaea2-2.0.16/PKG-INFO
--- old/django-eremaea2-2.0.15/PKG-INFO 2020-08-28 15:04:32.000000000 +0200
+++ new/django-eremaea2-2.0.16/PKG-INFO 2020-12-01 04:36:37.000000000 +0100
@@ -1,6 +1,6 @@
 Metadata-Version: 2.1
 Name: django-eremaea2
-Version: 2.0.15
+Version: 2.0.16
 Summary: A simple Django application to store and show webcam snapshots
 Home-page: https://github.com/matwey/django-eremaea2
 Author: Matwey V. Kornilov
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' 
old/django-eremaea2-2.0.15/django_eremaea2.egg-info/PKG-INFO 
new/django-eremaea2-2.0.16/django_eremaea2.egg-info/PKG-INFO
--- old/django-eremaea2-2.0.15/django_eremaea2.egg-info/PKG-INFO        
2020-08-28 15:04:32.000000000 +0200
+++ new/django-eremaea2-2.0.16/django_eremaea2.egg-info/PKG-INFO        
2020-12-01 04:36:37.000000000 +0100
@@ -1,6 +1,6 @@
 Metadata-Version: 2.1
 Name: django-eremaea2
-Version: 2.0.15
+Version: 2.0.16
 Summary: A simple Django application to store and show webcam snapshots
 Home-page: https://github.com/matwey/django-eremaea2
 Author: Matwey V. Kornilov
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/django-eremaea2-2.0.15/eremaea/ctl/client.py 
new/django-eremaea2-2.0.16/eremaea/ctl/client.py
--- old/django-eremaea2-2.0.15/eremaea/ctl/client.py    2020-08-28 
15:04:08.000000000 +0200
+++ new/django-eremaea2-2.0.16/eremaea/ctl/client.py    2020-12-01 
04:36:06.000000000 +0100
@@ -1,38 +1,37 @@
 import requests
 
 class Client(object):
-       def __init__(self, api, **kwargs):
+       def __init__(self, api, token = None):
                self.api = api.rstrip('/')
-               self.kwargs = kwargs
-               self._common_headers = self._get_common_headers()
-       def _get_common_headers(self):
-               headers = {}
-               if 'token' in self.kwargs:
-                       headers['Authorization'] = 'Token ' + 
self.kwargs['token']
-               return headers
+               self._session = requests.Session()
+
+               if token is not None:
+                       self._session.headers.update({'Authorization': 'Token 
{}'.format(token)})
+
        def upload(self, file, collection, retention_policy = None):
                url = self.api + '/snapshots/'
-               headers = self._common_headers
-               headers['Content-Disposition'] = 'attachment; filename=' + 
file.name
-               headers['Content-Type'] = file.mimetype
+               headers = {
+                       'Content-Disposition': 'attachment; 
filename=\"{}\"'.format(file.name),
+                       'Content-Type': file.mimetype
+               }
                params = {'collection': collection}
                if retention_policy:
                        params['retention_policy'] = retention_policy
-               r = requests.post(url, params=params, headers=headers, 
data=file.read())
+               r = self._session.post(url, params=params, headers=headers, 
data=file.content)
                if r.status_code == 201:
                        return True
                r.raise_for_status()
+
        def purge(self, retention_policy):
                url = self.api + '/retention_policies/' + retention_policy + 
"/purge/"
-               headers = self._common_headers
-               r = requests.post(url, headers=headers)
+               r = self._session.post(url)
                if r.status_code == 201:
                        return True
                r.raise_for_status()
+
        def retention_policies(self):
                url = self.api + '/retention_policies/'
-               headers = self._common_headers
-               r = requests.get(url, headers=headers)
+               r = self._session.get(url)
                if r.status_code == 200:
                        return [x["name"] for x in r.json()]
                r.raise_for_status()
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/django-eremaea2-2.0.15/eremaea/ctl/commandline.py 
new/django-eremaea2-2.0.16/eremaea/ctl/commandline.py
--- old/django-eremaea2-2.0.15/eremaea/ctl/commandline.py       2020-08-28 
15:04:08.000000000 +0200
+++ new/django-eremaea2-2.0.16/eremaea/ctl/commandline.py       2020-12-01 
04:36:06.000000000 +0100
@@ -1,7 +1,7 @@
 import cmdln
 import time
 import sys
-from eremaea.ctl.file import FileFactory
+from eremaea.ctl.file import create_stream
 from eremaea.ctl.client import Client
 from django.utils.dateparse import parse_duration
 
@@ -31,7 +31,7 @@
                ${cmd_usage}
                ${cmd_option_list}
                """
-               self.client.upload(FileFactory().create(file), collection, 
opts.retention_policy)
+               self.client.upload(next(create_stream(file)), collection, 
opts.retention_policy)
        @cmdln.option("--all", dest="all", action="store_true", help="purge all 
retention policies")
        def do_purge(self, subcmd, opts, *retention_policies):
                """${cmd_name}: purge retention policies
@@ -55,10 +55,10 @@
                ${cmd_option_list}
                """
                duration = parse_duration(opts.interval)
-               filetype = FileFactory().resolve(file)
+               stream = create_stream(file)
                while True:
                        try:
-                               self.client.upload(filetype(file), collection, 
opts.retention_policy)
+                               self.client.upload(next(stream), collection, 
opts.retention_policy)
                        except Exception as e:
                                if not opts.quite:
                                        sys.stderr.write(str(e) + "\n")
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/django-eremaea2-2.0.15/eremaea/ctl/file.py 
new/django-eremaea2-2.0.16/eremaea/ctl/file.py
--- old/django-eremaea2-2.0.15/eremaea/ctl/file.py      2020-08-28 
15:04:08.000000000 +0200
+++ new/django-eremaea2-2.0.16/eremaea/ctl/file.py      2020-12-01 
04:36:06.000000000 +0100
@@ -1,68 +1,72 @@
+from collections import namedtuple
+from six import Iterator
 from six.moves.urllib.parse import urlparse
 import os.path
-import requests
 import mimetypes
+import requests
+from requests_toolbelt.auth.guess import GuessAuth
+
 
-class File(object):
-       def __init__(self, name, mimetype):
-               self.name = name
-               self.mimetype = mimetype
-       def read(self):
-               raise NotImplementedError("File.read")
-
-class LocalFile(File):
-       def __init__(self, path):
-               self._file = open(path, "rb")
-               (mimetype, encoding) = mimetypes.guess_type(path)
-               name = os.path.basename(path)
-               super(LocalFile, self).__init__(name, mimetype)
-       def read(self):
-               return self._file.read()
-
-class ContentFile(File):
-       def __init__(self, content, name, mimetype):
-               self.content = content
-               super(ContentFile, self).__init__(name, mimetype)
-       def read(self):
-               return self.content
+File = namedtuple("File", ("name", "mimetype", "content"))
 
-class HTTPFile(File):
+class Stream(Iterator):
        def __init__(self, url):
-               creds = None
+               self._url = url
+
+       def __iter__(self):
+               return self
+
+       def __next__(self):
+               raise NotImplementedError
+
+class LocalFileStream(Stream):
+       def __init__(self, url):
+               super(LocalFileStream, self).__init__(url)
+
+       def __next__(self):
+               with open(self._url, "rb") as f:
+                       (mimetype, _encoding) = mimetypes.guess_type(self._url)
+                       name = os.path.basename(self._url)
+                       return File(name, mimetype, f.read())
+
+class HTTPFileStream(Stream):
+       def __init__(self, url):
+               super(HTTPFileStream, self).__init__(url)
+
                parsed_url = urlparse(url)
+
+               self._name = [x for x in parsed_url.path.rsplit("/") if x][-1]
+               self._session = requests.Session()
+               self._session.allow_redirects = True
+
                if parsed_url.username is not None:
                        login = parsed_url.username
-                       passwd = parsed_url.password
-                       if parsed_url.password is None:
-                               passwd = ''
-                       creds = (login, passwd)
-               self.response = requests.get(url, allow_redirects=True, 
auth=creds)
-               self.response.raise_for_status()
-               name = [x for x in urlparse(url).path.rsplit("/") if x][-1]
-               mimetype = None
-               if 'content-type' in self.response.headers:
-                       mimetype = self.response.headers['content-type']
-               if not mimetype:
-                       (mimetype, encoding) = mimetypes.guess_type(name)
-               super(HTTPFile, self).__init__(name, mimetype)
-       def read(self):
-               return self.response.content
-
-class FileFactory(object):
-       def __init__(self):
-               self._scheme = {}
-               self._scheme[''] = LocalFile
-               self._scheme['http'] = HTTPFile
-               self._scheme['https'] = HTTPFile
-
-       def resolve(self, url):
-               scheme = urlparse(url).scheme
-               if scheme in self._scheme:
-                       return self._scheme[scheme]
-               return None
-       def create(self, url):
-               filetype = self.resolve(url)
-               if filetype:
-                       return filetype(url)
-               return None
+                       passwd = parsed_url.password if parsed_url.password is 
not None else ""
+                       self._session.auth = GuessAuth(login, passwd)
+
+       def __next__(self):
+               r = self._session.get(self._url)
+               r.raise_for_status()
+
+               mimetype = r.headers.get('content-type')
+               if mimetype is None:
+                       (mimetype, _encoding) = mimetypes.guess_type(self._name)
+
+               return File(self._name, mimetype, r.content)
+
+
+def create_stream(url):
+       streams = {
+               '': LocalFileStream,
+               'http': HTTPFileStream,
+               'https': HTTPFileStream,
+       }
+
+       parsed_url = urlparse(url)
+
+       stream_class = streams.get(parsed_url.scheme)
+
+       if stream_class is not None:
+               return stream_class(url)
 
+       return None
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/django-eremaea2-2.0.15/setup.py 
new/django-eremaea2-2.0.16/setup.py
--- old/django-eremaea2-2.0.15/setup.py 2020-08-28 15:04:08.000000000 +0200
+++ new/django-eremaea2-2.0.16/setup.py 2020-12-01 04:36:06.000000000 +0100
@@ -1,5 +1,4 @@
 import os
-import sys
 from setuptools import setup
 
 with open(os.path.join(os.path.dirname(__file__), 'README.md')) as readme:
@@ -10,7 +9,7 @@
 
 setup(
        name='django-eremaea2',
-       version='2.0.15',
+       version='2.0.16',
        packages=['eremaea','eremaea.ctl','eremaea.migrations'],
        entry_points={'console_scripts': [
                'eremaeactl = eremaea.ctl.commandline:execute_from_commandline',
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/django-eremaea2-2.0.15/tests/ctl/client.py 
new/django-eremaea2-2.0.16/tests/ctl/client.py
--- old/django-eremaea2-2.0.15/tests/ctl/client.py      2020-08-28 
15:04:08.000000000 +0200
+++ new/django-eremaea2-2.0.16/tests/ctl/client.py      2020-12-01 
04:36:06.000000000 +0100
@@ -1,11 +1,10 @@
 from datetime import datetime, timedelta
-from django.core.files.base import ContentFile as DjangoContentFile
+from django.core.files.base import ContentFile
 from django.contrib.auth.models import User
-from django.contrib.staticfiles.testing import StaticLiveServerTestCase
 from django.test import LiveServerTestCase
 from eremaea import models
 from eremaea.ctl.client import Client
-from eremaea.ctl.file import ContentFile
+from eremaea.ctl.file import File
 from rest_framework.authentication import TokenAuthentication
 from rest_framework.authtoken.models import Token
 from rest_framework.permissions import IsAuthenticatedOrReadOnly
@@ -17,19 +16,23 @@
        from mock import patch
 
 class ClientTest(LiveServerTestCase):
+       @property
+       def api_endpoint(self):
+               return self.live_server_url
+
        def setUp(self):
-               self.client = Client(self.live_server_url)
+               self.client = Client(self.api_endpoint)
 
        def test_upload1(self):
                content = b"123"
                retention_policy = 
models.RetentionPolicy.objects.create(name="hourly", 
duration=timedelta(hours=1))
                collection = models.Collection.objects.create(name="mycol", 
default_retention_policy=retention_policy)
-               
self.assertTrue(self.client.upload(ContentFile(content,"file.jpg","image/jpeg"),
 "mycol"))
+               
self.assertTrue(self.client.upload(File("file.jpg","image/jpeg",content), 
"mycol"))
                snapshot = models.Snapshot.objects.all()[0]
                self.assertEqual(snapshot.retention_policy, retention_policy)
                self.assertEqual(snapshot.file.read(), content)
        def test_purge1(self):
-               file = DjangoContentFile(b"123")
+               file = ContentFile(b"123")
                file.name = "file.jpg"
                retention_policy = 
models.RetentionPolicy.objects.create(name="hourly", 
duration=timedelta(hours=1))
                collection = models.Collection.objects.create(name="mycol", 
default_retention_policy=retention_policy)
@@ -51,4 +54,4 @@
        def setUp(self):
                user = User.objects.create(username="test")
                token = Token.objects.create(user=user)
-               self.client = Client(self.live_server_url, token = token.key)
+               self.client = Client(self.api_endpoint, token = token.key)
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/django-eremaea2-2.0.15/tests/ctl/file.py 
new/django-eremaea2-2.0.16/tests/ctl/file.py
--- old/django-eremaea2-2.0.15/tests/ctl/file.py        2020-08-28 
15:04:08.000000000 +0200
+++ new/django-eremaea2-2.0.16/tests/ctl/file.py        2020-12-01 
04:36:06.000000000 +0100
@@ -1,5 +1,9 @@
-from eremaea.ctl.file import ContentFile, LocalFile, HTTPFile, FileFactory
+from eremaea.ctl.file import File, create_stream, LocalFileStream, 
HTTPFileStream
 from unittest import TestCase
+import hashlib
+import re
+import requests
+import requests_mock
 try:
        from unittest.mock import patch, mock_open, create_autospec
 except ImportError:
@@ -7,47 +11,76 @@
 
 class FileTest(TestCase):
        def test_content_file1(self):
-               f = ContentFile("content", "text.txt", "text/plain")
+               f = File("text.txt", "text/plain", "content")
                self.assertEqual(f.name, "text.txt")
                self.assertEqual(f.mimetype, "text/plain")
-               self.assertEqual(f.read(), "content")
+               self.assertEqual(f.content, "content")
 
        @patch('eremaea.ctl.file.open', mock_open(read_data='hello_world'), 
create=True)
        def test_local_file1(self):
-               f = LocalFile("path/text.txt")
+               f = next(LocalFileStream("path/text.txt"))
                self.assertEqual(f.name, "text.txt")
                self.assertEqual(f.mimetype, "text/plain")
-               self.assertEqual(f.read(), "hello_world")
+               self.assertEqual(f.content, "hello_world")
 
-       @patch('requests.get')
+       def test_create_stream1(self):
+               self.assertIsInstance(create_stream("text.txt"), 
LocalFileStream)
+               self.assertIsInstance(create_stream("/root/text.txt"), 
LocalFileStream)
+               
self.assertIsInstance(create_stream("http://localhost/root/text.txt";), 
HTTPFileStream)
+               
self.assertIsInstance(create_stream("https://localhost/root/text.txt";), 
HTTPFileStream)
+
+       @requests_mock.Mocker()
        def test_http_file1(self, mock):
-               class ResponseMock(object):
-                       def raise_for_status(self):
-                               pass
-               mock.return_value = ResponseMock()
-               mock.return_value.headers = {}
-               mock.return_value.content = "hello_world"
-               f = HTTPFile("http://localhost/path/text.txt";)
+               mock.register_uri("GET", "http://localhost/path/text.txt";, 
content=b"hello_world")
+               f = next(HTTPFileStream("http://localhost/path/text.txt";))
                self.assertEqual(f.name, "text.txt")
                self.assertEqual(f.mimetype, "text/plain")
-               self.assertEqual(f.read(), "hello_world")
+               self.assertEqual(f.content, b"hello_world")
 
-       @patch('requests.get')
+       @requests_mock.Mocker()
        def test_http_file2(self, mock):
-               class ResponseMock(object):
-                       def raise_for_status(self):
-                               pass
-               mock.return_value = ResponseMock()
-               mock.return_value.headers = {"content-type":"application/json"}
-               mock.return_value.content = "hello_world"
-               f = HTTPFile("http://localhost/path/text.txt";)
+               mock.register_uri("GET", "http://localhost/path/text.txt";, 
content=b"hello_world", headers={'content-type': 'application/json'})
+               f = next(HTTPFileStream("http://localhost/path/text.txt";))
                self.assertEqual(f.name, "text.txt")
                self.assertEqual(f.mimetype, "application/json")
-               self.assertEqual(f.read(), "hello_world")
+               self.assertEqual(f.content, b"hello_world")
+
+       @requests_mock.Mocker()
+       def test_http_file_auth_basic1(self, mock):
+               mock.register_uri("GET", 
"http://test:pwd@localhost/path/text.txt";, [
+                       {"status_code": 401, "headers": {"www-authenticate": 
"basic realm=\"test\""}},
+                       {"status_code": 200, "content": b"hello_world"}
+               ])
+               f = 
next(HTTPFileStream("http://test:pwd@localhost/path/text.txt";))
+               
self.assertNotIn("authorization",mock.request_history[0].headers)
+               
self.assertEqual(mock.request_history[1].headers['authorization'], 'Basic 
dGVzdDpwd2Q=')
+               self.assertEqual(f.name, "text.txt")
+               self.assertEqual(f.mimetype, "text/plain")
+               self.assertEqual(f.content, b"hello_world")
 
-       def test_factory1(self):
-               f = FileFactory()
-               self.assertEqual(LocalFile, f.resolve("text.txt"))
-               self.assertEqual(LocalFile, f.resolve("/root/text.txt"))
-               self.assertEqual(HTTPFile, 
f.resolve("http://localhost/root/text.txt";))
-               self.assertEqual(HTTPFile, 
f.resolve("https://localhost/root/text.txt";))
+       @requests_mock.Mocker()
+       def test_http_file_auth_basic2(self, mock):
+               mock.register_uri("GET", 
"http://test:pwd@localhost/path/text.txt";, status_code=401, 
headers={"www-authenticate": "basic realm=\"test\""})
+               self.assertRaises(requests.exceptions.HTTPError, lambda: 
next(HTTPFileStream("http://test:pwd@localhost/path/text.txt";)))
+
+       @requests_mock.Mocker()
+       def test_http_file_auth_digest1(self, mock):
+               HA1 = hashlib.md5(b"test:test:pwd").hexdigest()
+               HA2 = hashlib.md5(b"GET:/path/text.txt").hexdigest()
+               expected = hashlib.md5(HA1.encode("ascii") + b":1234:" + 
HA2.encode("ascii")).hexdigest()
+               reg=re.compile(r'(\w+)[=] ?"?(\w+)"?')
+               mock.register_uri("GET", 
"http://test:pwd@localhost/path/text.txt";, [
+                       {"status_code": 401, "headers": {"www-authenticate": 
"digest realm=\"test\", nonce=\"1234\""}},
+                       {"status_code": 200, "content": b"hello_world"}
+               ])
+               f = 
next(HTTPFileStream("http://test:pwd@localhost/path/text.txt";))
+               
self.assertNotIn("authorization",mock.request_history[0].headers)
+               
self.assertDictEqual(dict(reg.findall(mock.request_history[1].headers['authorization'])),
 {
+                       'nonce': '1234',
+                       'realm': 'test',
+                       'response': expected,
+                       'username': 'test',
+               })
+               self.assertEqual(f.name, "text.txt")
+               self.assertEqual(f.mimetype, "text/plain")
+               self.assertEqual(f.content, b"hello_world")
_______________________________________________
openSUSE Commits mailing list -- [email protected]
To unsubscribe, email [email protected]
List Netiquette: https://en.opensuse.org/openSUSE:Mailing_list_netiquette
List Archives: 
https://lists.opensuse.org/archives/list/[email protected]

Reply via email to