Ejemplo n.º 1
0
    def __init__(self,
                 table_path: str,
                 key_field: str,
                 value_fields: _tp.Sequence[str],
                 where_clause: _tp.Union[None, str, _q.Where] = None,
                 **kwargs):
        _vld.raise_if(
            len(value_fields) <= 1, ValueError,
            f'{RowLookup.__name__} expects multiple value fields: use {ValueLookup.__name__} instead'
        )
        _vld.pass_if(
            all(
                _vld.has_value(v)
                for v in (table_path, key_field, value_fields[0])), ValueError,
            f'{RowLookup.__name__} requires valid table_path, key_field and value_fields arguments'
        )

        # User cannot override row processor function for this class
        if _ROWFUNC_ARG in kwargs:
            del kwargs[_ROWFUNC_ARG]

        self._dupekeys = kwargs.get(_DUPEKEYS_ARG, False)
        self._rowtype = list if kwargs.get(_MUTABLE_ARG, False) else tuple
        super(RowLookup, self).__init__(table_path, key_field, value_fields,
                                        where_clause, **kwargs)

        self._fieldmap = {
            name.lower(): i
            for i, name in enumerate(value_fields)
        }
Ejemplo n.º 2
0
 def __init__(self, table_path, key_field, value_field, where_clause=None, **kwargs):
     _vld.raise_if(_vld.is_iterable(value_field), ValueError,
                   '{} expects a single value field: use {} instead'.format(ValueLookup.__name__,
                                                                            RowLookup.__name__))
     _vld.pass_if(all(_vld.has_value(v) for v in (table_path, key_field, value_field)), ValueError,
                  '{} requires valid table_path, key_field and value_field arguments')
     super(ValueLookup, self).__init__(table_path, key_field, value_field, where_clause, **kwargs)
Ejemplo n.º 3
0
def add_where(keyword_args, where_clause, datasource=None):
    """
    Updates the keyword arguments dictionary with a where clause (string or ``Where`` instance).

    :param keyword_args:    A keyword argument dictionary.
    :param where_clause:    A query string or a :class:`Where` instance.
    :param datasource:      If the data source path is specified, the field delimiters are updated accordingly.
                            This only has an effect if *where_clause* is a :class:`Where` instance.
    :raises ValueError:     If *where_clause* is not a string or :class:`Where` instance,
                            or if *keyword_args* is not a ``dict``.
    """
    if not where_clause:
        return

    _vld.pass_if(isinstance(keyword_args, dict), ValueError,
                 'keyword_args must be a dict')

    if isinstance(where_clause, Where):
        if datasource:
            where_clause.delimit_fields(datasource)
        keyword_args[WHERE_KWARG] = unicode(where_clause)
    elif isinstance(where_clause, (str, unicode)):
        keyword_args[WHERE_KWARG] = where_clause
    else:
        raise ValueError('{!r} must be a string or {} instance'.format(
            WHERE_KWARG, Where.__name__))
Ejemplo n.º 4
0
def format_timedelta(start: _dt, stop: _dt = None) -> str:
    """
    Calculates the time difference between `start` and `stop` datetime objects and returns a pretty-printed time delta.
    If `stop` is omitted, the current time (:func:`now`) will be used.
    The smallest time unit that can be expressed is in (floating point) seconds. The largest time unit is in days.

    Example:

        >>> t0 = _dt(2019, 1, 1, 1, 1, 1)  # where _dt = datetime
        >>> format_timedelta(t0)
        '1 day, 3 hours, 4 minutes and 5.2342 seconds'

    :param start:   The start time (t0) for the time delta calculation.
    :param stop:    The end time (t1) for the time delta calculation or :func:`now` when omitted.
    """
    # Attribute validation
    _vld.pass_if(isinstance(start, _dt), TypeError,
                 "'start' attribute must be a datetime instance")
    _vld.pass_if(not stop or isinstance(stop, _dt), TypeError,
                 "'stop' attribute must be a datetime instance (or None)")

    td = abs((stop or _dt.now()) - start)
    total_h, rem = divmod(td.total_seconds(), 3600)
    d, h = divmod(total_h, 24)
    m, s = divmod(rem, 60)
    t_comp = (format_plural(f, t)
              for f, t in (('day', int(d)), ('hour', int(h)),
                           ('minute', int(m)), ('second', s)) if t > 0)
    return format_iterable([t for t in t_comp] or ['0 seconds'])
Ejemplo n.º 5
0
    def __init__(self, *attributes):
        self._blacklist = frozenset(dir(Bucket) + dir(namedtuple(_DUMMY, _tu.EMPTY_STR)))

        _vld.pass_if(attributes, TypeError,
                     '{!r} must be instantiated with 1 or more attribute names'.format(BucketFactory.__name__))

        self._attr_names = self._set_attrs(attributes)
Ejemplo n.º 6
0
def get_abs(path, base=None):
    """
    Creates a normalized absolute path based on *path* relative to *base*. If *base* is not specified,
    the base will be the directory to the file path of the calling function, i.e. when a script 'test.py' calls
    make_absolute(), the directory which contains 'test.py' will be the base.

    :param path:        The relative path to turn into an absolute one.
    :param base:        The base path that serves as the 'root' of the relative path.
    :type path:         str, unicode
    :type base:         str, unicode
    :rtype:             str, unicode
    :raises ValueError: If the *base* path is ``None`` and no valid base directory was found using the caller path.
    """
    _vld.pass_if(isinstance(path, basestring), TypeError,
                 'path attribute must be a string')
    _vld.pass_if(isinstance(base, (basestring, type(None))), TypeError,
                 'base attribute must be a string or None')

    if _os.path.isabs(path):
        return normalize(path, False)
    if not base:
        # Get the base path by looking at the function that called get_abs().
        # The caller frame should be the second frame (1) in the stack.
        # This returns a tuple of which the first value (0) is the frame object.
        # Note: that path returned by inspect.getabsfile() is lower case!
        frame = _inspect.stack()[1][0]
        base = _os.path.dirname(_inspect.getabsfile(frame))
        if not _os.path.isdir(base):
            raise ValueError('Failed to determine base path from caller')
    return concat(base, path)
Ejemplo n.º 7
0
 def _fix_operator(operator, allowed_operators=__SUPPORTED_OPERATORS):
     """ Makes *operator* lowercase and checks if it's a valid operator. """
     operator = operator.strip().lower()
     _vld.pass_if(
         operator in allowed_operators, OperatorError,
         'The {!r} operator is not allowed or supported'.format(operator))
     return operator
Ejemplo n.º 8
0
def split_gdbpath(path: str, remove_qualifier: bool = True) -> tuple:
    """
    Splits the Esri Geodatabase *path* into a ``tuple`` of *(workspace, feature_dataset, feature_class/table)*.
    If any of the output tuple parts is not present, these will be set to an empty string.
    Note that if the path refers to a table or feature class that is not stored in a feature dataset,
    the *feature_dataset* part in the output tuple will be an empty string and the last part will contain the table
    or feature class name.

    Examples:

        >>> split_gdbpath('C:/test.gdb/table')
        ('C:\\test.gdb', '', 'table')
        >>> split_gdbpath('C:/test.sde/qualifier.featureclass', False)
        ('C:\\test.sde', '', 'qualifier.featureclass')
        >>> split_gdbpath('C:/test.sde/qualifier.fds/qualifier.fc')
        ('C:\\test.sde', 'fds', 'fc')

    :param path:                The full path to the Geodatabase feature class or table.
    :param remove_qualifier:    If ``True``, the split parts are "unqualified". If any DB qualifiers exist in the path,
                                they will be removed. This is the default behaviour.
                                Set this parameter to ``False`` if qualifiers must be persisted.
    :raises ValueError:         If the given path does not seem to be a Geodatabase path or
                                there are more than 2 levels in the Geodatabase.
    """
    _vld.pass_if(is_gdbpath(path), ValueError,
                 f'{path} does not seem to be a valid Esri Geodatabase path')
    path = normalize(path, False)
    path_parts = path.split('\\')
    num_parts = len(path_parts)

    # Find the part that contains the GDB extension
    endpos = 1
    for endpos, p in enumerate(path_parts):
        if p.lower().endswith(ESRI_GDB_EXTENSIONS):
            break
    startpos = endpos + 1

    # Set the workspace path and return it if the path isn't any longer than that
    ws_path = '\\'.join(path_parts[:startpos])
    if num_parts == startpos:
        return ws_path, _const.CHAR_EMPTY, _const.CHAR_EMPTY

    # If there are more than 2 levels (i.e. feature dataset and feature class), raise an error
    _vld.raise_if(num_parts > startpos + 2, ValueError,
                  'Geodatabase path cannot be more than 2 levels deep')

    last_parts = [(unqualify(p) if remove_qualifier else p)
                  for p in path_parts[startpos:]]
    if len(last_parts) == 2:
        # If the path is 2 elements deep, output all as-is
        return tuple([ws_path] + last_parts)
    else:
        # Detect if the input path was a feature dataset or not and output accordingly
        try:
            meta = _arcpy.Describe(path)
            if not meta.dataType == 'FeatureDataset':
                raise ValueError
        except (RuntimeError, IOError, AttributeError, ValueError, NameError):
            return ws_path, _const.CHAR_EMPTY, last_parts[0]
        return ws_path, last_parts[0], _const.CHAR_EMPTY
Ejemplo n.º 9
0
def get_xyz(*args) -> _tp.Tuple[float]:
    """
    Returns a floating point coordinate XYZ tuple for a given coordinate.
    Valid input includes EsriJSON, ArcPy Point or PointGeometry instances or a minimum of 2 floating point values.
    If the geometry is not Z aware, the Z value in the output tuple will be set to ``None``.

    :param args:    A tuple of floating point values, an EsriJSON dictionary, an ArcPy Point or PointGeometry instance.

    .. note::       For Point geometries, M and ID values are ignored.
    """
    p_args = args

    if len(args) == 1:
        a = _iter.first(args)

        # Unfortunately, we can't really rely on isinstance() to check if it's a PointGeometry or Point.
        # However, if it's a PointGeometry, it must have a pointCount attribute with a value of 1.
        if getattr(a, 'pointCount', 0) == 1:
            # Get first Point from PointGeometry...
            a = a.firstPoint

        if hasattr(a, 'X') and hasattr(a, 'Y'):
            # Get X, Y and Z properties from Point
            p_args = a.X, a.Y, a.Z
        elif isinstance(a, dict):
            # Assume argument is JSON(-like) input: read x, y and z keys
            p_args = tuple(v for k, v in sorted(a.items()) if k.lower() in ('x', 'y', 'z'))
            # Validate values
            for a in p_args:
                _vld.pass_if(_vld.is_number(a), ValueError, 'Failed to parse coordinate from JSON'.format(args))
        else:
            raise ValueError('Input is not a Point, PointGeometry, JSON dictionary or iterable of float')

    return tuple(_fix_coord(*p_args, dim=3))
Ejemplo n.º 10
0
    def qualify(self,
                name: str,
                qualifier: str = None,
                separator: str = None) -> str:
        """
        Qualifies (prefixes) a data element name for SDE workspaces.
        If the workspace is not an SDE workspace or the name is qualified already, the input name is returned as-is.

        :param name:        Feature dataset, feature class or table name.
        :param qualifier:   Optional qualifier if the one derived from the DB connection should be overridden.
        :param separator:   Optional separator if the initial one should be overridden (defaults to ``'.'``).
        :raises ValueError: If no table name was specified.
        """
        _vld.pass_if(name, ValueError, 'qualify() requires a table name')
        if not self._qualifier or self._sep in name:
            # return immediately when name seems to contain a qualifier or workspace is local (i.e. not qualified)
            return name

        # make sure that the name does not start with a qualifier already
        name = unqualify(name)

        if qualifier:
            # use the qualifier override instead of self._qualifier
            return self._fix_qualifier(qualifier, separator
                                       or self._sep) + name

        # return the name with the default self._qualifier
        return self._qualifier + name
Ejemplo n.º 11
0
def format_iterable(iterable, conjunction=AND):
    """
    Function that pretty-prints an iterable, separated by commas and adding a conjunction before the last item.

    Example:

        >>> iterable = [1, 2, 3, 4]
        >>> format_iterable(iterable)
        '1, 2, 3 and 4'

    :param iterable:    The iterable (e.g. list or tuple) to format.
    :param conjunction: The conjunction to use before the last item. Defaults to "and".
    :type iterable:     list, tuple
    :type conjunction:  str
    """
    _vld.pass_if(_vld.is_iterable(iterable), TypeError,
                 "'iterable' attribute must be an iterable (e.g. list)")

    num_items = len(iterable)
    if num_items == 0:
        return ''
    if num_items == 1:
        return to_str(iterable[-1])
    return '{} {} {}'.format(', '.join(to_str(v) for v in iterable[:-1]),
                             conjunction, iterable[-1])
Ejemplo n.º 12
0
def combine(where_clause):
    """
    The `combine` function wraps a :class:`Where` instance in parenthesis "()".
    This is typically used to combine 2 or more SQL clauses (delimited by AND or OR) into one.

    Example:

        >>> combine(Where('A').Equals(1).And('B').Equals(0)).Or('C').IsNull()
        (A = 1 AND B = 0) OR C IS NULL

    **Params:**

    -   **where_clause** (:class:`Where`):

        Another `Where` instance that should be wrapped in parenthesis.
    """

    # Check if clause is another Where instance
    _vld.pass_if(isinstance(where_clause, Where), ValueError,
                 'Input clause must be of type {!r}'.format(Where.__name__))

    # Since we will wrap the query in parenthesis, it must be a complete query (not dirty)
    _vld.pass_if(where_clause.is_ready, ValueError,
                 'Cannot wrap incomplete query in parenthesis')

    wrapper = Where(where_clause)
    wrapper._parts.insert(0, (u'(', False))
    wrapper._parts.append((u')', False))
    return wrapper
Ejemplo n.º 13
0
 def __init__(self, table_path, key_field, value_fields, where_clause=None, **kwargs):
     _vld.raise_if(len(value_fields) <= 1, ValueError, '{} expects multiple value fields: use {} instead'.format(
             RowLookup.__name__, ValueLookup.__name__))
     _vld.pass_if(all(_vld.has_value(v) for v in (table_path, key_field, value_fields[0])), ValueError,
                  '{} requires valid table_path, key_field and value_fields arguments')
     super(RowLookup, self).__init__(table_path, key_field, value_fields, where_clause, **kwargs)
     self._fieldmap = {name.lower(): i for i, name in enumerate(value_fields)}
Ejemplo n.º 14
0
    def __init__(self, definitions, filter_prefix):
        _vld.pass_if(isinstance(definitions, DefinitionTable), ValueError,
                     "'definitions' argument must be a DefinitionTable instance")
        self._def = definitions
        self._prefix = filter_prefix

        # This feels a bit hacky, but it's actually similar to how it is determined in the GEONIS core code...
        self._override = self._def.get(_EN_KEY, _EN_VAL) != _EN_VAL
Ejemplo n.º 15
0
def find_parent(path, name):
    """
    Finds within *path* the parent directory that contains a file or directory with the given *name*.
    Note that *path* and *name* values are matched case-insensitively, but the found path is returned
    in the original case (as a normalized path).

    If no matches have been found or if the match has no parent, an empty string is returned.
    If there are multiple matches, the parent path of the first match is returned.

    Examples:

        >>> find_parent('C:\\Projects\\parent\\LEVEL0\\level1\\level2.txt', 'level0')
        'C:\\Projects\\parent'
        >>> find_parent('C:\\Projects\\parent\\LEVEL\\level\\level.txt', 'level')
        'C:\\Projects\\parent'
        >>> find_parent('C:\\Projects\\some_dir', 'C:')
        ''
        >>> find_parent('C:\\Projects\\parent\\must_include_extension.txt', 'must_include_extension')
        ''

    :param path:    The path to search.
    :param name:    The name for which to search the parent directory in *path*.
                    This value can be a file name (with extension!) or a directory name.
                    Partial paths, pre- or suffixes or regular expressions are not allowed here and will return None.
    :type path:     str, unicode
    :type name:     str, unicode
    :rtype:         str, unicode
    """
    def match_parts(plist, n):
        # Search for n in plist and yield the parts up to n
        n_ci = n.lower()
        if n_ci not in (p.lower() for p in plist):
            return

        for p in plist:
            if p.lower() == n_ci:
                return
            yield p

    # Get path type (unicode or str) so we correctly join the path parts without encoding issues
    path_type = type(path)

    # Validate arguments
    _vld.pass_if(issubclass(path_type, basestring), TypeError,
                 'path attribute must be a string')
    _vld.pass_if(isinstance(name, basestring), TypeError,
                 'name attribute must be a string')

    # Split path into parts list, encode or decode name if name type does not match path type
    parts = normalize(path, False).split(_os.sep)
    if not isinstance(name, path_type):
        name = _tu.to_str(name) if isinstance(
            name, unicode) else _tu.to_unicode(name)

    # Return the concatenated path parts up til the match (or an empty string if nothing was found)
    return path_type(_os.sep).join(match_parts(parts, name)) or path_type(
        _const.CHAR_EMPTY)
Ejemplo n.º 16
0
    def _check_fields(user_fields, table_fields):
        """
        Checks if the given *user_fields* exist in *table_fields*.

        :raises ValueError: When a field does not exist in the source table.
        """
        for field in user_fields:
            _vld.pass_if('@' in field or field.upper() in table_fields,
                         ValueError, 'Field {} does not exist'.format(field))
Ejemplo n.º 17
0
def unquote(text: str) -> str:
    """
    Strips trailing quotes from a text string and returns it.

    :param text:    The string to strip.
    """
    _vld.pass_if(_vld.is_text(text), TypeError,
                 "'text' attribute must be a string (got {!r})".format(text))
    return text.strip('\'"`')
Ejemplo n.º 18
0
    def _get_fields(table_path):
        """
        Gets all field names in the table.

        :raises RuntimeError:   When the table metadata could not be retrieved.
        :rtype:                 list
        """
        desc = _meta.Describe(table_path)
        _vld.pass_if(desc, RuntimeError, 'Failed to create lookup for {}'.format(_tu.to_repr(table_path)))
        return desc.get_fields(True, True)
Ejemplo n.º 19
0
    def _get_fields(table_path: str) -> _tp.List[str]:
        """
        Gets all field names in the table.

        :raises RuntimeError:   When the table metadata could not be retrieved.
        """
        desc = _meta.Describe(table_path)
        _vld.pass_if(desc, RuntimeError,
                     f'Failed to create lookup for {_tu.to_repr(table_path)}')
        return desc.get_fields(True, True)
Ejemplo n.º 20
0
def concat(*args: str) -> str:
    """
    Joins (concatenates) one or more paths together to create a complete path and normalizes it.

    :param args:    One or more paths/parts.
    """
    _vld.pass_if(args and all(isinstance(a, str) for a in args), TypeError,
                 'All arguments must be strings')

    return normalize(_os.path.join(*args), False)
Ejemplo n.º 21
0
 def _check_values(self, values: _tp.Sequence, min_required: int,
                   operator: str) -> list:
     """ Flattens the IN/BETWEEN query values, performs several checks, and returns the list if ok. """
     output = [v for v in _iter.collapse(values, levels=1)]
     _vld.pass_if(
         len(output) >= min_required, ValueError,
         f'{operator} query requires at least {min_required} value')
     _vld.pass_if(self._check_types(*output), ValueError,
                  f'{operator} query values must have similar data types')
     return output
Ejemplo n.º 22
0
 def _check_values(self, values, min_required, operator):
     """ Flattens the IN/BETWEEN query values, performs several checks, and returns the list if ok. """
     output = [v for v in _iter.collapse(values, levels=1)]
     _vld.pass_if(
         len(output) >= min_required, ValueError,
         '{} query requires at least {} value'.format(
             operator, min_required))
     _vld.pass_if(
         self._check_types(*output), ValueError,
         '{} query values must have similar data types'.format(operator))
     return output
Ejemplo n.º 23
0
    def _check_fields(user_fields: _tp.Iterable[str],
                      table_fields: _tp.Iterable[str]):
        """
        Checks if the given *user_fields* exist in *table_fields*.

        :raises ValueError: When a field does not exist in the source table.
        """
        for field in user_fields:
            _vld.pass_if(
                _const.CHAR_AT in field or field.upper() in table_fields,
                ValueError, 'Field {} does not exist'.format(field))
Ejemplo n.º 24
0
    def extend(self, values: _tp.Iterable):
        """
        Adds multiple coordinates to the geometry.

        :param values:      An iterable of numeric coordinate values, ``Point``, ``Array`` or ``ShapeBuilder`` objects.
        :type values:       tuple, list
        :raises ValueError: If the *values* argument is not an iterable.
        """
        _vld.pass_if(_vld.is_iterable(values), ValueError, 'extend() expects an iterable')
        for v in values:
            self.append(v)
Ejemplo n.º 25
0
def capitalize(text: str) -> str:
    """
    Function that works similar to the built-in string method :func:`str.capitalize`,
    except that it only makes the first character uppercase, and leaves the other characters unchanged.

    :param text:    The string to capitalize.
    """
    _vld.pass_if(_vld.is_text(text), TypeError,
                 "'text' attribute must be a string (got {!r})".format(text))
    if len(text) < 2:
        return text.upper()
    return f'{text[0].upper()}{text[1:]}'
Ejemplo n.º 26
0
    def __init__(self, table_path, key_field, value_field, where_clause=None, **kwargs):
        _vld.raise_if(_vld.is_iterable(value_field), ValueError,
                      '{} expects a single value field: use {} instead'.format(ValueLookup.__name__,
                                                                               RowLookup.__name__))
        _vld.pass_if(all(_vld.has_value(v) for v in (table_path, key_field, value_field)), ValueError,
                     '{} requires valid table_path, key_field and value_field arguments'.format(ValueLookup.__name__))

        # User cannot override row processor function for this class
        if _ROWFUNC_ARG in kwargs:
            del kwargs[_ROWFUNC_ARG]

        self._dupekeys = kwargs.get(_DUPEKEYS_ARG, False)
        super(ValueLookup, self).__init__(table_path, key_field, value_field, where_clause, **kwargs)
Ejemplo n.º 27
0
def normalize(path: str, lowercase: bool = True) -> str:
    """
    Normalizes a path and turns it into lowercase, unless *lowercase* is set to ``False``.

    :param path:        The path to normalize.
    :param lowercase:   If ``True`` (default), the path will be turned into lowercase.
                        If ``False``, the case remains unchanged.
    """
    _vld.pass_if(isinstance(path, str), TypeError,
                 'Path attribute must be a string')

    norm_path = _os.path.normpath(path)
    return norm_path.lower() if lowercase else norm_path
Ejemplo n.º 28
0
    def as_polyline(self, spatial_reference: _tp.Union[str, int, _arcpy.SpatialReference, None] = None,
                    has_z: bool = False, has_m: bool = False) -> _arcpy.Polyline:
        """
        Returns the constructed geometry as an Esri ``Polyline``.

        :param spatial_reference:   An optional spatial reference. Defaults to 'Unknown'.
        :param has_z:               If ``True``, the geometry is Z aware. Defaults to ``False``.
        :param has_m:               If ``True``, the geometry is M aware. Defaults to ``False``.
        :type spatial_reference:    str, int, arcpy.SpatialReference
        :raises GeometryError:      If there are less than 2 coordinates.
        """
        _vld.pass_if(self.num_coords >= 2, GeometryError, 'Polyline must have at least 2 coordinates')
        return self._output(_arcpy.Polyline, self._arr, spatial_reference, has_z, has_m)
Ejemplo n.º 29
0
    def __init__(self, path, base=None):
        _vld.pass_if(_vld.is_text(path, False), TypeError,
                     "Attribute 'path' should be a non-empty string")
        self._path = _os.path.normpath(path)

        if base:
            _vld.raise_if(
                _os.path.isabs(self._path), ValueError,
                f'{self.__class__.__name__} expects a relative path when root has been set'
            )
            self._path = get_abs(self._path, base)

        self._head, self._tail = _os.path.split(self._path)
        self._end, self._ext = _os.path.splitext(self._tail)
Ejemplo n.º 30
0
def get_alphachars(text: str) -> str:
    """
    Returns all alphabetic characters [a-zA-Z] in string *text* in a new (concatenated) string.

    Example:

        >>> get_alphachars('Test123')
        'Test'

    :param text:    The string to search.
    """
    _vld.pass_if(_vld.is_text(text), TypeError,
                 "'text' attribute must be a string (got {!r})".format(text))
    return _const.CHAR_EMPTY.join(s for s in text if s.isalpha())