Refactoring rsync_url according test failures

Change-Id: Ib815c322848024d6b0e945bf250988fe6d776fc5
diff --git a/trsync/objects/rsync_mirror.py b/trsync/objects/rsync_mirror.py
index 139f3f6..f7ab0ce 100644
--- a/trsync/objects/rsync_mirror.py
+++ b/trsync/objects/rsync_mirror.py
@@ -150,7 +150,7 @@
             self.url.a_dir(self.snapshot_dir),
             pattern=r'^{}-{}$'.format(
                 repo_name,
-                self.timestamp.snapshot_stamp_regexp
+                self.timestamp.snapshot_stamp_pattern
             )
         )
         links = self.ls_symlinks(self.url.a_dir())
diff --git a/trsync/objects/rsync_remote.py b/trsync/objects/rsync_remote.py
index 19cc01b..0ba4b0c 100644
--- a/trsync/objects/rsync_remote.py
+++ b/trsync/objects/rsync_remote.py
@@ -63,10 +63,10 @@
     def _rsync_ls(self, dirname=None, pattern=r'.*', opts=''):
         extra = '--no-v'
         out = self._rsync_push(dest=dirname, opts=opts, extra=extra)
-        regexp = re.compile(pattern)
+        pattern = re.compile(pattern)
         out = [_ for _ in out.splitlines()
                if (_.split()[-1] != '.') and
-               (regexp.match(_.split()[-1]) is not None)]
+               (pattern.match(_.split()[-1]) is not None)]
         return out
 
     def ls(self, dirname=None, pattern=r'.*'):
diff --git a/trsync/objects/rsync_url.py b/trsync/objects/rsync_url.py
index f86bd96..af15da0 100644
--- a/trsync/objects/rsync_url.py
+++ b/trsync/objects/rsync_url.py
@@ -1,4 +1,4 @@
-#-*- coding: utf-8 -*-
+# -*- coding: utf-8 -*-
 
 import os
 import re
@@ -20,174 +20,122 @@
 
         self._url = remote_url
         self._url_type = False
+        self._sep = '/'
 
-        self.regexps = {
+        self.pattern_tpls = {
+            'protocol': r'((?P<protocol>^[^/:]+)://)',
+            'user': r'((?P<user>[^@/:]+)@)',
+            'host': r'(?P<host>[^@:/]+)',
+            'port': r'(:(?P<port>[^@:/]+))',
+            'module': r'((?P<module>[^@:/]+)/?)',
+            'path': r'(?P<path>[^@:]*$)',
+        }
+
+        self.patterns = {
             # ssh: [USER@]HOST:SRC
             'ssh': re.compile(
-                r'^'
-                r'(?P<user>[-\w]+@)?'
-                r'(?P<host>[-\.\w]+){1}'
-                r':'
-                r'(?P<path>(~{0,1}[\w/-]*)){1}'
-                r'$'
+                '^{user}?{host}:(?!//){path}?$'
+                ''.format(**self.pattern_tpls)
             ),
             # rsync: [USER@]HOST::SRC
             'rsync1': re.compile(
-                r'^'
-                r'(?P<user>[-\w]+@)?'
-                r'(?P<host>[-\.\w]+){1}'
-                r'::'
-                r'(?P<module>[\w-]+){1}'
-                r'(?P<path>[\w/-]*)?'
-                r'$'
+                '^{user}?{host}(::(?!/){module}?){path}?$'
+                ''.format(**self.pattern_tpls)
             ),
             # rsync://[USER@]HOST[:PORT]/SRC
             'rsync2': re.compile(
-                r'^rsync://'
-                r'(?P<user>[-\w]+@)?'
-                r'(?P<host>[-\.\w]+){1}'
-                r'(?P<port>:[\d]+)?'
-                r'(?P<module>/[\w-]*)?'
-                r'(?P<path>[\w/-]*)?'
-                r'$'
+                '^{protocol}{user}?{host}{port}?(/{module})?{path}?$'
+                ''.format(**self.pattern_tpls)
             ),
             # local/path/to/directory
             'path': re.compile(
-                r'^'
-                r'(?P<path>(~{0,1}[\w/-]+)){1}'
-                r'$'
+                '^{path}$'
+                ''.format(**self.pattern_tpls)
             ),
         }
 
-        self._match = self._get_matching_regexp()
+        self.templates = {
+            # ssh: [USER@]HOST:SRC
+            'ssh': ('{user}@', '{host}:', '{path}'),
+            # rsync: [USER@]HOST::SRC
+            'rsync1': ('{user}@', '{host}::', '{module}', '/{path}'),
+            # rsync://[USER@]HOST[:PORT]/SRC
+            'rsync2': ('{protocol}://', '{user}@', '{host}', ':{port}',
+                       '/{module}', '/{path}'),
+            # local/path/to/directory
+            'path': ('{path}', ),
+        }
+
+        self._match = self._get_matching_pattern()
         if self.match is None:
-            self.user, self.host, self.module, self.port, self.path = \
-                None, None, None, None, None
-            self._root = self.url_dir()
+            self._parsed_url = utils.bunch()
+            self._parsed_url.protocol = None,
+            self._parsed_url.user = None,
+            self._parsed_url.host = None,
+            self._parsed_url.port = None,
+            self._parsed_url.module = None,
+            self._parsed_url.path = None,
         else:
             self._parse_rsync_url(self.match)
 
-    def _get_matching_regexp(self):
-        regexps = self._get_all_matching_regexps()
-        regexps_len = len(regexps)
-        #if regexps_len > 1:
-        #    raise Exception('Rsync location {} matches with {} regexps {}'
-        #                    ''.format(self.url, len(regexps), str(regexps)))
-            # TODO: Possible may be better remove this raise and keep
-            # only warning with request to fail bug. rsync will parse this
-            # remote later
-        if regexps_len != 1:
-            logger.warn('Rsync location "{}" matches with {} regexps: {}.'
-                        'Please fail a bug on {} if it is wrong.'
-                        ''.format(self.url, len(regexps), str(regexps), '...'))
-        if regexps_len == 0:
+    def _get_matching_pattern(self):
+        patterns = self._get_all_matching_patterns()
+        patterns_len = len(patterns)
+        if patterns_len != 1:
+            logger.warn('Rsync location "{}" matches with {} patterns: {}.'
+                        'Please file a bug on {} if it is wrong.'
+                        ''.format(self.url,
+                                  len(patterns),
+                                  [str(_.pattern) for _ in patterns],
+                                  '...'))
+        if patterns_len == 0:
             self._url_type = None
             return None
         else:
-            return regexps[0]
+            return patterns[0]
 
-    def _get_all_matching_regexps(self):
-        regexps = list()
-        for url_type, regexp in self.regexps.items():
-            match = regexp.match(self.url)
+    def _get_all_matching_patterns(self):
+        patterns = list()
+        for url_type, pattern in self.patterns.items():
+            match = pattern.match(self.url)
             if match is not None:
                 if self.url_type is False:
                     self._url_type = url_type
-                regexps.append(regexp)
-        return regexps
+                patterns.append(pattern)
+        return patterns
 
-    def _parse_rsync_url(self, regexp):
+    def _parse_rsync_url(self, pattern):
         # parse remote url
 
-        for match in re.finditer(regexp, self._url):
-
-            self.path = match.group('path')
-            if self.path is None:
-                self.path = ''
-
-            try:
-                self.host = match.group('host')
-            except IndexError:
-                self.host = None
-
-            try:
-                self.user = match.group('user')
-            except IndexError:
-                self.user = None
-            else:
-                if self.user is not None:
-                    self.user = self.user.strip('@')
-
-            try:
-                self.port = match.group('port')
-            except IndexError:
-                self.port = None
-            else:
-                if self.port is not None:
-                    self.port = int(self.port.strip(':'))
-
-            try:
-                self.module = match.group('module')
-            except IndexError:
-                self.module = None
-            else:
-                if self.module is not None:
-                    self.module = self.module.strip('/')
-                if not self.module:
-                    self.module = None
+        match = pattern.match(self._url)
+        self._parsed_url = utils.bunch(match.groupdict())
 
         if self.url_type == 'ssh':
-            if self.path == '':
-                self.path = '~'
+            if self._parsed_url.path == '':
+                self._parsed_url.path = '~'
 
             if self.path.startswith('/'):
-                self._rootpath = '/'
+                self._parsed_url.rootpath = '/'
             else:
-                self._rootpath = '~/'
-
-            self._netloc = '{}:'.format(self.url.split(':')[0])
-            self._root = '{}{}'.format(self._netloc, self._rootpath)
-            self._url = '{}{}'.format(self._netloc, self.path)
+                self._parsed_url.rootpath = '~/'
 
         elif self.url_type.startswith('rsync'):
-            if self.path == '':
-                self.path = '/'
-            self._rootpath = '/'
+            if self._parsed_url.module:
+                if self._parsed_url.path == '':
+                    self._parsed_url.path = '/'
+                self._parsed_url.rootpath = '/'
+            else:
+                self._parsed_url.path = None
 
-            if self.url_type == 'rsync1':
-                root_parts = ['{}::'.format(self.url.split('::')[0])]
-                if self.module is not None:
-                    root_parts.append('{}'.format(self.module))
-
-            elif self.url_type == 'rsync2':
-                root_parts = ['rsync://']
-                if self.user is not None:
-                    root_parts.append('{}@'.format(self.user))
-                root_parts.append('{}'.format(self.host))
-                if self.port is not None:
-                    root_parts.append(':{}'.format(self.port))
-                if self.module is not None:
-                    root_parts.append('/{}'.format(self.module))
-
-            self._netloc = ''.join(root_parts)
-            if self.module is not None:
-                root_parts.append('{}'.format(self._rootpath))
-            self._root = ''.join(root_parts)
-            self._url = '{}{}'.format(self._netloc, self.path)
+            if self.url_type == 'rsync2':
+                if self.protocol != 'rsync':
+                    msg = 'Wrong URL protocol == "{}"'.format(self.protocol)
+                    logger.error(msg)
+                    raise Exception(msg)
 
         elif self.url_type == 'path':
-            if self.path == '':
-                self.path = '.'
-            self._rootpath = self.a_dir(self.path)
-            self._netloc = None
-            self._root = self._rootpath
-            self._url = '{}'.format(self.path)
-
-        else:
-            self._netloc = None
-            self._root = self._url
-            self._url = '{}'.format(self._rootpath)
-
+            self._sep = os.path.sep
+            self._parsed_url.rootpath = self.a_dir(self.path)
 
     @property
     def match(self):
@@ -198,6 +146,94 @@
         return self._url_type
 
     @property
+    def url(self):
+        return self._url
+
+    @property
+    def protocol(self):
+        return self._parsed_url.get('protocol', None)
+
+    @property
+    def user(self):
+        return self._parsed_url.get('user', None)
+
+    @property
+    def host(self):
+        return self._parsed_url.get('host', None)
+
+    @property
+    def port(self):
+        return self._parsed_url.get('port', None)
+
+    @property
+    def module(self):
+        return self._parsed_url.get('module', None)
+
+    @property
+    def path(self):
+        return self._parsed_url.get('path', '')
+
+    @property
+    def sep(self):
+        return self._sep
+
+    @property
+    def root(self):
+
+        templates = {
+            # ssh: [USER@]HOST:SRC
+            'ssh': ('{user}@', '{host}:', '{rootpath}'),
+            # rsync: [USER@]HOST::SRC
+            'rsync1': ('{user}@', '{host}::', '{module}'),
+            # rsync://[USER@]HOST[:PORT]/SRC
+            'rsync2': ('{protocol}://', '{user}@', '{host}', ':{port}',
+                       '/{module}'),
+            # local/path/to/directory
+            'path': ('{rootpath}', ),
+        }
+        return self.by_template(templates[self.url_type])
+
+    @property
+    def netloc(self):
+
+        templates = {
+            # ssh: [USER@]HOST:SRC
+            'ssh': ('{user}@', '{host}:'),
+            # rsync: [USER@]HOST::SRC
+            'rsync1': ('{user}@', '{host}::', '{module}'),
+            # rsync://[USER@]HOST[:PORT]/SRC
+            'rsync2': ('{protocol}://', '{user}@', '{host}', ':{port}',
+                       '/{module}'),
+            # local/path/to/directory
+            'path': ('', ),
+        }
+        return self.by_template(templates[self.url_type])
+
+    @property
+    def parsed_url(self):
+        if self.url_type is None:
+            return None
+        parsed_dict = utils.bunch()
+        for part in ('protocol', 'user', 'host', 'port', 'module', 'path',
+                     'rootpath'):
+            for tplpart in self.templates[self.url_type]:
+                if '{' + part + '}' in tplpart:
+                    value = self._parsed_url.get(part)
+                    if value:
+                        parsed_dict[part] = value
+        return parsed_dict
+
+    def by_template(self, template_list):
+        template = ''
+        for part in ('protocol', 'user', 'host', 'port', 'module', 'path',
+                     'rootpath'):
+            for tplpart in template_list:
+                if '{' + part + '}' in tplpart:
+                    if self._parsed_url.get(part):
+                        template += tplpart
+        return template.format(**self._parsed_url)
+
+    @property
     def is_valid(self):
         if self.match is None:
             return False
@@ -213,10 +249,11 @@
 
     def _fn_join(self, *parts):
         ''' Joins filenames with ignoring empty parts (None, '', etc)'''
+
         parts = [_ for _ in parts if _]
 
         if len(parts) > 0:
-            if parts[-1].endswith(os.path.sep):
+            if parts[-1].endswith(self.sep):
                 isdir = True
             else:
                 isdir = False
@@ -227,31 +264,24 @@
         if first is None:
             first = ''
         if len(first) > 1:
-            while first.endswith(os.path.sep):
+            while first.endswith(self.sep):
                 first = first[:-1]
 
-        subs = os.path.sep.join([_ for _ in parts if _]).split(os.path.sep)
+        subs = self.sep.join([_ for _ in parts if _]).split(self.sep)
         subs = [_ for _ in subs if _]
 
-        result = re.sub(r'^//', r'/', os.path.sep.join([first, ] + subs))
+        result = re.sub(r'^//', r'/', self.sep.join([first, ] + subs))
         result = re.sub(r'([^:])//', r'\1/', result)
-        if not result.endswith(os.path.sep) and isdir:
-            result += os.path.sep
+        if not result.endswith(self.sep) and isdir:
+            result += self.sep
         return result
 
-    @property
-    def url(self):
-        return self._url
-
-    @property
-    def root(self):
-        return self._root
-
     def join(self, *parts):
         return self._fn_join(*parts)
 
     def urljoin(self, *parts):
-        return self.join(self.url, *parts)
+        return self.join(self.by_template(self.templates[self.url_type]),
+                         *parts)
 
     def a_dir(self, *path):
         result = self._fn_join(*path)
@@ -260,14 +290,16 @@
         return result
 
     def url_dir(self, *path):
-        return self.a_dir(self.url, *path)
+        return self.a_dir(self.by_template(self.templates[self.url_type]),
+                          *path)
 
     def a_file(self, *path):
         result = self._fn_join(*path)
         if len(result) > 1:
-            while result.endswith(os.path.sep):
+            while result.endswith(self.sep):
                 result = result[:-1]
         return result
 
     def url_file(self, *path):
-        return self.a_file(self.url, *path)
+        return self.a_file(self.by_template(self.templates[self.url_type]),
+                           *path)
diff --git a/trsync/tests/test_rsync_url.py b/trsync/tests/test_rsync_url.py
index 9ba305e..751ab77 100644
--- a/trsync/tests/test_rsync_url.py
+++ b/trsync/tests/test_rsync_url.py
@@ -1,4 +1,4 @@
-#-*- coding: utf-8 -*-
+# -*- coding: utf-8 -*-
 
 import os
 import unittest
@@ -25,8 +25,8 @@
         logger.info('For "{}" should be {}'.format(remote, expected_result))
         url = rsync_url.RsyncUrl(remote)
         self.log_locals(url)
-        matching_regexps = url._get_all_matching_regexps()
-        self.assertEqual(len(matching_regexps), expected_result)
+        matching_patterns = url._get_all_matching_patterns()
+        self.assertEqual(len(matching_patterns), expected_result)
 
     def classed(self, remote, expected_result):
         logger.info('For "{}" should be {}'.format(remote, expected_result))
@@ -64,12 +64,24 @@
         self.log_locals(url)
         self.assertEqual(url.url, expected_result)
 
+    def parsed_url(self, remote, expected_result):
+        logger.info('For "{}" should be {}'.format(remote, expected_result))
+        url = rsync_url.RsyncUrl(remote)
+        self.log_locals(url)
+        self.assertEqual(url.parsed_url, expected_result)
+
     def root(self, remote, expected_result):
         logger.info('For "{}" should be {}'.format(remote, expected_result))
         url = rsync_url.RsyncUrl(remote)
         self.log_locals(url)
         self.assertEqual(url.root, expected_result)
 
+    def netloc(self, remote, expected_result):
+        logger.info('For "{}" should be {}'.format(remote, expected_result))
+        url = rsync_url.RsyncUrl(remote)
+        self.log_locals(url)
+        self.assertEqual(url.netloc, expected_result)
+
     def urljoin(self, remote, expected_result):
         logger.info('For "{}" should be {}'.format(remote, expected_result))
         url = rsync_url.RsyncUrl(remote)
diff --git a/trsync/tests/test_rsync_url.yaml b/trsync/tests/test_rsync_url.yaml
index efdd764..496f9bd 100644
--- a/trsync/tests/test_rsync_url.yaml
+++ b/trsync/tests/test_rsync_url.yaml
@@ -9,7 +9,12 @@
 
 'ubuntu@172.18.66.89:~/':
   url: 'ubuntu@172.18.66.89:~/'
+  parsed_url:
+      user: 'ubuntu'
+      host: '172.18.66.89'
+      path: '~/'
   root: 'ubuntu@172.18.66.89:~/'
+  netloc: 'ubuntu@172.18.66.89:'
   urljoin:
     null: 'ubuntu@172.18.66.89:~/'
     '': 'ubuntu@172.18.66.89:~/'
@@ -56,8 +61,13 @@
 
 
 'ubuntu@172.18.66.89:':
-  url: 'ubuntu@172.18.66.89:~'
+  url: 'ubuntu@172.18.66.89:'
+  parsed_url:
+      user: 'ubuntu'
+      host: '172.18.66.89'
+      path: '~'
   root: 'ubuntu@172.18.66.89:~/'
+  netloc: 'ubuntu@172.18.66.89:'
   urljoin:
     null: 'ubuntu@172.18.66.89:~'
     '': 'ubuntu@172.18.66.89:~'
@@ -87,7 +97,12 @@
 
 'ubuntu@172.18.66.89:sub/dir':
   url: 'ubuntu@172.18.66.89:sub/dir'
+  parsed_url:
+      user: 'ubuntu'
+      host: '172.18.66.89'
+      path: 'sub/dir'
   root: 'ubuntu@172.18.66.89:~/'
+  netloc: 'ubuntu@172.18.66.89:'
   urljoin:
     null: 'ubuntu@172.18.66.89:sub/dir'
     '': 'ubuntu@172.18.66.89:sub/dir'
@@ -117,7 +132,12 @@
 
 'ubuntu@172.18.66.89:~':
   url: 'ubuntu@172.18.66.89:~'
+  parsed_url:
+      user: 'ubuntu'
+      host: '172.18.66.89'
+      path: '~'
   root: 'ubuntu@172.18.66.89:~/'
+  netloc: 'ubuntu@172.18.66.89:'
   urljoin:
     null: 'ubuntu@172.18.66.89:~'
     '': 'ubuntu@172.18.66.89:~'
@@ -147,7 +167,12 @@
 
 'ubuntu@172.18.66.89:~/sub/dir/':
   url: 'ubuntu@172.18.66.89:~/sub/dir/'
+  parsed_url:
+      user: 'ubuntu'
+      host: '172.18.66.89'
+      path: '~/sub/dir/'
   root: 'ubuntu@172.18.66.89:~/'
+  netloc: 'ubuntu@172.18.66.89:'
   urljoin:
     null: 'ubuntu@172.18.66.89:~/sub/dir/'
     '': 'ubuntu@172.18.66.89:~/sub/dir/'
@@ -177,7 +202,12 @@
 
 'ubuntu@172.18.66.89:~/sub/dir':
   url: 'ubuntu@172.18.66.89:~/sub/dir'
+  parsed_url:
+      user: 'ubuntu'
+      host: '172.18.66.89'
+      path: '~/sub/dir'
   root: 'ubuntu@172.18.66.89:~/'
+  netloc: 'ubuntu@172.18.66.89:'
   urljoin:
     null: 'ubuntu@172.18.66.89:~/sub/dir'
     '': 'ubuntu@172.18.66.89:~/sub/dir'
@@ -207,7 +237,12 @@
 
 'ubuntu@172.18.66.89:/':
   url: 'ubuntu@172.18.66.89:/'
+  parsed_url:
+      user: 'ubuntu'
+      host: '172.18.66.89'
+      path: '/'
   root: 'ubuntu@172.18.66.89:/'
+  netloc: 'ubuntu@172.18.66.89:'
   urljoin:
     null: 'ubuntu@172.18.66.89:/'
     '': 'ubuntu@172.18.66.89:/'
@@ -237,7 +272,12 @@
 
 'johnivanov@172.18.66.89:/mirror-sync/otlichniy/reg/exp':
   url: 'johnivanov@172.18.66.89:/mirror-sync/otlichniy/reg/exp'
+  parsed_url:
+      user: 'johnivanov'
+      host: '172.18.66.89'
+      path: '/mirror-sync/otlichniy/reg/exp'
   root: 'johnivanov@172.18.66.89:/'
+  netloc: 'johnivanov@172.18.66.89:'
   exact_match_num: 1
   classed: 'ssh'
   parsed:
@@ -249,7 +289,11 @@
 
 '172.18.66.89:/mirror-sync/otlichniy/reg/exp':
   url: '172.18.66.89:/mirror-sync/otlichniy/reg/exp'
+  parsed_url:
+      host: '172.18.66.89'
+      path: '/mirror-sync/otlichniy/reg/exp'
   root: '172.18.66.89:/'
+  netloc: '172.18.66.89:'
   exact_match_num: 1
   classed: 'ssh'
   parsed:
@@ -261,7 +305,11 @@
 
 '172.18.66.89:/':
   url: '172.18.66.89:/'
+  parsed_url:
+      host: '172.18.66.89'
+      path: '/'
   root: '172.18.66.89:/'
+  netloc: '172.18.66.89:'
   exact_match_num: 1
   classed: 'ssh'
   parsed:
@@ -272,8 +320,12 @@
 
 
 '172.18.66.89:':
-  url: '172.18.66.89:~'
+  url: '172.18.66.89:'
+  parsed_url:
+      host: '172.18.66.89'
+      path: '~'
   root: '172.18.66.89:~/'
+  netloc: '172.18.66.89:'
   exact_match_num: 1
   classed: 'ssh'
   parsed:
@@ -285,7 +337,13 @@
 
 'johnivanov@172.18.66.89::mirror-sync/otlichniy/reg/exp':
   url: 'johnivanov@172.18.66.89::mirror-sync/otlichniy/reg/exp'
-  root: 'johnivanov@172.18.66.89::mirror-sync/'
+  parsed_url:
+      user: 'johnivanov'
+      host: '172.18.66.89'
+      module: 'mirror-sync'
+      path: 'otlichniy/reg/exp'
+  root: 'johnivanov@172.18.66.89::mirror-sync'
+  netloc: 'johnivanov@172.18.66.89::mirror-sync'
   urljoin:
     null: 'johnivanov@172.18.66.89::mirror-sync/otlichniy/reg/exp'
     '': 'johnivanov@172.18.66.89::mirror-sync/otlichniy/reg/exp'
@@ -311,13 +369,18 @@
     - '172.18.66.89'
     - null
     - 'mirror-sync'
-    - '/otlichniy/reg/exp'
+    - 'otlichniy/reg/exp'
   valid: True
 
 
 '172.18.66.89::mirror-sync/otlichniy/reg/exp':
   url: '172.18.66.89::mirror-sync/otlichniy/reg/exp'
-  root: '172.18.66.89::mirror-sync/'
+  parsed_url:
+      host: '172.18.66.89'
+      module: 'mirror-sync'
+      path: 'otlichniy/reg/exp'
+  root: '172.18.66.89::mirror-sync'
+  netloc: '172.18.66.89::mirror-sync'
   exact_match_num: 1
   classed: 'rsync1'
   parsed_rsync:
@@ -325,13 +388,18 @@
     - '172.18.66.89'
     - null
     - 'mirror-sync'
-    - '/otlichniy/reg/exp'
+    - 'otlichniy/reg/exp'
   valid: True
 
 
 '172.18.66.89::mirror-sync/':
   url: '172.18.66.89::mirror-sync/'
-  root: '172.18.66.89::mirror-sync/'
+  parsed_url:
+      host: '172.18.66.89'
+      module: 'mirror-sync'
+      path: '/'
+  root: '172.18.66.89::mirror-sync'
+  netloc: '172.18.66.89::mirror-sync'
   exact_match_num: 1
   classed: 'rsync1'
   parsed_rsync:
@@ -344,8 +412,13 @@
 
 
 '172.18.66.89::mirror-sync':
-  url: '172.18.66.89::mirror-sync/'
-  root: '172.18.66.89::mirror-sync/'
+  url: '172.18.66.89::mirror-sync'
+  parsed_url:
+      host: '172.18.66.89'
+      module: 'mirror-sync'
+      path: '/'
+  root: '172.18.66.89::mirror-sync'
+  netloc: '172.18.66.89::mirror-sync'
   exact_match_num: 1
   classed: 'rsync1'
   parsed_rsync:
@@ -359,6 +432,7 @@
 
 'johnivanov@172.18.66.89::/mirror-sync/otlichniy/reg/exp':
   url: 'johnivanov@172.18.66.89::/mirror-sync/otlichniy/reg/exp'
+  parsed_url: null
   exact_match_num: 0
   classed: null
   valid: False
@@ -366,6 +440,7 @@
 
 '172.18.66.89::/mirror-sync/otlichniy/reg/exp':
   url: '172.18.66.89::/mirror-sync/otlichniy/reg/exp'
+  parsed_url: null
   exact_match_num: 0
   classed: null
   valid: False
@@ -373,6 +448,7 @@
 
 '172.18.66.89::/':
   url: '172.18.66.89::/'
+  parsed_url: null
   exact_match_num: 0
   classed: null
   valid: False
@@ -380,6 +456,8 @@
 
 '172.18.66.89::':
   url: '172.18.66.89::'
+  parsed_url:
+      host: '172.18.66.89'
   exact_match_num: 1
   classed: 'rsync1'
   parsed_rsync:
@@ -387,14 +465,22 @@
     - '172.18.66.89'
     - null
     - null
-    - '/'
+    - null
   valid: False
 
 
 
 'rsync://mirror-sync@172.18.66.89:7327/otlichniy/reg/exp':
   url: 'rsync://mirror-sync@172.18.66.89:7327/otlichniy/reg/exp'
-  root: 'rsync://mirror-sync@172.18.66.89:7327/otlichniy/'
+  parsed_url:
+      protocol: 'rsync'
+      user: 'mirror-sync'
+      host: '172.18.66.89'
+      port: '7327'
+      module: 'otlichniy'
+      path: 'reg/exp'
+  root: 'rsync://mirror-sync@172.18.66.89:7327/otlichniy'
+  netloc: 'rsync://mirror-sync@172.18.66.89:7327/otlichniy'
   urljoin:
     null: 'rsync://mirror-sync@172.18.66.89:7327/otlichniy/reg/exp'
     '': 'rsync://mirror-sync@172.18.66.89:7327/otlichniy/reg/exp'
@@ -418,29 +504,42 @@
   parsed_rsync:
     - 'mirror-sync'
     - '172.18.66.89'
-    - 7327
+    - '7327'
     - 'otlichniy'
-    - '/reg/exp'
+    - 'reg/exp'
   valid: True
 
 
 'rsync://172.18.66.89:7327/mirror-sync/otlichniy/reg/exp':
   url: 'rsync://172.18.66.89:7327/mirror-sync/otlichniy/reg/exp'
-  root: 'rsync://172.18.66.89:7327/mirror-sync/'
+  parsed_url:
+      protocol: 'rsync'
+      host: '172.18.66.89'
+      port: '7327'
+      module: 'mirror-sync'
+      path: 'otlichniy/reg/exp'
+  root: 'rsync://172.18.66.89:7327/mirror-sync'
+  netloc: 'rsync://172.18.66.89:7327/mirror-sync'
   exact_match_num: 1
   classed: 'rsync2'
   parsed_rsync:
     - null
     - '172.18.66.89'
-    - 7327
+    - '7327'
     - 'mirror-sync'
-    - '/otlichniy/reg/exp'
+    - 'otlichniy/reg/exp'
   valid: True
 
 
 'rsync://172.18.66.89/mirror-sync/otlichniy/reg/exp':
   url: 'rsync://172.18.66.89/mirror-sync/otlichniy/reg/exp'
-  root: 'rsync://172.18.66.89/mirror-sync/'
+  parsed_url:
+      protocol: 'rsync'
+      host: '172.18.66.89'
+      module: 'mirror-sync'
+      path: 'otlichniy/reg/exp'
+  root: 'rsync://172.18.66.89/mirror-sync'
+  netloc: 'rsync://172.18.66.89/mirror-sync'
   exact_match_num: 1
   classed: 'rsync2'
   parsed_rsync:
@@ -448,13 +547,19 @@
     - '172.18.66.89'
     - null
     - 'mirror-sync'
-    - '/otlichniy/reg/exp'
+    - 'otlichniy/reg/exp'
   valid: True
 
 
 'rsync://172.18.66.89/mirror-sync/':
   url: 'rsync://172.18.66.89/mirror-sync/'
-  root: 'rsync://172.18.66.89/mirror-sync/'
+  parsed_url:
+      protocol: 'rsync'
+      host: '172.18.66.89'
+      module: 'mirror-sync'
+      path: '/'
+  root: 'rsync://172.18.66.89/mirror-sync'
+  netloc: 'rsync://172.18.66.89/mirror-sync'
   exact_match_num: 1
   classed: 'rsync2'
   parsed_rsync:
@@ -468,7 +573,11 @@
 
 'rsync://172.18.66.89/':
   url: 'rsync://172.18.66.89/'
+  parsed_url:
+      protocol: 'rsync'
+      host: '172.18.66.89'
   root: 'rsync://172.18.66.89'
+  netloc: 'rsync://172.18.66.89'
   exact_match_num: 1
   classed: 'rsync2'
   parsed_rsync:
@@ -476,13 +585,17 @@
     - '172.18.66.89'
     - null
     - null
-    - '/'
+    - null
   valid: False
 
 
 'rsync://172.18.66.89':
-  url: 'rsync://172.18.66.89/'
+  url: 'rsync://172.18.66.89'
+  parsed_url:
+      protocol: 'rsync'
+      host: '172.18.66.89'
   root: 'rsync://172.18.66.89'
+  netloc: 'rsync://172.18.66.89'
   exact_match_num: 1
   classed: 'rsync2'
   parsed_rsync:
@@ -490,13 +603,16 @@
     - '172.18.66.89'
     - null
     - null
-    - '/'
+    - null
   valid: False
 
 
 '/':
   url: '/'
+  parsed_url:
+      path: '/'
   root: '/'
+  netloc: ''
   urljoin:
     null: '/'
     '': '/'
@@ -526,7 +642,10 @@
 
 'dir':
   url: 'dir'
+  parsed_url:
+      path: 'dir'
   root: 'dir/'
+  netloc: ''
   exact_match_num: 1
   classed: 'path'
   parsed:
@@ -538,7 +657,10 @@
 
 '/dir':
   url: '/dir'
+  parsed_url:
+      path: '/dir'
   root: '/dir/'
+  netloc: ''
   exact_match_num: 1
   classed: 'path'
   parsed:
@@ -550,7 +672,10 @@
 
 '/dir/subdir/':
   url: '/dir/subdir/'
+  parsed_url:
+      path: '/dir/subdir/'
   root: '/dir/subdir/'
+  netloc: ''
   exact_match_num: 1
   classed: 'path'
   parsed:
@@ -558,3 +683,31 @@
     - null
     - '/dir/subdir/'
   valid: True
+
+
+'rsync://localhost/mirror-sync/mos-repos/centos/mos9.0-centos7':
+  url: 'rsync://localhost/mirror-sync/mos-repos/centos/mos9.0-centos7'
+  parsed_url:
+    protocol: 'rsync'
+    host: 'localhost'
+    module: 'mirror-sync'
+    path: 'mos-repos/centos/mos9.0-centos7'
+  root: 'rsync://localhost/mirror-sync'
+  netloc: 'rsync://localhost/mirror-sync'
+  exact_match_num: 1
+  classed: 'rsync2'
+  valid: True
+
+
+'rsync://localhost/mirror-sync/mos-repos/ubuntu':
+  url: 'rsync://localhost/mirror-sync/mos-repos/ubuntu'
+  parsed_url:
+    protocol: 'rsync'
+    host: 'localhost'
+    module: 'mirror-sync'
+    path: 'mos-repos/ubuntu'
+  root: 'rsync://localhost/mirror-sync'
+  netloc: 'rsync://localhost/mirror-sync'
+  exact_match_num: 1
+  classed: 'rsync2'
+  valid: True
diff --git a/trsync/utils/utils.py b/trsync/utils/utils.py
index d95165a..cdd4bcb 100644
--- a/trsync/utils/utils.py
+++ b/trsync/utils/utils.py
@@ -55,7 +55,7 @@
     def __init__(self, now=None):
         # now='2015-06-18-104259'
         self.snapshot_stamp_format = r'%Y-%m-%d-%H%M%S'
-        self.snapshot_stamp_regexp = r'[0-9]{4}-[0-9]{2}-[0-9]{2}-[0-9]{6}'
+        self.snapshot_stamp_pattern = r'[0-9]{4}-[0-9]{2}-[0-9]{2}-[0-9]{6}'
 
         if now is None:
             self.now = datetime.datetime.utcnow()
@@ -125,3 +125,9 @@
         raise ResultNotProduced('Result "{}" was not produced during '
                                 '{} attempts.'
                                 ''.format(expected_result, attempt - 1))
+
+
+class bunch(dict):
+    def __init__(self, *args, **kwargs):
+        super(bunch, self).__init__(*args, **kwargs)
+        self.__dict__ = self