<commit_before>"""
If Task B has Task A as a dependency, it means that the
Product of A should be used by B in some way (e.g. Task A produces a table
and Task B pivots it), placeholders help avoid redundancy when building tasks,
if you declare that Product A is "schema"."table", the use of placeholders
prevents "schema"."table" to be explicitely declared in B, since B depends
on A, this information from A is passed to B. Placeholders fill that purpose,
they are placeholders that will be filled at rendering time so that
parameteters are only declared once.

They serve a second, more advanced use case. It is recommended for Tasks to
have no parameters and be fully declared by specifying their code, product
and upstream dependencies. However, there is one use case where parameters
are useful: batch processing and parallelization. For example, if we are
operating on a 10-year databse, a single task might take too long, but we
could split the data in 1-year chunks and process them in parallel, in such
use case we could create 10 task instances, each one with a different year
parameters and process them independently. So, apart from upstream and product
placeholders, arbitrary parameters can also be placeholders.

These classes are not intended to be used by the end user, since Task and
Product objects create placeholders from strings.
"""
from pathlib import Path
import inspect

from dstools.templates.StrictTemplate import StrictTemplate

class TemplatedPlaceholder:
    """
    There are two types of placeholders, templated strings (whose
    parameteters are rendered using jinja templates and then executed) and
    native Python code (a callable object), which has no render logic, but
    still needs to offer the same API for compatibility, this class only
    helps identify which placeholders are from the first class since in some
    execution points, they need to be treated differently (e.g. for templated
    placeholders it is possible to check whether any passed parameter is
    unused at rendering time, since python code does not have a render step,
    this is not possible)

    """
    pass


class StringPlaceholder(TemplatedPlaceholder):
    """
    StringPlaceholders are StrictTemplates that store its rendered version
    in the same object so it can later be accesed
    """

    def __init__(self, source):
        if isinstance(source, Path):
            source = str(source)

        self._source = StrictTemplate(source)
        self._rendered_value = None

    @property
    def _rendered(self):
        if self._rendered_value is None:
            raise RuntimeError('Tried to read {} {} without '
                               'rendering first'
                               .format(type(self).__name__,
                                       repr(self)))

        return self._rendered_value

    def render(self, params, **kwargs):
        self._rendered_value = self._source.render(params, **kwargs)
        return self

    def __repr__(self):
        return 'Placeholder({})'.format(self._source.raw)

    def __str__(self):
        return self._rendered


class ClientCodePlaceholder(StringPlaceholder):
    """An object that represents client code

    Notes
    -----
    This is really just a StrictTemplate object that stores its rendered
    version in the same object and raises an Exception if attempted
    """

    def __init__(self, source):
        # the only difference between this and the original placeholder
        # is how they treat pathlib.Path
        self._source = StrictTemplate(source)
        self._rendered_value = None


class SQLRelationPlaceholder(TemplatedPlaceholder):
    """An identifier that represents a database relation (table or view)
    """

    def __init__(self, source):
        if len(source) != 3:
            raise ValueError('{} must be initialized with 3 elements, '
                             'got: {}'
                             .format(type(self).__name__, len(source)))

        schema, name, kind = source

        if schema is None:
            raise ValueError('schema cannot be None')

        if name is None:
            raise ValueError('name cannot be None')

        if kind not in ('view', 'table'):
            raise ValueError('kind must be one of ["view", "table"] '<commit_msg>adds a new method in ClientCodePlaceholder to save the rendered content to a tmp file<commit_after>"""
If Task B has Task A as a dependency, it means that the
Product of A should be used by B in some way (e.g. Task A produces a table
and Task B pivots it), placeholders help avoid redundancy when building tasks,
if you declare that Product A is "schema"."table", the use of placeholders
prevents "schema"."table" to be explicitely declared in B, since B depends
on A, this information from A is passed to B. Placeholders fill that purpose,
they are placeholders that will be filled at rendering time so that
parameteters are only declared once.

They serve a second, more advanced use case. It is recommended for Tasks to
have no parameters and be fully declared by specifying their code, product
and upstream dependencies. However, there is one use case where parameters
are useful: batch processing and parallelization. For example, if we are
operating on a 10-year databse, a single task might take too long, but we
could split the data in 1-year chunks and process them in parallel, in such
use case we could create 10 task instances, each one with a different year
parameters and process them independently. So, apart from upstream and product
placeholders, arbitrary parameters can also be placeholders.

These classes are not intended to be used by the end user, since Task and
Product objects create placeholders from strings.
"""
import tempfile
from pathlib import Path
import inspect

from dstools.templates.StrictTemplate import StrictTemplate


class TemplatedPlaceholder:
    """
    There are two types of placeholders, templated strings (whose
    parameteters are rendered using jinja templates and then executed) and
    native Python code (a callable object), which has no render logic, but
    still needs to offer the same API for compatibility, this class only
    helps identify which placeholders are from the first class since in some
    execution points, they need to be treated differently (e.g. for templated
    placeholders it is possible to check whether any passed parameter is
    unused at rendering time, since python code does not have a render step,
    this is not possible)

    """
    pass


class StringPlaceholder(TemplatedPlaceholder):
    """
    StringPlaceholders templated strings (using StrictTemplates) that store
    its rendered version in the same object so it can later be accesed,
    if a pathlib.Path object is used as source, it is casted to str. If the
    contents of the file represent the placeholder's content, use
    ClientCodePlaceholder instead
    """

    def __init__(self, source):
        if isinstance(source, Path):
            source = str(source)

        self._source = StrictTemplate(source)
        self._rendered_value = None

    @property
    def _rendered(self):
        if self._rendered_value is None:
            raise RuntimeError('Tried to read {} {} without '
                               'rendering first'
                               .format(type(self).__name__,
                                       repr(self)))

        return self._rendered_value

    def render(self, params, **kwargs):
        self._rendered_value = self._source.render(params, **kwargs)
        return self

    def __repr__(self):
        return 'Placeholder({})'.format(self._source.raw)

    def __str__(self):
        return self._rendered


class ClientCodePlaceholder(StringPlaceholder):
    """
    An object that represents client code, if a pathlib.Path object is passed,
    its contents are read and interpreted as the placeholder's content

    Notes
    -----
    This is really just a StrictTemplate object that stores its rendered
    version in the same object and raises an Exception if attempted
    """

    def __init__(self, source):
        # the only difference between this and the original placeholder
        # is how they treat pathlib.Path
        self._source = StrictTemplate(source)
        self._rendered_value = None

    def save_to_tmp_file(self):
        """Save contents of rendered version to a tmp file
        """
        _, path_to_tmp = tempfile.mkstemp()
        Path(path_to_tmp).write_text(str(self))
        return path_to_tmp


class SQLRelationPlaceholder(TemplatedPlaceholder):
    """An identifier that represents a database relation (table or view)
    """

    def __init__(self, source):
        if len(source) != 3:
            raise ValueError('{} must be initialized with 3 elements, '
                             'got: {}'
                             .format(type(self).__name__, len(source)))

        schema, name, kind = source

        if schema is None:
            raise ValueError('schema cannot be None')

        if name is None:
            raise ValueError('name cannot be None')

        if kind not in ('view', 'table'):
            raise ValueError('kind must be one of ["view", "table"] '
{'fraction_non_alphanumeric': 0.06003894418000866, 'fraction_numerical': 0.0015144958892254435, 'mean_word_length': 2.9008438818565403, 'pattern_counts': {'":': 0, '<': 3, '<?xml version=': 0, '>': 3, 'https://': 0, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 2, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '5005595', 'n_tokens_mistral': 2287, 'n_tokens_neox': 2215, 'n_words': 1114}