def cached_property(*args, **kwargs):
    """
    Decorator that creates a property that is cached.

    Keyword Arguments
    -----------------
    use_cache_ctrl : bool
        If True, the property is cached using the Node cache_ctrl. If False, the property is only cached as a private
        attribute. Default False.
    expires : float, datetime, timedelta
        Expiration date. If a timedelta is supplied, the expiration date will be calculated from the current time.
        Ignored if use_cache_ctrl=False.

    Notes
    -----
    Podpac caching using the cache_ctrl will be unreliable if the property depends on any non-tagged traits.
    The property should only use node attrs (traits tagged with ``attr=True``).

    Examples
    --------

    >>> class MyNode(Node):
        # property that is recomputed every time
        @property
        def my_property(self):
            return 0

        # property is computed once for each object
        @cached_property
        def my_cached_property(self):
            return 1

        # property that is computed once and can be reused by other Nodes or sessions, depending on the cache_ctrl
        @cached_property(use_cache_ctrl=True)
        def my_persistent_cached_property(self):
            return 2
    """

    use_cache_ctrl = kwargs.pop("use_cache_ctrl", False)
    expires = kwargs.pop("expires", None)

    if args and (len(args) != 1 or not callable(args[0])):
        raise TypeError("cached_property decorator does not accept any positional arguments")

    if kwargs:
        raise TypeError("cached_property decorator does not accept keyword argument '%s'" % list(kwargs.keys())[0])

    def d(fn):
        key = "_podpac_cached_property_%s" % fn.__name__

        @property
        def wrapper(self):
            if hasattr(self, key):
                value = getattr(self, key)
            elif use_cache_ctrl and self.has_cache(key):
                value = self.get_cache(key)
                setattr(self, key, value)
            else:
                value = fn(self)
                setattr(self, key, value)
                if use_cache_ctrl:
                    self.put_cache(value, key, expires=expires)
            return value

        return wrapper

    if args:
        return d(args[0])
    else:
        return d