Repository: iterative/gto (language: python)

Issue:
Add pylint and mypy to pre-commit



Discussion / hints:

Fix patch:
diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
index c1ca8241..0acffe54 100644
--- a/.pre-commit-config.yaml
+++ b/.pre-commit-config.yaml
@@ -26,3 +26,20 @@ repos:
     hooks:
       - id: isort
         args: ["--profile", "black"]
+  - repo: 'https://github.com/pre-commit/mirrors-mypy'
+    rev: v0.910
+    hooks:
+      - id: mypy
+        additional_dependencies:
+          - types-requests
+          - types-six
+          - types-PyYAML
+          - pydantic
+          - types-filelock
+  - repo: local
+    hooks:
+      - id: pylint
+        name: pylint
+        entry: pylint
+        language: system
+        types: [ python ]
diff --git a/.pylintrc b/.pylintrc
new file mode 100644
index 00000000..baa2b08c
--- /dev/null
+++ b/.pylintrc
@@ -0,0 +1,635 @@
+[MASTER]
+
+# A comma-separated list of package or module names from where C extensions may
+# be loaded. Extensions are loading into the active Python interpreter and may
+# run arbitrary code.
+extension-pkg-allow-list=pydantic,pytest-lazy-fixture,pytest
+
+# A comma-separated list of package or module names from where C extensions may
+# be loaded. Extensions are loading into the active Python interpreter and may
+# run arbitrary code. (This is an alternative name to extension-pkg-allow-list
+# for backward compatibility.)
+extension-pkg-whitelist=
+
+# Return non-zero exit code if any of these messages/categories are detected,
+# even if score is above --fail-under value. Syntax same as enable. Messages
+# specified are enabled, while categories only check already-enabled messages.
+fail-on=
+
+# Specify a score threshold to be exceeded before program exits with error.
+fail-under=10.0
+
+# Files or directories to be skipped. They should be base names, not paths.
+ignore=CVS
+
+# Add files or directories matching the regex patterns to the ignore-list. The
+# regex matches against paths.
+ignore-paths=
+
+# Files or directories matching the regex patterns are skipped. The regex
+# matches against base names, not paths.
+ignore-patterns=
+
+# Python code to execute, usually for sys.path manipulation such as
+# pygtk.require().
+#init-hook=
+
+# Use multiple processes to speed up Pylint. Specifying 0 will auto-detect the
+# number of processors available to use.
+jobs=0
+
+# Control the amount of potential inferred values when inferring a single
+# object. This can help the performance when dealing with large functions or
+# complex, nested conditions.
+limit-inference-results=100
+
+# List of plugins (as comma separated values of python module names) to load,
+# usually to register additional checkers.
+load-plugins=pylint_pytest
+
+# Pickle collected data for later comparisons.
+persistent=yes
+
+# When enabled, pylint would attempt to guess common misconfiguration and emit
+# user-friendly hints instead of false-positive error messages.
+suggestion-mode=yes
+
+# Allow loading of arbitrary C extensions. Extensions are imported into the
+# active Python interpreter and may run arbitrary code.
+unsafe-load-any-extension=no
+
+
+[MESSAGES CONTROL]
+
+# Only show warnings with the listed confidence levels. Leave empty to show
+# all. Valid levels: HIGH, INFERENCE, INFERENCE_FAILURE, UNDEFINED.
+confidence=
+
+# Disable the message, report, category or checker with the given id(s). You
+# can either give multiple identifiers separated by comma (,) or put this
+# option multiple times (only on the command line, not in the configuration
+# file where it should appear only once). You can also use "--disable=all" to
+# disable everything first and then reenable specific checks. For example, if
+# you want to run only the similarities checker, you can use "--disable=all
+# --enable=similarities". If you want to run only the classes checker, but have
+# no Warning level messages displayed, use "--disable=all --enable=classes
+# --disable=W".
+disable=print-statement,
+        parameter-unpacking,
+        unpacking-in-except,
+        old-raise-syntax,
+        backtick,
+        long-suffix,
+        old-ne-operator,
+        old-octal-literal,
+        import-star-module-level,
+        non-ascii-bytes-literal,
+        raw-checker-failed,
+        bad-inline-option,
+        locally-disabled,
+        file-ignored,
+        suppressed-message,
+        useless-suppression,
+        deprecated-pragma,
+        use-symbolic-message-instead,
+        apply-builtin,
+        basestring-builtin,
+        buffer-builtin,
+        cmp-builtin,
+        coerce-builtin,
+        execfile-builtin,
+        file-builtin,
+        long-builtin,
+        raw_input-builtin,
+        reduce-builtin,
+        standarderror-builtin,
+        unicode-builtin,
+        xrange-builtin,
+        coerce-method,
+        delslice-method,
+        getslice-method,
+        setslice-method,
+        no-absolute-import,
+        old-division,
+        dict-iter-method,
+        dict-view-method,
+        next-method-called,
+        metaclass-assignment,
+        indexing-exception,
+        raising-string,
+        reload-builtin,
+        oct-method,
+        hex-method,
+        nonzero-method,
+        cmp-method,
+        input-builtin,
+        round-builtin,
+        intern-builtin,
+        unichr-builtin,
+        map-builtin-not-iterating,
+        zip-builtin-not-iterating,
+        range-builtin-not-iterating,
+        filter-builtin-not-iterating,
+        using-cmp-argument,
+        eq-without-hash,
+        div-method,
+        idiv-method,
+        rdiv-method,
+        exception-message-attribute,
+        invalid-str-codec,
+        sys-max-int,
+        bad-python3-import,
+        deprecated-string-function,
+        deprecated-str-translate-call,
+        deprecated-itertools-function,
+        deprecated-types-field,
+        next-method-defined,
+        dict-items-not-iterating,
+        dict-keys-not-iterating,
+        dict-values-not-iterating,
+        deprecated-operator-function,
+        deprecated-urllib-function,
+        xreadlines-attribute,
+        deprecated-sys-function,
+        exception-escape,
+        comprehension-escape,
+        # all above are defaults; below is what was added manually
+        fixme,
+        missing-module-docstring, # TODO
+        missing-function-docstring, # TODO
+        missing-class-docstring, # TODO
+        empty-docstring, # TODO
+        line-too-long, # TODO
+        too-many-arguments,
+        too-few-public-methods,
+        redefined-builtin, # TODO
+        wrong-import-order, # handeled by isort,
+        cannot-enumerate-pytest-fixtures, # TODO
+
+# Enable the message, report, category or checker with the given id(s). You can
+# either give multiple identifier separated by comma (,) or put this option
+# multiple time (only on the command line, not in the configuration file where
+# it should appear only once). See also the "--disable" option for examples.
+enable=c-extension-no-member
+
+
+[REPORTS]
+
+# Python expression which should return a score less than or equal to 10. You
+# have access to the variables 'error', 'warning', 'refactor', and 'convention'
+# which contain the number of messages in each category, as well as 'statement'
+# which is the total number of statements analyzed. This score is used by the
+# global evaluation report (RP0004).
+evaluation=10.0 - ((float(5 * error + warning + refactor + convention) / statement) * 10)
+
+# Template used to display messages. This is a python new-style format string
+# used to format the message information. See doc for all details.
+#msg-template=
+
+# Set the output format. Available formats are text, parseable, colorized, json
+# and msvs (visual studio). You can also give a reporter class, e.g.
+# mypackage.mymodule.MyReporterClass.
+output-format=text
+
+# Tells whether to display a full report or only the messages.
+reports=no
+
+# Activate the evaluation score.
+score=yes
+
+
+[REFACTORING]
+
+# Maximum number of nested blocks for function / method body
+max-nested-blocks=5
+
+# Complete name of functions that never returns. When checking for
+# inconsistent-return-statements if a never returning function is called then
+# it will be considered as an explicit return statement and no message will be
+# printed.
+never-returning-functions=sys.exit,argparse.parse_error
+
+
+[LOGGING]
+
+# The type of string formatting that logging methods do. `old` means using %
+# formatting, `new` is for `{}` formatting.
+logging-format-style=old
+
+# Logging modules to check that the string format arguments are in logging
+# function parameter format.
+logging-modules=logging
+
+
+[SPELLING]
+
+# Limits count of emitted suggestions for spelling mistakes.
+max-spelling-suggestions=4
+
+# Spelling dictionary name. Available dictionaries: none. To make it work,
+# install the 'python-enchant' package.
+spelling-dict=
+
+# List of comma separated words that should be considered directives if they
+# appear and the beginning of a comment and should not be checked.
+spelling-ignore-comment-directives=fmt: on,fmt: off,noqa:,noqa,nosec,isort:skip,mypy:
+
+# List of comma separated words that should not be checked.
+spelling-ignore-words=
+
+# A path to a file that contains the private dictionary; one word per line.
+spelling-private-dict-file=
+
+# Tells whether to store unknown words to the private dictionary (see the
+# --spelling-private-dict-file option) instead of raising a message.
+spelling-store-unknown-words=no
+
+
+[MISCELLANEOUS]
+
+# List of note tags to take in consideration, separated by a comma.
+notes=FIXME,
+      XXX,
+      TODO
+
+# Regular expression of note tags to take in consideration.
+#notes-rgx=
+
+
+[TYPECHECK]
+
+# List of decorators that produce context managers, such as
+# contextlib.contextmanager. Add to this list to register other decorators that
+# produce valid context managers.
+contextmanager-decorators=contextlib.contextmanager
+
+# List of members which are set dynamically and missed by pylint inference
+# system, and so shouldn't trigger E1101 when accessed. Python regular
+# expressions are accepted.
+generated-members=
+
+# Tells whether missing members accessed in mixin class should be ignored. A
+# mixin class is detected if its name ends with "mixin" (case insensitive).
+ignore-mixin-members=yes
+
+# Tells whether to warn about missing members when the owner of the attribute
+# is inferred to be None.
+ignore-none=yes
+
+# This flag controls whether pylint should warn about no-member and similar
+# checks whenever an opaque object is returned when inferring. The inference
+# can return multiple potential results while evaluating a Python object, but
+# some branches might not be evaluated, which results in partial inference. In
+# that case, it might be useful to still emit no-member and other checks for
+# the rest of the inferred objects.
+ignore-on-opaque-inference=yes
+
+# List of class names for which member attributes should not be checked (useful
+# for classes with dynamically set attributes). This supports the use of
+# qualified names.
+ignored-classes=optparse.Values,thread._local,_thread._local
+
+# List of module names for which member attributes should not be checked
+# (useful for modules/projects where namespaces are manipulated during runtime
+# and thus existing member attributes cannot be deduced by static analysis). It
+# supports qualified module names, as well as Unix pattern matching.
+ignored-modules=
+
+# Show a hint with possible names when a member name was not found. The aspect
+# of finding the hint is based on edit distance.
+missing-member-hint=yes
+
+# The minimum edit distance a name should have in order to be considered a
+# similar match for a missing member name.
+missing-member-hint-distance=1
+
+# The total number of similar names that should be taken in consideration when
+# showing a hint for a missing member.
+missing-member-max-choices=1
+
+# List of decorators that change the signature of a decorated function.
+signature-mutators=
+
+
+[VARIABLES]
+
+# List of additional names supposed to be defined in builtins. Remember that
+# you should avoid defining new builtins when possible.
+additional-builtins=
+
+# Tells whether unused global variables should be treated as a violation.
+allow-global-unused-variables=yes
+
+# List of names allowed to shadow builtins
+allowed-redefined-builtins=
+
+# List of strings which can identify a callback function by name. A callback
+# name must start or end with one of those strings.
+callbacks=cb_,
+          _cb
+
+# A regular expression matching the name of dummy variables (i.e. expected to
+# not be used).
+dummy-variables-rgx=_+$|(_[a-zA-Z0-9_]*[a-zA-Z0-9]+?$)|dummy|^ignored_|^unused_
+
+# Argument names that match this expression will be ignored. Default to name
+# with leading underscore.
+ignored-argument-names=_.*|^ignored_|^unused_
+
+# Tells whether we should check for unused import in __init__ files.
+init-import=no
+
+# List of qualified module names which can have objects that can redefine
+# builtins.
+redefining-builtins-modules=six.moves,past.builtins,future.builtins,builtins,io
+
+
+[FORMAT]
+
+# Expected format of line ending, e.g. empty (any line ending), LF or CRLF.
+expected-line-ending-format=
+
+# Regexp for a line that is allowed to be longer than the limit.
+ignore-long-lines=^\s*(# )?<?https?://\S+>?$
+
+# Number of spaces of indent required inside a hanging or continued line.
+indent-after-paren=4
+
+# String used as indentation unit. This is usually "    " (4 spaces) or "\t" (1
+# tab).
+indent-string='    '
+
+# Maximum number of characters on a single line.
+max-line-length=100
+
+# Maximum number of lines in a module.
+max-module-lines=1000
+
+# Allow the body of a class to be on the same line as the declaration if body
+# contains single statement.
+single-line-class-stmt=no
+
+# Allow the body of an if to be on the same line as the test if there is no
+# else.
+single-line-if-stmt=no
+
+
+[SIMILARITIES]
+
+# Ignore comments when computing similarities.
+ignore-comments=yes
+
+# Ignore docstrings when computing similarities.
+ignore-docstrings=yes
+
+# Ignore imports when computing similarities.
+ignore-imports=no
+
+# Ignore function signatures when computing similarities.
+ignore-signatures=no
+
+# Minimum lines number of a similarity.
+min-similarity-lines=15
+
+
+[BASIC]
+
+# Naming style matching correct argument names.
+argument-naming-style=snake_case
+
+# Regular expression matching correct argument names. Overrides argument-
+# naming-style.
+#argument-rgx=
+
+# Naming style matching correct attribute names.
+attr-naming-style=snake_case
+
+# Regular expression matching correct attribute names. Overrides attr-naming-
+# style.
+#attr-rgx=
+
+# Bad variable names which should always be refused, separated by a comma.
+bad-names=foo,
+          bar,
+          baz,
+          toto,
+          tutu,
+          tata
+
+# Bad variable names regexes, separated by a comma. If names match any regex,
+# they will always be refused
+bad-names-rgxs=
+
+# Naming style matching correct class attribute names.
+class-attribute-naming-style=any
+
+# Regular expression matching correct class attribute names. Overrides class-
+# attribute-naming-style.
+#class-attribute-rgx=
+
+# Naming style matching correct class constant names.
+class-const-naming-style=UPPER_CASE
+
+# Regular expression matching correct class constant names. Overrides class-
+# const-naming-style.
+#class-const-rgx=
+
+# Naming style matching correct class names.
+class-naming-style=PascalCase
+
+# Regular expression matching correct class names. Overrides class-naming-
+# style.
+#class-rgx=
+
+# Naming style matching correct constant names.
+const-naming-style=UPPER_CASE
+
+# Regular expression matching correct constant names. Overrides const-naming-
+# style.
+#const-rgx=
+
+# Minimum line length for functions/classes that require docstrings, shorter
+# ones are exempt.
+docstring-min-length=-1
+
+# Naming style matching correct function names.
+function-naming-style=snake_case
+
+# Regular expression matching correct function names. Overrides function-
+# naming-style.
+#function-rgx=
+
+# Good variable names which should always be accepted, separated by a comma.
+good-names=i,
+           j,
+           k,
+           ex,
+           Run,
+           _
+
+# Good variable names regexes, separated by a comma. If names match any regex,
+# they will always be accepted
+good-names-rgxs=
+
+# Include a hint for the correct naming format with invalid-name.
+include-naming-hint=no
+
+# Naming style matching correct inline iteration names.
+inlinevar-naming-style=any
+
+# Regular expression matching correct inline iteration names. Overrides
+# inlinevar-naming-style.
+#inlinevar-rgx=
+
+# Naming style matching correct method names.
+method-naming-style=snake_case
+
+# Regular expression matching correct method names. Overrides method-naming-
+# style.
+#method-rgx=
+
+# Naming style matching correct module names.
+module-naming-style=snake_case
+
+# Regular expression matching correct module names. Overrides module-naming-
+# style.
+#module-rgx=
+
+# Colon-delimited sets of names that determine each other's naming style when
+# the name regexes allow several styles.
+name-group=
+
+# Regular expression which should only match function or class names that do
+# not require a docstring.
+no-docstring-rgx=^_
+
+# List of decorators that produce properties, such as abc.abstractproperty. Add
+# to this list to register other decorators that produce valid properties.
+# These decorators are taken in consideration only for invalid-name.
+property-classes=abc.abstractproperty
+
+# Naming style matching correct variable names.
+variable-naming-style=snake_case
+
+# Regular expression matching correct variable names. Overrides variable-
+# naming-style.
+#variable-rgx=
+
+
+[STRING]
+
+# This flag controls whether inconsistent-quotes generates a warning when the
+# character used as a quote delimiter is used inconsistently within a module.
+check-quote-consistency=no
+
+# This flag controls whether the implicit-str-concat should generate a warning
+# on implicit string concatenation in sequences defined over several lines.
+check-str-concat-over-line-jumps=no
+
+
+[IMPORTS]
+
+# List of modules that can be imported at any level, not just the top level
+# one.
+allow-any-import-level=
+
+# Allow wildcard imports from modules that define __all__.
+allow-wildcard-with-all=no
+
+# Analyse import fallback blocks. This can be used to support both Python 2 and
+# 3 compatible code, which means that the block might have code that exists
+# only in one or another interpreter, leading to false positives when analysed.
+analyse-fallback-blocks=no
+
+# Deprecated modules which should not be used, separated by a comma.
+deprecated-modules=
+
+# Output a graph (.gv or any supported image format) of external dependencies
+# to the given file (report RP0402 must not be disabled).
+ext-import-graph=
+
+# Output a graph (.gv or any supported image format) of all (i.e. internal and
+# external) dependencies to the given file (report RP0402 must not be
+# disabled).
+import-graph=
+
+# Output a graph (.gv or any supported image format) of internal dependencies
+# to the given file (report RP0402 must not be disabled).
+int-import-graph=
+
+# Force import order to recognize a module as part of the standard
+# compatibility libraries.
+known-standard-library=
+
+# Force import order to recognize a module as part of a third party library.
+known-third-party=enchant
+
+# Couples of modules and preferred modules, separated by a comma.
+preferred-modules=
+
+
+[CLASSES]
+
+# Warn about protected attribute access inside special methods
+check-protected-access-in-special-methods=no
+
+# List of method names used to declare (i.e. assign) instance attributes.
+defining-attr-methods=__init__,
+                      __new__,
+                      setUp,
+                      __post_init__
+
+# List of member names, which should be excluded from the protected access
+# warning.
+exclude-protected=_asdict,
+                  _fields,
+                  _replace,
+                  _source,
+                  _make
+
+# List of valid names for the first argument in a class method.
+valid-classmethod-first-arg=cls
+
+# List of valid names for the first argument in a metaclass class method.
+valid-metaclass-classmethod-first-arg=cls
+
+
+[DESIGN]
+
+# Maximum number of arguments for function / method.
+max-args=5
+
+# Maximum number of attributes for a class (see R0902).
+max-attributes=7
+
+# Maximum number of boolean expressions in an if statement (see R0916).
+max-bool-expr=5
+
+# Maximum number of branch for function / method body.
+max-branches=12
+
+# Maximum number of locals for function / method body.
+max-locals=15
+
+# Maximum number of parents for a class (see R0901).
+max-parents=7
+
+# Maximum number of public methods for a class (see R0904).
+max-public-methods=20
+
+# Maximum number of return / yield for function / method body.
+max-returns=6
+
+# Maximum number of statements in function / method body.
+max-statements=50
+
+# Minimum number of public methods for a class (see R0903).
+min-public-methods=2
+
+
+[EXCEPTIONS]
+
+# Exceptions that will emit a warning when being caught. Defaults to
+# "BaseException, Exception".
+overgeneral-exceptions=BaseException,
+                       Exception
diff --git a/gitops/__init__.py b/gitops/__init__.py
index 23e061b9..9dd73f2f 100644
--- a/gitops/__init__.py
+++ b/gitops/__init__.py
@@ -4,8 +4,9 @@
 from .tag import TagBasedRegistry
 
 
-def init_registry(repo: git.Repo = git.Repo("."), base=CONFIG.BASE):
+def init_registry(repo=".", base=CONFIG.BASE):
+    repo = git.Repo(repo)
     if base != "tag":
         raise ValueError("Other implementations except of tag-based aren't supported")
     base_map = {"tag": TagBasedRegistry}
-    return base_map[base]()
+    return base_map[base](repo=repo)
diff --git a/gitops/base.py b/gitops/base.py
index dfab32ee..5d3185bd 100644
--- a/gitops/base.py
+++ b/gitops/base.py
@@ -1,4 +1,4 @@
-from typing import List, Optional
+from typing import Dict, List, Optional
 
 import click
 import git
@@ -6,7 +6,7 @@
 
 from .config import CONFIG
 from .exceptions import (
-    ModelNotFound,
+    ObjectNotFound,
     UnknownEnvironment,
     VersionAlreadyRegistered,
     VersionExistsForCommit,
@@ -14,16 +14,22 @@
 )
 
 
-class BaseLabel:
-    model: str
-    version: str
-    name: str
+class BaseLabel:  # pylint: disable=too-many-instance-attributes
     unregistered_date: Optional[pd.Timestamp] = None
 
     def __init__(
-        self, model, version, label, creation_date, author, commit_hexsha, tag_name
+        self,
+        category,
+        object,
+        version,
+        label,
+        creation_date,
+        author,
+        commit_hexsha,
+        tag_name,
     ) -> None:
-        self.model = model
+        self.category = category
+        self.object = object
         self.version = version
         self.name = label
         self.creation_date = creation_date
@@ -32,19 +38,20 @@ def __init__(
         self.tag_name = tag_name
 
     def __repr__(self) -> str:
-        return f"Label('{self.model}', '{self.version}', '{self.name}')"
+        return f"Label('{self.object}', '{self.version}', '{self.name}')"
 
 
 class BaseVersion:
-    model: str
+    object: str
     name: str
     creation_date: str
     unregistered_date: Optional[pd.Timestamp] = None
 
     def __init__(
-        self, model, version, creation_date, author, commit_hexsha, tag_name
+        self, category, object, version, creation_date, author, commit_hexsha, tag_name
     ) -> None:
-        self.model = model
+        self.category = category
+        self.object = object
         self.name = version
         self.creation_date = creation_date
         self.author = author
@@ -52,15 +59,12 @@ def __init__(
         self.tag_name = tag_name
 
     def __repr__(self) -> str:
-        return f"Version('{self.model}', '{self.name}')"
-
+        return f"Version('{self.object}', '{self.name}')"
 
-class BaseModel:
-    name: str
-    versions: List[BaseVersion]
-    labels: List[BaseLabel]
 
-    def __init__(self, name, versions, labels) -> None:
+class BaseObject:
+    def __init__(self, category, name, versions, labels) -> None:
+        self.category = category
         self.name = name
         self._versions = versions
         self.labels = labels
@@ -72,7 +76,7 @@ def unique_labels(self):
     def __repr__(self) -> str:
         versions = ", ".join(f"'{v.name}'" for v in self.versions)
         labels = ", ".join(f"'{l}'" for l in self.unique_labels)
-        return f"Model(versions=[{versions}], labels=[{labels}])"
+        return f"Object(versions=[{versions}], labels=[{labels}])"
 
     @property
     def latest_version(self) -> str:
@@ -82,19 +86,19 @@ def latest_version(self) -> str:
         )[-1].name
 
     @property
-    def latest_labels(self) -> List[BaseLabel]:
+    def latest_labels(self) -> Dict[str, Optional[BaseLabel]]:
         labels = {}
-        for l in self.unique_labels:
+        for label in self.unique_labels:
             found = sorted(
                 filter(
-                    lambda x: x.name == l
+                    lambda x: x.name == label  # pylint: disable=cell-var-from-loop
                     and x.unregistered_date is None
                     and self.find_version(x.version) is not None,
                     self.labels,
                 ),
                 key=lambda x: x.creation_date,
             )
-            labels[l] = found[-1] if found else None
+            labels[label] = found[-1] if found else None
         return labels
 
     @property
@@ -117,7 +121,7 @@ def find_version(
         ]
         if raise_if_not_found:
             assert len(versions) == 1, (
-                f"{len(versions)} versions of model {self.name} found"
+                f"{len(versions)} versions of object {self.name} found"
                 + ", skipping unregistered"
                 if skip_unregistered
                 else ""
@@ -125,7 +129,7 @@ def find_version(
             return versions[0]
 
         assert len(versions) <= 1, (
-            f"{len(versions)} versions of model {self.name} found"
+            f"{len(versions)} versions of object {self.name} found"
             + ", skipping unregistered"
             if skip_unregistered
             else ""
@@ -135,19 +139,22 @@ def find_version(
 
 class BaseRegistry:
     repo: git.Repo
-    models: List[BaseModel]
+    objects: List[BaseObject]
     config = CONFIG
+    Object: BaseObject
 
     def __init__(self, repo: git.Repo = git.Repo(".")):
         self.repo = repo
 
-    def find_model(self, model, allow_new=False):
-        models = [m for m in self.models if m.name == model]
-        if allow_new and not models:
-            return self.Model(model, [], [])
-        if not models:
-            raise ModelNotFound(model)
-        return models[0]
+    def find_object(self, category, object, allow_new=False):
+        objects = [
+            m for m in self.objects if m.name == object and m.category == category
+        ]
+        if allow_new and not objects:
+            return self.Object(category, object, [], [])
+        if not objects:
+            raise ObjectNotFound(object)
+        return objects[0]
 
     @property
     def _labels(self):
@@ -157,42 +164,52 @@ def _labels(self):
     def labels(self):
         return sorted(set(self._labels))
 
-    def _register(self, model, version, ref, message):
+    def _register(self, category, object, version, ref, message):
         raise NotImplementedError
 
-    def register(self, model, version, ref=None):
-        """Register model version"""
+    def register(self, category, object, version, ref=None):
+        """Register object version"""
         if ref is None:
             ref = self.repo.active_branch.commit.hexsha
-        found_model = self.find_model(model, allow_new=True)
-        found_version = found_model.find_version(version, skip_unregistered=False)
+        found_object = self.find_object(category, object, allow_new=True)
+        found_version = found_object.find_version(version, skip_unregistered=False)
         if found_version is not None:
             raise VersionAlreadyRegistered(version)
-        found_version = found_model.find_version(None, ref, skip_unregistered=True)
+        found_version = found_object.find_version(None, ref, skip_unregistered=True)
         if found_version is not None:
-            raise VersionExistsForCommit(model, found_version.name)
+            raise VersionExistsForCommit(object, found_version.name)
         if (
-            found_model.versions
-            and self.config.__versions__(version) < found_model.latest_version
+            found_object.versions
+            and self.config.__versions__(version) < found_object.latest_version
         ):
-            raise VersionIsOld(latest=found_model.latest_version, suggested=version)
+            raise VersionIsOld(latest=found_object.latest_version, suggested=version)
         self._register(
-            model, version, ref, message=f"Registering model {model} version {version}"
+            category,
+            object,
+            version,
+            ref,
+            message=f"Registering object {object} version {version}",
         )
 
-    def unregister(self, model, version):
+    def unregister(self, category, object, version):
         raise NotImplementedError
 
-    def find_commit(self, model, version):
+    def find_commit(self, category, object, version):
         raise NotImplementedError
 
-    def _promote(model, label, ref, message):
+    def _promote(self, category, object, label, ref, message):
         raise NotImplementedError
 
     def promote(
-        self, model, label, promote_version=None, promote_commit=None, name_version=None
+        self,
+        category,
+        object,
+        label,
+        promote_version=None,
+        promote_commit=None,
+        name_version=None,
     ):
-        """Assign label to specific model version"""
+        """Assign label to specific object version"""
         if label not in self.config.ENVIRONMENTS:
             raise UnknownEnvironment(label)
         if promote_version is None and promote_commit is None:
@@ -200,48 +217,58 @@ def promote(
         if promote_version is not None and promote_commit is not None:
             raise ValueError("Only one of version or commit must be specified")
         try:
-            found_model = self.find_model(model)
-        except ModelNotFound:
+            found_object = self.find_object(category, object)
+        except ObjectNotFound as exc:
             raise BaseException(
-                "To promote a model automatically you need to manually register it once."
-            )
+                "To promote a object automatically you need to manually register it once."
+            ) from exc
         if promote_version is not None:
-            found_version = found_model.find_version(promote_version)
+            found_version = found_object.find_version(promote_version)
             if found_version is None:
                 raise BaseException("Version is not found")
-            promote_commit = self.find_commit(model, promote_version)
+            promote_commit = self.find_commit(category, object, promote_version)
         else:
-            found_version = found_model.find_version(None, promote_commit)
+            found_version = found_object.find_version(None, promote_commit)
             if found_version is None:
                 if name_version is None:
-                    last_version = self.find_model(model).latest_version
+                    last_version = self.find_object(category, object).latest_version
                     promote_version = (
                         self.config.__versions__(last_version).bump().version
                     )
-                self.register(model, name_version, ref=promote_commit)
+                self.register(category, object, name_version, ref=promote_commit)
                 click.echo(
-                    f"Registered new version '{promote_version}' of model '{model}' at commit '{promote_commit}'"
+                    f"Registered new version '{promote_version}' of {category} '{object}' at commit '{promote_commit}'"
                 )
         self._promote(
-            model,
+            category,
+            object,
             label,
             ref=promote_commit,
-            message=f"Promoting model {model} version {promote_version} to label {label}",
+            message=f"Promoting {category} {object} version {promote_version} to label {label}",
         )
         return {"version": promote_version}
 
-    def demote(self, model, label):
-        """De-promote model from given label"""
+    def _demote(self, category, object, label, message):
+        raise NotImplementedError
+
+    def demote(self, category, object, label):
+        """De-promote object from given label"""
         # TODO: check if label wasn't demoted already
         assert (
-            self.find_model(model).latest_labels.get(label) is not None
-        ), f"No active label '{label}' was found for model '{model}'"
-        self._demote(model, label, message=f"Demoting model {model} from label {label}")
+            self.find_object(category, object).latest_labels.get(label) is not None
+        ), f"No active label '{label}' was found for {category} '{object}'"
+        return self._demote(
+            category,
+            object,
+            label,
+            message=f"Demoting {category} {object} from label {label}",
+        )
 
-    def which(self, model, label, raise_if_not_found=True):
-        """Return version of model with specific label active"""
-        latest_labels = self.find_model(model).latest_labels
+    def which(self, category, object, label, raise_if_not_found=True):
+        """Return version of object with specific label active"""
+        latest_labels = self.find_object(category, object).latest_labels
         if label in latest_labels:
             return latest_labels[label].version
         if raise_if_not_found:
-            raise ValueError(f"Label {label} not found for model {model}")
+            raise ValueError(f"Label {label} not found for {category} {object}")
+        return None
diff --git a/gitops/cli.py b/gitops/cli.py
index 09ca0fd6..1790a0d2 100644
--- a/gitops/cli.py
+++ b/gitops/cli.py
@@ -7,45 +7,49 @@
 
 from . import init_registry
 
+arg_category = click.argument("category")
+arg_object = click.argument("object")
+arg_version = click.argument("version")
+arg_label = click.argument("label")
+
 
 @click.group()
 def cli():
     """Early prototype for registering/label assignment for tags-based approach"""
-    pass
 
 
 @cli.command()
-@click.argument("model")
-@click.argument("version")
-def register(model, version):
-    """Register new model version"""
-    init_registry().register(model, version)
-    click.echo(f"Registered model {model} version {version}")
+@arg_category
+@arg_object
+@arg_version
+def register(category, object, version):
+    """Register new object version"""
+    init_registry().register(category, object, version)
+    click.echo(f"Registered {category} {object} version {version}")
 
 
 @cli.command()
-@click.argument("model")
-@click.argument("version")
-def unregister(model, version):
-    """Unregister model version"""
-    init_registry().unregister(model, version)
-    click.echo(f"Unregistered model {model} version {version}")
+@arg_category
+@arg_object
+@arg_version
+def unregister(category, object, version):
+    """Unregister object version"""
+    init_registry().unregister(category, object, version)
+    click.echo(f"Unregistered {category} {object} version {version}")
 
 
 @cli.command()
-@click.argument("model")
-@click.argument("label")
+@arg_category
+@arg_object
+@arg_label
 @click.option(
     "--version",
     default=None,
     help="If you provide --commit, this will be used to name new version",
 )
-@click.option(
-    "--commit",
-    default=None,
-)
-def promote(model, label, version, commit):
-    """Assign label to specific model version"""
+@click.option("--commit", default=None)
+def promote(category, object, label, version, commit):
+    """Assign label to specific object version"""
     if commit is not None:
         name_version = version
         promote_version = None
@@ -53,38 +57,47 @@ def promote(model, label, version, commit):
         name_version = None
         promote_version = version
     result = init_registry().promote(
-        model, label, promote_version, commit, name_version
+        category, object, label, promote_version, commit, name_version
+    )
+    click.echo(
+        f"Promoted {category} {object} version {result['version']} to label {label}"
     )
-    click.echo(f"Promoted model {model} version {result['version']} to label {label}")
 
 
 @cli.command()
-@click.argument("model")
-def latest(model):
-    """Return latest version for model"""
-    model = [m for m in init_registry().models if m.name == model][0]
-    click.echo(model.latest_version)
+@arg_category
+@arg_object
+def latest(category, object):
+    """Return latest version for object"""
+    obj = [
+        m
+        for m in init_registry().objects
+        if m.name == object and m.category == category
+    ][0]
+    click.echo(obj.latest_version)
 
 
 @cli.command()
-@click.argument("model")
-@click.argument("label")
-def which(model, label):
-    """Return version of model with specific label active"""
-    version = init_registry().which(model, label, raise_if_not_found=False)
+@arg_category
+@arg_object
+@arg_label
+def which(category, object, label):
+    """Return version of object with specific label active"""
+    version = init_registry().which(category, object, label, raise_if_not_found=False)
     if version:
         click.echo(version)
     else:
-        click.echo(f"No version of model '{model}' with label '{label}' active")
+        click.echo(f"No version of {category} '{object}' with label '{label}' active")
 
 
 @cli.command()
-@click.argument("model")
-@click.argument("label")
-def demote(model, label):
-    """De-promote model from given label"""
-    init_registry().demote(model, label)
-    click.echo(f"Demoted model {model} from label {label}")
+@arg_category
+@arg_object
+@arg_label
+def demote(category, object, label):
+    """De-promote object from given label"""
+    init_registry().demote(category, object, label)
+    click.echo(f"Demoted {category} {object} from label {label}")
 
 
 @cli.command()
@@ -107,7 +120,7 @@ def show():
                 for l in m.unique_labels
             ]
         )
-        for m in reg.models
+        for m in reg.objects
     }
     print("\n=== Current labels (MLflow dashboard) ===")
     display(pd.DataFrame.from_records(models_state).T)
@@ -123,7 +136,7 @@ def show():
             "tag_name": l.tag_name,
             "unregistered_date": l.unregistered_date,
         }
-        for m in reg.models
+        for m in reg.objects
         for l in m.labels
     ]
     print("\n=== Label assignment audit trail ===")
@@ -143,7 +156,7 @@ def show():
             "tag_name": v.tag_name,
             "unregistered_date": v.unregistered_date,
         }
-        for m in reg.models
+        for m in reg.objects
         for v in m.versions
     ]
     print("\n=== Model registration audit trail ===")
diff --git a/gitops/config.py b/gitops/config.py
index 7dbf12ce..f0ac52ee 100644
--- a/gitops/config.py
+++ b/gitops/config.py
@@ -4,6 +4,8 @@
 import yaml
 from pydantic import BaseSettings
 
+from .versions import NumberedVersion, SemVer
+
 CONFIG_FILE = Path(__file__).parent.parent / "gitops_config.yaml"
 
 
@@ -46,14 +48,10 @@ def customise_sources(
 
     @property
     def __versions__(self):
-        if self.VERSIONS == "NumberedVersion":
-            from .versions import NumberedVersion
-
-            return NumberedVersion
-        elif self.VERSIONS == "SemVer":
-            from .versions import SemVer
-
-            return SemVer
+        versions = {"NumberedVersion": NumberedVersion, "SemVer": SemVer}
+        if self.VERSIONS not in versions:
+            raise ValueError(f"Unknown versioning system {self.VERSIONS}")
+        return versions[self.VERSIONS]
 
 
 CONFIG = RegistryConfig()
diff --git a/gitops/exceptions.py b/gitops/exceptions.py
index 5bceb0a5..c8a6cf02 100644
--- a/gitops/exceptions.py
+++ b/gitops/exceptions.py
@@ -1,7 +1,7 @@
 from .config import CONFIG
 
 
-class BaseException(Exception):
+class BaseException(Exception):  # pylint: disable=redefined-builtin
     """Base class for all prototype exceptions."""
 
     def __init__(self, msg, *args):
@@ -10,7 +10,7 @@ def __init__(self, msg, *args):
         super().__init__(msg, *args)
 
 
-class ModelNotFound(BaseException):
+class ObjectNotFound(BaseException):
     _message = "Requested model '{model}' wasn't found in registry"
 
     def __init__(self, model) -> None:
diff --git a/gitops/tag.py b/gitops/tag.py
index f2103384..8e83df47 100644
--- a/gitops/tag.py
+++ b/gitops/tag.py
@@ -3,21 +3,28 @@
 import git
 import pandas as pd
 
-from .base import BaseLabel, BaseModel, BaseRegistry, BaseVersion
+from .base import BaseLabel, BaseObject, BaseRegistry, BaseVersion
 
 REGISTER = "register"
 UNREGISTER = "unregister"
 PROMOTE = "promote"
 DEMOTE = "demote"
 
+ACTION = "action"
+CATEGORY = "category"
+OBJECT = "object"
+VERSION = "version"
+LABEL = "label"
+NUMBER = "number"
 
-def name(action, model, version=None, label=None, repo=None):
+
+def name_tag(action, category, object, version=None, label=None, repo=None):
     if action == REGISTER:
-        return f"model-{model}-{REGISTER}-{version}"
+        return f"{category}-{object}-{REGISTER}-{version}"
     if action == UNREGISTER:
-        return f"model-{model}-{UNREGISTER}-{version}"
+        return f"{category}-{object}-{UNREGISTER}-{version}"
 
-    basename = f"model-{model}-{PROMOTE}-{label}"
+    basename = f"{category}-{object}-{PROMOTE}-{label}"
     existing_names = [c.name for c in repo.tags if c.name.startswith(basename)]
     if existing_names:
         last_number = 1 + max(int(n[len(basename) + 1 :]) for n in existing_names)
@@ -26,43 +33,65 @@ def name(action, model, version=None, label=None, repo=None):
     if action == PROMOTE:
         return f"{basename}-{last_number}"
     if action == DEMOTE:
-        return f"model-{model}-{DEMOTE}-{label}-{last_number}"
+        return f"{category}-{object}-{DEMOTE}-{label}-{last_number}"
     raise ValueError(f"Unknown action: {action}")
 
 
-def parse(name, raise_on_fail=True):
-    def add_dashes(x):
-        return f"-{x}-"
+def parse_name(name, raise_on_fail=True):
+    def add_dashes(string):
+        return f"-{string}-"
+
+    def deduce_category(object):
+        i = object.index("-")
+        category, object = object[:i], object[i + 1 :]
+        return category, object
 
     if isinstance(name, git.Tag):
         name = name.name
     if add_dashes(UNREGISTER) in name:
-        model, version = name.split(add_dashes(UNREGISTER))
-        model = model[len("model-") :]
-        return dict(action=UNREGISTER, model=model, version=version)
+        object, version = name.split(add_dashes(UNREGISTER))
+        category, object = deduce_category(object)
+        return {
+            CATEGORY: category,
+            ACTION: UNREGISTER,
+            OBJECT: object,
+            VERSION: version,
+        }
     if add_dashes(REGISTER) in name:
-        model, version = name.split(add_dashes(REGISTER))
-        model = model[len("model-") :]
-        return dict(action=REGISTER, model=model, version=version)
+        object, version = name.split(add_dashes(REGISTER))
+        category, object = deduce_category(object)
+        return {CATEGORY: category, ACTION: REGISTER, OBJECT: object, VERSION: version}
     if add_dashes(PROMOTE) in name:
-        model, label = name.split(add_dashes(PROMOTE))
-        model = model[len("model-") :]
+        object, label = name.split(add_dashes(PROMOTE))
+        category, object = deduce_category(object)
         label, number = label.split("-")
-        return dict(action=PROMOTE, model=model, label=label, number=int(number))
+        return {
+            CATEGORY: category,
+            ACTION: PROMOTE,
+            OBJECT: object,
+            LABEL: label,
+            NUMBER: int(number),
+        }
     if add_dashes(DEMOTE) in name:
-        model, label = name.split(add_dashes(DEMOTE))
-        model = model[len("model-") :]
+        object, label = name.split(add_dashes(DEMOTE))
+        category, object = deduce_category(object)
         label, number = label.split("-")
-        return dict(action=DEMOTE, model=model, label=label, number=int(number))
+        return {
+            CATEGORY: category,
+            ACTION: DEMOTE,
+            OBJECT: object,
+            LABEL: label,
+            NUMBER: int(number),
+        }
     if raise_on_fail:
         raise ValueError(f"Unknown tag name: {name}")
-    else:
-        return dict()
+    return {}
 
 
 def find(
     action=None,
-    model=None,
+    category=None,
+    object=None,
     version=None,
     label=None,
     repo=None,
@@ -70,75 +99,89 @@ def find(
     tags=None,
 ):
     if tags is None:
-        tags = [t for t in repo.tags if parse(t.name, raise_on_fail=False)]
+        tags = [t for t in repo.tags if parse_name(t.name, raise_on_fail=False)]
+    if category:
+        tags = [t for t in tags if parse_name(t.name)[CATEGORY] == category]
     if action:
-        tags = [t for t in tags if parse(t.name)["action"] == action]
-    if model:
-        tags = [t for t in tags if parse(t.name).get("model") == model]
+        tags = [t for t in tags if parse_name(t.name)[ACTION] == action]
+    if object:
+        tags = [t for t in tags if parse_name(t.name).get(OBJECT) == object]
     if version:
-        tags = [t for t in tags if parse(t.name).get("version") == version]
+        tags = [t for t in tags if parse_name(t.name).get(VERSION) == version]
     if label:
-        tags = [t for t in tags if parse(t.name).get("label") == label]
+        tags = [t for t in tags if parse_name(t.name).get(LABEL) == label]
     if sort == "by_time":
         tags = sorted(tags, key=lambda t: t.tag.tagged_date)
+    else:
+        raise NotImplementedError(f"Unknown sort: {sort}")
     return tags
 
 
-def find_registered(model, repo):
-    """Return all registered versions for model"""
-    register_tags = find(action=REGISTER, model=model, repo=repo)
-    unregister_tags = find(action=UNREGISTER, model=model, repo=repo)
+def find_registered(category, object, repo):
+    """Return all registered versions for object"""
+    register_tags = find(category=category, action=REGISTER, object=object, repo=repo)
+    unregister_tags = find(
+        category=category, action=UNREGISTER, object=object, repo=repo
+    )
     return [
         r
         for r in register_tags
         if not any(
             r.commit.hexsha == u.commit.hexsha
-            and parse(r.name)["model"] == parse(u.name)["model"]
-            and parse(r.name)["version"] == parse(u.name)["version"]
+            and parse_name(r.name)[CATEGORY] == parse_name(u.name)[CATEGORY]
+            and parse_name(r.name)[OBJECT] == parse_name(u.name)[OBJECT]
+            and parse_name(r.name)[VERSION] == parse_name(u.name)[VERSION]
             for u in unregister_tags
         )
     ]
 
 
-def find_latest(model, repo):
-    """Return latest registered version for model"""
-    return find_registered(model, repo)[-1]
+def find_latest(category, object, repo):
+    """Return latest registered version for object"""
+    return find_registered(category, object, repo)[-1]
 
 
-def find_promoted(model, label, repo):
-    """Return all promoted versions for model"""
-    promote_tags = find(action=PROMOTE, model=model, label=label, repo=repo)
-    demote_tags = find(action=DEMOTE, model=model, label=label, repo=repo)
-    # what we do if someone promotes and demotes one model+commit several times?
+def find_promoted(category, object, label, repo):
+    """Return all promoted versions for object"""
+    promote_tags = find(
+        category=category, action=PROMOTE, object=object, label=label, repo=repo
+    )
+    demote_tags = find(
+        category=category, action=DEMOTE, object=object, label=label, repo=repo
+    )
+    # what we do if someone promotes and demotes one object+commit several times?
     return [
         p
         for p in promote_tags
         if not any(
             p.commit.hexsha == d.commit.hexsha
-            and parse(p.name)["model"] == parse(d.name)["model"]
-            and parse(p.name)["label"] == parse(d.name)["label"]
+            and parse_name(p.name)[CATEGORY] == parse_name(d.name)[CATEGORY]
+            and parse_name(p.name)[OBJECT] == parse_name(d.name)[OBJECT]
+            and parse_name(p.name)[LABEL] == parse_name(d.name)[LABEL]
             for d in demote_tags
         )
     ]
 
 
-def find_current_promoted(model, label, repo):
-    """Return latest promoted version for model"""
-    return find_promoted(model, label, repo)[-1]
+def find_current_promoted(category, object, label, repo):
+    """Return latest promoted version for object"""
+    return find_promoted(category, object, label, repo)[-1]
 
 
-def find_version(model, label, repo):
-    """Return version of model with specific label active"""
-    tags = find(action=PROMOTE, model=model, label=label, repo=repo)
+def find_version(category, object, label, repo):
+    """Return version of object with specific label active"""
+    tags = find(
+        category=category, action=PROMOTE, object=object, label=label, repo=repo
+    )
     version_sha = tags[-1].commit.hexsha
 
-    # if this commit has been tagged several times (model-v1, model-v2)
+    # if this commit has been tagged several times (object-v1, object-v2)
     # you may have several tags with different versions
-    # so when you PROMOTE model, you won't know which version you've promoted
+    # so when you PROMOTE object, you won't know which version you've promoted
     # v1 or v2
-    tags = find(action=REGISTER, model=model, repo=repo)
+    tags = find(category=category, action=REGISTER, object=object, repo=repo)
     tags = [t for t in tags if t.commit.hexsha == version_sha]
-    return parse(tags[-1].name)["version"]
+    return parse_name(tags[-1].name)["version"]
 
 
 def create_tag(repo, name, ref, message):
@@ -153,12 +196,30 @@ def create_tag(repo, name, ref, message):
     )
 
 
+class ObjectTag:
+    object: str
+    version: Optional[str]
+    label: Optional[str]
+    tag: git.Tag
+
+    def __init__(self, tag) -> None:
+        parsed = parse_name(tag.name)
+        self.action = parsed[ACTION]
+        self.category = parsed[CATEGORY]
+        self.object = parsed[OBJECT]
+        self.version = parsed.get(VERSION)
+        self.label = parsed.get(LABEL)
+        self.creation_date = pd.Timestamp(tag.tag.tagged_date * 10 ** 9)
+        self.tag = tag
+
+
 class TagBasedVersion(BaseVersion):
     @classmethod
     def from_tag(cls, tag):
-        mtag = ModelTag(tag)
+        mtag = ObjectTag(tag)
         return cls(
-            mtag.model,
+            mtag.category,
+            mtag.object,
             mtag.version,
             mtag.creation_date,
             tag.tag.tagger.name,
@@ -169,21 +230,27 @@ def from_tag(cls, tag):
 
 class TagBasedLabel(BaseLabel):
     @classmethod
-    def from_tag(cls, tag: git.Tag) -> None:
-        mtag = ModelTag(tag)
+    def from_tag(cls, tag: git.Tag) -> "TagBasedLabel":
+        mtag = ObjectTag(tag)
         version_candidates = [
             t
-            for t in find(action=REGISTER, model=mtag.model, repo=tag.repo)
+            for t in find(
+                category=mtag.category,
+                action=REGISTER,
+                object=mtag.object,
+                repo=tag.repo,
+            )
             if t.commit.hexsha == tag.commit.hexsha
         ]
         if len(version_candidates) != 1:
             # TODO: resolve this
             raise ValueError(
-                f"Found {len(version_candidates)} tags for model '{mtag.model}' label '{mtag.label}'"
+                f"Found {len(version_candidates)} tags for {mtag.category} '{mtag.object}' label '{mtag.label}'"
             )
-        version = ModelTag(version_candidates[0]).version
+        version = ObjectTag(version_candidates[0]).version
         return cls(
-            mtag.model,
+            mtag.category,
+            mtag.object,
             version,
             mtag.label,
             mtag.creation_date,
@@ -193,101 +260,98 @@ def from_tag(cls, tag: git.Tag) -> None:
         )
 
 
-class ModelTag:
-    model: str
-    version: Optional[str]
-    label: Optional[str]
-    tag: git.Tag
-
-    def __init__(self, tag) -> None:
-        parsed = parse(tag.name)
-        self.action = parsed["action"]
-        self.model = parsed["model"]
-        self.version = parsed.get("version")
-        self.label = parsed.get("label")
-        self.creation_date = pd.Timestamp(tag.tag.tagged_date * 10 ** 9)
-        self.tag = tag
-
-
-class TagBasedModel(BaseModel):
+class TagBasedObject(BaseObject):
     def index_tag(self, tag: git.Tag) -> None:
-        mtag = ModelTag(tag)
+        mtag = ObjectTag(tag)
         if mtag.action == REGISTER:
             self.versions.append(TagBasedVersion.from_tag(tag))
         if mtag.action == UNREGISTER:
-            self.find_version(mtag.version).unregistered_date = mtag.creation_date
+            self.find_version(mtag.version).unregistered_date = mtag.creation_date  # type: ignore
         if mtag.action == PROMOTE:
             self.labels.append(TagBasedLabel.from_tag(tag))
         if mtag.action == DEMOTE:
             if mtag.label not in self.latest_labels:
                 raise ValueError(f"Active label '{mtag.label}' not found")
-            self.latest_labels[mtag.label].unregistered_date = mtag.creation_date
+            self.latest_labels[mtag.label].unregistered_date = mtag.creation_date  # type: ignore
 
 
 class TagBasedRegistry(BaseRegistry):
 
-    Model = TagBasedModel
+    Object = TagBasedObject  # type: ignore
 
     @property
-    def models(self):
+    def objects(self):
         # tags are sorted and then indexed by timestamp
         # this is important to check that history is not broken
-        tags = [ModelTag(t) for t in find(repo=self.repo)]
-        models = {}
-        for t in tags:
-            if t.model not in models:
-                models[t.model] = TagBasedModel(t.model, [], [])
-            models[t.model].index_tag(t.tag)
-        return [models[k] for k in models]
+        tags = [ObjectTag(t) for t in find(repo=self.repo)]
+        objects = {}
+        for tag in tags:
+            # add category to this check?
+            if tag.object not in objects:
+                objects[tag.object] = TagBasedObject(tag.category, tag.object, [], [])
+            objects[tag.object].index_tag(tag.tag)
+        return objects.items()
 
     @property
     def _labels(self):
         return [
-            parse(t.name)["label"]
+            parse_name(t.name)[LABEL]
             for t in find(repo=self.repo)
-            if "label" in parse(t.name)
+            if LABEL in parse_name(t.name)
         ]
 
-    def _register(self, model, version, ref, message):
+    def _register(self, category, object, version, ref, message):
         create_tag(
             self.repo,
-            name(REGISTER, model, version=version, repo=self.repo),
+            name_tag(REGISTER, category, object, version=version, repo=self.repo),
             ref=ref,
             message=message,
         )
 
-    def unregister(self, model, version):
-        """Unregister model version"""
-        tags = find(action=REGISTER, model=model, version=version, repo=self.repo)
+    def unregister(self, category, object, version):
+        """Unregister object version"""
+        tags = find(
+            action=REGISTER,
+            category=category,
+            object=object,
+            version=version,
+            repo=self.repo,
+        )
         if len(tags) != 1:
             raise ValueError(
-                f"Found {len(tags)} git tags for model {model} version {version}"
+                f"Found {len(tags)} git tags for {category} {object} version {version}"
             )
         create_tag(
             self.repo,
-            name(UNREGISTER, model, version=version, repo=self.repo),
+            name_tag(UNREGISTER, category, object, version=version, repo=self.repo),
             ref=tags[0].commit.hexsha,
-            message=f"Unregistering model {model} version {version}",
+            message=f"Unregistering {category} {object} version {version}",
         )
 
-    def find_commit(self, model, version):
-        return self.repo.tags[name(REGISTER, model, version=version)].commit.hexsha
+    def find_commit(self, category, object, version):
+        return self.repo.tags[
+            name_tag(REGISTER, category, object, version=version)
+        ].commit.hexsha
 
-    def _promote(self, model, label, ref, message):
+    def _promote(self, category, object, label, ref, message):
         create_tag(
             self.repo,
-            name(PROMOTE, model, label=label, repo=self.repo),
+            name_tag(PROMOTE, category, object, label=label, repo=self.repo),
             ref=ref,
             message=message,
         )
 
-    def _demote(self, model, label, message):
-        promoted_tag = find(action=PROMOTE, model=model, label=label, repo=self.repo)[
-            -1
-        ]
+    def _demote(self, category, object, label, message):
+        promoted_tag = find(
+            action=PROMOTE,
+            category=category,
+            object=object,
+            label=label,
+            repo=self.repo,
+        )[-1]
         create_tag(
             self.repo,
-            name(DEMOTE, model, label=label, repo=self.repo),
+            name_tag(DEMOTE, category, object, label=label, repo=self.repo),
             ref=promoted_tag.commit.hexsha,
             message=message,
         )
diff --git a/gitops/versions.py b/gitops/versions.py
index 7963e59a..a9f22315 100644
--- a/gitops/versions.py
+++ b/gitops/versions.py
@@ -11,7 +11,7 @@ def __init__(self, version) -> None:
         self.version = version
 
     @classmethod
-    def is_valid(cls) -> bool:
+    def is_valid(cls, version) -> bool:
         raise NotImplementedError
 
     def __eq__(self, other):
@@ -27,7 +27,7 @@ def bump(self):
 @total_ordering
 class NumberedVersion(AbstractVersion):
     @classmethod
-    def is_valid(self, version):
+    def is_valid(cls, version):
         return version.startswith("v") and version[1:].isdigit()
 
     def to_number(self):
@@ -53,5 +53,5 @@ def bump(self):
         return NumberedVersion(f"v{self.to_number() + 1}")
 
 
-class SemVer(AbstractVersion):
+class SemVer(AbstractVersion):  # pylint: disable=abstract-method
     pass
diff --git a/setup.py b/setup.py
index f9a3177b..0a05492c 100644
--- a/setup.py
+++ b/setup.py
@@ -7,13 +7,26 @@
     "gitpython",
     "click",
     "pandas",
-    "ipython",
     "numpy",
     "pydantic",
     "pyyaml",
 ]
 
 
+tests = [
+    "pytest",
+    "pytest-cov",
+    "pytest-lazy-fixture==0.6.3",
+    "pytest-mock",
+    "pylint",
+    # we use this to suppress pytest-related false positives in our tests.
+    "pylint-pytest",
+    # we use this to suppress some messages in tests, eg: foo/bar naming,
+    # and, protected method calls in our tests
+    "pylint-plugin-utils",
+]
+
+
 setup_args = dict(  # noqa: C408
     name="gitops",
     version="0.0.1",
@@ -24,6 +37,7 @@
     download_url="https://github.com/iterative/gitops-object-registry",
     license="Apache License 2.0",
     install_requires=install_requires,
+    extras_require={"tests": tests},
     keywords="git gitops mlops object registry developer-tools collaboration",
     python_requires=">=3.6",
     classifiers=[
diff --git a/showcase.sh b/showcase.sh
index 6e5e2021..cf278b2c 100644
--- a/showcase.sh
+++ b/showcase.sh
@@ -26,8 +26,8 @@ git add models
 git commit -am "Create models"
 
 echo "Register new model"
-gitops register models/random-forest.pkl v1
-gitops register models/neural-network.pkl v1
+gitops register model models/random-forest.pkl v1
+gitops register model models/neural-network.pkl v1
 
 echo "Update the model"
 sleep 1
@@ -35,18 +35,18 @@ echo "2nd version" > models/random-forest.pkl
 git commit -am "Update model"
 
 echo "Register models"
-gitops register models/random-forest.pkl v2
+gitops register model models/random-forest.pkl v2
 
 echo "Promote models"
-gitops promote models/neural-network.pkl staging --version v1
+gitops promote model models/neural-network.pkl staging --version v1
 sleep 1
-gitops promote models/random-forest.pkl production --version v1
+gitops promote model models/random-forest.pkl production --version v1
 sleep 1
-gitops promote models/random-forest.pkl staging --commit `git rev-parse HEAD`
+gitops promote model models/random-forest.pkl staging --commit `git rev-parse HEAD`
 sleep 1
-gitops promote models/random-forest.pkl production --commit `git rev-parse HEAD`
+gitops promote model models/random-forest.pkl production --commit `git rev-parse HEAD`
 sleep 1
-gitops promote models/random-forest.pkl production --version v1
+gitops promote model models/random-forest.pkl production --version v1
 
 gitops show
 


Test patch:
diff --git a/tests/test_tag.py b/tests/test_tag.py
index 9f5e70c6..ff17d280 100644
--- a/tests/test_tag.py
+++ b/tests/test_tag.py
@@ -1,21 +1,21 @@
-from gitops.tag import DEMOTE, PROMOTE, REGISTER, UNREGISTER, name, parse
+from gitops.tag import DEMOTE, PROMOTE, REGISTER, UNREGISTER, name_tag, parse_name
 
 
-def test_name():
-    assert name(REGISTER, "model", "v1") == f"model-model-{REGISTER}-v1"
-    assert name(UNREGISTER, "model", "v1") == f"model-model-{UNREGISTER}-v1"
+def test_name_tag():
+    assert name_tag(REGISTER, "model", "v1") == f"model-model-{REGISTER}-v1"
+    assert name_tag(UNREGISTER, "model", "v1") == f"model-model-{UNREGISTER}-v1"
 
 
-def test_parse():
-    assert parse(f"model-model-{REGISTER}-v1") == dict(
+def test_parse_name():
+    assert parse_name(f"model-model-{REGISTER}-v1") == dict(
         model="model", version="v1", action=REGISTER
     )
-    assert parse(f"model-model-{UNREGISTER}-v1") == dict(
+    assert parse_name(f"model-model-{UNREGISTER}-v1") == dict(
         model="model", version="v1", action=UNREGISTER
     )
-    assert parse(f"model-model-{PROMOTE}-stage-1") == dict(
+    assert parse_name(f"model-model-{PROMOTE}-stage-1") == dict(
         model="model", action=PROMOTE, label="stage", number=1
     )
-    assert parse(f"model-model-{DEMOTE}-stage-1") == dict(
+    assert parse_name(f"model-model-{DEMOTE}-stage-1") == dict(
         model="model", action=DEMOTE, label="stage", number=1
     )
diff --git a/tests/test_versions.py b/tests/test_versions.py
index 34c1d3b8..c5ee2bd2 100644
--- a/tests/test_versions.py
+++ b/tests/test_versions.py
@@ -16,6 +16,5 @@ def test_number_version_is_not_valid(version):
 def test_numbered_version_comparison():
     assert NumberedVersion("v1") < NumberedVersion("v2")
     assert NumberedVersion("v1") == NumberedVersion("v1")
-    assert not NumberedVersion("v1") > NumberedVersion("v2")
     assert NumberedVersion("v1") < "v2"
     assert NumberedVersion("v1") == "v1"