From d56a3966b2a2df6b33c0dd97c93c5d65bd5276f9 Mon Sep 17 00:00:00 2001 From: Deepyaman Datta Date: Fri, 28 Jul 2023 12:17:40 -0500 Subject: [PATCH 01/70] Remove non-user-facing release notes about pip pin (#2854) Signed-off-by: Jason Hite --- RELEASE.md | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/RELEASE.md b/RELEASE.md index 35e5273527..6e268830e8 100644 --- a/RELEASE.md +++ b/RELEASE.md @@ -19,10 +19,9 @@ * Consolidated dependencies and optional dependencies in `pyproject.toml`. * Made validation of unique node outputs much faster. * Updated `kedro catalog list` to show datasets generated with factories. -* Pinned `pip<23.2` for CI due to a breaking change. See https://github.com/kedro-org/kedro/pull/2813 ## Documentation changes -- Recommended `ruff` as the linter and removed mentions of `pylint`, `isort`, `flake8`. +* Recommended `ruff` as the linter and removed mentions of `pylint`, `isort`, `flake8`. ## Breaking changes to the API From d87bd721cd176d215ba75415baf1bc484086ae88 Mon Sep 17 00:00:00 2001 From: Jason Hite Date: Fri, 28 Jul 2023 14:56:55 -0400 Subject: [PATCH 02/70] Allow for customization of pipeline templates Controlled via `settings.PIPELINE_TEMPLATE_PATH`. Signed-off-by: Jason Hite --- kedro/framework/cli/pipeline.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/kedro/framework/cli/pipeline.py b/kedro/framework/cli/pipeline.py index 09e3c6986c..96a3542e3e 100644 --- a/kedro/framework/cli/pipeline.py +++ b/kedro/framework/cli/pipeline.py @@ -192,7 +192,11 @@ def _create_pipeline(name: str, output_dir: Path) -> Path: # noqa: import-outside-toplevel from cookiecutter.main import cookiecutter - template_path = Path(kedro.__file__).parent / "templates" / "pipeline" + try: + template_path = Path(settings.pipeline_template_path) + except AttributeError: + template_path = Path(kedro.__file__).parent / "templates" / "pipeline" + cookie_context = {"pipeline_name": name, "kedro_version": kedro.__version__} click.echo(f"Creating the pipeline '{name}': ", nl=False) From 20f7812fac41c1ef6bdc04d445a69a95fa23c22d Mon Sep 17 00:00:00 2001 From: Jason Hite Date: Fri, 28 Jul 2023 14:56:55 -0400 Subject: [PATCH 03/70] Update lookup based on feedback Instead of lookups on `settings.py` just statically look for a `templates` directory in the project root. If this folder is present it will override the default global template, otherwise fall back to the previous global default. Signed-off-by: Jason Hite --- kedro/framework/cli/pipeline.py | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/kedro/framework/cli/pipeline.py b/kedro/framework/cli/pipeline.py index 96a3542e3e..beaa832494 100644 --- a/kedro/framework/cli/pipeline.py +++ b/kedro/framework/cli/pipeline.py @@ -107,7 +107,13 @@ def create_pipeline( f"Make sure it exists in the project configuration." ) - result_path = _create_pipeline(name, package_dir / "pipelines") + # Check for existence of a template directory in the project and fall back + # TODO will be to also optionally override this from the CLI + template_path = Path(metadata.project_path / "templates" / "pipeline") + if not template_path.exists(): + template_path = Path(kedro.__file__).parent / "templates" / "pipeline" + + result_path = _create_pipeline(name, template_path, package_dir / "pipelines") _copy_pipeline_tests(name, result_path, package_dir) _copy_pipeline_configs(result_path, project_conf_path, skip_config, env=env) click.secho(f"\nPipeline '{name}' was successfully created.\n", fg="green") @@ -187,16 +193,11 @@ def _echo_deletion_warning(message: str, **paths: list[Path]): click.echo(indent(paths_str, " " * 2)) -def _create_pipeline(name: str, output_dir: Path) -> Path: +def _create_pipeline(name: str, template_path: Path, output_dir: Path) -> Path: with _filter_deprecation_warnings(): # noqa: import-outside-toplevel from cookiecutter.main import cookiecutter - try: - template_path = Path(settings.pipeline_template_path) - except AttributeError: - template_path = Path(kedro.__file__).parent / "templates" / "pipeline" - cookie_context = {"pipeline_name": name, "kedro_version": kedro.__version__} click.echo(f"Creating the pipeline '{name}': ", nl=False) From c27eb117c45caa441ff795feff202976a4447450 Mon Sep 17 00:00:00 2001 From: Jason Hite Date: Fri, 28 Jul 2023 14:56:55 -0400 Subject: [PATCH 04/70] Add CLI flag for pipeline creation template -t/--template Precedence for template_path is: command line > project templates/pipeline dir > global default pipeline create now will also print the template folder used. Signed-off-by: Jason Hite --- kedro/framework/cli/pipeline.py | 25 +++++++++++++++++++------ 1 file changed, 19 insertions(+), 6 deletions(-) diff --git a/kedro/framework/cli/pipeline.py b/kedro/framework/cli/pipeline.py index beaa832494..addfb9c46e 100644 --- a/kedro/framework/cli/pipeline.py +++ b/kedro/framework/cli/pipeline.py @@ -90,10 +90,17 @@ def pipeline(): is_flag=True, help="Skip creation of config files for the new pipeline(s).", ) +@click.option( + "template_path", + "-t", + "--template", + type=click.Path(file_okay=False, dir_okay=True, exists=True, path_type=Path), + help="Path to cookiecutter template to use for pipeline(s). Will override any local templates.", +) @env_option(help="Environment to create pipeline configuration in. Defaults to `base`.") @click.pass_obj # this will pass the metadata as first argument def create_pipeline( - metadata: ProjectMetadata, name, skip_config, env, **kwargs + metadata: ProjectMetadata, name, template_path, skip_config, env, **kwargs ): # noqa: unused-argument """Create a new modular pipeline by providing a name.""" package_dir = metadata.source_dir / metadata.package_name @@ -107,11 +114,17 @@ def create_pipeline( f"Make sure it exists in the project configuration." ) - # Check for existence of a template directory in the project and fall back - # TODO will be to also optionally override this from the CLI - template_path = Path(metadata.project_path / "templates" / "pipeline") - if not template_path.exists(): - template_path = Path(kedro.__file__).parent / "templates" / "pipeline" + # Precedence for template_path is: command line > project templates/pipeline dir > global default + # If passed on the CLI, click will verify that the path exists so no need to check again + if template_path is None: + # No path provided on the CLI, try `PROJECT_PATH/templates/pipeline` + template_path = Path(metadata.project_path / "templates" / "pipeline") + + if not template_path.exists(): + # and if that folder doesn't exist fall back to the global default + template_path = Path(kedro.__file__).parent / "templates" / "pipeline" + + click.secho(f"Using pipeline template at: '{template_path}'") result_path = _create_pipeline(name, template_path, package_dir / "pipelines") _copy_pipeline_tests(name, result_path, package_dir) From b3bdb46de3122fd284b2b791f411d8d8ec6f2baf Mon Sep 17 00:00:00 2001 From: Jason Hite Date: Fri, 28 Jul 2023 14:56:55 -0400 Subject: [PATCH 05/70] First basic unit test TODO is testing the CLI flag too, and maybe testing that the cleanup works with custom templates. Signed-off-by: Jason Hite --- tests/framework/cli/conftest.py | 52 +++++++++++++++++++ tests/framework/cli/pipeline/test_pipeline.py | 25 ++++++++- 2 files changed, 76 insertions(+), 1 deletion(-) diff --git a/tests/framework/cli/conftest.py b/tests/framework/cli/conftest.py index b6bba00a23..cc75f0b274 100644 --- a/tests/framework/cli/conftest.py +++ b/tests/framework/cli/conftest.py @@ -78,6 +78,58 @@ def dummy_config(fake_root_dir, fake_metadata): return config_path +def write_file_contents(fname: Path, contents: str): + """Little helper to make setting up a test template dir easier. + + Automatically creates the parent dir of a file first if it doesn't exist to cut + down on extraneous LOC. SO BE WARNED you need to clean up after yourself. + """ + fname.parent.mkdir(parents=True, exist_ok=True) + + with fname.open('w') as f: + f.write(contents) + +@fixture() +def fake_local_template_dir(fake_repo_path): + """Set up a local template directory. This won't be functional we're just testing the actual layout works. + + Note that this is not scoped to module because we don't want to have this folder present in most of the tests, + so we will tear it down every time. + """ + template_path = (fake_repo_path / Path("templates")) + pipeline_template_path = template_path / Path("pipeline") + cookiecutter_template_path = pipeline_template_path / "{{ cookiecutter.pipeline_name }}" + + cookiecutter_template_path.mkdir(parents=True) + + # Create the absolute bare minimum files + write_file_contents( + pipeline_template_path / "cookiecutter.json", + """ + {"pipeline_name": "default", "kedro_version": "{{ cookiecutter.kedro_version }}"} + """.strip() + ) + + write_file_contents( + cookiecutter_template_path / "pipeline_{{ cookiecutter.pipeline_name }}.py", + r"print('hello world')", + ) + + write_file_contents(cookiecutter_template_path / "__init__.py", "") + + write_file_contents( + cookiecutter_template_path / r"config/parameters/{{ cookiecutter.pipeline_name }}.yml", + "foo: bar", + ) + + write_file_contents( + cookiecutter_template_path / r"tests/test_{{ cookiecutter.pipeline_name }}.py", + "", + ) + + yield template_path.resolve() + + shutil.rmtree(template_path) @fixture(scope="module") def fake_metadata(fake_root_dir): diff --git a/tests/framework/cli/pipeline/test_pipeline.py b/tests/framework/cli/pipeline/test_pipeline.py index 4bdd965526..4ee9beb30d 100644 --- a/tests/framework/cli/pipeline/test_pipeline.py +++ b/tests/framework/cli/pipeline/test_pipeline.py @@ -77,9 +77,32 @@ def test_create_pipeline( # pylint: disable=too-many-locals actual_files = {f.name for f in test_dir.iterdir()} assert actual_files == expected_files + @pytest.mark.parametrize("env", [None, "local"]) + def test_create_pipeline_template( # pylint: disable=too-many-locals + self, fake_repo_path, fake_project_cli, fake_metadata, env, fake_package_path, fake_local_template_dir + ): + pipelines_dir = fake_package_path / "pipelines" + assert pipelines_dir.is_dir() + + assert not (pipelines_dir / PIPELINE_NAME).exists() + + cmd = ["pipeline", "create", PIPELINE_NAME] + cmd += ["-e", env] if env else [] + result = CliRunner().invoke(fake_project_cli, cmd, obj=fake_metadata) + + assert f"Using pipeline template at: '{fake_repo_path / 'templates'}" in result.output + assert f"Creating the pipeline '{PIPELINE_NAME}': OK" in result.output + assert f"Location: '{pipelines_dir / PIPELINE_NAME}'" in result.output + assert f"Pipeline '{PIPELINE_NAME}' was successfully created." in result.output + + # Dummy pipeline rendered correctly + assert((pipelines_dir / PIPELINE_NAME / f"pipeline_{PIPELINE_NAME}.py").exists()) + + assert result.exit_code == 0 + @pytest.mark.parametrize("env", [None, "local"]) def test_create_pipeline_skip_config( - self, fake_repo_path, fake_project_cli, fake_metadata, env + self, fake_repo_path, fake_project_cli, fake_metadata, env ): """Test creation of a pipeline with no config""" From 1ddb98d0466b88a0dcccdd8bea115367c428ea37 Mon Sep 17 00:00:00 2001 From: Jason Hite Date: Fri, 28 Jul 2023 14:56:55 -0400 Subject: [PATCH 06/70] Add test to check overriding template path on the CLI Signed-off-by: Jason Hite --- tests/framework/cli/pipeline/test_pipeline.py | 31 +++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/tests/framework/cli/pipeline/test_pipeline.py b/tests/framework/cli/pipeline/test_pipeline.py index 4ee9beb30d..801483b546 100644 --- a/tests/framework/cli/pipeline/test_pipeline.py +++ b/tests/framework/cli/pipeline/test_pipeline.py @@ -100,6 +100,37 @@ def test_create_pipeline_template( # pylint: disable=too-many-locals assert result.exit_code == 0 + @pytest.mark.parametrize("env", [None, "local"]) + def test_create_pipeline_template_command_line_override( # pylint: disable=too-many-locals + self, fake_repo_path, fake_project_cli, fake_metadata, env, fake_package_path, fake_local_template_dir + ): + pipelines_dir = fake_package_path / "pipelines" + assert pipelines_dir.is_dir() + + assert not (pipelines_dir / PIPELINE_NAME).exists() + + # Rename the local template dir to something else so we know the command line flag is taking precedence + try: + # Can skip if already there but copytree has a dirs_exist_ok flag in >python 3.8 only + shutil.copytree(fake_local_template_dir, fake_repo_path / "local_templates") + except FileExistsError: + pass + + cmd = ["pipeline", "create", PIPELINE_NAME] + cmd += ["-t", str(fake_repo_path / "local_templates/pipeline")] + cmd += ["-e", env] if env else [] + result = CliRunner().invoke(fake_project_cli, cmd, obj=fake_metadata) + + assert f"Using pipeline template at: '{fake_repo_path / 'local_templates'}" in result.output + assert f"Creating the pipeline '{PIPELINE_NAME}': OK" in result.output + assert f"Location: '{pipelines_dir / PIPELINE_NAME}'" in result.output + assert f"Pipeline '{PIPELINE_NAME}' was successfully created." in result.output + + # Dummy pipeline rendered correctly + assert((pipelines_dir / PIPELINE_NAME / f"pipeline_{PIPELINE_NAME}.py").exists()) + + assert result.exit_code == 0 + @pytest.mark.parametrize("env", [None, "local"]) def test_create_pipeline_skip_config( self, fake_repo_path, fake_project_cli, fake_metadata, env From 9ec4a537fc691b996cc0c3a51bd6709c94d9a22d Mon Sep 17 00:00:00 2001 From: Jason Hite Date: Fri, 28 Jul 2023 15:13:39 -0400 Subject: [PATCH 07/70] Linter formatting fixes Signed-off-by: Jason Hite --- tests/framework/cli/conftest.py | 16 +++++++--- tests/framework/cli/pipeline/test_pipeline.py | 32 +++++++++++++++---- 2 files changed, 36 insertions(+), 12 deletions(-) diff --git a/tests/framework/cli/conftest.py b/tests/framework/cli/conftest.py index cc75f0b274..160b390cc1 100644 --- a/tests/framework/cli/conftest.py +++ b/tests/framework/cli/conftest.py @@ -78,6 +78,7 @@ def dummy_config(fake_root_dir, fake_metadata): return config_path + def write_file_contents(fname: Path, contents: str): """Little helper to make setting up a test template dir easier. @@ -86,9 +87,10 @@ def write_file_contents(fname: Path, contents: str): """ fname.parent.mkdir(parents=True, exist_ok=True) - with fname.open('w') as f: + with fname.open("w") as f: f.write(contents) + @fixture() def fake_local_template_dir(fake_repo_path): """Set up a local template directory. This won't be functional we're just testing the actual layout works. @@ -96,9 +98,11 @@ def fake_local_template_dir(fake_repo_path): Note that this is not scoped to module because we don't want to have this folder present in most of the tests, so we will tear it down every time. """ - template_path = (fake_repo_path / Path("templates")) + template_path = fake_repo_path / Path("templates") pipeline_template_path = template_path / Path("pipeline") - cookiecutter_template_path = pipeline_template_path / "{{ cookiecutter.pipeline_name }}" + cookiecutter_template_path = ( + pipeline_template_path / "{{ cookiecutter.pipeline_name }}" + ) cookiecutter_template_path.mkdir(parents=True) @@ -107,7 +111,7 @@ def fake_local_template_dir(fake_repo_path): pipeline_template_path / "cookiecutter.json", """ {"pipeline_name": "default", "kedro_version": "{{ cookiecutter.kedro_version }}"} - """.strip() + """.strip(), ) write_file_contents( @@ -118,7 +122,8 @@ def fake_local_template_dir(fake_repo_path): write_file_contents(cookiecutter_template_path / "__init__.py", "") write_file_contents( - cookiecutter_template_path / r"config/parameters/{{ cookiecutter.pipeline_name }}.yml", + cookiecutter_template_path + / r"config/parameters/{{ cookiecutter.pipeline_name }}.yml", "foo: bar", ) @@ -131,6 +136,7 @@ def fake_local_template_dir(fake_repo_path): shutil.rmtree(template_path) + @fixture(scope="module") def fake_metadata(fake_root_dir): metadata = ProjectMetadata( diff --git a/tests/framework/cli/pipeline/test_pipeline.py b/tests/framework/cli/pipeline/test_pipeline.py index 801483b546..57a2e7ebcb 100644 --- a/tests/framework/cli/pipeline/test_pipeline.py +++ b/tests/framework/cli/pipeline/test_pipeline.py @@ -79,7 +79,13 @@ def test_create_pipeline( # pylint: disable=too-many-locals @pytest.mark.parametrize("env", [None, "local"]) def test_create_pipeline_template( # pylint: disable=too-many-locals - self, fake_repo_path, fake_project_cli, fake_metadata, env, fake_package_path, fake_local_template_dir + self, + fake_repo_path, + fake_project_cli, + fake_metadata, + env, + fake_package_path, + fake_local_template_dir, ): pipelines_dir = fake_package_path / "pipelines" assert pipelines_dir.is_dir() @@ -90,19 +96,28 @@ def test_create_pipeline_template( # pylint: disable=too-many-locals cmd += ["-e", env] if env else [] result = CliRunner().invoke(fake_project_cli, cmd, obj=fake_metadata) - assert f"Using pipeline template at: '{fake_repo_path / 'templates'}" in result.output + assert ( + f"Using pipeline template at: '{fake_repo_path / 'templates'}" + in result.output + ) assert f"Creating the pipeline '{PIPELINE_NAME}': OK" in result.output assert f"Location: '{pipelines_dir / PIPELINE_NAME}'" in result.output assert f"Pipeline '{PIPELINE_NAME}' was successfully created." in result.output # Dummy pipeline rendered correctly - assert((pipelines_dir / PIPELINE_NAME / f"pipeline_{PIPELINE_NAME}.py").exists()) + assert (pipelines_dir / PIPELINE_NAME / f"pipeline_{PIPELINE_NAME}.py").exists() assert result.exit_code == 0 @pytest.mark.parametrize("env", [None, "local"]) def test_create_pipeline_template_command_line_override( # pylint: disable=too-many-locals - self, fake_repo_path, fake_project_cli, fake_metadata, env, fake_package_path, fake_local_template_dir + self, + fake_repo_path, + fake_project_cli, + fake_metadata, + env, + fake_package_path, + fake_local_template_dir, ): pipelines_dir = fake_package_path / "pipelines" assert pipelines_dir.is_dir() @@ -121,19 +136,22 @@ def test_create_pipeline_template_command_line_override( # pylint: disable=too- cmd += ["-e", env] if env else [] result = CliRunner().invoke(fake_project_cli, cmd, obj=fake_metadata) - assert f"Using pipeline template at: '{fake_repo_path / 'local_templates'}" in result.output + assert ( + f"Using pipeline template at: '{fake_repo_path / 'local_templates'}" + in result.output + ) assert f"Creating the pipeline '{PIPELINE_NAME}': OK" in result.output assert f"Location: '{pipelines_dir / PIPELINE_NAME}'" in result.output assert f"Pipeline '{PIPELINE_NAME}' was successfully created." in result.output # Dummy pipeline rendered correctly - assert((pipelines_dir / PIPELINE_NAME / f"pipeline_{PIPELINE_NAME}.py").exists()) + assert (pipelines_dir / PIPELINE_NAME / f"pipeline_{PIPELINE_NAME}.py").exists() assert result.exit_code == 0 @pytest.mark.parametrize("env", [None, "local"]) def test_create_pipeline_skip_config( - self, fake_repo_path, fake_project_cli, fake_metadata, env + self, fake_repo_path, fake_project_cli, fake_metadata, env ): """Test creation of a pipeline with no config""" From b43dbc253fd35874a5a232b93148eb69c830a8df Mon Sep 17 00:00:00 2001 From: Sajid Alam <90610031+SajidAlamQB@users.noreply.github.com> Date: Tue, 1 Aug 2023 15:00:32 +0100 Subject: [PATCH 08/70] Release 0.18.12 (#2871) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * bump version and prepare for new release Signed-off-by: SajidAlamQB <90610031+SajidAlamQB@users.noreply.github.com> * Update RELEASE.md Added community contributions * add DeltaTableDataSet to rst Signed-off-by: SajidAlamQB <90610031+SajidAlamQB@users.noreply.github.com> * Update setup.py Signed-off-by: SajidAlamQB <90610031+SajidAlamQB@users.noreply.github.com> * Update setup.py Signed-off-by: SajidAlamQB <90610031+SajidAlamQB@users.noreply.github.com> * Update setup.py Co-authored-by: Juan Luis Cano Rodríguez * temporarily use the fixed kedro-plugins branch Signed-off-by: SajidAlamQB <90610031+SajidAlamQB@users.noreply.github.com> * Update setup.py Signed-off-by: SajidAlamQB <90610031+SajidAlamQB@users.noreply.github.com> * test docsbuild on merels branch Signed-off-by: SajidAlamQB <90610031+SajidAlamQB@users.noreply.github.com> * use kedro-datasets new release Signed-off-by: SajidAlamQB <90610031+SajidAlamQB@users.noreply.github.com> --------- Signed-off-by: SajidAlamQB <90610031+SajidAlamQB@users.noreply.github.com> Co-authored-by: Jo Stichbury Co-authored-by: Juan Luis Cano Rodríguez Signed-off-by: Jason Hite --- CITATION.cff | 12 ++++++++++-- RELEASE.md | 18 +++++++++++++++++- docs/source/conf.py | 1 + docs/source/development/commands_reference.md | 2 +- docs/source/extend_kedro/plugins.md | 2 +- docs/source/kedro_datasets.rst | 1 + kedro/__init__.py | 2 +- setup.py | 2 +- 8 files changed, 33 insertions(+), 7 deletions(-) diff --git a/CITATION.cff b/CITATION.cff index 256c577eb8..6ebc728b72 100644 --- a/CITATION.cff +++ b/CITATION.cff @@ -5,6 +5,8 @@ authors: given-names: Sajid - family-names: Chan given-names: Nok Lam +- family-names: Couto + given-names: Laura - family-names: Dada given-names: Yetunde - family-names: Danov @@ -13,6 +15,8 @@ authors: given-names: Deepyaman - family-names: DeBold given-names: Tynan +- family-names: Gundaniya + given-names: Jitendra - family-names: Holzer given-names: Jannic - family-names: Kaiser @@ -33,6 +37,8 @@ authors: given-names: Antony - family-names: Nguyen given-names: Huong +- family-names: Nikolic + given-names: Vladimir - family-names: Okwa given-names: Nero - family-names: Cano Rodríguez @@ -40,11 +46,13 @@ authors: orcid: https://orcid.org/0000-0002-2187-161X - family-names: Schwarzmann given-names: Joel +- family-names: Sorokin + given-names: Dmitry - family-names: Stichbury given-names: Jo - family-names: Theisen given-names: Merel title: Kedro -version: 0.18.11 -date-released: 2023-07-03 +version: 0.18.12 +date-released: 2023-07-31 url: https://github.com/kedro-org/kedro diff --git a/RELEASE.md b/RELEASE.md index 6e268830e8..2fcf553bcf 100644 --- a/RELEASE.md +++ b/RELEASE.md @@ -8,7 +8,19 @@ ## Migration guide from Kedro 0.18.* to 0.19.* -# Upcoming Release 0.18.12 +# Upcoming Release 0.18.13 + +## Major features and improvements + +## Bug fixes and other changes + +## Documentation changes + +## Breaking changes to the API + +## Upcoming deprecations for Kedro 0.19.0 + +# Release 0.18.12 ## Major features and improvements * Added dataset factories feature which uses pattern matching to reduce the number of catalog entries. @@ -23,6 +35,10 @@ ## Documentation changes * Recommended `ruff` as the linter and removed mentions of `pylint`, `isort`, `flake8`. +## Community contributions + +Thanks to [Laíza Milena Scheid Parizotto](https://github.com/laizaparizotto) and [Chris Schopp](https://github.com/cschopp-simwell). + ## Breaking changes to the API ## Upcoming deprecations for Kedro 0.19.0 diff --git a/docs/source/conf.py b/docs/source/conf.py index 5accc21725..205f98416e 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -142,6 +142,7 @@ "PluginManager", "_DI", "_DO", + "deltalake.table.Metadata", # The statements below were added after subclassing UserDict in AbstractConfigLoader. "None. Remove all items from D.", "a shallow copy of D", diff --git a/docs/source/development/commands_reference.md b/docs/source/development/commands_reference.md index 6d65cf7321..39c162dafc 100644 --- a/docs/source/development/commands_reference.md +++ b/docs/source/development/commands_reference.md @@ -115,7 +115,7 @@ Returns output similar to the following, depending on the version of Kedro used | |/ / _ \/ _` | '__/ _ \ | < __/ (_| | | | (_) | |_|\_\___|\__,_|_| \___/ -v0.18.11 +v0.18.12 Kedro is a Python framework for creating reproducible, maintainable diff --git a/docs/source/extend_kedro/plugins.md b/docs/source/extend_kedro/plugins.md index 51cb3b1946..c7a0b10979 100644 --- a/docs/source/extend_kedro/plugins.md +++ b/docs/source/extend_kedro/plugins.md @@ -84,7 +84,7 @@ setup( After that you can use this starter with `kedro new --starter=test_plugin_starter`. ```{note} -If your starter lives on a git repository, by default Kedro attempts to use a tag or branch labelled with your version of Kedro, e.g. `0.18.11`. This means that you can host different versions of your starter template on the same repository, and the correct one will automatically be used. If you do not wish to follow this structure, you should override it with the `checkout` flag, e.g. `kedro new --starter=test_plugin_starter --checkout=main`. +If your starter lives on a git repository, by default Kedro attempts to use a tag or branch labelled with your version of Kedro, e.g. `0.18.12`. This means that you can host different versions of your starter template on the same repository, and the correct one will automatically be used. If you do not wish to follow this structure, you should override it with the `checkout` flag, e.g. `kedro new --starter=test_plugin_starter --checkout=main`. ``` ## Working with `click` diff --git a/docs/source/kedro_datasets.rst b/docs/source/kedro_datasets.rst index b3d3ab328b..18bff88086 100644 --- a/docs/source/kedro_datasets.rst +++ b/docs/source/kedro_datasets.rst @@ -24,6 +24,7 @@ kedro_datasets kedro_datasets.networkx.GraphMLDataSet kedro_datasets.networkx.JSONDataSet kedro_datasets.pandas.CSVDataSet + kedro_datasets.pandas.DeltaTableDataSet kedro_datasets.pandas.ExcelDataSet kedro_datasets.pandas.FeatherDataSet kedro_datasets.pandas.GBQQueryDataSet diff --git a/kedro/__init__.py b/kedro/__init__.py index de25eb79a4..39f84743b2 100644 --- a/kedro/__init__.py +++ b/kedro/__init__.py @@ -6,7 +6,7 @@ import sys import warnings -__version__ = "0.18.11" +__version__ = "0.18.12" class KedroPythonVersionWarning(UserWarning): diff --git a/setup.py b/setup.py index b7ba8b0988..8232f8a315 100644 --- a/setup.py +++ b/setup.py @@ -97,7 +97,7 @@ def _collect_requirements(requires): "sphinxcontrib-mermaid~=0.7.1", "myst-parser~=1.0.0", "Jinja2<3.1.0", - "kedro-datasets[all]~=1.4.2", + "kedro-datasets[all,pandas-deltatabledataset]~=1.5.1", ], "geopandas": _collect_requirements(geopandas_require), "matplotlib": _collect_requirements(matplotlib_require), From 51a022e01529a298d1cd4b0a1e75c84aeae07064 Mon Sep 17 00:00:00 2001 From: Deepyaman Datta Date: Wed, 2 Aug 2023 08:25:42 -0700 Subject: [PATCH 09/70] Fix broken link to Prefect website in deploy guide (#2885) Signed-off-by: Jason Hite --- docs/source/deployment/prefect.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/source/deployment/prefect.md b/docs/source/deployment/prefect.md index 64d1018984..b602b499ec 100644 --- a/docs/source/deployment/prefect.md +++ b/docs/source/deployment/prefect.md @@ -1,6 +1,6 @@ # Prefect -This page explains how to run your Kedro pipeline using [Prefect 2.0](https://www.prefect.io/products/core/), an open-source workflow management system. +This page explains how to run your Kedro pipeline using [Prefect 2.0](https://www.prefect.io/opensource), an open-source workflow management system. The scope of this documentation is the deployment to a self hosted [Prefect Server](https://docs.prefect.io/2.10.17/host/), which is an open-source backend that makes it easy to monitor and execute your Prefect flows and automatically extends Prefect 2.0. We will use an [Agent that dequeues submitted flow runs from a Work Queue](https://docs.prefect.io/2.10.17/tutorial/deployments/#why-workpools-and-workers). From 141d50237d1b295120226a521792bb5c691064c2 Mon Sep 17 00:00:00 2001 From: Jonathan Cohen Date: Wed, 2 Aug 2023 12:01:13 -0400 Subject: [PATCH 10/70] Clarify the argument to kedro micropkg package (#2835) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Juan Luis Cano Rodríguez Co-authored-by: Merel Theisen <49397448+merelcht@users.noreply.github.com> Signed-off-by: Jason Hite --- docs/source/nodes_and_pipelines/micro_packaging.md | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/docs/source/nodes_and_pipelines/micro_packaging.md b/docs/source/nodes_and_pipelines/micro_packaging.md index d72a0c3b4f..a3f9392808 100644 --- a/docs/source/nodes_and_pipelines/micro_packaging.md +++ b/docs/source/nodes_and_pipelines/micro_packaging.md @@ -4,7 +4,11 @@ Micro-packaging allows users to share Kedro micro-packages across codebases, org ## Package a micro-package -You can package a micro-package by executing: `kedro micropkg package ` +You can package a micro-package by executing: `kedro micropkg package `. + +`` should be a Python module path like what would be used in an `import` statement, for example + +`kedro micropkg package pipelines.data_processing` * This will generate a new [source distribution](https://docs.python.org/3/distutils/sourcedist.html) for this micro-package. * By default, the tar file will be saved into `dist/` directory inside your project. From c373b67b00d5d9241f3df7e144736abb512e5218 Mon Sep 17 00:00:00 2001 From: Ankita Katiyar <110245118+ankatiyar@users.noreply.github.com> Date: Wed, 2 Aug 2023 18:22:09 +0100 Subject: [PATCH 11/70] Allow registering of custom resolvers to `OmegaConfigLoader` (#2869) * Allow registering of custom resolvers to OCL Signed-off-by: Ankita Katiyar * Complete doc string Signed-off-by: Ankita Katiyar * Add test for overwritten resolvers Signed-off-by: Ankita Katiyar * Update test for overwritten resolvers Signed-off-by: Ankita Katiyar * Remove replace=True by default Signed-off-by: Ankita Katiyar * Update release notes Signed-off-by: Ankita Katiyar * Update release notes Signed-off-by: Ankita Katiyar * Add debug level log for registering new resolver Signed-off-by: Ankita Katiyar --------- Signed-off-by: Ankita Katiyar Signed-off-by: Jason Hite --- RELEASE.md | 1 + kedro/config/omegaconf_config.py | 17 ++++++++++++++++- tests/config/test_omegaconf_config.py | 22 ++++++++++++++++++++++ 3 files changed, 39 insertions(+), 1 deletion(-) diff --git a/RELEASE.md b/RELEASE.md index 2fcf553bcf..ae0714b8d9 100644 --- a/RELEASE.md +++ b/RELEASE.md @@ -11,6 +11,7 @@ # Upcoming Release 0.18.13 ## Major features and improvements +* Allowed registering of custom resolvers to `OmegaConfigLoader` through `CONFIG_LOADER_ARGS`. ## Bug fixes and other changes diff --git a/kedro/config/omegaconf_config.py b/kedro/config/omegaconf_config.py index d7d9bd245b..4d2ace59d4 100644 --- a/kedro/config/omegaconf_config.py +++ b/kedro/config/omegaconf_config.py @@ -7,7 +7,7 @@ import logging import mimetypes from pathlib import Path -from typing import Any, Iterable +from typing import Any, Callable, Iterable import fsspec from omegaconf import OmegaConf @@ -82,6 +82,7 @@ def __init__( # noqa: too-many-arguments config_patterns: dict[str, list[str]] = None, base_env: str = "base", default_run_env: str = "local", + custom_resolvers: dict[str, Callable] = None, ): """Instantiates a ``OmegaConfigLoader``. @@ -97,6 +98,8 @@ def __init__( # noqa: too-many-arguments the configuration paths. default_run_env: Name of the default run environment. Defaults to `"local"`. Can be overridden by supplying the `env` argument. + custom_resolvers: A dictionary of custom resolvers to be registered. For more information, + see here: https://omegaconf.readthedocs.io/en/2.3_branch/custom_resolvers.html#custom-resolvers """ self.base_env = base_env self.default_run_env = default_run_env @@ -111,6 +114,9 @@ def __init__( # noqa: too-many-arguments # Deactivate oc.env built-in resolver for OmegaConf OmegaConf.clear_resolver("oc.env") + # Register user provided custom resolvers + if custom_resolvers: + self._register_new_resolvers(custom_resolvers) file_mimetype, _ = mimetypes.guess_type(conf_source) if file_mimetype == "application/x-tar": @@ -302,6 +308,15 @@ def _is_valid_config_path(self, path): ".json", ] + @staticmethod + def _register_new_resolvers(resolvers: dict[str, Callable]): + """Register custom resolvers""" + for name, resolver in resolvers.items(): + if not OmegaConf.has_resolver(name): + msg = f"Registering new custom resolver: {name}" + _config_logger.debug(msg) + OmegaConf.register_new_resolver(name=name, resolver=resolver) + @staticmethod def _check_duplicates(seen_files_to_keys: dict[Path, set[Any]]): duplicates = [] diff --git a/tests/config/test_omegaconf_config.py b/tests/config/test_omegaconf_config.py index dd49292019..af57b52224 100644 --- a/tests/config/test_omegaconf_config.py +++ b/tests/config/test_omegaconf_config.py @@ -649,3 +649,25 @@ def test_variable_interpolation_in_catalog_with_separate_templates_file( conf = OmegaConfigLoader(str(tmp_path)) conf.default_run_env = "" assert conf["catalog"]["companies"]["type"] == "pandas.CSVDataSet" + + def test_custom_resolvers(self, tmp_path): + base_params = tmp_path / _BASE_ENV / "parameters.yml" + param_config = { + "model_options": { + "param1": "${add: 3, 4}", + "param2": "${plus_2: 1}", + "param3": "${oc.env: VAR}", + } + } + _write_yaml(base_params, param_config) + custom_resolvers = { + "add": lambda *x: sum(x), + "plus_2": lambda x: x + 2, + "oc.env": oc.env, + } + os.environ["VAR"] = "my_env_variable" + conf = OmegaConfigLoader(tmp_path, custom_resolvers=custom_resolvers) + conf.default_run_env = "" + assert conf["parameters"]["model_options"]["param1"] == 7 + assert conf["parameters"]["model_options"]["param2"] == 3 + assert conf["parameters"]["model_options"]["param3"] == "my_env_variable" From d20af7626639a4817049b84ed61c10455c83899e Mon Sep 17 00:00:00 2001 From: Ankita Katiyar <110245118+ankatiyar@users.noreply.github.com> Date: Mon, 7 Aug 2023 12:57:06 +0100 Subject: [PATCH 12/70] Document the use of custom resolvers with `OmegaConfigLoader` (#2896) * Update docs for custom resolvers for OCL Signed-off-by: Ankita Katiyar * Update FAQ with correct link Signed-off-by: Ankita Katiyar * Fix typo Co-authored-by: Merel Theisen <49397448+merelcht@users.noreply.github.com> * Add note for oc.env Signed-off-by: Ankita Katiyar * Suggestions from code review Signed-off-by: Ankita Katiyar --------- Signed-off-by: Ankita Katiyar Co-authored-by: Merel Theisen <49397448+merelcht@users.noreply.github.com> Signed-off-by: Jason Hite --- .../configuration/advanced_configuration.md | 95 +++++++++++-------- docs/source/faq/faq.md | 2 +- 2 files changed, 57 insertions(+), 40 deletions(-) diff --git a/docs/source/configuration/advanced_configuration.md b/docs/source/configuration/advanced_configuration.md index efd71a8564..ecfe45a71f 100644 --- a/docs/source/configuration/advanced_configuration.md +++ b/docs/source/configuration/advanced_configuration.md @@ -124,7 +124,7 @@ This section contains a set of guidance for advanced configuration requirements * [How to bypass the configuration loading rules](#how-to-bypass-the-configuration-loading-rules) * [How to use Jinja2 syntax in configuration](#how-to-use-jinja2-syntax-in-configuration) * [How to do templating with the `OmegaConfigLoader`](#how-to-do-templating-with-the-omegaconfigloader) -* [How to use custom resolvers in the `OmegaConfigLoader`](#how-to-use-custom-resolvers-in-the-omegaconfigloader) +* [How to use resolvers in the `OmegaConfigLoader`](#how-to-use-resolvers-in-the-omegaconfigloader) * [How to load credentials through environment variables](#how-to-load-credentials-through-environment-variables) ### How to change which configuration files are loaded @@ -262,58 +262,75 @@ Since both of the file names (`catalog.yml` and `catalog_globals.yml`) match the #### Other configuration files It's also possible to use variable interpolation in configuration files other than parameters and catalog, such as custom spark or mlflow configuration. This works in the same way as variable interpolation in parameter files. You can still use the underscore for the templated values if you want, but it's not mandatory like it is for catalog files. -### How to use custom resolvers in the `OmegaConfigLoader` -`Omegaconf` provides functionality to [register custom resolvers](https://omegaconf.readthedocs.io/en/2.3_branch/usage.html#resolvers) for templated values. You can use these custom resolves within Kedro by extending the [`OmegaConfigLoader`](/kedro.config.OmegaConfigLoader) class. +### How to use resolvers in the `OmegaConfigLoader` +Instead of hard-coding values in your configuration files, you can also dynamically compute them using [`OmegaConf`'s +resolvers functionality](https://omegaconf.readthedocs.io/en/2.3_branch/custom_resolvers.html#resolvers). You use resolvers to define custom +logic to calculate values of parameters or catalog entries, or inject these values from elsewhere. To use this feature with Kedro, pass a +`dict` of custom resolvers to `OmegaConfigLoader` through `CONFIG_LOADER_ARGS` in your project's `src//settings.py`. The example below illustrates this: ```python +import polars as pl +from datetime import date + from kedro.config import OmegaConfigLoader -from omegaconf import OmegaConf -from typing import Any, Dict - - -class CustomOmegaConfigLoader(OmegaConfigLoader): - def __init__( - self, - conf_source: str, - env: str = None, - runtime_params: Dict[str, Any] = None, - ): - super().__init__( - conf_source=conf_source, env=env, runtime_params=runtime_params - ) - - # Register a customer resolver that adds up numbers. - self.register_custom_resolver("add", lambda *numbers: sum(numbers)) - - @staticmethod - def register_custom_resolver(name, function): - """ - Helper method that checks if the resolver has already been registered and registers the - resolver if it's new. The check is needed, because omegaconf will throw an error - if a resolver with the same name is registered twice. - Alternatively, you can call `register_new_resolver()` with `replace=True`. - """ - if not OmegaConf.has_resolver(name): - OmegaConf.register_new_resolver(name, function) -``` -In order to use this custom configuration loader, you will need to set it as the project configuration loader in `src//settings.py`: +CONFIG_LOADER_CLASS = OmegaConfigLoader -```python -from package_name.custom_configloader import CustomOmegaConfigLoader -CONFIG_LOADER_CLASS = CustomOmegaConfigLoader -``` +def date_today(): + return date.today() -You can then use the custom "add" resolver in your `parameters.yml` as follows: +CONFIG_LOADER_ARGS = { + "custom_resolvers": { + "add": lambda *my_list: sum(my_list), + "polars": lambda x: getattr(pl, x), + "today": lambda: date_today(), + } +} +``` +These custom resolvers are then registered using `OmegaConf.register_new_resolver()` under the hood and can be used in any of the +configuration files in your project. For example, you can use the `add` or the `today` resolver defined above in your `parameters.yml` like this: ```yaml model_options: - test_size: ${add:1,2,3} + test_size: "${add:1,2,3}" random_state: 3 + +date: "${today:}" ``` +The values of these parameters will be computed at access time and will be passed on to your nodes. +Resolvers can also be used in your `catalog.yml`. In the example below, we use the `polars` resolver defined above to pass non-primitive +types to the catalog entry. +```yaml +my_polars_dataset: + type: polars.CSVDataSet + filepath: data/01_raw/my_dataset.csv + load_args: + dtypes: + product_age: "${polars:Float64}" + group_identifier: "${polars:Utf8}" + try_parse_dates: true +``` +`OmegaConf` also comes with some [built-in resolvers](https://omegaconf.readthedocs.io/en/latest/custom_resolvers.html#built-in-resolvers) +that you can use with the `OmegaConfigLoader` in Kedro. All built-in resolvers except for [`oc.env`](https://omegaconf.readthedocs.io/en/latest/custom_resolvers.html#oc-env) +are enabled by default. `oc.env` is only turned on for loading credentials. You can, however, turn this on for all configurations through your project's `src//settings.py` in a similar way: +```{note} +This is an advanced feature and should be used with caution. We do not recommend using environment variables for configurations other than credentials. +``` +```python +from omegaconf.resolvers import oc +from kedro.config import OmegaConfigLoader + +CONFIG_LOADER_CLASS = OmegaConfigLoader + +CONFIG_LOADER_ARGS = { + "custom_resolvers": { + "oc.env": oc.env, + } +} +``` ### How to load credentials through environment variables The [`OmegaConfigLoader`](/kedro.config.OmegaConfigLoader) enables you to load credentials from environment variables. To achieve this you have to use the `OmegaConfigLoader` and the `omegaconf` [`oc.env` resolver](https://omegaconf.readthedocs.io/en/2.3_branch/custom_resolvers.html#oc-env). To use the `OmegaConfigLoader` in your project, set the `CONFIG_LOADER_CLASS` constant in your [`src//settings.py`](../kedro_project_setup/settings.md): diff --git a/docs/source/faq/faq.md b/docs/source/faq/faq.md index 7847e1991a..9087f29def 100644 --- a/docs/source/faq/faq.md +++ b/docs/source/faq/faq.md @@ -34,7 +34,7 @@ * [How do I bypass the configuration loading rules](../configuration/advanced_configuration.md#how-to-bypass-the-configuration-loading-rules)? * [How do I use Jinja2 syntax in configuration](../configuration/advanced_configuration.md#how-to-use-jinja2-syntax-in-configuration)? * [How do I do templating with the `OmegaConfigLoader`](../configuration/advanced_configuration.md#how-to-do-templating-with-the-omegaconfigloader)? -* [How do I use custom resolvers in the `OmegaConfigLoader`](../configuration/advanced_configuration.md#how-to-use-custom-resolvers-in-the-omegaconfigloader)? +* [How do I use resolvers in the `OmegaConfigLoader`](../configuration/advanced_configuration.md#how-to-use-resolvers-in-the-omegaconfigloader)? * [How do I load credentials through environment variables](../configuration/advanced_configuration.md#how-to-load-credentials-through-environment-variables)? ## Datasets and the Data Catalog From 307c18649528c8ec73396c37d3c4f41727b174ac Mon Sep 17 00:00:00 2001 From: Dmitry Sorokin <40151847+DimedS@users.noreply.github.com> Date: Mon, 7 Aug 2023 16:25:12 +0100 Subject: [PATCH 13/70] Update kedro pipeline create to use new /conf file structure (#2856) * add parameters_pipelineNane.yml template in the conf folder Signed-off-by: Dmitry Sorokin * del yml template from parameters folder Signed-off-by: Dmitry Sorokin * modify delete pipeline command to use correct path to yml files Signed-off-by: Dmitry Sorokin * modify test_pipeline move parameters foldername to the yml filename Signed-off-by: Dmitry Sorokin * collect configs to package not only from parameters folder, but from core conf folder also Signed-off-by: Dmitry Sorokin * move parameters foldername to the yml filename, remove nesting Signed-off-by: Dmitry Sorokin * move parameters foldername to the yml filename, remove nesting Signed-off-by: Dmitry Sorokin * Add changes to RELEASE.md Signed-off-by: Dmitry Sorokin * Update RELEASE.md Co-authored-by: Merel Theisen <49397448+merelcht@users.noreply.github.com> * Update RELEASE.md, change to release 0.18.13 Signed-off-by: Dmitry Sorokin * save old structure for 'pipeline delete' command for backward compatibility Signed-off-by: Dmitry Sorokin * modify test_pipeline for 'pipeline delete' backward compability Signed-off-by: Dmitry Sorokin --------- Signed-off-by: Dmitry Sorokin Co-authored-by: Merel Theisen <49397448+merelcht@users.noreply.github.com> Signed-off-by: Jason Hite --- RELEASE.md | 1 + kedro/framework/cli/micropkg.py | 9 +++- kedro/framework/cli/pipeline.py | 8 +++- ...ters_{{ cookiecutter.pipeline_name }}.yml} | 0 .../cli/micropkg/test_micropkg_package.py | 8 ++-- .../cli/micropkg/test_micropkg_pull.py | 33 +++++-------- tests/framework/cli/pipeline/test_pipeline.py | 48 +++++++++---------- 7 files changed, 52 insertions(+), 55 deletions(-) rename kedro/templates/pipeline/{{ cookiecutter.pipeline_name }}/config/{parameters/{{ cookiecutter.pipeline_name }}.yml => parameters_{{ cookiecutter.pipeline_name }}.yml} (100%) diff --git a/RELEASE.md b/RELEASE.md index ae0714b8d9..9d7d712eb2 100644 --- a/RELEASE.md +++ b/RELEASE.md @@ -14,6 +14,7 @@ * Allowed registering of custom resolvers to `OmegaConfigLoader` through `CONFIG_LOADER_ARGS`. ## Bug fixes and other changes +* Updated `kedro pipeline create` to use new `/conf` file structure. ## Documentation changes diff --git a/kedro/framework/cli/micropkg.py b/kedro/framework/cli/micropkg.py index 3e0a559cf1..9f095d76b2 100644 --- a/kedro/framework/cli/micropkg.py +++ b/kedro/framework/cli/micropkg.py @@ -613,9 +613,16 @@ def _package_micropkg( ) # as the source distribution will only contain parameters, we aren't listing other # config files not to confuse users and avoid useless file copies + # collect configs to package not only from parameters folder, but from core conf folder also + # because parameters had been moved from foldername to yml filename configs_to_package = _find_config_files( package_conf, - [f"parameters*/**/{micropkg_name}.yml", f"parameters*/**/{micropkg_name}/**/*"], + [ + f"**/parameters_{micropkg_name}.yml", + f"**/{micropkg_name}/**/*", + f"parameters*/**/{micropkg_name}.yml", + f"parameters*/**/{micropkg_name}/**/*", + ], ) source_paths = (package_source, package_tests, configs_to_package) diff --git a/kedro/framework/cli/pipeline.py b/kedro/framework/cli/pipeline.py index addfb9c46e..b3ddb46b6d 100644 --- a/kedro/framework/cli/pipeline.py +++ b/kedro/framework/cli/pipeline.py @@ -159,10 +159,14 @@ def delete_pipeline( pipeline_artifacts = _get_pipeline_artifacts(metadata, pipeline_name=name, env=env) files_to_delete = [ - pipeline_artifacts.pipeline_conf / confdir / f"{name}.yml" + pipeline_artifacts.pipeline_conf / filepath for confdir in ("parameters", "catalog") - if (pipeline_artifacts.pipeline_conf / confdir / f"{name}.yml").is_file() + # Since we remove nesting in 'parameters' and 'catalog' folders, + # we want to also del the old project's structure for backward compatibility + for filepath in (Path(f"{confdir}_{name}.yml"), Path(confdir) / f"{name}.yml") + if (pipeline_artifacts.pipeline_conf / filepath).is_file() ] + dirs_to_delete = [ path for path in (pipeline_artifacts.pipeline_dir, pipeline_artifacts.pipeline_tests) diff --git a/kedro/templates/pipeline/{{ cookiecutter.pipeline_name }}/config/parameters/{{ cookiecutter.pipeline_name }}.yml b/kedro/templates/pipeline/{{ cookiecutter.pipeline_name }}/config/parameters_{{ cookiecutter.pipeline_name }}.yml similarity index 100% rename from kedro/templates/pipeline/{{ cookiecutter.pipeline_name }}/config/parameters/{{ cookiecutter.pipeline_name }}.yml rename to kedro/templates/pipeline/{{ cookiecutter.pipeline_name }}/config/parameters_{{ cookiecutter.pipeline_name }}.yml diff --git a/tests/framework/cli/micropkg/test_micropkg_package.py b/tests/framework/cli/micropkg/test_micropkg_package.py index 4c3daf7abe..3207bb15f5 100644 --- a/tests/framework/cli/micropkg/test_micropkg_package.py +++ b/tests/framework/cli/micropkg/test_micropkg_package.py @@ -33,7 +33,7 @@ def assert_sdist_contents_correct( f"{package_name}-{version}/{package_name}/README.md", f"{package_name}-{version}/{package_name}/nodes.py", f"{package_name}-{version}/{package_name}/pipeline.py", - f"{package_name}-{version}/{package_name}/config/parameters/{package_name}.yml", + f"{package_name}-{version}/{package_name}/config/parameters_{package_name}.yml", f"{package_name}-{version}/tests/__init__.py", f"{package_name}-{version}/tests/test_pipeline.py", } @@ -354,9 +354,9 @@ def test_package_modular_pipeline_with_nested_parameters( assert ( "retail-0.1/retail/config/parameters/retail/params1.yml" in sdist_contents ) - assert "retail-0.1/retail/config/parameters/retail.yml" in sdist_contents + assert "retail-0.1/retail/config/parameters_retail.yml" in sdist_contents assert ( - "retail-0.1/retail/config/parameters/retail_banking.yml" + "retail-0.1/retail/config/parameters_retail_banking.yml" not in sdist_contents ) @@ -424,7 +424,7 @@ def test_package_pipeline_with_deep_nested_parameters( "retail-0.1/retail/config/parameters/retail/deep/params1.yml" in sdist_contents ) - assert "retail-0.1/retail/config/parameters/retail.yml" in sdist_contents + assert "retail-0.1/retail/config/parameters_retail.yml" in sdist_contents assert "retail-0.1/retail/config/parameters/deep/retail.yml" in sdist_contents assert ( "retail-0.1/retail/config/parameters/a/b/c/d/retail/params3.yml" diff --git a/tests/framework/cli/micropkg/test_micropkg_pull.py b/tests/framework/cli/micropkg/test_micropkg_pull.py index 9cbad00a90..6a9a4073ae 100644 --- a/tests/framework/cli/micropkg/test_micropkg_pull.py +++ b/tests/framework/cli/micropkg/test_micropkg_pull.py @@ -115,8 +115,7 @@ def test_pull_local_sdist( fake_repo_path / settings.CONF_SOURCE / config_env - / "parameters" - / f"{pipeline_name}.yml" + / f"parameters_{pipeline_name}.yml" ) self.assert_package_files_exist(source_dest) @@ -160,8 +159,7 @@ def test_pull_local_sdist_compare( fake_repo_path / settings.CONF_SOURCE / "base" - / "parameters" - / f"{PIPELINE_NAME}.yml" + / f"parameters_{PIPELINE_NAME}.yml" ) sdist_file = ( @@ -189,8 +187,7 @@ def test_pull_local_sdist_compare( fake_repo_path / settings.CONF_SOURCE / config_env - / "parameters" - / f"{pipeline_name}.yml" + / f"parameters_{pipeline_name}.yml" ) assert not filecmp.dircmp(source_path, source_dest).diff_files @@ -237,8 +234,7 @@ def test_micropkg_pull_same_alias_package_name( fake_repo_path / settings.CONF_SOURCE / config_env - / "parameters" - / f"{pipeline_name}.yml" + / f"parameters_{pipeline_name}.yml" ) self.assert_package_files_exist(source_dest) @@ -287,8 +283,7 @@ def test_micropkg_pull_nested_destination( fake_repo_path / settings.CONF_SOURCE / config_env - / "parameters" - / f"{pipeline_name}.yml" + / f"parameters_{pipeline_name}.yml" ) self.assert_package_files_exist(source_dest) @@ -451,8 +446,7 @@ def test_pull_tests_missing( fake_repo_path / settings.CONF_SOURCE / "base" - / "parameters" - / f"{PIPELINE_NAME}.yml" + / f"parameters_{PIPELINE_NAME}.yml" ) # Make sure the files actually deleted before pulling from the sdist file. assert not source_path.exists() @@ -480,8 +474,7 @@ def test_pull_tests_missing( fake_repo_path / settings.CONF_SOURCE / config_env - / "parameters" - / f"{pipeline_name}.yml" + / f"parameters_{pipeline_name}.yml" ) self.assert_package_files_exist(source_dest) @@ -509,8 +502,7 @@ def test_pull_config_missing( fake_repo_path / settings.CONF_SOURCE / "base" - / "parameters" - / f"{PIPELINE_NAME}.yml" + / f"parameters_{PIPELINE_NAME}.yml" ) source_params_config.unlink() call_micropkg_package(fake_project_cli, fake_metadata) @@ -544,8 +536,7 @@ def test_pull_config_missing( fake_repo_path / settings.CONF_SOURCE / config_env - / "parameters" - / f"{pipeline_name}.yml" + / f"parameters_{pipeline_name}.yml" ) self.assert_package_files_exist(source_dest) @@ -586,8 +577,7 @@ def test_pull_from_pypi( fake_repo_path / settings.CONF_SOURCE / "base" - / "parameters" - / f"{PIPELINE_NAME}.yml" + / f"parameters_{PIPELINE_NAME}.yml" ) # Make sure the files actually deleted before pulling from pypi. assert not source_path.exists() @@ -645,8 +635,7 @@ def get_all(self, name, failobj=None): # pylint: disable=unused-argument fake_repo_path / settings.CONF_SOURCE / config_env - / "parameters" - / f"{pipeline_name}.yml" + / f"parameters_{pipeline_name}.yml" ) self.assert_package_files_exist(source_dest) diff --git a/tests/framework/cli/pipeline/test_pipeline.py b/tests/framework/cli/pipeline/test_pipeline.py index 57a2e7ebcb..2426a352af 100644 --- a/tests/framework/cli/pipeline/test_pipeline.py +++ b/tests/framework/cli/pipeline/test_pipeline.py @@ -1,4 +1,3 @@ -import os import shutil from pathlib import Path @@ -20,14 +19,17 @@ def make_pipelines(request, fake_repo_path, fake_package_path, mocker): source_path = fake_package_path / "pipelines" / PIPELINE_NAME tests_path = fake_repo_path / "src" / "tests" / "pipelines" / PIPELINE_NAME - conf_path = fake_repo_path / settings.CONF_SOURCE / request.param / "parameters" + conf_path = fake_repo_path / settings.CONF_SOURCE / request.param + # old conf structure for 'pipeline delete' command backward compatibility + old_conf_path = conf_path / "parameters" - for path in (source_path, tests_path, conf_path): + for path in (source_path, tests_path, conf_path, old_conf_path): path.mkdir(parents=True, exist_ok=True) - (conf_path / f"{PIPELINE_NAME}.yml").touch() (tests_path / "test_pipe.py").touch() (source_path / "pipe.py").touch() + (conf_path / f"parameters_{PIPELINE_NAME}.yml").touch() + (old_conf_path / f"{PIPELINE_NAME}.yml").touch() yield mocker.stopall() @@ -67,8 +69,8 @@ def test_create_pipeline( # pylint: disable=too-many-locals # config conf_env = env or "base" conf_dir = (fake_repo_path / settings.CONF_SOURCE / conf_env).resolve() - actual_configs = list(conf_dir.glob(f"**/{PIPELINE_NAME}.yml")) - expected_configs = [conf_dir / "parameters" / f"{PIPELINE_NAME}.yml"] + actual_configs = list(conf_dir.glob(f"**/*{PIPELINE_NAME}.yml")) + expected_configs = [conf_dir / f"parameters_{PIPELINE_NAME}.yml"] assert actual_configs == expected_configs # tests @@ -164,7 +166,7 @@ def test_create_pipeline_skip_config( assert f"Pipeline '{PIPELINE_NAME}' was successfully created." in result.output conf_dirs = list((fake_repo_path / settings.CONF_SOURCE).rglob(PIPELINE_NAME)) - assert conf_dirs == [] # no configs created for the pipeline + assert not conf_dirs # no configs created for the pipeline test_dir = fake_repo_path / "src" / "tests" / "pipelines" / PIPELINE_NAME assert test_dir.is_dir() @@ -189,13 +191,12 @@ def test_catalog_and_params( # pylint: disable=too-many-locals "filepath": "data/01_raw/iris.csv", } } - catalog_file = conf_dir / "catalog" / f"{PIPELINE_NAME}.yml" - catalog_file.parent.mkdir() + catalog_file = conf_dir / f"catalog_{PIPELINE_NAME}.yml" with catalog_file.open("w") as f: yaml.dump(catalog_dict, f) # write pipeline parameters - params_file = conf_dir / "parameters" / f"{PIPELINE_NAME}.yml" + params_file = conf_dir / f"parameters_{PIPELINE_NAME}.yml" assert params_file.is_file() params_dict = {"params_from_pipeline": {"p1": [1, 2, 3], "p2": None}} with params_file.open("w") as f: @@ -215,8 +216,7 @@ def test_skip_copy(self, fake_repo_path, fake_project_cli, fake_metadata): fake_repo_path / settings.CONF_SOURCE / "base" - / dirname - / f"{PIPELINE_NAME}.yml" + / f"{dirname}_{PIPELINE_NAME}.yml" ) path.parent.mkdir(exist_ok=True) path.touch() @@ -238,7 +238,7 @@ def test_skip_copy(self, fake_repo_path, fake_project_cli, fake_metadata): assert result.exit_code == 0 assert "__init__.py': SKIPPED" in result.output - assert f"parameters{os.sep}{PIPELINE_NAME}.yml': SKIPPED" in result.output + assert f"parameters_{PIPELINE_NAME}.yml': SKIPPED" in result.output assert result.output.count("SKIPPED") == 2 # only 2 files skipped def test_failed_copy( @@ -341,17 +341,15 @@ def test_delete_pipeline( source_path = fake_package_path / "pipelines" / PIPELINE_NAME tests_path = fake_repo_path / "src" / "tests" / "pipelines" / PIPELINE_NAME - params_path = ( - fake_repo_path - / settings.CONF_SOURCE - / expected_conf - / "parameters" - / f"{PIPELINE_NAME}.yml" - ) + conf_path = fake_repo_path / settings.CONF_SOURCE / expected_conf + params_path = conf_path / f"parameters_{PIPELINE_NAME}.yml" + # old params structure for 'pipeline delete' command backward compatibility + old_params_path = conf_path / "parameters" / f"{PIPELINE_NAME}.yml" assert f"Deleting '{source_path}': OK" in result.output assert f"Deleting '{tests_path}': OK" in result.output assert f"Deleting '{params_path}': OK" in result.output + assert f"Deleting '{old_params_path}': OK" in result.output assert f"Pipeline '{PIPELINE_NAME}' was successfully deleted." in result.output assert ( @@ -362,6 +360,7 @@ def test_delete_pipeline( assert not source_path.exists() assert not tests_path.exists() assert not params_path.exists() + assert not params_path.exists() def test_delete_pipeline_skip( self, fake_repo_path, fake_project_cli, fake_metadata, fake_package_path @@ -381,8 +380,7 @@ def test_delete_pipeline_skip( fake_repo_path / settings.CONF_SOURCE / "base" - / "parameters" - / f"{PIPELINE_NAME}.yml" + / f"parameters_{PIPELINE_NAME}.yml" ) assert f"Deleting '{source_path}'" not in result.output @@ -473,8 +471,7 @@ def test_pipeline_delete_confirmation( fake_repo_path / settings.CONF_SOURCE / "base" - / "parameters" - / f"{PIPELINE_NAME}.yml" + / f"parameters_{PIPELINE_NAME}.yml" ) assert "The following paths will be removed:" in result.output @@ -514,8 +511,7 @@ def test_pipeline_delete_confirmation_skip( fake_repo_path / settings.CONF_SOURCE / "base" - / "parameters" - / f"{PIPELINE_NAME}.yml" + / f"parameters_{PIPELINE_NAME}.yml" ) assert "The following paths will be removed:" in result.output From 81dd36f9813e1c4a62f4abf6486349773c0177ae Mon Sep 17 00:00:00 2001 From: Dmitry Sorokin <40151847+DimedS@users.noreply.github.com> Date: Tue, 8 Aug 2023 10:41:13 +0100 Subject: [PATCH 14/70] Update kedro catalog create to use new /conf structure (#2884) * modify test_catalog, remove nesting in the catalog folder Signed-off-by: Dmitry Sorokin * move catalog foldername to the yml filename, remove nesting Signed-off-by: Dmitry Sorokin * Update RELEASE.md Signed-off-by: Dmitry Sorokin * Modify docs, fix broken link Signed-off-by: Dmitry Sorokin --------- Signed-off-by: Dmitry Sorokin Signed-off-by: Jason Hite --- RELEASE.md | 2 +- .../configuration/configuration_basics.md | 2 +- kedro/framework/cli/catalog.py | 3 +-- tests/framework/cli/test_catalog.py | 19 ++++++++----------- 4 files changed, 11 insertions(+), 15 deletions(-) diff --git a/RELEASE.md b/RELEASE.md index 9d7d712eb2..a1fac7935a 100644 --- a/RELEASE.md +++ b/RELEASE.md @@ -14,7 +14,7 @@ * Allowed registering of custom resolvers to `OmegaConfigLoader` through `CONFIG_LOADER_ARGS`. ## Bug fixes and other changes -* Updated `kedro pipeline create` to use new `/conf` file structure. +* Updated `kedro pipeline create` and `kedro catalog create` to use new `/conf` file structure. ## Documentation changes diff --git a/docs/source/configuration/configuration_basics.md b/docs/source/configuration/configuration_basics.md index 197d8b2478..2e964b512f 100644 --- a/docs/source/configuration/configuration_basics.md +++ b/docs/source/configuration/configuration_basics.md @@ -45,7 +45,7 @@ Kedro merges configuration information and returns a configuration dictionary ac * If any two configuration files located inside the **same** environment path (such as `conf/base/`) contain the same top-level key, the configuration loader raises a `ValueError` indicating that duplicates are not allowed. * If two configuration files contain the same top-level key but are in **different** environment paths (for example, one in `conf/base/`, another in `conf/local/`) then the last loaded path (`conf/local/`) takes precedence as the key value. `ConfigLoader.get` does not raise any errors but a `DEBUG` level log message is emitted with information on the overridden keys. -When using any of the configuration loaders, any top-level keys that start with `_` are considered hidden (or reserved) and are ignored. Those keys will neither trigger a key duplication error nor appear in the resulting configuration dictionary. However, you can still use such keys, for example, as [YAML anchors and aliases](https://www.educative.io/blog/advanced-yaml-syntax-cheatsheet#anchors) +When using any of the configuration loaders, any top-level keys that start with `_` are considered hidden (or reserved) and are ignored. Those keys will neither trigger a key duplication error nor appear in the resulting configuration dictionary. However, you can still use such keys, for example, as [YAML anchors and aliases](https://www.educative.io/blog/advanced-yaml-syntax-cheatsheet) or [to enable templating in the catalog when using the `OmegaConfigLoader`](advanced_configuration.md#how-to-do-templating-with-the-omegaconfigloader). ### Configuration file names diff --git a/kedro/framework/cli/catalog.py b/kedro/framework/cli/catalog.py index 7323a65cc6..7bfa5f54cb 100644 --- a/kedro/framework/cli/catalog.py +++ b/kedro/framework/cli/catalog.py @@ -170,8 +170,7 @@ def create_catalog(metadata: ProjectMetadata, pipeline_name, env): context.project_path / settings.CONF_SOURCE / env - / "catalog" - / f"{pipeline_name}.yml" + / f"catalog_{pipeline_name}.yml" ) _add_missing_datasets_to_catalog(missing_ds, catalog_path) click.echo(f"Data Catalog YAML configuration was created: {catalog_path}") diff --git a/tests/framework/cli/test_catalog.py b/tests/framework/cli/test_catalog.py index 0296fe4814..a0ee78662b 100644 --- a/tests/framework/cli/test_catalog.py +++ b/tests/framework/cli/test_catalog.py @@ -1,5 +1,3 @@ -import shutil - import pytest import yaml from click.testing import CliRunner @@ -242,11 +240,12 @@ class TestCatalogCreateCommand: @staticmethod @pytest.fixture(params=["base"]) def catalog_path(request, fake_repo_path): - catalog_path = fake_repo_path / "conf" / request.param / "catalog" + catalog_path = fake_repo_path / "conf" / request.param yield catalog_path - shutil.rmtree(catalog_path, ignore_errors=True) + for file in catalog_path.glob("catalog_*"): + file.unlink() def test_pipeline_argument_is_required(self, fake_project_cli): result = CliRunner().invoke(fake_project_cli, ["catalog", "create"]) @@ -278,7 +277,7 @@ def test_catalog_is_created_in_base_by_default( main_catalog_config = yaml.safe_load(main_catalog_path.read_text()) assert "example_iris_data" in main_catalog_config - data_catalog_file = catalog_path / f"{self.PIPELINE_NAME}.yml" + data_catalog_file = catalog_path / f"catalog_{self.PIPELINE_NAME}.yml" result = CliRunner().invoke( fake_project_cli, @@ -302,9 +301,9 @@ def test_catalog_is_created_in_base_by_default( def test_catalog_is_created_in_correct_env( self, fake_project_cli, fake_metadata, catalog_path ): - data_catalog_file = catalog_path / f"{self.PIPELINE_NAME}.yml" + data_catalog_file = catalog_path / f"catalog_{self.PIPELINE_NAME}.yml" - env = catalog_path.parent.name + env = catalog_path.name result = CliRunner().invoke( fake_project_cli, ["catalog", "create", "--pipeline", self.PIPELINE_NAME, "--env", env], @@ -335,7 +334,7 @@ def test_no_missing_datasets( ) data_catalog_file = ( - fake_repo_path / "conf" / "base" / "catalog" / f"{self.PIPELINE_NAME}.yml" + fake_repo_path / "conf" / "base" / f"catalog_{self.PIPELINE_NAME}.yml" ) result = CliRunner().invoke( @@ -351,9 +350,7 @@ def test_no_missing_datasets( def test_missing_datasets_appended( self, fake_project_cli, fake_metadata, catalog_path ): - data_catalog_file = catalog_path / f"{self.PIPELINE_NAME}.yml" - assert not catalog_path.exists() - catalog_path.mkdir() + data_catalog_file = catalog_path / f"catalog_{self.PIPELINE_NAME}.yml" catalog_config = { "example_test_x": {"type": "pandas.CSVDataSet", "filepath": "test.csv"} From 5d11f7fa344bad204ef000873cafed13b107ab68 Mon Sep 17 00:00:00 2001 From: Merel Theisen <49397448+merelcht@users.noreply.github.com> Date: Tue, 8 Aug 2023 11:39:17 +0100 Subject: [PATCH 15/70] Add migration steps for `ConfigLoader` to `OmegaConfigLoader` (#2887) * Add migration steps for CL to OCL Signed-off-by: Merel Theisen Signed-off-by: Jason Hite --- RELEASE.md | 1 + .../configuration/config_loader_migration.md | 62 +++++++++++++++++++ docs/source/configuration/index.md | 1 + 3 files changed, 64 insertions(+) create mode 100644 docs/source/configuration/config_loader_migration.md diff --git a/RELEASE.md b/RELEASE.md index a1fac7935a..71b8a4748b 100644 --- a/RELEASE.md +++ b/RELEASE.md @@ -17,6 +17,7 @@ * Updated `kedro pipeline create` and `kedro catalog create` to use new `/conf` file structure. ## Documentation changes +* Added migration guide from the `ConfigLoader` to the `OmegaConfigLoader`. The `ConfigLoader` is deprecated and will be removed in the `0.19.0` release. ## Breaking changes to the API diff --git a/docs/source/configuration/config_loader_migration.md b/docs/source/configuration/config_loader_migration.md new file mode 100644 index 0000000000..f27b244afe --- /dev/null +++ b/docs/source/configuration/config_loader_migration.md @@ -0,0 +1,62 @@ +# Migration guide for config loaders +The `ConfigLoader` and `TemplatedConfigLoader` classes have been deprecated since Kedro `0.18.12` and will be removed in Kedro `0.19.0`. To ensure a smooth transition, we strongly recommend you adopt the [`OmegaConfigLoader`](/kedro.config.OmegaConfigLoader) as soon as possible. +This migration guide outlines the primary distinctions between the old loaders and the `OmegaConfigLoader`, providing step-by-step instructions on updating your code base to utilise the new class effectively. + +## [`ConfigLoader`](/kedro.config.ConfigLoader) to [`OmegaConfigLoader`](/kedro.config.OmegaConfigLoader) + +### 1. Install the Required Library +The [`OmegaConfigLoader`](advanced_configuration.md#omegaconfigloader) was introduced in Kedro `0.18.5` and is based on [OmegaConf](https://omegaconf.readthedocs.io/). In order to use it you need to ensure you have both a version of Kedro of `0.18.5` or above and `omegaconf` installed. +You can install both using `pip`: + +```bash +pip install kedro==0.18.5 +``` +This would be the minimum required Kedro version which includes `omegaconf` as a dependency. +Or you can run: +```bash +pip install -U kedro +``` + +This command installs the most recent version of Kedro which also includes `omegaconf` as a dependency. + +### 2. Use the `OmegaConfigLoader` +To use `OmegaConfigLoader` in your project, set the `CONFIG_LOADER_CLASS` constant in your [`src//settings.py`](../kedro_project_setup/settings.md): + +```diff ++ from kedro.config import OmegaConfigLoader # new import + ++ CONFIG_LOADER_CLASS = OmegaConfigLoader +``` + +### 3. Import Statements +Replace the import statement for `ConfigLoader` with the one for `OmegaConfigLoader`: + +```diff +- from kedro.config import ConfigLoader + ++ from kedro.config import OmegaConfigLoader +``` + +### 4. File Format Support +`OmegaConfigLoader` supports only `yaml` and `json` file formats. Make sure that all your configuration files are in one of these formats. If you previously used other formats with `ConfigLoader`, convert them to `yaml` or `json`. + +### 5. Load Configuration +The method to load the configuration using `OmegaConfigLoader` differs slightly from that used by `ConfigLoader`, which allowed users to access configuration through the `.get()` method and required patterns as argument. +When you migrate to use `OmegaConfigLoader` it requires you to fetch configuration through a configuration key that points to [configuration patterns specified in the loader class](configuration_basics.md#configuration-patterns) or [provided in the `CONFIG_LOADER_ARGS`](advanced_configuration.md#how-to-change-which-configuration-files-are-loaded) in `settings.py`. + +```diff +- conf_path = str(project_path / settings.CONF_SOURCE) +- conf_loader = ConfigLoader(conf_source=conf_path, env="local") +- catalog = conf_loader.get("catalog*") + ++ conf_path = str(project_path / settings.CONF_SOURCE) ++ config_loader = OmegaConfigLoader(conf_source=conf_path, env="local") ++ catalog = config_loader["catalog"] +``` + +In this example, `"catalog"` is the key to the default catalog patterns specified in the `OmegaConfigLoader` class. + +### 6. Exception Handling +For error and exception handling, most errors are the same. Those you need to be aware of that are different between the original `ConfigLoader` and `OmegaConfigLoader` are as follows: +* `OmegaConfigLoader` throws a `MissingConfigException` when configuration paths don't exist, rather than the `ValueError` used in `ConfigLoader`. +* In `OmegaConfigLoader`, if there is bad syntax in your configuration files, it will trigger a `ParserError` instead of a `BadConfigException` used in `ConfigLoader`. diff --git a/docs/source/configuration/index.md b/docs/source/configuration/index.md index 3f554e1e91..291a4fbf65 100644 --- a/docs/source/configuration/index.md +++ b/docs/source/configuration/index.md @@ -6,5 +6,6 @@ configuration_basics credentials parameters +config_loader_migration advanced_configuration ``` From 65c7f335042dcf87ed053b0d22644da4023ff771 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?La=C3=ADza=20Milena=20Scheid=20Parizotto?= <41296740+laizaparizotto@users.noreply.github.com> Date: Tue, 8 Aug 2023 08:56:30 -0300 Subject: [PATCH 16/70] Fix #2498 Adding logging issue 2498 (#2842) * Fixing typo n on line 114 file /docs/source/nodes_and_pipelines/run_a_pipeline.md from issue #2607 Signed-off-by: laizaparizotto * Removed redundant packages at lines 66, 67 and 68 on file kedro/docs/source/nodes_and_pipelines/run_a_pipeline.md, packages: from kedro.io import AbstractDataSet, DataCatalog, MemoryDataSet, from kedro.pipeline import Pipeline and from kedro.runner.runner import AbstractRunner Signed-off-by: laizaparizotto * Add logger object for logging when computing micropackage version Signed-off-by: laizaparizotto * Added new format to the logging Signed-off-by: laizaparizotto --------- Signed-off-by: laizaparizotto Co-authored-by: Merel Theisen <49397448+merelcht@users.noreply.github.com> Signed-off-by: Jason Hite --- kedro/framework/cli/micropkg.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/kedro/framework/cli/micropkg.py b/kedro/framework/cli/micropkg.py index 9f095d76b2..36d103a332 100644 --- a/kedro/framework/cli/micropkg.py +++ b/kedro/framework/cli/micropkg.py @@ -2,6 +2,7 @@ # ruff: noqa: I001 # https://github.com/kedro-org/kedro/pull/2634 from __future__ import annotations +import logging import re import shutil import sys @@ -52,6 +53,8 @@ find = {{}} """ +logger = logging.getLogger(__name__) + class _EquivalentRequirement(Requirement): """Parse a requirement according to PEP 508. @@ -592,6 +595,12 @@ def _get_default_version(metadata: ProjectMetadata, micropkg_module_path: str) - ) return micropkg_module.__version__ # type: ignore except (AttributeError, ModuleNotFoundError): + logger.warning( + "Micropackage version not found in '%s.%s', will take the top-level one in '%s'", + metadata.package_name, + micropkg_module_path, + metadata.package_name, + ) # if micropkg version doesn't exist, take the project one project_module = import_module(f"{metadata.package_name}") return project_module.__version__ # type: ignore From 480b320db0b1e8c07bb12a4bfd3fb1c65d75fd40 Mon Sep 17 00:00:00 2001 From: Merel Theisen <49397448+merelcht@users.noreply.github.com> Date: Tue, 8 Aug 2023 13:27:53 +0100 Subject: [PATCH 17/70] Try only trigger docs build when release notes updated (#2907) Signed-off-by: Merel Theisen Signed-off-by: Jason Hite --- .github/workflows/all-checks.yml | 2 ++ .github/workflows/docs-only-checks.yml | 2 ++ 2 files changed, 4 insertions(+) diff --git a/.github/workflows/all-checks.yml b/.github/workflows/all-checks.yml index 767cc46378..e3759460ac 100644 --- a/.github/workflows/all-checks.yml +++ b/.github/workflows/all-checks.yml @@ -7,12 +7,14 @@ on: - develop paths-ignore: - "docs/**" + - '**.md' pull_request: branches: - main - develop paths-ignore: - "docs/**" + - '**.md' jobs: unit-tests: diff --git a/.github/workflows/docs-only-checks.yml b/.github/workflows/docs-only-checks.yml index 4d6870d627..536fb515a4 100644 --- a/.github/workflows/docs-only-checks.yml +++ b/.github/workflows/docs-only-checks.yml @@ -7,12 +7,14 @@ on: - develop paths: - "docs/**" + - '**.md' pull_request: branches: - main - develop paths: - "docs/**" + - '**.md' jobs: lint-tests: From 51a07455da3620f6af4f464997b6a86ba3665b37 Mon Sep 17 00:00:00 2001 From: Sajid Alam <90610031+SajidAlamQB@users.noreply.github.com> Date: Tue, 8 Aug 2023 14:21:25 +0100 Subject: [PATCH 18/70] Add Python 3.11 support to Kedro (#2851) * add 3.11 Signed-off-by: SajidAlamQB <90610031+SajidAlamQB@users.noreply.github.com> * Update setup.py Signed-off-by: SajidAlamQB <90610031+SajidAlamQB@users.noreply.github.com> * limit number of python version tests Signed-off-by: SajidAlamQB <90610031+SajidAlamQB@users.noreply.github.com> * fix setup.py Signed-off-by: SajidAlamQB <90610031+SajidAlamQB@users.noreply.github.com> * delta-spark version Signed-off-by: SajidAlamQB <90610031+SajidAlamQB@users.noreply.github.com> * try original setup Signed-off-by: SajidAlamQB <90610031+SajidAlamQB@users.noreply.github.com> * remove 3.11 from circleci Signed-off-by: SajidAlamQB <90610031+SajidAlamQB@users.noreply.github.com> * Update setup.py Signed-off-by: SajidAlamQB <90610031+SajidAlamQB@users.noreply.github.com> * Update setup.py Signed-off-by: SajidAlamQB <90610031+SajidAlamQB@users.noreply.github.com> * pin matplotlib to 3.7.2 for python 3.11 Signed-off-by: SajidAlamQB <90610031+SajidAlamQB@users.noreply.github.com> * Update test_import.py Signed-off-by: SajidAlamQB <90610031+SajidAlamQB@users.noreply.github.com> * add noks test adjustments Signed-off-by: SajidAlamQB <90610031+SajidAlamQB@users.noreply.github.com> * Update setup.py Signed-off-by: SajidAlamQB <90610031+SajidAlamQB@users.noreply.github.com> * Update setup.py Signed-off-by: SajidAlamQB <90610031+SajidAlamQB@users.noreply.github.com> * replace eq with is for multi_catalog Signed-off-by: SajidAlamQB <90610031+SajidAlamQB@users.noreply.github.com> * minor fixes Signed-off-by: SajidAlamQB <90610031+SajidAlamQB@users.noreply.github.com> * remove matplotlib 3.11 pin Signed-off-by: SajidAlamQB <90610031+SajidAlamQB@users.noreply.github.com> * matplotlib pin for 3.11 Signed-off-by: SajidAlamQB <90610031+SajidAlamQB@users.noreply.github.com> * run original matplotlib pin with ubuntu only Signed-off-by: SajidAlamQB <90610031+SajidAlamQB@users.noreply.github.com> * run circleci Signed-off-by: SajidAlamQB <90610031+SajidAlamQB@users.noreply.github.com> * pin spark < 3.4 as its breaking Signed-off-by: SajidAlamQB <90610031+SajidAlamQB@users.noreply.github.com> * spark datasets revert earlier changes back to original Signed-off-by: SajidAlamQB <90610031+SajidAlamQB@users.noreply.github.com> * delta Signed-off-by: SajidAlamQB <90610031+SajidAlamQB@users.noreply.github.com> * remove all windows tests from GHA Signed-off-by: SajidAlamQB <90610031+SajidAlamQB@users.noreply.github.com> * skip coverage as not needed Signed-off-by: SajidAlamQB <90610031+SajidAlamQB@users.noreply.github.com> * Checking only WIN tests (will revert) Signed-off-by: SajidAlamQB <90610031+SajidAlamQB@users.noreply.github.com> * Update unit-tests.yml Signed-off-by: SajidAlamQB <90610031+SajidAlamQB@users.noreply.github.com> * undo previous commit Signed-off-by: SajidAlamQB <90610031+SajidAlamQB@users.noreply.github.com> * add msbuild Signed-off-by: SajidAlamQB <90610031+SajidAlamQB@users.noreply.github.com> * add ubuntu tests back in and remove 3.11 tests from circleci add in msbuild for the other tests Signed-off-by: SajidAlamQB <90610031+SajidAlamQB@users.noreply.github.com> * set hdf5 disable check and lint Signed-off-by: SajidAlamQB <90610031+SajidAlamQB@users.noreply.github.com> * pin tables for 3.8 Signed-off-by: SajidAlamQB <90610031+SajidAlamQB@users.noreply.github.com> * pin 3.8 tables Signed-off-by: SajidAlamQB <90610031+SajidAlamQB@users.noreply.github.com> * lint Signed-off-by: SajidAlamQB <90610031+SajidAlamQB@users.noreply.github.com> * remove 3.11 completely from circleci and tidy up Signed-off-by: SajidAlamQB <90610031+SajidAlamQB@users.noreply.github.com> * add 3.11 to pyarrow pin for docs Signed-off-by: SajidAlamQB <90610031+SajidAlamQB@users.noreply.github.com> * undo previous commit Signed-off-by: SajidAlamQB <90610031+SajidAlamQB@users.noreply.github.com> * update rtd to python 3.11 Signed-off-by: SajidAlamQB <90610031+SajidAlamQB@users.noreply.github.com> * try 3.10 on rtd Signed-off-by: SajidAlamQB <90610031+SajidAlamQB@users.noreply.github.com> * fix outdated pylint to ruff Signed-off-by: SajidAlamQB <90610031+SajidAlamQB@users.noreply.github.com> * changes based on review Signed-off-by: SajidAlamQB <90610031+SajidAlamQB@users.noreply.github.com> * add mock for pyarrow and deltalake for docs Signed-off-by: SajidAlamQB <90610031+SajidAlamQB@users.noreply.github.com> * undo previous commit Signed-off-by: SajidAlamQB <90610031+SajidAlamQB@users.noreply.github.com> * Update setup.py Signed-off-by: SajidAlamQB <90610031+SajidAlamQB@users.noreply.github.com> * Update .readthedocs.yml Signed-off-by: SajidAlamQB <90610031+SajidAlamQB@users.noreply.github.com> * Update conf.py Signed-off-by: SajidAlamQB <90610031+SajidAlamQB@users.noreply.github.com> * changes based on review Signed-off-by: SajidAlamQB <90610031+SajidAlamQB@users.noreply.github.com> * fix comments Signed-off-by: SajidAlamQB <90610031+SajidAlamQB@users.noreply.github.com> * test running 3.10 parallel Signed-off-by: SajidAlamQB <90610031+SajidAlamQB@users.noreply.github.com> * Add to release notes and update badges on docs Signed-off-by: SajidAlamQB <90610031+SajidAlamQB@users.noreply.github.com> * Add migration tips Signed-off-by: SajidAlamQB <90610031+SajidAlamQB@users.noreply.github.com> --------- Signed-off-by: SajidAlamQB <90610031+SajidAlamQB@users.noreply.github.com> Signed-off-by: Jason Hite --- .github/workflows/all-checks.yml | 8 +-- .github/workflows/docs-only-checks.yml | 2 +- .github/workflows/e2e-tests.yml | 3 + .github/workflows/pip-compile.yml | 3 + .github/workflows/unit-tests.yml | 13 ++-- README.md | 2 +- RELEASE.md | 6 ++ docs/source/conf.py | 3 +- docs/source/index.rst | 4 +- kedro/__init__.py | 2 +- .../datasets/spark/deltatable_dataset.py | 7 +- kedro/extras/datasets/spark/spark_dataset.py | 14 ++-- .../datasets/spark/spark_jdbc_dataset.py | 2 +- setup.py | 31 +++++---- .../datasets/spark/test_deltatable_dataset.py | 18 +++-- .../datasets/spark/test_spark_dataset.py | 21 ++++-- .../datasets/spark/test_spark_hive_dataset.py | 9 +-- .../datasets/spark/test_spark_jdbc_dataset.py | 66 ++++++++----------- tests/framework/cli/test_cli.py | 30 ++++----- tests/framework/cli/test_cli_hooks.py | 8 +-- tests/io/test_data_catalog.py | 55 ++++++++-------- tests/test_import.py | 8 +-- tests/tools/test_cli.py | 24 +++---- 23 files changed, 187 insertions(+), 152 deletions(-) diff --git a/.github/workflows/all-checks.yml b/.github/workflows/all-checks.yml index e3759460ac..51efe0e4db 100644 --- a/.github/workflows/all-checks.yml +++ b/.github/workflows/all-checks.yml @@ -21,7 +21,7 @@ jobs: strategy: matrix: os: [ ubuntu-latest, windows-latest ] - python-version: [ "3.7", "3.8", "3.9", "3.10" ] + python-version: [ "3.7", "3.8", "3.9", "3.10", "3.11" ] uses: ./.github/workflows/unit-tests.yml with: os: ${{ matrix.os }} @@ -31,7 +31,7 @@ jobs: strategy: matrix: os: [ ubuntu-latest ] - python-version: [ "3.10" ] + python-version: [ "3.11" ] uses: ./.github/workflows/lint.yml with: os: ${{ matrix.os }} @@ -41,7 +41,7 @@ jobs: strategy: matrix: os: [ ubuntu-latest, windows-latest ] - python-version: [ "3.7", "3.8", "3.9", "3.10" ] + python-version: [ "3.7", "3.8", "3.9", "3.10", "3.11" ] uses: ./.github/workflows/e2e-tests.yml with: os: ${{ matrix.os }} @@ -51,7 +51,7 @@ jobs: strategy: matrix: os: [ ubuntu-latest, windows-latest ] - python-version: [ "3.7", "3.8", "3.9", "3.10" ] + python-version: [ "3.7", "3.8", "3.9", "3.10", "3.11" ] uses: ./.github/workflows/pip-compile.yml with: os: ${{ matrix.os }} diff --git a/.github/workflows/docs-only-checks.yml b/.github/workflows/docs-only-checks.yml index 536fb515a4..b7940e85be 100644 --- a/.github/workflows/docs-only-checks.yml +++ b/.github/workflows/docs-only-checks.yml @@ -21,7 +21,7 @@ jobs: strategy: matrix: os: [ ubuntu-latest ] - python-version: [ "3.7", "3.8", "3.9", "3.10" ] + python-version: [ "3.7", "3.8", "3.9", "3.10", "3.11" ] uses: ./.github/workflows/lint.yml with: os: ${{ matrix.os }} diff --git a/.github/workflows/e2e-tests.yml b/.github/workflows/e2e-tests.yml index 593af27299..0369e4b134 100644 --- a/.github/workflows/e2e-tests.yml +++ b/.github/workflows/e2e-tests.yml @@ -35,6 +35,9 @@ jobs: with: path: ~\AppData\Local\pip\Cache key: ${{inputs.os}}-python-${{inputs.python-version}} + - name: Add MSBuild to PATH + if: inputs.os == 'windows-latest' + uses: microsoft/setup-msbuild@v1 - name: Install dependencies run: | make install-test-requirements diff --git a/.github/workflows/pip-compile.yml b/.github/workflows/pip-compile.yml index 796ba6049c..b5b1453782 100644 --- a/.github/workflows/pip-compile.yml +++ b/.github/workflows/pip-compile.yml @@ -31,6 +31,9 @@ jobs: with: path: ~\AppData\Local\pip\Cache key: ${{inputs.os}}-python-${{inputs.python-version}} + - name: Add MSBuild to PATH + if: inputs.os == 'windows-latest' + uses: microsoft/setup-msbuild@v1 - name: Install dependencies run: | make install-test-requirements diff --git a/.github/workflows/unit-tests.yml b/.github/workflows/unit-tests.yml index 2434f7811f..c56a67c707 100644 --- a/.github/workflows/unit-tests.yml +++ b/.github/workflows/unit-tests.yml @@ -30,6 +30,9 @@ jobs: with: path: ~\AppData\Local\pip\Cache key: ${{inputs.os}}-python-${{inputs.python-version}} + - name: Add MSBuild to PATH + if: inputs.os == 'windows-latest' + uses: microsoft/setup-msbuild@v1 - name: Install dependencies run: | make install-test-requirements @@ -39,15 +42,9 @@ jobs: run: pip install tables - name: pip freeze run: pip freeze - - name: Run unit tests sequentially - if: inputs.os == 'ubuntu-latest' && inputs.python-version == '3.10' - run: make test-sequential - name: Run unit tests - if: inputs.os == 'ubuntu-latest' && inputs.python-version != '3.10' + if: inputs.os == 'ubuntu-latest' run: make test - - name: Run unit tests without spark sequentially (Windows) - if: inputs.os == 'windows-latest' && inputs.python-version == '3.10' - run: make test-no-spark-sequential - name: Run unit tests without spark (Windows) - if: inputs.os == 'windows-latest' && inputs.python-version != '3.10' + if: inputs.os == 'windows-latest' run: make test-no-spark diff --git a/README.md b/README.md index a909df2535..aed1d6894c 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ ![Kedro Logo Banner - Light](.github/demo-dark.png#gh-dark-mode-only) ![Kedro Logo Banner - Dark](.github/demo-light.png#gh-light-mode-only) -[![Python version](https://img.shields.io/badge/python-3.7%20%7C%203.8%20%7C%203.9%20%7C%203.10-blue.svg)](https://pypi.org/project/kedro/) +[![Python version](https://img.shields.io/badge/python-3.7%20%7C%203.8%20%7C%203.9%20%7C%203.10%20%7C%203.11-blue.svg)](https://pypi.org/project/kedro/) [![PyPI version](https://badge.fury.io/py/kedro.svg)](https://pypi.org/project/kedro/) [![Conda version](https://img.shields.io/conda/vn/conda-forge/kedro.svg)](https://anaconda.org/conda-forge/kedro) [![License](https://img.shields.io/badge/license-Apache%202.0-blue.svg)](https://github.com/kedro-org/kedro/blob/main/LICENSE.md) diff --git a/RELEASE.md b/RELEASE.md index 71b8a4748b..a9fe6bfcbf 100644 --- a/RELEASE.md +++ b/RELEASE.md @@ -12,6 +12,7 @@ ## Major features and improvements * Allowed registering of custom resolvers to `OmegaConfigLoader` through `CONFIG_LOADER_ARGS`. +* Added support for Python 3.11. This includes tackling challenges like dependency pinning and test adjustments to ensure a smooth experience. Detailed migration tips are provided below for further context. ## Bug fixes and other changes * Updated `kedro pipeline create` and `kedro catalog create` to use new `/conf` file structure. @@ -19,6 +20,11 @@ ## Documentation changes * Added migration guide from the `ConfigLoader` to the `OmegaConfigLoader`. The `ConfigLoader` is deprecated and will be removed in the `0.19.0` release. +## Migration Tips for Python 3.11: +* PyTables on Windows: Users on Windows with Python >=3.8 should note we've pinned `pytables` to `3.8.0` due to compatibility issues. +* Spark Dependency: We've set an upper version limit for `pyspark` at <3.4 due to breaking changes in 3.4. +* Testing with Python 3.10: The latest `moto` version now supports parallel test execution for Python 3.10, resolving previous issues. + ## Breaking changes to the API ## Upcoming deprecations for Kedro 0.19.0 diff --git a/docs/source/conf.py b/docs/source/conf.py index 205f98416e..804bbbbfa9 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -226,7 +226,8 @@ "https://github.com/kedro-org/kedro/blob/main/README.md#the-humans-behind-kedro", # "anchor not found" but is valid "https://opensource.org/license/apache2-0-php/", "https://docs.github.com/en/rest/overview/other-authentication-methods#via-username-and-password", - "https://docs.snowflake.com/en/developer-guide/snowpark/reference/python/api/snowflake.snowpark.DataFrameWriter.saveAsTable.html" + "https://docs.snowflake.com/en/developer-guide/snowpark/reference/python/api/snowflake.snowpark.DataFrameWriter.saveAsTable.html", + "https://www.educative.io/blog/advanced-yaml-syntax-cheatsheet#anchors" ] # retry before render a link broken (fix for "too many requests") diff --git a/docs/source/index.rst b/docs/source/index.rst index f9c78a2748..ac106f9c48 100644 --- a/docs/source/index.rst +++ b/docs/source/index.rst @@ -23,9 +23,9 @@ Welcome to Kedro's documentation! :target: https://opensource.org/license/apache2-0-php/ :alt: License is Apache 2.0 -.. image:: https://img.shields.io/badge/python-3.7%20%7C%203.8%20%7C%203.9%20%7C%203.10-blue.svg +.. image:: https://img.shields.io/badge/python-3.7%20%7C%203.8%20%7C%203.9%20%7C%203.10%20%7C%203.11-blue.svg :target: https://pypi.org/project/kedro/ - :alt: Python version 3.7, 3.8, 3.9, 3.10 + :alt: Python version 3.7, 3.8, 3.9, 3.10, 3.11 .. image:: https://badge.fury.io/py/kedro.svg :target: https://pypi.org/project/kedro/ diff --git a/kedro/__init__.py b/kedro/__init__.py index 39f84743b2..7a7db37ae2 100644 --- a/kedro/__init__.py +++ b/kedro/__init__.py @@ -18,7 +18,7 @@ class KedroPythonVersionWarning(UserWarning): if not sys.warnoptions: warnings.simplefilter("error", KedroPythonVersionWarning) -if sys.version_info >= (3, 11): +if sys.version_info >= (3, 12): warnings.warn( """Kedro is not yet fully compatible with this Python version. To proceed at your own risk and ignore this warning, diff --git a/kedro/extras/datasets/spark/deltatable_dataset.py b/kedro/extras/datasets/spark/deltatable_dataset.py index 3d56f81048..fc6c1d5d97 100644 --- a/kedro/extras/datasets/spark/deltatable_dataset.py +++ b/kedro/extras/datasets/spark/deltatable_dataset.py @@ -100,7 +100,12 @@ def _exists(self) -> bool: try: self._get_spark().read.load(path=load_path, format="delta") except AnalysisException as exception: - if "is not a Delta table" in exception.desc: + # `AnalysisException.desc` is deprecated with pyspark >= 3.4 + message = ( + exception.desc if hasattr(exception, "desc") else exception.message + ) + + if "Path does not exist:" in message or "is not a Delta table" in message: return False raise diff --git a/kedro/extras/datasets/spark/spark_dataset.py b/kedro/extras/datasets/spark/spark_dataset.py index b27147b7a6..0d60d943ac 100644 --- a/kedro/extras/datasets/spark/spark_dataset.py +++ b/kedro/extras/datasets/spark/spark_dataset.py @@ -41,7 +41,8 @@ def _parse_glob_pattern(pattern: str) -> str: def _split_filepath(filepath: str) -> Tuple[str, str]: split_ = filepath.split("://", 1) - if len(split_) == 2: # noqa: PLR2004 + MIN_SPLIT_SIZE = 2 + if len(split_) == MIN_SPLIT_SIZE: return split_[0] + "://", split_[1] return "", split_[0] @@ -232,7 +233,7 @@ class SparkDataSet(AbstractVersionedDataSet[DataFrame, DataFrame]): DEFAULT_LOAD_ARGS = {} # type: Dict[str, Any] DEFAULT_SAVE_ARGS = {} # type: Dict[str, Any] - def __init__( # noqa: too-many-arguments + def __init__( # ruff: noqa: PLR0913 self, filepath: str, file_format: str = "parquet", @@ -401,10 +402,11 @@ def _exists(self) -> bool: try: self._get_spark().read.load(load_path, self._file_format) except AnalysisException as exception: - if ( - exception.desc.startswith("Path does not exist:") - or "is not a Delta table" in exception.desc - ): + # `AnalysisException.desc` is deprecated with pyspark >= 3.4 + message = ( + exception.desc if hasattr(exception, "desc") else exception.message + ) + if "Path does not exist:" in message or "is not a Delta table" in message: return False raise return True diff --git a/kedro/extras/datasets/spark/spark_jdbc_dataset.py b/kedro/extras/datasets/spark/spark_jdbc_dataset.py index 26a1ed2481..15e01c4468 100644 --- a/kedro/extras/datasets/spark/spark_jdbc_dataset.py +++ b/kedro/extras/datasets/spark/spark_jdbc_dataset.py @@ -168,7 +168,7 @@ def _describe(self) -> Dict[str, Any]: } @staticmethod - def _get_spark(): + def _get_spark(): # pragma: no cover return SparkSession.builder.getOrCreate() def _load(self) -> DataFrame: diff --git a/setup.py b/setup.py index 8232f8a315..e78ea817a7 100644 --- a/setup.py +++ b/setup.py @@ -5,7 +5,7 @@ # at least 1.3 to be able to use XMLDataSet and pandas integration with fsspec PANDAS = "pandas~=1.3" -SPARK = "pyspark>=2.2, <4.0" +SPARK = "pyspark>=2.2, <3.4" HDFS = "hdfs>=2.5.8, <3.0" S3FS = "s3fs>=0.3.0, <0.5" @@ -30,7 +30,7 @@ def _collect_requirements(requires): "geopandas.GeoJSONDataSet": ["geopandas>=0.6.0, <1.0", "pyproj~=3.0"] } matplotlib_require = {"matplotlib.MatplotlibWriter": ["matplotlib>=3.0.3, <4.0"]} -holoviews_require = {"holoviews.HoloviewsWriter": ["holoviews~=1.13.0"]} +holoviews_require = {"holoviews.HoloviewsWriter": ["holoviews>=1.13.0"]} networkx_require = {"networkx.NetworkXDataSet": ["networkx~=2.4"]} pandas_require = { "pandas.CSVDataSet": [PANDAS], @@ -143,14 +143,15 @@ def _collect_requirements(requires): "compress-pickle[lz4]~=2.1.0", "coverage[toml]", "dask[complete]~=2021.10", # pinned by Snyk to avoid a vulnerability - "delta-spark~=1.2.1", # 1.2.0 has a bug that breaks some of our tests: https://github.com/delta-io/delta/issues/1070 + "delta-spark>=1.2.1; python_version >= '3.11'", # 1.2.0 has a bug that breaks some of our tests: https://github.com/delta-io/delta/issues/1070 + "delta-spark~=1.2.1; python_version < '3.11'", "dill~=0.3.1", "filelock>=3.4.0, <4.0", "gcsfs>=2021.4, <=2023.1; python_version == '3.7'", "gcsfs>=2023.1, <2023.3; python_version >= '3.8'", "geopandas>=0.6.0, <1.0", "hdfs>=2.5.8, <3.0", - "holoviews~=1.13.0", + "holoviews>=1.13.0", "import-linter[toml]==1.8.0", "ipython>=7.31.1, <8.0; python_version < '3.8'", "ipython~=8.10; python_version >= '3.8'", @@ -162,23 +163,25 @@ def _collect_requirements(requires): "jupyter~=1.0", "lxml~=4.6", "matplotlib>=3.0.3, <3.4; python_version < '3.10'", # 3.4.0 breaks holoviews - "matplotlib>=3.5, <3.6; python_version == '3.10'", + "matplotlib>=3.5, <3.6; python_version >= '3.10'", "memory_profiler>=0.50.0, <1.0", "moto==1.3.7; python_version < '3.10'", - "moto==3.0.4; python_version == '3.10'", + "moto==4.1.12; python_version >= '3.10'", "networkx~=2.4", "opencv-python~=4.5.5.64", "openpyxl>=3.0.3, <4.0", - "pandas-gbq>=0.12.0, <0.18.0", + "pandas-gbq>=0.12.0, <0.18.0; python_version < '3.11'", + "pandas-gbq>=0.18.0; python_version >= '3.11'", "pandas~=1.3 # 1.3 for read_xml/to_xml", "Pillow~=9.0", "plotly>=4.8.0, <6.0", "pre-commit>=2.9.2, <3.0", # The hook `mypy` requires pre-commit version 2.9.2. - "psutil~=5.8", - "pyarrow>=6.0", + "pyarrow>=1.0; python_version < '3.11'", + "pyarrow>=7.0; python_version >= '3.11'", # Adding to avoid numpy build errors "pylint>=2.17.0, <3.0", "pyproj~=3.0", - "pyspark>=2.2, <4.0", + "pyspark>=2.2, <3.4; python_version < '3.11'", + "pyspark>=3.4; python_version >= '3.11'", "pytest-cov~=3.0", "pytest-mock>=1.7.1, <2.0", "pytest-xdist[psutil]~=2.2.1", @@ -187,10 +190,12 @@ def _collect_requirements(requires): "requests-mock~=1.6", "requests~=2.20", "s3fs>=0.3.0, <0.5", # Needs to be at least 0.3.0 to make use of `cachable` attribute on S3FileSystem. - "scikit-learn~=1.0.2", - "scipy~=1.7.3", + "scikit-learn>=1.0.2,<2", + "scipy>=1.7.3", + "semver", "SQLAlchemy~=1.2", - "tables~=3.6.0; platform_system == 'Windows' and python_version<'3.9'", + "tables~=3.6.0; platform_system == 'Windows' and python_version<'3.8'", + "tables~=3.8.0; platform_system == 'Windows' and python_version>='3.8'", # Import issues with python 3.8 with pytables pinning to 3.8.0 fixes this https://github.com/PyTables/PyTables/issues/933#issuecomment-1555917593 "tables~=3.6; platform_system != 'Windows'", "tensorflow~=2.0; platform_system != 'Darwin' or platform_machine != 'arm64'", # https://developer.apple.com/metal/tensorflow-plugin/ diff --git a/tests/extras/datasets/spark/test_deltatable_dataset.py b/tests/extras/datasets/spark/test_deltatable_dataset.py index 00eb313f6a..a0ad5bc9d9 100644 --- a/tests/extras/datasets/spark/test_deltatable_dataset.py +++ b/tests/extras/datasets/spark/test_deltatable_dataset.py @@ -1,8 +1,10 @@ import pytest from delta import DeltaTable +from pyspark import __version__ from pyspark.sql import SparkSession from pyspark.sql.types import IntegerType, StringType, StructField, StructType from pyspark.sql.utils import AnalysisException +from semver import VersionInfo from kedro.extras.datasets.spark import DeltaTableDataSet, SparkDataSet from kedro.io import DataCatalog, DatasetError @@ -10,6 +12,8 @@ from kedro.pipeline.modular_pipeline import pipeline as modular_pipeline from kedro.runner import ParallelRunner +SPARK_VERSION = VersionInfo.parse(__version__) + @pytest.fixture def sample_spark_df(): @@ -65,10 +69,16 @@ def test_exists(self, tmp_path, sample_spark_df): def test_exists_raises_error(self, mocker): delta_ds = DeltaTableDataSet(filepath="") - mocker.patch.object( - delta_ds, "_get_spark", side_effect=AnalysisException("Other Exception", []) - ) - + if SPARK_VERSION.match(">=3.4.0"): + mocker.patch.object( + delta_ds, "_get_spark", side_effect=AnalysisException("Other Exception") + ) + else: + mocker.patch.object( + delta_ds, + "_get_spark", + side_effect=AnalysisException("Other Exception", []), + ) with pytest.raises(DatasetError, match="Other Exception"): delta_ds.exists() diff --git a/tests/extras/datasets/spark/test_spark_dataset.py b/tests/extras/datasets/spark/test_spark_dataset.py index da979901ac..a491ef6aeb 100644 --- a/tests/extras/datasets/spark/test_spark_dataset.py +++ b/tests/extras/datasets/spark/test_spark_dataset.py @@ -7,6 +7,7 @@ import pandas as pd import pytest from moto import mock_s3 +from pyspark import __version__ from pyspark.sql import SparkSession from pyspark.sql.functions import col from pyspark.sql.types import ( @@ -17,6 +18,7 @@ StructType, ) from pyspark.sql.utils import AnalysisException +from semver import VersionInfo from kedro.extras.datasets.pandas import CSVDataSet, ParquetDataSet from kedro.extras.datasets.pickle import PickleDataSet @@ -60,6 +62,8 @@ (HDFS_PREFIX + "/2019-02-01T00.00.00.000Z", [], ["other_file"]), ] +SPARK_VERSION = VersionInfo.parse(__version__) + @pytest.fixture def sample_pandas_df() -> pd.DataFrame: @@ -405,11 +409,18 @@ def test_exists_raises_error(self, mocker): # exists should raise all errors except for # AnalysisExceptions clearly indicating a missing file spark_data_set = SparkDataSet(filepath="") - mocker.patch.object( - spark_data_set, - "_get_spark", - side_effect=AnalysisException("Other Exception", []), - ) + if SPARK_VERSION.match(">=3.4.0"): + mocker.patch.object( + spark_data_set, + "_get_spark", + side_effect=AnalysisException("Other Exception"), + ) + else: + mocker.patch.object( # pylint: disable=expression-not-assigned + spark_data_set, + "_get_spark", + side_effect=AnalysisException("Other Exception", []), + ) with pytest.raises(DatasetError, match="Other Exception"): spark_data_set.exists() diff --git a/tests/extras/datasets/spark/test_spark_hive_dataset.py b/tests/extras/datasets/spark/test_spark_hive_dataset.py index ba7fc734a6..399ebc4169 100644 --- a/tests/extras/datasets/spark/test_spark_hive_dataset.py +++ b/tests/extras/datasets/spark/test_spark_hive_dataset.py @@ -293,12 +293,9 @@ def test_read_from_non_existent_table(self): ) with pytest.raises( DatasetError, - match=r"Failed while loading data from data set " - r"SparkHiveDataSet\(database=default_1, format=hive, " - r"table=table_doesnt_exist, table_pk=\[\], write_mode=append\)\.\n" - r"Table or view not found: default_1.table_doesnt_exist;\n" - r"'UnresolvedRelation \[default_1, " - r"table_doesnt_exist\], \[\], false\n", + match=r"Failed while loading data from data set SparkHiveDataSet" + r"|table_doesnt_exist" + r"|UnresolvedRelation", ): dataset.load() diff --git a/tests/extras/datasets/spark/test_spark_jdbc_dataset.py b/tests/extras/datasets/spark/test_spark_jdbc_dataset.py index fa7af0f966..6d89251fc5 100644 --- a/tests/extras/datasets/spark/test_spark_jdbc_dataset.py +++ b/tests/extras/datasets/spark/test_spark_jdbc_dataset.py @@ -1,5 +1,3 @@ -from unittest import mock - import pytest from kedro.extras.datasets.spark import SparkJDBCDataSet @@ -53,59 +51,52 @@ def test_missing_table(): SparkJDBCDataSet(url="dummy_url", table=None) -def mock_save(arg_dict): - mock_data = mock.Mock() - data_set = SparkJDBCDataSet(**arg_dict) +def test_save(mocker, spark_jdbc_args): + mock_data = mocker.Mock() + data_set = SparkJDBCDataSet(**spark_jdbc_args) data_set.save(mock_data) - return mock_data - - -def test_save(spark_jdbc_args): - data = mock_save(spark_jdbc_args) - data.write.jdbc.assert_called_with("dummy_url", "dummy_table") + mock_data.write.jdbc.assert_called_with("dummy_url", "dummy_table") -def test_save_credentials(spark_jdbc_args_credentials): - data = mock_save(spark_jdbc_args_credentials) - data.write.jdbc.assert_called_with( +def test_save_credentials(mocker, spark_jdbc_args_credentials): + mock_data = mocker.Mock() + data_set = SparkJDBCDataSet(**spark_jdbc_args_credentials) + data_set.save(mock_data) + mock_data.write.jdbc.assert_called_with( "dummy_url", "dummy_table", properties={"user": "dummy_user", "password": "dummy_pw"}, ) -def test_save_args(spark_jdbc_args_save_load): - data = mock_save(spark_jdbc_args_save_load) - data.write.jdbc.assert_called_with( +def test_save_args(mocker, spark_jdbc_args_save_load): + mock_data = mocker.Mock() + data_set = SparkJDBCDataSet(**spark_jdbc_args_save_load) + data_set.save(mock_data) + mock_data.write.jdbc.assert_called_with( "dummy_url", "dummy_table", properties={"driver": "dummy_driver"} ) -def test_except_bad_credentials(spark_jdbc_args_credentials_with_none_password): +def test_except_bad_credentials(mocker, spark_jdbc_args_credentials_with_none_password): pattern = r"Credential property 'password' cannot be None(.+)" with pytest.raises(DatasetError, match=pattern): - mock_save(spark_jdbc_args_credentials_with_none_password) + mock_data = mocker.Mock() + data_set = SparkJDBCDataSet(**spark_jdbc_args_credentials_with_none_password) + data_set.save(mock_data) -@mock.patch( - "kedro.extras.datasets.spark.spark_jdbc_dataset.SparkSession.builder.getOrCreate" -) -def mock_load(mock_get_or_create, arg_dict): - spark = mock_get_or_create.return_value - data_set = SparkJDBCDataSet(**arg_dict) +def test_load(mocker, spark_jdbc_args): + spark = mocker.patch.object(SparkJDBCDataSet, "_get_spark").return_value + data_set = SparkJDBCDataSet(**spark_jdbc_args) data_set.load() - return spark - - -def test_load(spark_jdbc_args): - # pylint: disable=no-value-for-parameter - spark = mock_load(arg_dict=spark_jdbc_args) spark.read.jdbc.assert_called_with("dummy_url", "dummy_table") -def test_load_credentials(spark_jdbc_args_credentials): - # pylint: disable=no-value-for-parameter - spark = mock_load(arg_dict=spark_jdbc_args_credentials) +def test_load_credentials(mocker, spark_jdbc_args_credentials): + spark = mocker.patch.object(SparkJDBCDataSet, "_get_spark").return_value + data_set = SparkJDBCDataSet(**spark_jdbc_args_credentials) + data_set.load() spark.read.jdbc.assert_called_with( "dummy_url", "dummy_table", @@ -113,9 +104,10 @@ def test_load_credentials(spark_jdbc_args_credentials): ) -def test_load_args(spark_jdbc_args_save_load): - # pylint: disable=no-value-for-parameter - spark = mock_load(arg_dict=spark_jdbc_args_save_load) +def test_load_args(mocker, spark_jdbc_args_save_load): + spark = mocker.patch.object(SparkJDBCDataSet, "_get_spark").return_value + data_set = SparkJDBCDataSet(**spark_jdbc_args_save_load) + data_set.load() spark.read.jdbc.assert_called_with( "dummy_url", "dummy_table", properties={"driver": "dummy_driver"} ) diff --git a/tests/framework/cli/test_cli.py b/tests/framework/cli/test_cli.py index 6788f349f0..8c33f4e2ae 100644 --- a/tests/framework/cli/test_cli.py +++ b/tests/framework/cli/test_cli.py @@ -329,14 +329,14 @@ def test_init_error_is_caught(self, entry_points, entry_point): class TestKedroCLI: def test_project_commands_no_clipy(self, mocker, fake_metadata): - mocker.patch( - "kedro.framework.cli.cli.importlib.import_module", - side_effect=cycle([ModuleNotFoundError()]), - ) mocker.patch("kedro.framework.cli.cli._is_project", return_value=True) mocker.patch( "kedro.framework.cli.cli.bootstrap_project", return_value=fake_metadata ) + mocker.patch( + "kedro.framework.cli.cli.importlib.import_module", + side_effect=cycle([ModuleNotFoundError()]), + ) kedro_cli = KedroCLI(fake_metadata.project_path) print(kedro_cli.project_groups) assert len(kedro_cli.project_groups) == 6 @@ -356,26 +356,26 @@ def test_project_commands_no_project(self, mocker, tmp_path): assert kedro_cli._metadata is None def test_project_commands_invalid_clipy(self, mocker, fake_metadata): - mocker.patch( - "kedro.framework.cli.cli.importlib.import_module", return_value=None - ) mocker.patch("kedro.framework.cli.cli._is_project", return_value=True) mocker.patch( "kedro.framework.cli.cli.bootstrap_project", return_value=fake_metadata ) + mocker.patch( + "kedro.framework.cli.cli.importlib.import_module", return_value=None + ) with raises(KedroCliError, match="Cannot load commands from"): _ = KedroCLI(fake_metadata.project_path) def test_project_commands_valid_clipy(self, mocker, fake_metadata): Module = namedtuple("Module", ["cli"]) - mocker.patch( - "kedro.framework.cli.cli.importlib.import_module", - return_value=Module(cli=cli), - ) mocker.patch("kedro.framework.cli.cli._is_project", return_value=True) mocker.patch( "kedro.framework.cli.cli.bootstrap_project", return_value=fake_metadata ) + mocker.patch( + "kedro.framework.cli.cli.importlib.import_module", + return_value=Module(cli=cli), + ) kedro_cli = KedroCLI(fake_metadata.project_path) assert len(kedro_cli.project_groups) == 7 assert kedro_cli.project_groups == [ @@ -402,14 +402,14 @@ def test_kedro_cli_no_project(self, mocker, tmp_path): def test_kedro_cli_with_project(self, mocker, fake_metadata): Module = namedtuple("Module", ["cli"]) - mocker.patch( - "kedro.framework.cli.cli.importlib.import_module", - return_value=Module(cli=cli), - ) mocker.patch("kedro.framework.cli.cli._is_project", return_value=True) mocker.patch( "kedro.framework.cli.cli.bootstrap_project", return_value=fake_metadata ) + mocker.patch( + "kedro.framework.cli.cli.importlib.import_module", + return_value=Module(cli=cli), + ) kedro_cli = KedroCLI(fake_metadata.project_path) assert len(kedro_cli.global_groups) == 2 diff --git a/tests/framework/cli/test_cli_hooks.py b/tests/framework/cli/test_cli_hooks.py index 0f7866f45f..41fbdaa705 100644 --- a/tests/framework/cli/test_cli_hooks.py +++ b/tests/framework/cli/test_cli_hooks.py @@ -98,10 +98,6 @@ def test_kedro_cli_should_invoke_cli_hooks_from_plugin( caplog.set_level(logging.DEBUG, logger="kedro") Module = namedtuple("Module", ["cli"]) - mocker.patch( - "kedro.framework.cli.cli.importlib.import_module", - return_value=Module(cli=cli), - ) mocker.patch( "kedro.framework.cli.cli._is_project", return_value=True, @@ -110,6 +106,10 @@ def test_kedro_cli_should_invoke_cli_hooks_from_plugin( "kedro.framework.cli.cli.bootstrap_project", return_value=fake_metadata, ) + mocker.patch( + "kedro.framework.cli.cli.importlib.import_module", + return_value=Module(cli=cli), + ) kedro_cli = KedroCLI(fake_metadata.project_path) result = CliRunner().invoke(kedro_cli, [command]) assert ( diff --git a/tests/io/test_data_catalog.py b/tests/io/test_data_catalog.py index 574f349809..9c61a9d3ec 100644 --- a/tests/io/test_data_catalog.py +++ b/tests/io/test_data_catalog.py @@ -336,7 +336,6 @@ def test_multi_catalog_list_bad_regex(self, multi_catalog): multi_catalog.list("((") def test_eq(self, multi_catalog, data_catalog): - assert multi_catalog == multi_catalog # noqa: PLR0124 assert multi_catalog == multi_catalog.shallow_copy() assert multi_catalog != data_catalog @@ -377,13 +376,14 @@ def test_mutating_datasets_not_allowed(self, data_catalog_from_config): def test_confirm(self, mocker, caplog): """Confirm the dataset""" - mock_ds = mocker.Mock() - data_catalog = DataCatalog(data_sets={"mocked": mock_ds}) - data_catalog.confirm("mocked") - mock_ds.confirm.assert_called_once_with() - assert caplog.record_tuples == [ - ("kedro.io.data_catalog", logging.INFO, "Confirming dataset 'mocked'") - ] + with caplog.at_level(logging.INFO): + mock_ds = mocker.Mock() + data_catalog = DataCatalog(data_sets={"mocked": mock_ds}) + data_catalog.confirm("mocked") + mock_ds.confirm.assert_called_once_with() + assert caplog.record_tuples == [ + ("kedro.io.data_catalog", logging.INFO, "Confirming dataset 'mocked'") + ] @pytest.mark.parametrize( "dataset_name,error_pattern", @@ -567,24 +567,25 @@ def test_error_dataset_init(self, bad_config): def test_confirm(self, tmp_path, caplog, mocker): """Confirm the dataset""" - mock_confirm = mocker.patch("kedro.io.IncrementalDataset.confirm") - catalog = { - "ds_to_confirm": { - "type": "IncrementalDataset", - "dataset": "pandas.CSVDataSet", - "path": str(tmp_path), + with caplog.at_level(logging.INFO): + mock_confirm = mocker.patch("kedro.io.IncrementalDataset.confirm") + catalog = { + "ds_to_confirm": { + "type": "IncrementalDataset", + "dataset": "pandas.CSVDataSet", + "path": str(tmp_path), + } } - } - data_catalog = DataCatalog.from_config(catalog=catalog) - data_catalog.confirm("ds_to_confirm") - assert caplog.record_tuples == [ - ( - "kedro.io.data_catalog", - logging.INFO, - "Confirming dataset 'ds_to_confirm'", - ) - ] - mock_confirm.assert_called_once_with() + data_catalog = DataCatalog.from_config(catalog=catalog) + data_catalog.confirm("ds_to_confirm") + assert caplog.record_tuples == [ + ( + "kedro.io.data_catalog", + logging.INFO, + "Confirming dataset 'ds_to_confirm'", + ) + ] + mock_confirm.assert_called_once_with() @pytest.mark.parametrize( "dataset_name,pattern", @@ -735,8 +736,10 @@ def test_replacing_nonword_characters(self): assert "ds3__csv" in catalog.datasets.__dict__ assert "jalapeño" in catalog.datasets.__dict__ - def test_no_versions_with_cloud_protocol(self): + def test_no_versions_with_cloud_protocol(self, monkeypatch): """Check the error if no versions are available for load from cloud storage""" + monkeypatch.setenv("AWS_ACCESS_KEY_ID", "dummmy") + monkeypatch.setenv("AWS_SECRET_ACCESS_KEY", "dummmy") version = Version(load=None, save=None) versioned_dataset = CSVDataSet("s3://bucket/file.csv", version=version) pattern = re.escape( diff --git a/tests/test_import.py b/tests/test_import.py index 81436ecfc6..a9aa72e21a 100644 --- a/tests/test_import.py +++ b/tests/test_import.py @@ -4,8 +4,8 @@ def test_import_kedro_with_no_official_support_raise_error(mocker): - """Test importing kedro with python>=3.11 should fail""" - mocker.patch("kedro.sys.version_info", (3, 11)) + """Test importing kedro with python>=3.12 should fail""" + mocker.patch("kedro.sys.version_info", (3, 12)) # We use the parent class to avoid issues with `exec_module` with pytest.raises(UserWarning) as excinfo: @@ -15,8 +15,8 @@ def test_import_kedro_with_no_official_support_raise_error(mocker): def test_import_kedro_with_no_official_support_emits_warning(mocker): - """Test importing kedro python>=3.11 and controlled warnings should work""" - mocker.patch("kedro.sys.version_info", (3, 11)) + """Test importing kedro python>=3.12 and controlled warnings should work""" + mocker.patch("kedro.sys.version_info", (3, 12)) mocker.patch("kedro.sys.warnoptions", ["default:Kedro is not yet fully compatible"]) # We use the parent class to avoid issues with `exec_module` diff --git a/tests/tools/test_cli.py b/tests/tools/test_cli.py index 1b80ad8064..cf3ce71d1c 100644 --- a/tests/tools/test_cli.py +++ b/tests/tools/test_cli.py @@ -56,10 +56,6 @@ def fake_metadata(fake_root_dir): class TestCLITools: def test_get_cli_structure_raw(self, mocker, fake_metadata): Module = namedtuple("Module", ["cli"]) - mocker.patch( - "kedro.framework.cli.cli.importlib.import_module", - return_value=Module(cli=cli), - ) mocker.patch( "kedro.framework.cli.cli._is_project", return_value=True, @@ -68,6 +64,10 @@ def test_get_cli_structure_raw(self, mocker, fake_metadata): "kedro.framework.cli.cli.bootstrap_project", return_value=fake_metadata, ) + mocker.patch( + "kedro.framework.cli.cli.importlib.import_module", + return_value=Module(cli=cli), + ) kedro_cli = KedroCLI(fake_metadata.project_path) raw_cli_structure = get_cli_structure(kedro_cli, get_help=False) @@ -85,10 +85,6 @@ def test_get_cli_structure_raw(self, mocker, fake_metadata): def test_get_cli_structure_depth(self, mocker, fake_metadata): Module = namedtuple("Module", ["cli"]) - mocker.patch( - "kedro.framework.cli.cli.importlib.import_module", - return_value=Module(cli=cli), - ) mocker.patch( "kedro.framework.cli.cli._is_project", return_value=True, @@ -97,6 +93,10 @@ def test_get_cli_structure_depth(self, mocker, fake_metadata): "kedro.framework.cli.cli.bootstrap_project", return_value=fake_metadata, ) + mocker.patch( + "kedro.framework.cli.cli.importlib.import_module", + return_value=Module(cli=cli), + ) kedro_cli = KedroCLI(fake_metadata.project_path) raw_cli_structure = get_cli_structure(kedro_cli, get_help=False) assert isinstance(raw_cli_structure["kedro"]["new"], dict) @@ -121,10 +121,6 @@ def test_get_cli_structure_depth(self, mocker, fake_metadata): def test_get_cli_structure_help(self, mocker, fake_metadata): Module = namedtuple("Module", ["cli"]) - mocker.patch( - "kedro.framework.cli.cli.importlib.import_module", - return_value=Module(cli=cli), - ) mocker.patch( "kedro.framework.cli.cli._is_project", return_value=True, @@ -133,6 +129,10 @@ def test_get_cli_structure_help(self, mocker, fake_metadata): "kedro.framework.cli.cli.bootstrap_project", return_value=fake_metadata, ) + mocker.patch( + "kedro.framework.cli.cli.importlib.import_module", + return_value=Module(cli=cli), + ) kedro_cli = KedroCLI(fake_metadata.project_path) help_cli_structure = get_cli_structure(kedro_cli, get_help=True) From 80516ddabe095cfe1b40935b1f14c2ce3426e6be Mon Sep 17 00:00:00 2001 From: Jo Stichbury Date: Tue, 8 Aug 2023 15:01:31 +0100 Subject: [PATCH 19/70] Revise FAQs and README (#2909) * Revise FAQs and README Signed-off-by: Jo Stichbury * Add back the data layers FAQ as I've no idea where else it fits Signed-off-by: Jo Stichbury * minor changes from review Signed-off-by: Jo Stichbury --------- Signed-off-by: Jo Stichbury Signed-off-by: Jason Hite --- CONTRIBUTING.md | 13 +--- README.md | 64 +++--------------- docs/source/faq/faq.md | 26 ++++++- docs/source/get_started/install.md | 4 +- docs/source/index.rst | 4 +- docs/source/meta/images/data_layers.png | Bin 0 -> 80345 bytes docs/source/resources/index.md | 2 +- docs/source/tutorial/spaceflights_tutorial.md | 2 +- .../visualisation/kedro-viz_visualisation.md | 2 +- 9 files changed, 45 insertions(+), 72 deletions(-) create mode 100644 docs/source/meta/images/data_layers.png diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 40da48ccdf..d4dbf8c451 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -17,20 +17,13 @@ The Kedro team pledges to foster and maintain a friendly community. We enforce a You can find the Kedro community on our [Slack organisation](https://slack.kedro.org/), which is where we share news and announcements, and general chat. You're also welcome to post links here to any articles or videos about Kedro that you create, or find, such as how-tos, showcases, demos, blog posts or tutorials. -We also curate a [GitHub repo that lists content created by the Kedro community](https://github.com/kedro-org/awesome-kedro). +We also curate a [GitHub repo that lists content created by the Kedro community](https://github.com/kedro-org/awesome-kedro). If you've made something with Kedro, simply add it to the list with a PR! ## Contribute to the project -There are quite a few ways to contribute to the project, find inspiration from the table below. +There are quite a few ways to contribute to Kedro, sich as answering questions about Kedro to help others, fixing a typo on the documentation, reporting a bug, reviewing pull requests or adding a feature. -|Activity|Description| -|-|-| -|Community Q&A|We encourage you to ask and answer technical questions on [GitHub discussions](https://github.com/kedro-org/kedro/discussions) or [Slack](https://slack.kedro.org/), but the former is often preferable since it will be picked up by search engines.| -|Report bugs and security vulnerabilities |We use [GitHub issues](https://github.com/kedro-org/kedro/issues) to keep track of known bugs and security vulnerabilities. We keep a close eye on them and update them when we have an internal fix in progress. Before you report a new issue, do your best to ensure your problem hasn't already been reported. If it has, just leave a comment on the existing issue, rather than create a new one.
If you have already checked the existing [GitHub issues](https://github.com/kedro-org/kedro/issues) and are still convinced that you have found odd or erroneous behaviour then please file a new one.| -|Propose a new feature|If you have new ideas for Kedro functionality then please open a [GitHub issue](https://github.com/kedro-org/kedro/issues) and describe the feature you would like to see, why you need it, and how it should work.| -|Review pull requests|Check the [Kedro repo to find open pull requests](https://github.com/kedro-org/kedro/pulls) and contribute a review!| -|Contribute a fix or feature|If you're interested in contributing fixes to code or documentation, first read our [guidelines for contributing developers](https://docs.kedro.org/en/stable/contribution/developer_contributor_guidelines.html) for an explanation of how to get set up and the process you'll follow. Once you are ready to contribute, a good place to start is to take a look at the `good first issues` and `help wanted issues` on [GitHub](https://github.com/kedro-org/kedro/issues).| -|Contribute to the documentation|You can help us improve the [Kedro documentation online](https://docs.kedro.org/en/stable/). Send us feedback as a [GitHub issue](https://github.com/kedro-org/kedro/issues) or start a documentation discussion on [GitHub](https://github.com/kedro-org/kedro/discussions).You are also welcome to make a raise a PR with a bug fix or addition to the documentation. First read the guide [Contribute to the Kedro documentation](https://docs.kedro.org/en/stable/contribution/documentation_contributor_guidelines.html). +Take a look at some of our [contribution suggestions on the Kedro GitHub Wiki](https://github.com/kedro-org/kedro/wiki/Contribute-to-Kedro)! ## Join our Technical Steering Committee diff --git a/README.md b/README.md index aed1d6894c..f329a8331f 100644 --- a/README.md +++ b/README.md @@ -5,6 +5,7 @@ [![Conda version](https://img.shields.io/conda/vn/conda-forge/kedro.svg)](https://anaconda.org/conda-forge/kedro) [![License](https://img.shields.io/badge/license-Apache%202.0-blue.svg)](https://github.com/kedro-org/kedro/blob/main/LICENSE.md) [![Slack Organisation](https://img.shields.io/badge/slack-chat-blueviolet.svg?label=Kedro%20Slack&logo=slack)](https://slack.kedro.org) +[![Slack Archive](https://img.shields.io/badge/slack-archive-blueviolet.svg?label=Kedro%20Slack%20)](https://linen-slack.kedro.org/) ![CircleCI - Main Branch](https://img.shields.io/circleci/build/github/kedro-org/kedro/main?label=main) ![Develop Branch Build](https://img.shields.io/circleci/build/github/kedro-org/kedro/develop?label=develop) [![Documentation](https://readthedocs.org/projects/kedro/badge/?version=stable)](https://docs.kedro.org/) @@ -14,7 +15,7 @@ ## What is Kedro? -Kedro is a toolbox for production-ready data science. It uses software engineering best practices to help you create data engineering and data science pipelines that are reproducible, maintainable, and modular. +Kedro is a toolbox for production-ready data science. It uses software engineering best practices to help you create data engineering and data science pipelines that are reproducible, maintainable, and modular. You can find out more at [kedro.org](https://kedro.org). Kedro is an open-source Python framework hosted by the [LF AI & Data Foundation](https://lfaidata.foundation/). @@ -51,12 +52,10 @@ _A pipeline visualisation generated using [Kedro-Viz](https://github.com/kedro-o The [Kedro documentation](https://docs.kedro.org/en/stable/) first explains [how to install Kedro](https://docs.kedro.org/en/stable/get_started/install.html) and then introduces [key Kedro concepts](https://docs.kedro.org/en/stable/get_started/kedro_concepts.html). -- The first example illustrates the [basics of a Kedro project](https://docs.kedro.org/en/stable/get_started/new_project.html) using the Iris dataset -- You can then review the [spaceflights tutorial](https://docs.kedro.org/en/stable/tutorial/tutorial_template.html) to build a Kedro project for hands-on experience +You can then review the [spaceflights tutorial](https://docs.kedro.org/en/stable/tutorial/spaceflights_tutorial.html) to build a Kedro project for hands-on experience -For new and intermediate Kedro users, there's a comprehensive section on [how to visualise Kedro projects using Kedro-Viz](https://docs.kedro.org/en/stable/visualisation/kedro-viz_visualisation.html) and [how to work with Kedro and Jupyter notebooks](https://docs.kedro.org/en/stable/notebooks_and_ipython/kedro_and_notebooks). +For new and intermediate Kedro users, there's a comprehensive section on [how to visualise Kedro projects using Kedro-Viz](https://docs.kedro.org/en/stable/visualisation/index.html) and [how to work with Kedro and Jupyter notebooks](https://docs.kedro.org/en/stable/notebooks_and_ipython/index.html). We also recommend the [API reference documentation](/kedro) for additional information. -Further documentation is available for more advanced Kedro usage and deployment. We also recommend the [glossary](https://docs.kedro.org/en/stable/resources/glossary.html) and the [API reference documentation](/kedro) for additional information. ## Why does Kedro exist? @@ -68,64 +67,21 @@ Kedro is built upon our collective best-practice (and mistakes) trying to delive - To increase efficiency, because applied concepts like modularity and separation of concerns inspire the creation of **reusable analytics code** +Find out more about how Kedro can answer your use cases from the [product FAQs on the Kedro website](https://kedro.org/#faq). + ## The humans behind Kedro The [Kedro product team](https://docs.kedro.org/en/stable/contribution/technical_steering_committee.html#kedro-maintainers) and a number of [open source contributors from across the world](https://github.com/kedro-org/kedro/releases) maintain Kedro. ## Can I contribute? -Yes! Want to help build Kedro? Check out our [guide to contributing to Kedro](https://github.com/kedro-org/kedro/blob/main/CONTRIBUTING.md). +Yes! We welcome all kinds of contributions. Check out our [guide to contributing to Kedro](https://github.com/kedro-org/kedro/wiki/Contribute-to-Kedro). ## Where can I learn more? -There is a growing community around Kedro. Have a look at the [Kedro FAQs](https://docs.kedro.org/en/stable/faq/faq.html#how-can-i-find-out-more-about-kedro) to find projects using Kedro and links to articles, podcasts and talks. - -## Who likes Kedro? - -There are Kedro users across the world, who work at start-ups, major enterprises and academic institutions like [Absa](https://www.absa.co.za/), -[Acensi](https://acensi.eu/page/home), -[Advanced Programming Solutions SL](https://www.linkedin.com/feed/update/urn:li:activity:6863494681372721152/), -[AI Singapore](https://makerspace.aisingapore.org/2020/08/leveraging-kedro-in-100e/), -[AMAI GmbH](https://www.am.ai/), -[Augment Partners](https://www.linkedin.com/posts/augment-partners_kedro-cheat-sheet-by-augment-activity-6858927624631283712-Ivqk), -[AXA UK](https://www.axa.co.uk/), -[Belfius](https://www.linkedin.com/posts/vangansen_mlops-machinelearning-kedro-activity-6772379995953238016-JUmo), -[Beamery](https://medium.com/hacking-talent/production-code-for-data-science-and-our-experience-with-kedro-60bb69934d1f), -[Caterpillar](https://www.caterpillar.com/), -[CRIM](https://www.crim.ca/en/), -[Dendra Systems](https://www.dendra.io/), -[Element AI](https://www.elementai.com/), -[GetInData](https://getindata.com/blog/running-machine-learning-pipelines-kedro-kubeflow-airflow), -[GMO](https://recruit.gmo.jp/engineer/jisedai/engineer/jisedai/engineer/jisedai/engineer/jisedai/engineer/jisedai/blog/kedro_and_mlflow_tracking/), -[Indicium](https://medium.com/indiciumtech/how-to-build-models-as-products-using-mlops-part-2-machine-learning-pipelines-with-kedro-10337c48de92), -[Imperial College London](https://github.com/dssg/barefoot-winnie-public), -[ING](https://www.ing.com), -[Jungle Scout](https://junglescouteng.medium.com/jungle-scout-case-study-kedro-airflow-and-mlflow-use-on-production-code-150d7231d42e), -[Helvetas](https://www.linkedin.com/posts/lionel-trebuchon_mlflow-kedro-ml-ugcPost-6747074322164154368-umKw), -[Leapfrog](https://www.lftechnology.com/blog/ai-pipeline-kedro/), -[McKinsey & Company](https://www.mckinsey.com/alumni/news-and-insights/global-news/firm-news/kedro-from-proprietary-to-open-source), -[Mercado Libre Argentina](https://www.mercadolibre.com.ar), -[Modec](https://www.modec.com/), -[Mosaic Data Science](https://www.youtube.com/watch?v=fCWGevB366g), -[NaranjaX](https://www.youtube.com/watch?v=_0kMmRfltEQ), -[NASA](https://github.com/nasa/ML-airport-taxi-out), -[NHS AI Lab](https://nhsx.github.io/skunkworks/synthetic-data-pipeline), -[Open Data Science LatAm](https://www.odesla.org/), -[Prediqt](https://prediqt.co/), -[QuantumBlack](https://medium.com/quantumblack/introducing-kedro-the-open-source-library-for-production-ready-machine-learning-code-d1c6d26ce2cf), -[ReSpo.Vision](https://neptune.ai/customers/respo-vision), -[Retrieva](https://tech.retrieva.jp/entry/2020/07/28/181414), -[Roche](https://www.roche.com/), -[Sber](https://www.linkedin.com/posts/seleznev-artem_welcome-to-kedros-documentation-kedro-activity-6767523561109385216-woTt), -[Société Générale](https://www.societegenerale.com/en), -[Telkomsel](https://medium.com/life-at-telkomsel/how-we-build-a-production-grade-data-pipeline-7004e56c8c98), -[Universidad Rey Juan Carlos](https://github.com/vchaparro/MasterThesis-wind-power-forecasting/blob/master/thesis.pdf), -[UrbanLogiq](https://urbanlogiq.com/), -[Wildlife Studios](https://wildlifestudios.com), -[WovenLight](https://www.wovenlight.com/) and -[XP](https://youtu.be/wgnGOVNkXqU?t=2210). - -Kedro won [Best Technical Tool or Framework for AI](https://awards.ai/the-awards/previous-awards/the-4th-ai-award-winners/) in the 2019 Awards AI competition and a merit award for the 2020 [UK Technical Communication Awards](https://uktcawards.com/announcing-the-award-winners-for-2020/). It is listed on the 2020 [ThoughtWorks Technology Radar](https://www.thoughtworks.com/radar/languages-and-frameworks/kedro) and the 2020 [Data & AI Landscape](https://mattturck.com/data2020/). Kedro has received an [honorable mention in the User Experience category in Fast Company’s 2022 Innovation by Design Awards](https://www.fastcompany.com/90772252/user-experience-innovation-by-design-2022). +There is a growing community around Kedro. We encourage you to ask and answer technical questions on [Slack](https://slack.kedro.org/) and bookmark the [Linen archive of past discussions](https://linen-slack.kedro.org/). + +We keep a list of [technical FAQs in the Kedro documentation](https://docs.kedro.org/en/stable/faq/faq.html) and you can find a growing list of blog posts, videos and projects that use Kedro over on the [`awesome-kedro` GitHub repository](https://github.com/kedro-org/awesome-kedro). If you have created anything with Kedro we'd love to include it on the list. Just make a PR to add it! ## How can I cite Kedro? diff --git a/docs/source/faq/faq.md b/docs/source/faq/faq.md index 9087f29def..75790690a9 100644 --- a/docs/source/faq/faq.md +++ b/docs/source/faq/faq.md @@ -1,4 +1,6 @@ -# Frequently asked questions +# FAQs + +This is a growing set of technical FAQs. The [product FAQs on the Kedro website](https://kedro.org/#faq) explain how Kedro can answer the typical use cases and requirements of data scientists, data engineers, machine learning engineers and product owners. ## Visualisation @@ -46,3 +48,25 @@ * [How do I create a modular pipeline](../nodes_and_pipelines/modular_pipelines.md#how-do-i-create-a-modular-pipeline)? * [Can I use generator functions in a node](../nodes_and_pipelines/nodes.md#how-to-use-generator-functions-in-a-node)? + +## What is data engineering convention? + +[Bruce Philp](https://github.com/bruceaphilp) and [Guilherme Braccialli](https://github.com/gbraccialli-qb) are the +brains behind a layered data-engineering convention as a model of managing data. You can find an [in-depth walk through of their convention](https://towardsdatascience.com/the-importance-of-layered-thinking-in-data-engineering-a09f685edc71) as a blog post on Medium. + +Refer to the following table below for a high level guide to each layer's purpose + +> **Note**:The data layers don’t have to exist locally in the `data` folder within your project, but we recommend that you structure your S3 buckets or other data stores in a similar way. + +![data_engineering_convention](../meta/images/data_layers.png) + +| Folder in data | Description | +| -------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| Raw | Initial start of the pipeline, containing the sourced data model(s) that should never be changed, it forms your single source of truth to work from. These data models are typically un-typed in most cases e.g. csv, but this will vary from case to case | +| Intermediate | Optional data model(s), which are introduced to type your :code:`raw` data model(s), e.g. converting string based values into their current typed representation | +| Primary | Domain specific data model(s) containing cleansed, transformed and wrangled data from either `raw` or `intermediate`, which forms your layer that you input into your feature engineering | +| Feature | Analytics specific data model(s) containing a set of features defined against the `primary` data, which are grouped by feature area of analysis and stored against a common dimension | +| Model input | Analytics specific data model(s) containing all :code:`feature` data against a common dimension and in the case of live projects against an analytics run date to ensure that you track the historical changes of the features over time | +| Models | Stored, serialised pre-trained machine learning models | +| Model output | Analytics specific data model(s) containing the results generated by the model based on the `model input` data | +| Reporting | Reporting data model(s) that are used to combine a set of `primary`, `feature`, `model input` and `model output` data used to drive the dashboard and the views constructed. It encapsulates and removes the need to define any blending or joining of data, improve performance and replacement of presentation layer without having to redefine the data models | diff --git a/docs/source/get_started/install.md b/docs/source/get_started/install.md index 0ce17301c5..8afea95a57 100644 --- a/docs/source/get_started/install.md +++ b/docs/source/get_started/install.md @@ -134,7 +134,7 @@ You should see an ASCII art graphic and the Kedro version number. For example: ![](../meta/images/kedro_graphic.png) -If you do not see the graphic displayed, or have any issues with your installation, check out the [searchable archive of Slack discussions](https://www.linen.dev/s/kedro), or post a new query on the [Slack organisation](https://slack.kedro.org). +If you do not see the graphic displayed, or have any issues with your installation, check out the [searchable archive of Slack discussions](https://linen-slack.kedro.org/), or post a new query on the [Slack organisation](https://slack.kedro.org). ## How to upgrade Kedro @@ -187,4 +187,4 @@ pip install kedro * Installation prerequisites include a virtual environment manager like `conda`, Python 3.7+, and `git`. * You should install Kedro using `pip install kedro`. -If you encounter any problems as you set up Kedro, ask for help on Kedro's [Slack organisation](https://slack.kedro.org) or review the [searchable archive of Slack discussions](https://www.linen.dev/s/kedro). +If you encounter any problems as you set up Kedro, ask for help on Kedro's [Slack organisation](https://slack.kedro.org) or review the [searchable archive of Slack discussions](https://linen-slack.kedro.org/). diff --git a/docs/source/index.rst b/docs/source/index.rst index ac106f9c48..5850f15f76 100644 --- a/docs/source/index.rst +++ b/docs/source/index.rst @@ -43,8 +43,8 @@ Welcome to Kedro's documentation! :target: https://slack.kedro.org :alt: Kedro's Slack organisation -.. image:: https://img.shields.io/badge/slack-archive-blue.svg?label=Kedro%20Slack%20 - :target: https://www.linen.dev/s/kedro +.. image:: https://img.shields.io/badge/slack-archive-blueviolet.svg?label=Kedro%20Slack%20 + :target: https://linen-slack.kedro.org/ :alt: Kedro's Slack archive .. image:: https://img.shields.io/badge/code%20style-black-black.svg diff --git a/docs/source/meta/images/data_layers.png b/docs/source/meta/images/data_layers.png new file mode 100644 index 0000000000000000000000000000000000000000..fd3798310ab0860ba4f8365501ee3891ba0c93a2 GIT binary patch literal 80345 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 E04nXLrT_o{ literal 0 HcmV?d00001 diff --git a/docs/source/resources/index.md b/docs/source/resources/index.md index 72493f112e..ce24876a0a 100644 --- a/docs/source/resources/index.md +++ b/docs/source/resources/index.md @@ -1,4 +1,4 @@ -# Resources +# FAQs and resources ```{toctree} :maxdepth: 1 diff --git a/docs/source/tutorial/spaceflights_tutorial.md b/docs/source/tutorial/spaceflights_tutorial.md index 0a65d0369b..da58578174 100644 --- a/docs/source/tutorial/spaceflights_tutorial.md +++ b/docs/source/tutorial/spaceflights_tutorial.md @@ -31,7 +31,7 @@ If you hit an issue with the tutorial: * Check the [spaceflights tutorial FAQ](spaceflights_tutorial_faqs.md) to see if we have answered the question already. * Use [Kedro-Viz](../visualisation/kedro-viz_visualisation) to visualise your project to better understand how the datasets, nodes and pipelines fit together. * Use the [#questions channel](https://slack.kedro.org/) on our Slack channel to ask the community for help. -* Search the [searchable archive of Slack discussions](https://www.linen.dev/s/kedro). +* Search the [searchable archive of Slack discussions](https://linen-slack.kedro.org/). ## Terminology diff --git a/docs/source/visualisation/kedro-viz_visualisation.md b/docs/source/visualisation/kedro-viz_visualisation.md index 0f6e207508..d546681106 100644 --- a/docs/source/visualisation/kedro-viz_visualisation.md +++ b/docs/source/visualisation/kedro-viz_visualisation.md @@ -42,7 +42,7 @@ You should see the following: If a visualisation panel opens up and a pipeline is not visible, refresh the view, and check that your tutorial project code is complete if you've not generated it from the starter template. If you still don't see the visualisation, the Kedro community can help: * use the [#questions channel](https://slack.kedro.org/) on our Slack channel to ask the community for help -* search the [searchable archive of Slack discussions](https://www.linen.dev/s/kedro) +* search the [searchable archive of Slack discussions](https://linen-slack.kedro.org/) To exit the visualisation, close the browser tab. To regain control of the terminal, enter `^+c` on Mac or `Ctrl+c` on Windows or Linux machines. From b4468de9bc3baf1fead9a7c2e42f2fca79cb13ea Mon Sep 17 00:00:00 2001 From: Nok Lam Chan Date: Tue, 8 Aug 2023 15:22:54 +0100 Subject: [PATCH 20/70] Update Generator example (#2858) * update release note Signed-off-by: Nok * update to binary mode when opening file Signed-off-by: Nok Chan * Simplify the implementation of Dataset Signed-off-by: Nok Chan * simplify the example - wip Signed-off-by: Nok Chan * Update the nodes Signed-off-by: Nok Chan * update example Signed-off-by: Nok Chan * Update example Signed-off-by: Nok * Apply suggestions from code review Co-authored-by: Jo Stichbury * Apply suggestions from code review Co-authored-by: Jo Stichbury * Apply suggestions from code review Co-authored-by: Merel Theisen <49397448+merelcht@users.noreply.github.com> Co-authored-by: Jo Stichbury * Apply suggestions from code review Co-authored-by: Jo Stichbury * add more instruction to guide user to create the custom dataset Signed-off-by: Nok * update the reference from `pipeline.py` -> `nodes.py` Signed-off-by: Nok * Collapse the nodes.py function into a expandable section Signed-off-by: Nok * Update nodes.md * Update nodes.md --------- Signed-off-by: Nok Signed-off-by: Nok Chan Co-authored-by: Jo Stichbury Co-authored-by: Merel Theisen <49397448+merelcht@users.noreply.github.com> Signed-off-by: Jason Hite --- RELEASE.md | 2 +- docs/source/nodes_and_pipelines/nodes.md | 247 +++++++++++------------ 2 files changed, 123 insertions(+), 126 deletions(-) diff --git a/RELEASE.md b/RELEASE.md index a9fe6bfcbf..6c0c894750 100644 --- a/RELEASE.md +++ b/RELEASE.md @@ -18,6 +18,7 @@ * Updated `kedro pipeline create` and `kedro catalog create` to use new `/conf` file structure. ## Documentation changes +* Update example of using generator functions in nodes. * Added migration guide from the `ConfigLoader` to the `OmegaConfigLoader`. The `ConfigLoader` is deprecated and will be removed in the `0.19.0` release. ## Migration Tips for Python 3.11: @@ -45,7 +46,6 @@ * Recommended `ruff` as the linter and removed mentions of `pylint`, `isort`, `flake8`. ## Community contributions - Thanks to [Laíza Milena Scheid Parizotto](https://github.com/laizaparizotto) and [Chris Schopp](https://github.com/cschopp-simwell). ## Breaking changes to the API diff --git a/docs/source/nodes_and_pipelines/nodes.md b/docs/source/nodes_and_pipelines/nodes.md index 825b4eaf0a..7a22b8765e 100644 --- a/docs/source/nodes_and_pipelines/nodes.md +++ b/docs/source/nodes_and_pipelines/nodes.md @@ -184,114 +184,53 @@ You can also call a node as a regular Python function: `adder_node(dict(a=2, b=3 ## How to use generator functions in a node -[Generator functions](https://learnpython.org/en/Generators) were introduced with [PEP 255](https://www.python.org/dev/peps/pep-0255). They are a special kind of function that returns lazy iterators but do not store their entire contents in memory all at once. +[Generator functions](https://learnpython.org/en/Generators) were introduced with [PEP 255](https://www.python.org/dev/peps/pep-0255) and are a special kind of function in Python that returns lazy iterators. They are often used for lazy-loading or lazy-saving of data, which can be particularly useful when dealing with large datasets that do not fit entirely into memory. In the context of Kedro, generator functions can be used in nodes to efficiently process and handle such large datasets. -The following code uses a `pandas chunksize` generator to process large datasets within the [`pandas-iris` starter](../kedro_project_setup/starters.md). First set up a project by following the [get started guide](../get_started/new_project.md#create-a-new-project-containing-example-code) to create a Kedro project with the `pandas-iris` starter example code. -Create a [custom dataset](../extend_kedro/custom_datasets.md) called `ChunkWiseCSVDataSet` in `src/YOUR_PROJECT_NAME/extras/datasets/chunkwise_dataset.py` for your `pandas-iris` project. This dataset is a simplified version of the `pandas.CSVDataSet` where the main change is to the `_save` method which should save the data in append-or-create mode, `a+`. +### Set up the project -

-Click to expand - -```python -from copy import deepcopy -from io import BytesIO -from pathlib import PurePosixPath -from typing import Any, Dict - -import fsspec -import pandas as pd +To demonstrate the use of generator functions in Kedro nodes, first, set up a Kedro project using the `pandas-iris` starter. If you haven't already created a Kedro project, you can follow the [get started guide](../get_started/new_project.md#create-a-new-project-containing-example-code) to create it. -from kedro.io.core import ( - AbstractVersionedDataSet, - Version, - get_filepath_str, - get_protocol_and_path, -) +Create the project with this command: +```bash +kedro new -s pandas-iris +``` +### Loading data with Generators +To use generator functions in Kedro nodes, you need to update the `catalog.yml` file to include the `chunksize` argument for the relevant dataset that will be processed using the generator. -class ChunkWiseCSVDataSet(AbstractVersionedDataSet[pd.DataFrame, pd.DataFrame]): - """``ChunkWiseCSVDataSet`` loads/saves data from/to a CSV file using an underlying - filesystem. It uses pandas to handle the CSV file. - """ - - DEFAULT_LOAD_ARGS = {} # type: Dict[str, Any] - DEFAULT_SAVE_ARGS = {"index": False} # type: Dict[str, Any] - - def __init__( - self, - filepath: str, - load_args: Dict[str, Any] = None, - save_args: Dict[str, Any] = None, - version: Version = None, - credentials: Dict[str, Any] = None, - fs_args: Dict[str, Any] = None, - ) -> None: - """Creates a new instance of ``ChunkWiseCSVDataSet`` pointing to a concrete CSV file - on a specific filesystem. - """ - _fs_args = deepcopy(fs_args) or {} - _credentials = deepcopy(credentials) or {} - - protocol, path = get_protocol_and_path(filepath, version) - if protocol == "file": - _fs_args.setdefault("auto_mkdir", True) - - self._protocol = protocol - self._storage_options = {**_credentials, **_fs_args} - self._fs = fsspec.filesystem(self._protocol, **self._storage_options) - - super().__init__( - filepath=PurePosixPath(path), - version=version, - exists_function=self._fs.exists, - glob_function=self._fs.glob, - ) - - # Handle default load and save arguments - self._load_args = deepcopy(self.DEFAULT_LOAD_ARGS) - if load_args is not None: - self._load_args.update(load_args) - self._save_args = deepcopy(self.DEFAULT_SAVE_ARGS) - if save_args is not None: - self._save_args.update(save_args) - - def _describe(self) -> Dict[str, Any]: - return { - "filepath": self._filepath, - "protocol": self._load_args, - "save_args": self._save_args, - "version": self._version, - } - - def _load(self) -> pd.DataFrame: - load_path = str(self._get_load_path()) - return pd.read_csv(load_path, **self._load_args) +You need to add a new dataset in your `catalog.yml` as follows: +```diff ++ X_test: ++ type: pandas.CSVDataSet ++ filepath: data/05_model_input/X_test.csv ++ load_args: ++ chunksize: 10 +``` - def _save(self, data: pd.DataFrame) -> None: - save_path = get_filepath_str(self._get_save_path(), self._protocol) +With `pandas` built-in support, you can use the `chunksize` argument to read data using generator. - buf = BytesIO() - data.to_csv(path_or_buf=buf, **self._save_args) +### Saving data with Generators +To use generators to save data lazily, you need do three things: +- Update the `make_prediction` function definition to use `return` instead of `yield`. +- Create a [custom dataset](../extend_kedro/custom_datasets.md) called `ChunkWiseCSVDataset` +- Update `catalog.yml` to use a newly created `ChunkWiseCSVDataset`. - with self._fs.open(save_path, mode="a+") as fs_file: - fs_file.write(buf.getvalue()) -``` -
+Copy the following code to `nodes.py`. The main change is to use a new model `DecisionTreeClassifier` to make prediction by chunks in `make_predictions`. -Modify `example_iris_data` in `catalog.yml` by changing `type` to the custom dataset you created above. Add `chunksize: 100` to `load_args` which will return an iterable object. The `chunksize` parameter refers to the number of rows in each chunk. +
+Click to open -```yaml -example_iris_data: - type: YOUR_PROJECT_NAME.extras.datasets.chunkwise_dataset.ChunkWiseCSVDataSet - filepath: data/01_raw/iris.csv - load_args: - chunksize: 100 -``` +```python +import logging +from typing import Any, Dict, Tuple, Iterator +from sklearn.preprocessing import LabelEncoder +from sklearn.tree import DecisionTreeClassifier +from sklearn.metrics import accuracy_score +import numpy as np +import pandas as pd -Next, in `nodes.py` we repurpose the existing `split_data` function to process chunk-wise data: -```python def split_data( data: pd.DataFrame, parameters: Dict[str, Any] ) -> Tuple[pd.DataFrame, pd.DataFrame, pd.Series, pd.Series]: @@ -303,39 +242,97 @@ def split_data( Returns: Split data. """ - # Loop through data in chunks building up the training and test sets - for chunk in data: # Iterate over the chunks from data - full_data = pd.concat( - [chunk] - ) # Converts the TextFileReader object into list of DataFrames - data_train = full_data.sample( - frac=parameters["train_fraction"], random_state=parameters["random_state"] - ) - data_test = full_data.drop(data_train.index) - - X_train = data_train.drop(columns=parameters["target_column"]) - X_test = data_test.drop(columns=parameters["target_column"]) - y_train = data_train[parameters["target_column"]] - y_test = data_test[parameters["target_column"]] - yield X_train, X_test, y_train, y_test # Use yield instead of return to get the generator object + + data_train = data.sample( + frac=parameters["train_fraction"], random_state=parameters["random_state"] + ) + data_test = data.drop(data_train.index) + + X_train = data_train.drop(columns=parameters["target_column"]) + X_test = data_test.drop(columns=parameters["target_column"]) + y_train = data_train[parameters["target_column"]] + y_test = data_test[parameters["target_column"]] + + label_encoder = LabelEncoder() + label_encoder.fit(pd.concat([y_train, y_test])) + y_train = label_encoder.transform(y_train) + + return X_train, X_test, y_train, y_test + + +def make_predictions( + X_train: pd.DataFrame, X_test: pd.DataFrame, y_train: pd.Series +) -> pd.Series: + """Use a DecisionTreeClassifier model to make prediction.""" + model = DecisionTreeClassifier() + model.fit(X_train, y_train) + + for chunk in X_test: + y_pred = model.predict(chunk) + y_pred = pd.DataFrame(y_pred) + yield y_pred + + +def report_accuracy(y_pred: pd.Series, y_test: pd.Series): + """Calculates and logs the accuracy. + + Args: + y_pred: Predicted target. + y_test: True target. + """ + accuracy = accuracy_score(y_test, y_pred) + logger = logging.getLogger(__name__) + logger.info("Model has accuracy of %.3f on test data.", accuracy) +``` +
+ + +The `ChunkWiseDataset` is a variant of the `pandas.CSVDataset` where the main change is to the `_save` method that appends data instead of overwriting it. You need to create a file `src//chunkwise.py` and put this class inside it. Below is an example of the `ChunkWiseCSVDataset` implementation: + +```python +import pandas as pd + +from kedro.io.core import ( + get_filepath_str, +) +from kedro.extras.datasets.pandas import CSVDataset + + +class ChunkWiseCSVDataset(CSVDataset): + """``ChunkWiseCSVDataset`` loads/saves data from/to a CSV file using an underlying + filesystem. It uses pandas to handle the CSV file. + """ + + _overwrite = True + + def _save(self, data: pd.DataFrame) -> None: + save_path = get_filepath_str(self._get_save_path(), self._protocol) + # Save the header for the first batch + if self._overwrite: + data.to_csv(save_path, index=False, mode="w") + self._overwrite = False + else: + data.to_csv(save_path, index=False, header=False, mode="a") ``` -We can now `kedro run` in the terminal. The output shows `X_train`, `X_test`, `y_train`, `y_test` saved in chunks: +After that, you need to update the `catalog.yml` to use this new dataset. + +```diff ++ y_pred: ++ type: .chunkwise.ChunkWiseCSVDataSet ++ filepath: data/07_model_output/y_pred.csv +``` + +With these changes, when you run `kedro run` in your terminal, you should see `y_pred`` being saved multiple times in the logs as the generator lazily processes and saves the data in smaller chunks. ``` ... -[02/10/23 12:42:55] INFO Loading data from 'example_iris_data' (ChunkWiseCSVDataSet)... data_catalog.py:343 - INFO Loading data from 'parameters' (MemoryDataSet)... data_catalog.py:343 - INFO Running node: split: split_data([example_iris_data,parameters]) -> node.py:329 - [X_train,X_test,y_train,y_test] - INFO Saving data to 'X_train' (MemoryDataSet)... data_catalog.py:382 - INFO Saving data to 'X_test' (MemoryDataSet)... data_catalog.py:382 - INFO Saving data to 'y_train' (MemoryDataSet)... data_catalog.py:382 - INFO Saving data to 'y_test' (MemoryDataSet)... data_catalog.py:382 - INFO Saving data to 'X_train' (MemoryDataSet)... data_catalog.py:382 - INFO Saving data to 'X_test' (MemoryDataSet)... data_catalog.py:382 - INFO Saving data to 'y_train' (MemoryDataSet)... data_catalog.py:382 - INFO Saving data to 'y_test' (MemoryDataSet)... data_catalog.py:382 - INFO Completed 1 out of 3 tasks sequential_runner.py:85 -... + INFO Loading data from 'y_train' (MemoryDataset)... data_catalog.py:475 + INFO Running node: make_predictions: make_predictions([X_train,X_test,y_train]) -> [y_pred] node.py:331 + INFO Saving data to 'y_pred' (ChunkWiseCSVDataSet)... data_catalog.py:514 + INFO Saving data to 'y_pred' (ChunkWiseCSVDataSet)... data_catalog.py:514 + INFO Saving data to 'y_pred' (ChunkWiseCSVDataSet)... data_catalog.py:514 + INFO Completed 2 out of 3 tasks sequential_runner.py:85 + INFO Loading data from 'y_pred' (ChunkWiseCSVDataSet)... data_catalog.py:475 +... runner.py:105 ``` From c4b02569d291adb1f0aac9b6484148955ad9ba21 Mon Sep 17 00:00:00 2001 From: Dmitry Sorokin <40151847+DimedS@users.noreply.github.com> Date: Wed, 9 Aug 2023 16:18:05 +0100 Subject: [PATCH 21/70] update docs to reflect change to /conf file structure (#2913) * update docs to reflect change to /conf file structure Signed-off-by: Dmitry Sorokin * update docs, folder structure diagrams Signed-off-by: Dmitry Sorokin --------- Signed-off-by: Dmitry Sorokin Signed-off-by: Jason Hite --- docs/source/data/data_catalog.md | 4 ++-- docs/source/development/commands_reference.md | 4 ++-- docs/source/nodes_and_pipelines/micro_packaging.md | 7 +++---- docs/source/nodes_and_pipelines/modular_pipelines.md | 3 +-- docs/source/tutorial/add_another_pipeline.md | 8 ++++---- docs/source/tutorial/create_a_pipeline.md | 2 +- 6 files changed, 13 insertions(+), 15 deletions(-) diff --git a/docs/source/data/data_catalog.md b/docs/source/data/data_catalog.md index 3cc2cbd90b..d7c73e4fdf 100644 --- a/docs/source/data/data_catalog.md +++ b/docs/source/data/data_catalog.md @@ -359,10 +359,10 @@ The list of all available parameters is given in the [Paramiko documentation](ht You can use the [`kedro catalog create` command to create a Data Catalog YAML configuration](../development/commands_reference.md#create-a-data-catalog-yaml-configuration-file). -This creates a `//catalog/.yml` configuration file with `MemoryDataSet` datasets for each dataset in a registered pipeline if it is missing from the `DataCatalog`. +This creates a `//catalog_.yml` configuration file with `MemoryDataSet` datasets for each dataset in a registered pipeline if it is missing from the `DataCatalog`. ```yaml -# //catalog/.yml +# //catalog_.yml rockets: type: MemoryDataSet scooters: diff --git a/docs/source/development/commands_reference.md b/docs/source/development/commands_reference.md index 39c162dafc..45801ea112 100644 --- a/docs/source/development/commands_reference.md +++ b/docs/source/development/commands_reference.md @@ -376,7 +376,7 @@ kedro micropkg pull The above command will take the bundled `.tar.gz` file and do the following: * Place source code in `src//pipelines/` -* Place parameters in `conf/base/parameters/.yml` +* Place parameters in `conf/base/parameters_.yml` * Pull out tests and place in `src/tests/pipelines/` `kedro micropkg pull` works with PyPI, local and cloud storage: @@ -512,7 +512,7 @@ kedro catalog create --pipeline= The command also accepts an optional `--env` argument that allows you to specify a configuration environment (defaults to `base`). -The command creates the following file: `//catalog/.yml` +The command creates the following file: `//catalog_.yml` #### Notebooks diff --git a/docs/source/nodes_and_pipelines/micro_packaging.md b/docs/source/nodes_and_pipelines/micro_packaging.md index a3f9392808..3ad1ddcc69 100644 --- a/docs/source/nodes_and_pipelines/micro_packaging.md +++ b/docs/source/nodes_and_pipelines/micro_packaging.md @@ -19,8 +19,7 @@ When you package your micro-package, such as a modular pipeline for example, Ked ```text ├── conf │ └── base -│ └── parameters -│ └── {{pipeline_name*}} <-- All parameter file(s) +│ └── parameters_{{pipeline_name*}} <-- All parameter file(s) └── src ├── my_project │ ├── __init__.py @@ -35,7 +34,7 @@ When you package your micro-package, such as a modular pipeline for example, Ked Kedro will also include any requirements found in `src//pipelines//requirements.txt` in the micro-package tar file. These requirements will later be taken into account when pulling a micro-package via `kedro micropkg pull`. ```{note} -Kedro will not package the catalog config files even if those are present in `conf//catalog/.yml`. +Kedro will not package the catalog config files even if those are present in `conf//catalog_.yml`. ``` If you plan to publish your packaged micro-package to some Python package repository like [PyPI](https://pypi.org/), you need to make sure that your micro-package name doesn't clash with any of the existing packages in that repository. However, there is no need to rename any of your source files if that is the case. Simply alias your package with a new name by running `kedro micropkg package --alias `. @@ -71,7 +70,7 @@ You can pull a micro-package from a tar file by executing `kedro micropkg pull < * The `` must either be a package name on PyPI or a path to the source distribution file. * Kedro will unpack the tar file, and install the files in following locations in your Kedro project: * All the micro-package code in `src///` - * Configuration files in `conf//parameters/.yml`, where `` defaults to `base`. + * Configuration files in `conf//parameters_.yml`, where `` defaults to `base`. * To place parameters from a different config environment, run `kedro micropkg pull --env ` * Unit tests in `src/tests/` * Kedro will also parse any requirements packaged with the micro-package and add them to project level `requirements.in`. diff --git a/docs/source/nodes_and_pipelines/modular_pipelines.md b/docs/source/nodes_and_pipelines/modular_pipelines.md index 12d69509c6..5064ae7b87 100644 --- a/docs/source/nodes_and_pipelines/modular_pipelines.md +++ b/docs/source/nodes_and_pipelines/modular_pipelines.md @@ -52,8 +52,7 @@ Running the `kedro pipeline create` command adds boilerplate folders and files f ```text ├── conf │ └── base -│ └── parameters -│ └── {{pipeline_name}}.yml <-- Pipeline-specific parameters +│ └── parameters_{{pipeline_name}}.yml <-- Pipeline-specific parameters └── src ├── my_project │ ├── __init__.py diff --git a/docs/source/tutorial/add_another_pipeline.md b/docs/source/tutorial/add_another_pipeline.md index 3e4c0089e2..95093b5d0b 100644 --- a/docs/source/tutorial/add_another_pipeline.md +++ b/docs/source/tutorial/add_another_pipeline.md @@ -17,7 +17,7 @@ The data science pipeline is made up of the following: * Two python files within `src/spaceflights/pipelines/data_science` * `nodes.py` (for the node functions that form the data processing) * `pipeline.py` (to build the pipeline) -* A yaml file: `conf/base/parameters/data_science.yml` to define the parameters used when running the pipeline +* A yaml file: `conf/base/parameters_data_science.yml` to define the parameters used when running the pipeline * `__init__.py` files in the required folders to ensure that Python can import the pipeline @@ -42,7 +42,7 @@ def split_data(data: pd.DataFrame, parameters: Dict) -> Tuple: Args: data: Data containing features and target. - parameters: Parameters defined in parameters/data_science.yml. + parameters: Parameters defined in parameters_data_science.yml. Returns: Split data. """ @@ -89,7 +89,7 @@ def evaluate_model( ## Input parameter configuration -Parameters that are used by the `DataCatalog` when the pipeline executes are stored in `conf/base/parameters/data_science.yml`: +Parameters that are used by the `DataCatalog` when the pipeline executes are stored in `conf/base/parameters_data_science.yml`:
Click to expand @@ -276,7 +276,7 @@ candidate_modelling_pipeline.regressor: ```

-2. Update the parameters file for the data science pipeline in `conf/base/parameters/data_science.yml` to replace the existing contents for `model_options` with the following for the two instances of the template pipeline: +2. Update the parameters file for the data science pipeline in `conf/base/parameters_data_science.yml` to replace the existing contents for `model_options` with the following for the two instances of the template pipeline:
Click to expand diff --git a/docs/source/tutorial/create_a_pipeline.md b/docs/source/tutorial/create_a_pipeline.md index d0173a1cc9..1828c41929 100644 --- a/docs/source/tutorial/create_a_pipeline.md +++ b/docs/source/tutorial/create_a_pipeline.md @@ -14,7 +14,7 @@ The data processing pipeline prepares the data for model building by combining t * Two python files within `src/spaceflights/pipelines/data_processing` * `nodes.py` (for the node functions that form the data processing) * `pipeline.py` (to build the pipeline) -* A yaml file: `conf/base/parameters/data_processing.yml` to define the parameters used when running the pipeline +* A yaml file: `conf/base/parameters_data_processing.yml` to define the parameters used when running the pipeline * `__init__.py` files in the required folders to ensure that Python can import the pipeline ```{note} From f753762836e80faed99f73419fcfd2cfb89f147b Mon Sep 17 00:00:00 2001 From: "L. R. Couto" <57910428+lrcouto@users.noreply.github.com> Date: Wed, 9 Aug 2023 13:08:56 -0300 Subject: [PATCH 22/70] Change CONTRIBUTING.md file based on PR #2894 (#2914) Signed-off-by: lrcouto Signed-off-by: Jason Hite --- CONTRIBUTING.md | 25 ++++++++----------------- 1 file changed, 8 insertions(+), 17 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index d4dbf8c451..966d123344 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,21 +1,10 @@ # Introduction -We welcome any and all contributions to Kedro, at whatever level you can manage. For example, you could: - -- [Join the community](#join-the-community-and-help-it-grow) -- [Contribute to the project](#contribute-to-the-project) -- [Join our Technical Steering Committee](#join-our-technical-steering-committee) - - -You can also suggest anything else that you think improves the community for us all! - -## Code of conduct - -The Kedro team pledges to foster and maintain a friendly community. We enforce a [Code of Conduct](./CODE_OF_CONDUCT.md) to ensure every Kedroid is welcomed and treated with respect. +We welcome any and all contributions to Kedro, at whatever level you can manage. Here are a few suggestions, but you are welcome to suggest anything else that you think improves the community for us all! ## Join the community -You can find the Kedro community on our [Slack organisation](https://slack.kedro.org/), which is where we share news and announcements, and general chat. You're also welcome to post links here to any articles or videos about Kedro that you create, or find, such as how-tos, showcases, demos, blog posts or tutorials. +You can find the Kedro community on our [Slack organisation](https://slack.kedro.org/), which is where we share news and announcements, and answer technical questions. You're welcome to post links to any articles or videos about Kedro that you create or find, such as how-tos, showcases, demos, blog posts or tutorials. We also curate a [GitHub repo that lists content created by the Kedro community](https://github.com/kedro-org/awesome-kedro). If you've made something with Kedro, simply add it to the list with a PR! @@ -25,10 +14,12 @@ There are quite a few ways to contribute to Kedro, sich as answering questions a Take a look at some of our [contribution suggestions on the Kedro GitHub Wiki](https://github.com/kedro-org/kedro/wiki/Contribute-to-Kedro)! +Kedro is an incubating project in [LF AI & Data](https://lfaidata.foundation/), a sub-organisation within the Linux Foundation that focuses on open innovation within the data and AI space. -## Join our Technical Steering Committee +The project is governed by a group of maintainers, known as the Technical Steering Committee (TSC); read more about the structure of our TSC in our [Technical Charter](./kedro_technical_charter.pdf). -Kedro is an incubating project in [LF AI & Data](https://lfaidata.foundation/), a sub-organisation within the Linux -Foundation that focuses on open innovation within the data and AI space. A group of maintainers, known as the Technical Steering Committee (TSC), govern the project. You can read more about the structure of our TSC in our [Technical Charter](./kedro_technical_charter.pdf). +We regularly invite community members to join the TSC and help define the future of the Kedro project. Read the [guidance on becoming a Kedro maintainer](https://docs.kedro.org/en/stable/contribution/technical_steering_committee.html) to understand the process of joining the TSC. -We invite community members to join the TSC and help define the future of the Kedro project. Read the [guidance on becoming a Kedro maintainer](https://docs.kedro.org/en/stable/contribution/technical_steering_committee.html) to understand the process of joining the TSC. +## Code of conduct + +The Kedro team pledges to foster and maintain a friendly community. We enforce a [Code of Conduct](./CODE_OF_CONDUCT.md) to ensure every Kedroid is welcomed and treated with respect. From 35335cb39d653bff6cc2bd1d8b3fcea4c320fa09 Mon Sep 17 00:00:00 2001 From: "L. R. Couto" <57910428+lrcouto@users.noreply.github.com> Date: Thu, 10 Aug 2023 04:41:34 -0300 Subject: [PATCH 23/70] Move contribution guidelines from CONTRIBUTING.md to the Wiki (#2894) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Add drafts for new contributing.md and wiki page Signed-off-by: lrcouto * Add contribution details to wiki page draft Signed-off-by: lrcouto * Remove contributing-to-Kedro.md draft from the branch Signed-off-by: lrcouto * Fix broken link to Prefect website in deploy guide (#2885) Signed-off-by: lrcouto * Clarify the argument to kedro micropkg package (#2835) Co-authored-by: Juan Luis Cano Rodríguez Co-authored-by: Merel Theisen <49397448+merelcht@users.noreply.github.com> Signed-off-by: lrcouto * Allow registering of custom resolvers to `OmegaConfigLoader` (#2869) * Allow registering of custom resolvers to OCL Signed-off-by: Ankita Katiyar * Complete doc string Signed-off-by: Ankita Katiyar * Add test for overwritten resolvers Signed-off-by: Ankita Katiyar * Update test for overwritten resolvers Signed-off-by: Ankita Katiyar * Remove replace=True by default Signed-off-by: Ankita Katiyar * Update release notes Signed-off-by: Ankita Katiyar * Update release notes Signed-off-by: Ankita Katiyar * Add debug level log for registering new resolver Signed-off-by: Ankita Katiyar --------- Signed-off-by: Ankita Katiyar Signed-off-by: lrcouto * Some adjustments to ordering of text Signed-off-by: Jo Stichbury Signed-off-by: lrcouto * Document the use of custom resolvers with `OmegaConfigLoader` (#2896) * Update docs for custom resolvers for OCL Signed-off-by: Ankita Katiyar * Update FAQ with correct link Signed-off-by: Ankita Katiyar * Fix typo Co-authored-by: Merel Theisen <49397448+merelcht@users.noreply.github.com> * Add note for oc.env Signed-off-by: Ankita Katiyar * Suggestions from code review Signed-off-by: Ankita Katiyar --------- Signed-off-by: Ankita Katiyar Co-authored-by: Merel Theisen <49397448+merelcht@users.noreply.github.com> Signed-off-by: lrcouto * Update kedro pipeline create to use new /conf file structure (#2856) * add parameters_pipelineNane.yml template in the conf folder Signed-off-by: Dmitry Sorokin * del yml template from parameters folder Signed-off-by: Dmitry Sorokin * modify delete pipeline command to use correct path to yml files Signed-off-by: Dmitry Sorokin * modify test_pipeline move parameters foldername to the yml filename Signed-off-by: Dmitry Sorokin * collect configs to package not only from parameters folder, but from core conf folder also Signed-off-by: Dmitry Sorokin * move parameters foldername to the yml filename, remove nesting Signed-off-by: Dmitry Sorokin * move parameters foldername to the yml filename, remove nesting Signed-off-by: Dmitry Sorokin * Add changes to RELEASE.md Signed-off-by: Dmitry Sorokin * Update RELEASE.md Co-authored-by: Merel Theisen <49397448+merelcht@users.noreply.github.com> * Update RELEASE.md, change to release 0.18.13 Signed-off-by: Dmitry Sorokin * save old structure for 'pipeline delete' command for backward compatibility Signed-off-by: Dmitry Sorokin * modify test_pipeline for 'pipeline delete' backward compability Signed-off-by: Dmitry Sorokin --------- Signed-off-by: Dmitry Sorokin Co-authored-by: Merel Theisen <49397448+merelcht@users.noreply.github.com> Signed-off-by: lrcouto * Update CONTRIBUTING.md Signed-off-by: lrcouto * Fix broken link Signed-off-by: Jo Stichbury Signed-off-by: lrcouto * Update kedro catalog create to use new /conf structure (#2884) * modify test_catalog, remove nesting in the catalog folder Signed-off-by: Dmitry Sorokin * move catalog foldername to the yml filename, remove nesting Signed-off-by: Dmitry Sorokin * Update RELEASE.md Signed-off-by: Dmitry Sorokin * Modify docs, fix broken link Signed-off-by: Dmitry Sorokin --------- Signed-off-by: Dmitry Sorokin Signed-off-by: lrcouto * Add migration steps for `ConfigLoader` to `OmegaConfigLoader` (#2887) * Add migration steps for CL to OCL Signed-off-by: Merel Theisen Signed-off-by: lrcouto * Fix #2498 Adding logging issue 2498 (#2842) * Fixing typo n on line 114 file /docs/source/nodes_and_pipelines/run_a_pipeline.md from issue #2607 Signed-off-by: laizaparizotto * Removed redundant packages at lines 66, 67 and 68 on file kedro/docs/source/nodes_and_pipelines/run_a_pipeline.md, packages: from kedro.io import AbstractDataSet, DataCatalog, MemoryDataSet, from kedro.pipeline import Pipeline and from kedro.runner.runner import AbstractRunner Signed-off-by: laizaparizotto * Add logger object for logging when computing micropackage version Signed-off-by: laizaparizotto * Added new format to the logging Signed-off-by: laizaparizotto --------- Signed-off-by: laizaparizotto Co-authored-by: Merel Theisen <49397448+merelcht@users.noreply.github.com> Signed-off-by: lrcouto * Try only trigger docs build when release notes updated (#2907) Signed-off-by: Merel Theisen Signed-off-by: lrcouto * Add Python 3.11 support to Kedro (#2851) * add 3.11 Signed-off-by: SajidAlamQB <90610031+SajidAlamQB@users.noreply.github.com> * Update setup.py Signed-off-by: SajidAlamQB <90610031+SajidAlamQB@users.noreply.github.com> * limit number of python version tests Signed-off-by: SajidAlamQB <90610031+SajidAlamQB@users.noreply.github.com> * fix setup.py Signed-off-by: SajidAlamQB <90610031+SajidAlamQB@users.noreply.github.com> * delta-spark version Signed-off-by: SajidAlamQB <90610031+SajidAlamQB@users.noreply.github.com> * try original setup Signed-off-by: SajidAlamQB <90610031+SajidAlamQB@users.noreply.github.com> * remove 3.11 from circleci Signed-off-by: SajidAlamQB <90610031+SajidAlamQB@users.noreply.github.com> * Update setup.py Signed-off-by: SajidAlamQB <90610031+SajidAlamQB@users.noreply.github.com> * Update setup.py Signed-off-by: SajidAlamQB <90610031+SajidAlamQB@users.noreply.github.com> * pin matplotlib to 3.7.2 for python 3.11 Signed-off-by: SajidAlamQB <90610031+SajidAlamQB@users.noreply.github.com> * Update test_import.py Signed-off-by: SajidAlamQB <90610031+SajidAlamQB@users.noreply.github.com> * add noks test adjustments Signed-off-by: SajidAlamQB <90610031+SajidAlamQB@users.noreply.github.com> * Update setup.py Signed-off-by: SajidAlamQB <90610031+SajidAlamQB@users.noreply.github.com> * Update setup.py Signed-off-by: SajidAlamQB <90610031+SajidAlamQB@users.noreply.github.com> * replace eq with is for multi_catalog Signed-off-by: SajidAlamQB <90610031+SajidAlamQB@users.noreply.github.com> * minor fixes Signed-off-by: SajidAlamQB <90610031+SajidAlamQB@users.noreply.github.com> * remove matplotlib 3.11 pin Signed-off-by: SajidAlamQB <90610031+SajidAlamQB@users.noreply.github.com> * matplotlib pin for 3.11 Signed-off-by: SajidAlamQB <90610031+SajidAlamQB@users.noreply.github.com> * run original matplotlib pin with ubuntu only Signed-off-by: SajidAlamQB <90610031+SajidAlamQB@users.noreply.github.com> * run circleci Signed-off-by: SajidAlamQB <90610031+SajidAlamQB@users.noreply.github.com> * pin spark < 3.4 as its breaking Signed-off-by: SajidAlamQB <90610031+SajidAlamQB@users.noreply.github.com> * spark datasets revert earlier changes back to original Signed-off-by: SajidAlamQB <90610031+SajidAlamQB@users.noreply.github.com> * delta Signed-off-by: SajidAlamQB <90610031+SajidAlamQB@users.noreply.github.com> * remove all windows tests from GHA Signed-off-by: SajidAlamQB <90610031+SajidAlamQB@users.noreply.github.com> * skip coverage as not needed Signed-off-by: SajidAlamQB <90610031+SajidAlamQB@users.noreply.github.com> * Checking only WIN tests (will revert) Signed-off-by: SajidAlamQB <90610031+SajidAlamQB@users.noreply.github.com> * Update unit-tests.yml Signed-off-by: SajidAlamQB <90610031+SajidAlamQB@users.noreply.github.com> * undo previous commit Signed-off-by: SajidAlamQB <90610031+SajidAlamQB@users.noreply.github.com> * add msbuild Signed-off-by: SajidAlamQB <90610031+SajidAlamQB@users.noreply.github.com> * add ubuntu tests back in and remove 3.11 tests from circleci add in msbuild for the other tests Signed-off-by: SajidAlamQB <90610031+SajidAlamQB@users.noreply.github.com> * set hdf5 disable check and lint Signed-off-by: SajidAlamQB <90610031+SajidAlamQB@users.noreply.github.com> * pin tables for 3.8 Signed-off-by: SajidAlamQB <90610031+SajidAlamQB@users.noreply.github.com> * pin 3.8 tables Signed-off-by: SajidAlamQB <90610031+SajidAlamQB@users.noreply.github.com> * lint Signed-off-by: SajidAlamQB <90610031+SajidAlamQB@users.noreply.github.com> * remove 3.11 completely from circleci and tidy up Signed-off-by: SajidAlamQB <90610031+SajidAlamQB@users.noreply.github.com> * add 3.11 to pyarrow pin for docs Signed-off-by: SajidAlamQB <90610031+SajidAlamQB@users.noreply.github.com> * undo previous commit Signed-off-by: SajidAlamQB <90610031+SajidAlamQB@users.noreply.github.com> * update rtd to python 3.11 Signed-off-by: SajidAlamQB <90610031+SajidAlamQB@users.noreply.github.com> * try 3.10 on rtd Signed-off-by: SajidAlamQB <90610031+SajidAlamQB@users.noreply.github.com> * fix outdated pylint to ruff Signed-off-by: SajidAlamQB <90610031+SajidAlamQB@users.noreply.github.com> * changes based on review Signed-off-by: SajidAlamQB <90610031+SajidAlamQB@users.noreply.github.com> * add mock for pyarrow and deltalake for docs Signed-off-by: SajidAlamQB <90610031+SajidAlamQB@users.noreply.github.com> * undo previous commit Signed-off-by: SajidAlamQB <90610031+SajidAlamQB@users.noreply.github.com> * Update setup.py Signed-off-by: SajidAlamQB <90610031+SajidAlamQB@users.noreply.github.com> * Update .readthedocs.yml Signed-off-by: SajidAlamQB <90610031+SajidAlamQB@users.noreply.github.com> * Update conf.py Signed-off-by: SajidAlamQB <90610031+SajidAlamQB@users.noreply.github.com> * changes based on review Signed-off-by: SajidAlamQB <90610031+SajidAlamQB@users.noreply.github.com> * fix comments Signed-off-by: SajidAlamQB <90610031+SajidAlamQB@users.noreply.github.com> * test running 3.10 parallel Signed-off-by: SajidAlamQB <90610031+SajidAlamQB@users.noreply.github.com> * Add to release notes and update badges on docs Signed-off-by: SajidAlamQB <90610031+SajidAlamQB@users.noreply.github.com> * Add migration tips Signed-off-by: SajidAlamQB <90610031+SajidAlamQB@users.noreply.github.com> --------- Signed-off-by: SajidAlamQB <90610031+SajidAlamQB@users.noreply.github.com> Signed-off-by: lrcouto * Revise FAQs and README (#2909) * Revise FAQs and README Signed-off-by: Jo Stichbury * Add back the data layers FAQ as I've no idea where else it fits Signed-off-by: Jo Stichbury * minor changes from review Signed-off-by: Jo Stichbury --------- Signed-off-by: Jo Stichbury * Update Generator example (#2858) * update release note Signed-off-by: Nok * update to binary mode when opening file Signed-off-by: Nok Chan * Simplify the implementation of Dataset Signed-off-by: Nok Chan * simplify the example - wip Signed-off-by: Nok Chan * Update the nodes Signed-off-by: Nok Chan * update example Signed-off-by: Nok Chan * Update example Signed-off-by: Nok * Apply suggestions from code review Co-authored-by: Jo Stichbury * Apply suggestions from code review Co-authored-by: Jo Stichbury * Apply suggestions from code review Co-authored-by: Merel Theisen <49397448+merelcht@users.noreply.github.com> Co-authored-by: Jo Stichbury * Apply suggestions from code review Co-authored-by: Jo Stichbury * add more instruction to guide user to create the custom dataset Signed-off-by: Nok * update the reference from `pipeline.py` -> `nodes.py` Signed-off-by: Nok * Collapse the nodes.py function into a expandable section Signed-off-by: Nok * Update nodes.md * Update nodes.md --------- Signed-off-by: Nok Signed-off-by: Nok Chan Co-authored-by: Jo Stichbury Co-authored-by: Merel Theisen <49397448+merelcht@users.noreply.github.com> Signed-off-by: lrcouto * Update CONTRIBUTING.md --------- Signed-off-by: lrcouto Signed-off-by: Ankita Katiyar Signed-off-by: Jo Stichbury Signed-off-by: Dmitry Sorokin Signed-off-by: Merel Theisen Signed-off-by: laizaparizotto Signed-off-by: SajidAlamQB <90610031+SajidAlamQB@users.noreply.github.com> Signed-off-by: Nok Signed-off-by: Nok Chan Co-authored-by: Deepyaman Datta Co-authored-by: Jonathan Cohen Co-authored-by: Juan Luis Cano Rodríguez Co-authored-by: Merel Theisen <49397448+merelcht@users.noreply.github.com> Co-authored-by: Ankita Katiyar <110245118+ankatiyar@users.noreply.github.com> Co-authored-by: Jo Stichbury Co-authored-by: Dmitry Sorokin <40151847+DimedS@users.noreply.github.com> Co-authored-by: Laíza Milena Scheid Parizotto <41296740+laizaparizotto@users.noreply.github.com> Co-authored-by: Sajid Alam <90610031+SajidAlamQB@users.noreply.github.com> Co-authored-by: Nok Lam Chan Signed-off-by: Jason Hite --- CONTRIBUTING.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 966d123344..bbc850442d 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -14,6 +14,7 @@ There are quite a few ways to contribute to Kedro, sich as answering questions a Take a look at some of our [contribution suggestions on the Kedro GitHub Wiki](https://github.com/kedro-org/kedro/wiki/Contribute-to-Kedro)! +## Join the Technical Steering Committee Kedro is an incubating project in [LF AI & Data](https://lfaidata.foundation/), a sub-organisation within the Linux Foundation that focuses on open innovation within the data and AI space. The project is governed by a group of maintainers, known as the Technical Steering Committee (TSC); read more about the structure of our TSC in our [Technical Charter](./kedro_technical_charter.pdf). From d6e8454bfd9a291e59f7c98a753655b34f62ee74 Mon Sep 17 00:00:00 2001 From: "L. R. Couto" <57910428+lrcouto@users.noreply.github.com> Date: Mon, 14 Aug 2023 12:43:45 -0300 Subject: [PATCH 24/70] Remove redundant pages and direct users to wiki (#2917) * Remove redundant pages and direct users to wiki Signed-off-by: lrcouto * Lint docs changes Signed-off-by: Jo Stichbury * Few tweaks to index page Signed-off-by: Jo Stichbury --------- Signed-off-by: lrcouto Signed-off-by: Jo Stichbury Co-authored-by: Jo Stichbury Signed-off-by: Jason Hite --- .../contribution/backwards_compatibility.md | 25 --- .../developer_contributor_guidelines.md | 197 ------------------ .../development_for_databricks.md | 95 --------- .../documentation_contributor_guidelines.md | 62 ------ docs/source/contribution/index.md | 13 +- .../technical_steering_committee.md | 6 +- 6 files changed, 9 insertions(+), 389 deletions(-) delete mode 100644 docs/source/contribution/backwards_compatibility.md delete mode 100644 docs/source/contribution/developer_contributor_guidelines.md delete mode 100644 docs/source/contribution/development_for_databricks.md delete mode 100644 docs/source/contribution/documentation_contributor_guidelines.md diff --git a/docs/source/contribution/backwards_compatibility.md b/docs/source/contribution/backwards_compatibility.md deleted file mode 100644 index 142cf929d2..0000000000 --- a/docs/source/contribution/backwards_compatibility.md +++ /dev/null @@ -1,25 +0,0 @@ -# Backwards compatibility & breaking changes - -This page explains the principles of backwards compatibility and breaking changes. - -A backwards-compatible change is any change that is not a breaking change. So what is a breaking change? - -A breaking change is any change that modifies Kedro's public APIs. Examples include making a change to the signature of public functions or removing a module. - -Your change is **not** considered a breaking change, and so is backwards compatible, **if a user can upgrade their Kedro version and include your change without anything breaking in their project**. - -## When should I make a breaking change? - -We aim to minimise the number of breaking changes to keep Kedro software stable and reduce the overhead for users as they migrate their projects. However, there are cases where a breaking change brings considerable value or increases the maintainability of the codebase. In these cases, breaking backwards compatibility can make sense. - -Before you contribute a breaking change, you should create a [GitHub Issue](https://github.com/kedro-org/kedro/issues) that describes the change and justifies the value gained by breaking backwards compatibility. - -## The Kedro release model - -All non-breaking changes go into `main`, from which a minor release can be deployed at any time. - -All breaking changes go into `develop`, from which a major release can be deployed at any time. The `develop` branch contains all commits from the `main` branch, but the `main` branch does not contain all the commits from `develop` until the next major release. - -![Kedro Gitflow Diagram](../meta/images/kedro_gitflow.svg) - -Got a question about the development process? Ask the community on [Slack](https://slack.kedro.org) if you need to! diff --git a/docs/source/contribution/developer_contributor_guidelines.md b/docs/source/contribution/developer_contributor_guidelines.md deleted file mode 100644 index 787a838d90..0000000000 --- a/docs/source/contribution/developer_contributor_guidelines.md +++ /dev/null @@ -1,197 +0,0 @@ -# Guidelines for contributing developers - -This page explains the principles and development process that we ask contributing developers to follow. - -**Any contributions you make will be under the [Apache 2.0 Software License](https://github.com/kedro-org/kedro/blob/main/LICENSE.md).** - -In short, when you submit code changes, your submissions are understood to be under the same the [Apache 2.0 License](https://github.com/kedro-org/kedro/blob/main/LICENSE.md) that covers the Kedro project. You should have permission to share the submitted code. - -```{note} -You don't need to contribute code to help the Kedro project. See our list of other ways [you can contribute to Kedro](https://github.com/kedro-org/kedro/blob/main/CONTRIBUTING.md). -``` - -## Introduction - -This guide is a practical description of: - -* How to set up your development environment to contribute to Kedro. -* How to prepare a pull request against the Kedro repository. - - -## Before you start: development set up - -To work on the Kedro codebase, you will need to be set up with Git, and Make. - -```{note} -If your development environment is Windows, you can use the `win_setup_conda` and `win_setup_env` commands from [Circle CI configuration](https://github.com/kedro-org/kedro/blob/main/.circleci/config.yml) to guide you in the correct way to do this. -``` - -You will also need to create and activate virtual environment. If this is unfamiliar to you, read through our [pre-requisites documentation](../get_started/install.md#installation-prerequisites). - -Next, you'll need to fork the [Kedro source code from the GitHub repository](https://github.com/kedro-org/kedro): - -* Fork the project by clicking **Fork** in the top-right corner of the [Kedro GitHub repository](https://github.com/kedro-org/kedro) -* Choose your target account - -If you need further guidance, consult the [GitHub documentation about forking a repo](https://docs.github.com/en/get-started/quickstart/fork-a-repo#forking-a-repository). - -You are almost ready to go. In your terminal, navigate to the folder into which you forked the Kedro code. - -Run these commands to install everything you need to work with Kedro: - -``` -make install-test-requirements -make install-pre-commit -``` - -Once the above commands have executed successfully, do a sanity check to ensure that `kedro` works in your environment: - -``` -make test -``` - -```{note} -If the tests in `tests/extras/datasets/spark` are failing, and you are not planning to work on [Spark](https://spark.apache.org) related features, then you can run a reduced test suite that excludes them. Do this by executing `make test-no-spark`. -``` - -## Get started: areas of contribution - -Once you are ready to contribute, a good place to start is to take a look at the `good first issues` and `help wanted issues` on [GitHub](https://github.com/kedro-org/kedro/issues). - -We focus on three areas for contribution: `core`, `extras` and `plugin`: - -- `core` refers to the primary Kedro library. Read the [`core` contribution process](#core-contribution-process) for details. -- `extras` refers to features that could be added to `core` that do not introduce too many dependencies or require new Kedro CLI commands to be created. Read the [`extras` contribution process](#extras-contribution-process) for more information. -- [`plugin`](../extend_kedro/plugins.md) refers to new functionality that requires a Kedro CLI command e.g. adding in Airflow functionality and [adding a new dataset](../extend_kedro/custom_datasets.md) to the `kedro-datasets` package. The [`plugin` development documentation](../extend_kedro/plugins.md) contains guidance on how to design and develop a Kedro `plugin`. - - -### `core` contribution process - -Typically, we only accept small contributions to the `core` Kedro library, but we accept new features as plugins or additions to the [`extras`](https://github.com/kedro-org/kedro/tree/main/kedro/extras) module. - -To contribute: - -1. Create a feature branch on your forked repository and push all your local changes to that feature branch. -2. Is your change [non-breaking and backwards-compatible](./backwards_compatibility.md)? Your feature branch should branch off from: -
    -
  1. main if you intend for it to be a non-breaking, backwards-compatible change.
  2. -
  3. develop if you intend for it to be a breaking change.
  4. -
-3. Before you submit a pull request (PR), please ensure that unit tests, end-to-end (E2E) tests and linters are passing for your changes by running `make test`, `make e2e-tests` and `make lint` locally; see the [development set up](#before-you-start-development-set-up) section above. -4. Open a PR: -
    -
  1. For backwards compatible changes, open a PR against the kedro-org:main branch from your feature branch.
  2. -
  3. For changes that are NOT backwards compatible, open a PR against the kedro-org:develop branch from your feature branch.
  4. -
- -5. Await reviewer comments. -6. Update the PR according to the reviewer's comments. -7. Your PR will be merged by the Kedro team once all the comments are addressed. - -```{note} -We will work with you to complete your contribution, but we reserve the right to take over abandoned PRs. -``` - -### `extras` contribution process - -You can add new work to `extras` if you do not need to create a new Kedro CLI command: - -1. Create an [issue](https://github.com/kedro-org/kedro/issues) describing your contribution. -2. Work in [`extras`](https://github.com/kedro-org/kedro/tree/main/kedro/extras) and create a feature branch on your forked repository and push all your local changes to that feature branch. -3. Before you submit a pull request, please ensure that unit tests, end-to-end (E2E) tests and linters are passing for your changes by running `make test`,`make e2e-tests` and `make lint` locally, have a look at the section [development set up](#before-you-start-development-set-up) section above. -4. Include a `README.md` with instructions on how to use your contribution. -5. Is your change [non-breaking and backwards-compatible](./backwards_compatibility.md)? -
    -
  1. For backwards compatible changes, open a PR against the kedro-org:main branch from your feature branch.
  2. -
  3. For changes that are NOT backwards compatible, open a PR against the kedro-org:develop branch from your feature branch.
  4. -
- -6. Reference your issue in the PR description (e.g., `Resolves #`). -7. Await review comments, then update the PR according to the reviewer's comments. -8. Your PR will be merged by the Kedro team once all the comments are addressed. - -```{note} -We will work with you to complete your contribution, but we reserve the right to take over abandoned PRs. -``` - -## Create a pull request - -[Give your pull request a descriptive title](#pull-request-title-conventions). Before you submit it, consider the following: - -* You should aim for cross-platform compatibility on Windows, macOS and Linux -* We use [Semantic Versioning](https://semver.org/) for versioning -* We have designed our code to be compatible with Python 3.7 onwards and our style guidelines are (in cascading order): - * [PEP 8 conventions](https://www.python.org/dev/peps/pep-0008/) for all Python code - * [Google docstrings](https://google.github.io/styleguide/pyguide.html#38-comments-and-docstrings) for code comments - * [PEP 484 type hints](https://www.python.org/dev/peps/pep-0484/) for all user-facing functions/class methods; e.g. - - ```python - def count_truthy(elements: List[Any]) -> int: - return sum(1 for elem in elements if element) - ``` - -Ensure that your PR builds cleanly before you submit it, by running the CI/CD checks locally, as follows: -* `make lint`: PEP-8 Standards (`ruff`, `black`) -* `make test`: unit tests, 100% coverage (`pytest`, `pytest-cov`) -* `make e2e-tests`: end-to-end tests (`behave`) - -```{note} -If Spark/PySpark/Hive tests for datasets are failing it might be due to the lack of Java>8 support from Spark. You can try using `export JAVA_HOME=$(/usr/libexec/java_home -v 1.8)` which [works under macOS or other workarounds](https://stackoverflow.com/questions/53583199/spark-error-unsupported-class-file-major-version). -``` - -```{note} -We place [conftest.py](https://docs.pytest.org/en/latest/reference/fixtures.html) files in some test directories to make fixtures reusable by any tests in that directory. If you need to see which test fixtures are available and where they come from, you can issue the following command `pytest --fixtures path/to/the/test/location.py`. -``` - -### Pull request title conventions - -The Kedro repository requires that you [squash and merge your pull request commits](https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/incorporating-changes-from-a-pull-request/about-pull-request-merges#squash-and-merge-your-commits), and, in most cases, the [merge message for a squash merge](https://docs.github.com/en/free-pro-team@latest/github/collaborating-with-issues-and-pull-requests/about-pull-request-merges#merge-message-for-a-squash-merge) then defaults to the pull request title. - -For clarity, your pull request title should be descriptive, and we ask you to follow some guidelines suggested by [Chris Beams](https://github.com/cbeams) in his post [How to Write a Git Commit Message](https://chris.beams.io/posts/git-commit/#seven-rules). In particular, for your pull request title, we suggest that you: - -* [Limit the length to 50 characters](https://chris.beams.io/posts/git-commit/#limit-50) -* [Capitalise the first letter of the first word](https://chris.beams.io/posts/git-commit/#capitalize) -* [Omit the period at the end](https://chris.beams.io/posts/git-commit/#end) -* [Use the imperative tense](https://chris.beams.io/posts/git-commit/#imperative) - -### Hints on `pre-commit` usage -[`pre-commit`](https://pre-commit.com) hooks run checks automatically on all the changed files on each commit but can be skipped with the `--no-verify` or `-n` flag: - -```bash -git commit --no-verify <...> -``` - -All checks will run during CI build, so skipping checks on commit will not allow you to merge your code with failing checks. You can uninstall the `pre-commit` hooks by running: - -```bash -make uninstall-pre-commit -``` -`pre-commit` will still be used by `make lint`, but will not install the git hooks. - -### Developer Certificate of Origin -We require that all contributions comply with the [Developer Certificate of Origin (DCO)](https://developercertificate.org/). This certifies that the contributor wrote or otherwise has the right to submit their contribution. - -All commits must be signed off by including a `Signed-off-by` line in the commit message: -``` -This is my commit message - -Signed-off-by: Random J Developer -``` - -The sign-off can be added automatically to your commit message using the `-s` option: -```bash -git commit -s -m "This is my commit message" -``` - -To avoid needing to remember the `-s` flag on every commit, you might like to set up a [git alias](https://git-scm.com/book/en/v2/Git-Basics-Git-Aliases) for `git commit -s`. Alternatively, run `make sign-off` to setup a [`commit-msg` Git hook](https://git-scm.com/docs/githooks#_commit_msg) that automatically signs off all commits (including merge commits) you make while working on the Kedro repository. - -If your PR is blocked due to unsigned commits, then you must follow the instructions under "Rebase the branch" on the GitHub Checks page for your PR. This will retroactively add the sign-off to all unsigned commits and allow the DCO check to pass. - -## Need help? - -Working on your first pull request? You can learn how from these resources: - -* [First timers only](https://www.firsttimersonly.com/) -* [How to contribute to an open source project on GitHub](https://egghead.io/courses/how-to-contribute-to-an-open-source-project-on-github) - -Previous Q&A on [GitHub discussions](https://github.com/kedro-org/kedro/discussions) and the [searchable archive of Slack discussions](https://www.linen.dev/s/kedro). You can ask new questions about the development process on [Slack](https://slack.kedro.org) too! diff --git a/docs/source/contribution/development_for_databricks.md b/docs/source/contribution/development_for_databricks.md deleted file mode 100644 index 571b2e65ad..0000000000 --- a/docs/source/contribution/development_for_databricks.md +++ /dev/null @@ -1,95 +0,0 @@ -# Contribute changes to Kedro that are tested on Databricks - -Many Kedro users deploy their projects to [Databricks](https://www.databricks.com/), a cloud-based platform for data engineering and data science. We encourage contributions to extend and improve the experience for Kedro users on Databricks; this guide explains how to efficiently test your locally modified version of Kedro on Databricks as part of a build-and-test development cycle. - -## How to deploy a development version of Kedro to Databricks - -```{note} -This page is for **contributors** developing changes to Kedro that need to test them on Databricks. If you are a Kedro user working on an individual or team project and need more information about workflows, consult the [documentation pages for developing a Kedro project on Databricks](../deployment/databricks/index.md). -``` - -## Prerequisites - -You will need the following to follow this guide: - -* Python **version >=3.8**. -* An activated Python virtual environment into which you have installed the [Databricks CLI](https://docs.databricks.com/dev-tools/cli/index.html) with [authentication for your workspace](https://docs.databricks.com/dev-tools/cli/index.html#set-up-the-cli). -* Access to a Databricks workspace with an [existing cluster](https://docs.databricks.com/clusters/create-cluster.html). -* [GNU `make`](https://www.gnu.org/software/make/). -* [`git`](https://git-scm.com/book/en/v2/Getting-Started-Installing-Git). -* A local clone of the [Kedro git repository](https://github.com/kedro-org/kedro). - -## How to install a build of Kedro onto Databricks - -The development workflow for Kedro on Databricks is similar to the one for Kedro in general, when you develop and test your changes locally. The main difference comes when manually testing your changes on Databricks, since you will need to build and deploy the wheel file to Databricks to test it on a cluster. - -To make developing Kedro for Databricks easier, Kedro comes with a `Makefile` target named `databricks-build` that automates the process of building a wheel file and installing this on your Databricks cluster to save development time. - -### How to set up the Databricks CLI to test a Kedro build - -Before you use `make databricks-build`, you must [set up the Databricks CLI](https://docs.databricks.com/dev-tools/cli/index.html#set-up-the-cli). - -Next, create and environment variable with the ID of the cluster you are using to test your Kedro build. You can find the ID by executing the Databricks CLI command `databricks clusters list` and looking for the Cluster ID to the left of the name of your chosen cluster, for instance: - -```bash -$ databricks clusters list -1234-567890-abcd1234 General Cluster TERMINATED -0987-654321-9876xywz Kedro Test Cluster TERMINATED -``` - -In this case, the cluster ID of `Kedro Test Cluster` is `0987-654321-9876xywz`. - -Once you have determined the cluster ID, you must export it to an environment variable named `DATABRICKS_CLUSTER_ID`: - -```bash -# Linux or macOS -export DATABRICKS_CLUSTER_ID= - -# Windows (PowerShell) -$Env:DATABRICKS_CLUSTER_ID = '' -``` - -### How to use `make databricks-build` to test your Kedro build - -With the setup complete, you can use `make databricks-build`. In your terminal, navigate to the parent directory of your Kedro development repository and run: - -```bash -make databricks-build -``` - -You should see a stream of messages being written to your terminal. Behind the scenes, `databricks-build` does the following: - -1. Builds a wheel file of your modified version of Kedro. -2. Uninstalls any library on your Databricks cluster with the same wheel file name. -3. Uploads your updated wheel file to DBFS (Databricks File System). -4. Queues your updated wheel file for installation -5. Restarts your cluster to apply the changes. - -Note that your cluster will be unavailable while it restarts. You can poll the status of the cluster using the Databricks CLI: - -```bash -# Linux or macOS -databricks clusters get --cluster-id $DATABRICKS_CLUSTER_ID | grep state - -# Windows (PowerShell) -databricks clusters get --cluster-id $Env:DATABRICKS_CLUSTER_ID | Select-String state -``` - -Once the cluster has restarted, you should verify that your modified version of Kedro has been correctly installed. Run `databricks libraries list --cluster-id `. If installation was successful, you should see the following output: - -```bash -{ - "cluster_id": "", - "library_statuses": [ - { - "library": { - "whl": "dbfs:/tmp/kedro-builds/kedro--py3-none-any.whl" - }, - "status": "INSTALLED", - "is_library_for_all_clusters": false - } - ] -} -``` - -Any runs of a Kedro project on this cluster will now reflect your latest local changes to Kedro. You can now test your changes to Kedro by using your cluster to run a Kedro project. diff --git a/docs/source/contribution/documentation_contributor_guidelines.md b/docs/source/contribution/documentation_contributor_guidelines.md deleted file mode 100644 index 85a4746a27..0000000000 --- a/docs/source/contribution/documentation_contributor_guidelines.md +++ /dev/null @@ -1,62 +0,0 @@ -# Contribute to the Kedro documentation - -You are welcome to contribute to the Kedro documentation if you find something incorrect or missing, or have other improvement suggestions. - -You can tell us what we should change or make a PR to change it yourself. - -Before you contribute any documentation changes, please read the [Kedro documentation style guidelines](https://github.com/kedro-org/kedro/wiki/Kedro-documentation-style-guide) on the GitHub wiki. - -## How do I rebuild the documentation after I make changes to it? - -Our documentation is written in Markdown and built from by Sphinx, coordinated by a [build script](https://github.com/kedro-org/kedro/blob/main/docs/build-docs.sh). - -If you make changes to the markdown for the Kedro documentation, you can rebuild it within a Unix-like environment (with `pandoc` installed). - -If you are a Windows user, you can still contribute to the documentation, but you cannot rebuild it. This is fine! As long as you have made an effort to verify that your Markdown is rendering correctly, and you have followed our basic guidelines, we will be happy to take your final draft as a pull request and rebuild it for you. - -The following instructions are specifically for people working with documentation who may not already have a development setup. If you are comfortable with virtual environments, cloning and branching from a git repo and using `make` you don't need them and can probably jump to the section called [Build the documentation](#build-the-documentation). - -### Set up to build Kedro documentation - -Follow the setup instructions in the [developer contributor guide](./developer_contributor_guidelines.md#before-you-start-development-set-up) -to fork the Kedro repo, create and activate a Python virtual environment and install the dependencies necessary to build the documentation. - - -### Build the documentation - -**MacOS users** can use `make` commands to build the documentation: - -```bash -make build-docs -``` - -The build will take a few minutes to finish, and a successful result is a set of HTML documentation in `docs/build/html`, which you can review by navigating to the following file and opening it: `docs/build/html/index.html`. - - -## Extend Kedro documentation - -### Add new pages - -All Kedro documentation is collated and built from a single index file, [`index.rst`](https://github.com/kedro-org/kedro/blob/main/docs/source/index.rst) found in the `docs/source` folder. - -If you add extra pages of documentation, you should always include them within `index.rst` file to include them in the table of contents and let Sphinx know to build them alongside the rest of the documentation. - -### Move or remove pages - -To move or remove a page of documentation, first locate it in the repo, and also locate where it is specified in the `index.rst` or `.rst` for the relevant section within the table of contents. - -### Create a pull request - -You need to submit any changes to the documentation via a branch. - -[Find out more about the process of submitting a PR to the Kedro project](./developer_contributor_guidelines.md). - -### Help! - -There is no shame in breaking the documentation build. Sphinx is incredibly fussy and even a single space in the wrong place will sometimes cause problems. A range of other issues can crop up and block you, whether you're technically experienced or less familiar with working with git, conda and Sphinx. - -Ask for help over on [GitHub discussions](https://github.com/kedro-org/kedro/discussions). - -## Kedro documentation style guide - -There is a lightweight [documentation style guide](https://github.com/kedro-org/kedro/wiki/Kedro-documentation-style-guide) on Kedro's GitHub wiki. diff --git a/docs/source/contribution/index.md b/docs/source/contribution/index.md index 40e660bcbd..a3e1aee36d 100644 --- a/docs/source/contribution/index.md +++ b/docs/source/contribution/index.md @@ -2,22 +2,21 @@ We welcome any and all contributions to Kedro, at whatever level you can manage. For example, you could: -- Join the community on [Slack](https://slack.kedro.org) +- Join the community on [Slack](https://slack.kedro.org) to answer questions - Review Kedro's [GitHub issues](https://github.com/kedro-org/kedro/issues) or raise your own issue to report a bug or feature request - Start a conversation about the Kedro project on [GitHub discussions](https://github.com/kedro-org/kedro/discussions) - Make a pull request on the [`awesome-kedro` GitHub repo](https://github.com/kedro-org/awesome-kedro) to update the curated list of Kedro community content - Report a bug or propose a new feature on [GitHub issues](https://github.com/kedro-org/kedro/issues) - [Review other contributors' PRs](https://github.com/kedro-org/kedro/pulls) -- [Contribute code](./developer_contributor_guidelines.md), for example to fix a bug or add a feature -- [Contribute to the documentation](documentation_contributor_guidelines.md) +- [Contribute code](https://github.com/kedro-org/kedro/wiki/Guidelines-for-contributing-developers), for example to fix a bug or add a feature +- [Contribute to the documentation](https://github.com/kedro-org/kedro/wiki/Contribute-to-the-Kedro-documentation) +- [Write a blog post for blog.kedro.org](https://github.com/kedro-org/kedro-devrel/wiki/Want-to-write-a-blog-post-for-Kedro%3F-Here's-how!) + +There is further information about contributing to Kedro, including our [developer guidelines](https://github.com/kedro-org/kedro/wiki/Contribute-to-Kedro), on the [Kedro wiki](https://github.com/kedro-org/kedro/wiki). ```{toctree} :hidden: -developer_contributor_guidelines -backwards_compatibility -documentation_contributor_guidelines technical_steering_committee -development_for_databricks ``` diff --git a/docs/source/contribution/technical_steering_committee.md b/docs/source/contribution/technical_steering_committee.md index 1760cfd6f6..fdcaebf2ff 100644 --- a/docs/source/contribution/technical_steering_committee.md +++ b/docs/source/contribution/technical_steering_committee.md @@ -1,10 +1,10 @@ -# Join the Technical Steering Committee +# Kedro's Technical Steering Committee -In the Kedro project's latest iteration it is an incubating project within [LF AI & Data](https://lfaidata.foundation/). +Kedro is an incubating project within [LF AI & Data](https://lfaidata.foundation/). The term "Technical Steering Committee" (TSC) describes the group of Kedro maintainers. We list [Kedro's current and past maintainers](#kedro-maintainers) on this page. -The TSC is responsible for the project's future development; you can read about our duties in our [Technical Charter](https://github.com/kedro-org/kedro/blob/main/kedro_technical_charter.pdf). We are happy to accept new members into the TSC to fuel Kedro's continued development. +The TSC is responsible for the project's future development; you can read about our duties in our [Technical Charter](https://github.com/kedro-org/kedro/blob/main/kedro_technical_charter.pdf). We accept new members into the TSC to fuel Kedro's continued development. On this page we describe: From 75a78a2ec748d31910eeaf210f37363988801aed Mon Sep 17 00:00:00 2001 From: Deepyaman Datta Date: Mon, 14 Aug 2023 13:08:55 -0500 Subject: [PATCH 25/70] Deprecate abstract "DataSet" in favor of "Dataset" (#2746) * Deprecate abstract "DataSet" in favor of "Dataset" Signed-off-by: Deepyaman Datta * Update docs references to abstract "DataSet" class Signed-off-by: Deepyaman Datta * Fix `AbstractDataSet` reference to `AbstractDataset` Signed-off-by: Deepyaman Datta * Update docs/source/conf.py Signed-off-by: Deepyaman Datta * Change remaining `_DEPRECATED_ERROR_CLASSES` to `_DEPRECATED_CLASSES`, update type hints Signed-off-by: Deepyaman Datta * Update RELEASE.md * Reformat kedro/io/__init__.py with Black and isort Signed-off-by: Deepyaman Datta * Remove duplicate imports Signed-off-by: Deepyaman Datta * Move imports Signed-off-by: Deepyaman Datta --------- Signed-off-by: Deepyaman Datta Signed-off-by: Jason Hite --- RELEASE.md | 6 ++ docs/source/data/data_catalog.md | 2 +- docs/source/data/kedro_io.md | 18 ++-- docs/source/deployment/dask.md | 4 +- docs/source/extend_kedro/custom_datasets.md | 36 ++++---- docs/source/extend_kedro/plugins.md | 2 +- docs/source/kedro.io.rst | 4 +- docs/source/nodes_and_pipelines/nodes.md | 18 ++-- .../nodes_and_pipelines/run_a_pipeline.md | 6 +- kedro/extras/datasets/README.md | 8 +- kedro/extras/datasets/__init__.py | 2 +- kedro/extras/datasets/api/api_dataset.py | 4 +- kedro/extras/datasets/biosequence/__init__.py | 2 +- .../biosequence/biosequence_dataset.py | 4 +- kedro/extras/datasets/dask/parquet_dataset.py | 4 +- kedro/extras/datasets/email/__init__.py | 2 +- .../extras/datasets/email/message_dataset.py | 4 +- kedro/extras/datasets/geopandas/__init__.py | 2 +- .../datasets/geopandas/geojson_dataset.py | 4 +- kedro/extras/datasets/holoviews/__init__.py | 2 +- .../datasets/holoviews/holoviews_writer.py | 4 +- kedro/extras/datasets/json/__init__.py | 2 +- kedro/extras/datasets/json/json_dataset.py | 4 +- kedro/extras/datasets/matplotlib/__init__.py | 2 +- .../datasets/matplotlib/matplotlib_writer.py | 4 +- kedro/extras/datasets/networkx/__init__.py | 2 +- kedro/extras/datasets/networkx/gml_dataset.py | 4 +- .../datasets/networkx/graphml_dataset.py | 4 +- .../extras/datasets/networkx/json_dataset.py | 4 +- kedro/extras/datasets/pandas/__init__.py | 2 +- kedro/extras/datasets/pandas/csv_dataset.py | 4 +- kedro/extras/datasets/pandas/excel_dataset.py | 4 +- .../extras/datasets/pandas/feather_dataset.py | 4 +- kedro/extras/datasets/pandas/gbq_dataset.py | 6 +- .../extras/datasets/pandas/generic_dataset.py | 4 +- kedro/extras/datasets/pandas/hdf_dataset.py | 4 +- kedro/extras/datasets/pandas/json_dataset.py | 4 +- .../extras/datasets/pandas/parquet_dataset.py | 4 +- kedro/extras/datasets/pandas/sql_dataset.py | 6 +- kedro/extras/datasets/pandas/xml_dataset.py | 4 +- kedro/extras/datasets/pickle/__init__.py | 2 +- .../extras/datasets/pickle/pickle_dataset.py | 4 +- kedro/extras/datasets/pillow/__init__.py | 2 +- kedro/extras/datasets/pillow/image_dataset.py | 4 +- kedro/extras/datasets/plotly/__init__.py | 2 +- kedro/extras/datasets/plotly/json_dataset.py | 4 +- kedro/extras/datasets/redis/__init__.py | 2 +- kedro/extras/datasets/redis/redis_dataset.py | 4 +- .../datasets/spark/deltatable_dataset.py | 6 +- kedro/extras/datasets/spark/spark_dataset.py | 6 +- .../datasets/spark/spark_hive_dataset.py | 8 +- .../datasets/spark/spark_jdbc_dataset.py | 4 +- kedro/extras/datasets/svmlight/__init__.py | 2 +- .../datasets/svmlight/svmlight_dataset.py | 4 +- .../tensorflow/tensorflow_model_dataset.py | 4 +- kedro/extras/datasets/text/__init__.py | 2 +- kedro/extras/datasets/text/text_dataset.py | 4 +- kedro/extras/datasets/video/video_dataset.py | 4 +- kedro/extras/datasets/yaml/__init__.py | 2 +- kedro/extras/datasets/yaml/yaml_dataset.py | 4 +- kedro/io/__init__.py | 12 ++- kedro/io/cached_dataset.py | 12 +-- kedro/io/core.py | 86 ++++++++++--------- kedro/io/data_catalog.py | 56 ++++++------ kedro/io/lambda_dataset.py | 8 +- kedro/io/memory_dataset.py | 4 +- kedro/io/partitioned_dataset.py | 20 ++--- kedro/runner/parallel_runner.py | 2 +- kedro/runner/runner.py | 6 +- kedro/runner/sequential_runner.py | 6 +- tests/io/test_core.py | 8 +- tests/io/test_data_catalog.py | 6 +- tests/io/test_incremental_dataset.py | 4 +- tests/io/test_partitioned_dataset.py | 2 +- tests/runner/test_parallel_runner.py | 4 +- tests/runner/test_sequential_runner.py | 4 +- tests/runner/test_thread_runner.py | 4 +- 77 files changed, 274 insertions(+), 254 deletions(-) diff --git a/RELEASE.md b/RELEASE.md index 6c0c894750..ea0fce323a 100644 --- a/RELEASE.md +++ b/RELEASE.md @@ -29,6 +29,12 @@ ## Breaking changes to the API ## Upcoming deprecations for Kedro 0.19.0 +* Renamed abstract dataset classes, in accordance with the [Kedro lexicon](https://github.com/kedro-org/kedro/wiki/Kedro-documentation-style-guide#kedro-lexicon). Dataset classes ending with "DataSet" are deprecated and will be removed in 0.19.0. Note that all of the below classes are also importable from `kedro.io`; only the module where they are defined is listed as the location. + +| Type | Deprecated Alias | Location | +| -------------------------- | -------------------------- | --------------- | +| `AbstractDataset` | `AbstractDataSet` | `kedro.io.core` | +| `AbstractVersionedDataset` | `AbstractVersionedDataSet` | `kedro.io.core` | # Release 0.18.12 diff --git a/docs/source/data/data_catalog.md b/docs/source/data/data_catalog.md index d7c73e4fdf..fb1f7ac3dc 100644 --- a/docs/source/data/data_catalog.md +++ b/docs/source/data/data_catalog.md @@ -783,7 +783,7 @@ gear = cars["gear"].values The following steps happened behind the scenes when `load` was called: - The value `cars` was located in the Data Catalog -- The corresponding `AbstractDataSet` object was retrieved +- The corresponding `AbstractDataset` object was retrieved - The `load` method of this dataset was called - This `load` method delegated the loading to the underlying pandas `read_csv` function diff --git a/docs/source/data/kedro_io.md b/docs/source/data/kedro_io.md index 6fdfefdd66..a38ea97fcb 100644 --- a/docs/source/data/kedro_io.md +++ b/docs/source/data/kedro_io.md @@ -1,7 +1,7 @@ # Kedro IO -In this tutorial, we cover advanced uses of [the Kedro IO module](/kedro.io) to understand the underlying implementation. The relevant API documentation is [kedro.io.AbstractDataSet](/kedro.io.AbstractDataSet) and [kedro.io.DataSetError](/kedro.io.DataSetError). +In this tutorial, we cover advanced uses of [the Kedro IO module](/kedro.io) to understand the underlying implementation. The relevant API documentation is [kedro.io.AbstractDataset](/kedro.io.AbstractDataset) and [kedro.io.DataSetError](/kedro.io.DataSetError). ## Error handling @@ -21,9 +21,9 @@ except DataSetError: ``` -## AbstractDataSet +## AbstractDataset -To understand what is going on behind the scenes, you should study the [AbstractDataSet interface](/kedro.io.AbstractDataSet). `AbstractDataSet` is the underlying interface that all datasets extend. It requires subclasses to override the `_load` and `_save` and provides `load` and `save` methods that enrich the corresponding private methods with uniform error handling. It also requires subclasses to override `_describe`, which is used in logging the internal information about the instances of your custom `AbstractDataSet` implementation. +To understand what is going on behind the scenes, you should study the [AbstractDataset interface](/kedro.io.AbstractDataset). `AbstractDataset` is the underlying interface that all datasets extend. It requires subclasses to override the `_load` and `_save` and provides `load` and `save` methods that enrich the corresponding private methods with uniform error handling. It also requires subclasses to override `_describe`, which is used in logging the internal information about the instances of your custom `AbstractDataset` implementation. If you have a dataset called `parts`, you can make direct calls to it like so: @@ -33,13 +33,13 @@ parts_df = parts.load() We recommend using a `DataCatalog` instead (for more details, see [the `DataCatalog` documentation](../data/data_catalog.md)) as it has been designed to make all datasets available to project members. -For contributors, if you would like to submit a new dataset, you must extend the `AbstractDataSet`. For a complete guide, please read [the section on custom datasets](../extend_kedro/custom_datasets.md). +For contributors, if you would like to submit a new dataset, you must extend the `AbstractDataset`. For a complete guide, please read [the section on custom datasets](../extend_kedro/custom_datasets.md). ## Versioning In order to enable versioning, you need to update the `catalog.yml` config file and set the `versioned` attribute to `true` for the given dataset. If this is a custom dataset, the implementation must also: - 1. extend `kedro.io.core.AbstractVersionedDataSet` AND + 1. extend `kedro.io.core.AbstractVersionedDataset` AND 2. add `version` namedtuple as an argument to its `__init__` method AND 3. call `super().__init__()` with positional arguments `filepath`, `version`, and, optionally, with `glob` and `exists` functions if it uses a non-local filesystem (see [kedro_datasets.pandas.CSVDataSet](/kedro_datasets.pandas.CSVDataSet) as an example) AND 4. modify its `_describe`, `_load` and `_save` methods respectively to support versioning (see [`kedro_datasets.pandas.CSVDataSet`](/kedro_datasets.pandas.CSVDataSet) for an example implementation) @@ -55,10 +55,10 @@ from pathlib import Path, PurePosixPath import pandas as pd -from kedro.io import AbstractVersionedDataSet +from kedro.io import AbstractVersionedDataset -class MyOwnDataSet(AbstractVersionedDataSet): +class MyOwnDataSet(AbstractVersionedDataset): def __init__(self, filepath, version, param1, param2=True): super().__init__(PurePosixPath(filepath), version) self._param1 = param1 @@ -314,7 +314,7 @@ Here is an exhaustive list of the arguments supported by `PartitionedDataSet`: | Argument | Required | Supported types | Description | | ----------------- | ------------------------------ | ------------------------------------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `path` | Yes | `str` | Path to the folder containing partitioned data. If path starts with the protocol (e.g., `s3://`) then the corresponding `fsspec` concrete filesystem implementation will be used. If protocol is not specified, local filesystem will be used | -| `dataset` | Yes | `str`, `Type[AbstractDataSet]`, `Dict[str, Any]` | Underlying dataset definition, for more details see the section below | +| `dataset` | Yes | `str`, `Type[AbstractDataset]`, `Dict[str, Any]` | Underlying dataset definition, for more details see the section below | | `credentials` | No | `Dict[str, Any]` | Protocol-specific options that will be passed to `fsspec.filesystemcall`, for more details see the section below | | `load_args` | No | `Dict[str, Any]` | Keyword arguments to be passed into `find()` method of the corresponding filesystem implementation | | `filepath_arg` | No | `str` (defaults to `filepath`) | Argument name of the underlying dataset initializer that will contain a path to an individual partition | @@ -326,7 +326,7 @@ Dataset definition should be passed into the `dataset` argument of the `Partitio ##### Shorthand notation -Requires you only to specify a class of the underlying dataset either as a string (e.g. `pandas.CSVDataSet` or a fully qualified class path like `kedro_datasets.pandas.CSVDataSet`) or as a class object that is a subclass of the [AbstractDataSet](/kedro.io.AbstractDataSet). +Requires you only to specify a class of the underlying dataset either as a string (e.g. `pandas.CSVDataSet` or a fully qualified class path like `kedro_datasets.pandas.CSVDataSet`) or as a class object that is a subclass of the [AbstractDataset](/kedro.io.AbstractDataset). ##### Full notation diff --git a/docs/source/deployment/dask.md b/docs/source/deployment/dask.md index 9c5734d744..a03b0fd24b 100644 --- a/docs/source/deployment/dask.md +++ b/docs/source/deployment/dask.md @@ -44,14 +44,14 @@ from kedro.framework.hooks.manager import ( _register_hooks_setuptools, ) from kedro.framework.project import settings -from kedro.io import AbstractDataSet, DataCatalog +from kedro.io import AbstractDataset, DataCatalog from kedro.pipeline import Pipeline from kedro.pipeline.node import Node from kedro.runner import AbstractRunner, run_node from pluggy import PluginManager -class _DaskDataSet(AbstractDataSet): +class _DaskDataSet(AbstractDataset): """``_DaskDataSet`` publishes/gets named datasets to/from the Dask scheduler.""" diff --git a/docs/source/extend_kedro/custom_datasets.md b/docs/source/extend_kedro/custom_datasets.md index 9e4b0713eb..c0aad914da 100644 --- a/docs/source/extend_kedro/custom_datasets.md +++ b/docs/source/extend_kedro/custom_datasets.md @@ -24,13 +24,13 @@ Consult the [Pillow documentation](https://pillow.readthedocs.io/en/stable/insta ## The anatomy of a dataset -At the minimum, a valid Kedro dataset needs to subclass the base [AbstractDataSet](/kedro.io.AbstractDataSet) and provide an implementation for the following abstract methods: +At the minimum, a valid Kedro dataset needs to subclass the base [AbstractDataset](/kedro.io.AbstractDataset) and provide an implementation for the following abstract methods: * `_load` * `_save` * `_describe` -`AbstractDataSet` is generically typed with an input data type for saving data, and an output data type for loading data. +`AbstractDataset` is generically typed with an input data type for saving data, and an output data type for loading data. This typing is optional however, and defaults to `Any` type. Here is an example skeleton for `ImageDataSet`: @@ -43,10 +43,10 @@ from typing import Any, Dict import numpy as np -from kedro.io import AbstractDataSet +from kedro.io import AbstractDataset -class ImageDataSet(AbstractDataSet[np.ndarray, np.ndarray]): +class ImageDataSet(AbstractDataset[np.ndarray, np.ndarray]): """``ImageDataSet`` loads / save image data from a given filepath as `numpy` array using Pillow. Example: @@ -108,11 +108,11 @@ import fsspec import numpy as np from PIL import Image -from kedro.io import AbstractDataSet +from kedro.io import AbstractDataset from kedro.io.core import get_filepath_str, get_protocol_and_path -class ImageDataSet(AbstractDataSet[np.ndarray, np.ndarray]): +class ImageDataSet(AbstractDataset[np.ndarray, np.ndarray]): def __init__(self, filepath: str): """Creates a new instance of ImageDataSet to load / save image data for given filepath. @@ -169,7 +169,7 @@ Similarly, we can implement the `_save` method as follows: ```python -class ImageDataSet(AbstractDataSet[np.ndarray, np.ndarray]): +class ImageDataSet(AbstractDataset[np.ndarray, np.ndarray]): def _save(self, data: np.ndarray) -> None: """Saves image data to the specified filepath.""" # using get_filepath_str ensures that the protocol and path are appended correctly for different filesystems @@ -193,7 +193,7 @@ You can open the file to verify that the data was written back correctly. The `_describe` method is used for printing purposes. The convention in Kedro is for the method to return a dictionary describing the attributes of the dataset. ```python -class ImageDataSet(AbstractDataSet[np.ndarray, np.ndarray]): +class ImageDataSet(AbstractDataset[np.ndarray, np.ndarray]): def _describe(self) -> Dict[str, Any]: """Returns a dict that describes the attributes of the dataset.""" return dict(filepath=self._filepath, protocol=self._protocol) @@ -214,11 +214,11 @@ import fsspec import numpy as np from PIL import Image -from kedro.io import AbstractDataSet +from kedro.io import AbstractDataset from kedro.io.core import get_filepath_str, get_protocol_and_path -class ImageDataSet(AbstractDataSet[np.ndarray, np.ndarray]): +class ImageDataSet(AbstractDataset[np.ndarray, np.ndarray]): """``ImageDataSet`` loads / save image data from a given filepath as `numpy` array using Pillow. Example: @@ -301,7 +301,7 @@ $ ls -la data/01_raw/pokemon-images-and-types/images/images/*.png | wc -l Versioning doesn't work with `PartitionedDataSet`. You can't use both of them at the same time. ``` To add [Versioning](../data/kedro_io.md#versioning) support to the new dataset we need to extend the - [AbstractVersionedDataSet](/kedro.io.AbstractVersionedDataSet) to: + [AbstractVersionedDataset](/kedro.io.AbstractVersionedDataset) to: * Accept a `version` keyword argument as part of the constructor * Adapt the `_save` and `_load` method to use the versioned data path obtained from `_get_save_path` and `_get_load_path` respectively @@ -320,11 +320,11 @@ import fsspec import numpy as np from PIL import Image -from kedro.io import AbstractVersionedDataSet +from kedro.io import AbstractVersionedDataset from kedro.io.core import get_filepath_str, get_protocol_and_path, Version -class ImageDataSet(AbstractVersionedDataSet[np.ndarray, np.ndarray]): +class ImageDataSet(AbstractVersionedDataset[np.ndarray, np.ndarray]): """``ImageDataSet`` loads / save image data from a given filepath as `numpy` array using Pillow. Example: @@ -391,14 +391,14 @@ The difference between the original `ImageDataSet` and the versioned `ImageDataS import numpy as np from PIL import Image --from kedro.io import AbstractDataSet +-from kedro.io import AbstractDataset -from kedro.io.core import get_filepath_str, get_protocol_and_path -+from kedro.io import AbstractVersionedDataSet ++from kedro.io import AbstractVersionedDataset +from kedro.io.core import get_filepath_str, get_protocol_and_path, Version --class ImageDataSet(AbstractDataSet[np.ndarray, np.ndarray]): -+class ImageDataSet(AbstractVersionedDataSet[np.ndarray, np.ndarray]): +-class ImageDataSet(AbstractDataset[np.ndarray, np.ndarray]): ++class ImageDataSet(AbstractVersionedDataset[np.ndarray, np.ndarray]): """``ImageDataSet`` loads / save image data from a given filepath as `numpy` array using Pillow. Example: @@ -537,7 +537,7 @@ These parameters are then passed to the dataset constructor so you can use them import fsspec -class ImageDataSet(AbstractVersionedDataSet): +class ImageDataSet(AbstractVersionedDataset): def __init__( self, filepath: str, diff --git a/docs/source/extend_kedro/plugins.md b/docs/source/extend_kedro/plugins.md index c7a0b10979..61b82fcfbc 100644 --- a/docs/source/extend_kedro/plugins.md +++ b/docs/source/extend_kedro/plugins.md @@ -196,7 +196,7 @@ When you are ready to submit your code: ## Supported Kedro plugins - [Kedro-Datasets](https://github.com/kedro-org/kedro-plugins/tree/main/kedro-datasets), a collection of all of Kedro's data connectors. These data -connectors are implementations of the `AbstractDataSet` +connectors are implementations of the `AbstractDataset` - [Kedro-Docker](https://github.com/kedro-org/kedro-plugins/tree/main/kedro-docker), a tool for packaging and shipping Kedro projects within containers - [Kedro-Airflow](https://github.com/kedro-org/kedro-plugins/tree/main/kedro-airflow), a tool for converting your Kedro project into an Airflow project - [Kedro-Viz](https://github.com/kedro-org/kedro-viz), a tool for visualising your Kedro pipelines diff --git a/docs/source/kedro.io.rst b/docs/source/kedro.io.rst index f86bb0558d..56c6a7d6d5 100644 --- a/docs/source/kedro.io.rst +++ b/docs/source/kedro.io.rst @@ -11,8 +11,8 @@ kedro.io :toctree: :template: autosummary/class.rst - kedro.io.AbstractDataSet - kedro.io.AbstractVersionedDataSet + kedro.io.AbstractDataset + kedro.io.AbstractVersionedDataset kedro.io.CachedDataSet kedro.io.CachedDataset kedro.io.DataCatalog diff --git a/docs/source/nodes_and_pipelines/nodes.md b/docs/source/nodes_and_pipelines/nodes.md index 7a22b8765e..a41f147244 100644 --- a/docs/source/nodes_and_pipelines/nodes.md +++ b/docs/source/nodes_and_pipelines/nodes.md @@ -287,7 +287,7 @@ def report_accuracy(y_pred: pd.Series, y_test: pd.Series):
-The `ChunkWiseDataset` is a variant of the `pandas.CSVDataset` where the main change is to the `_save` method that appends data instead of overwriting it. You need to create a file `src//chunkwise.py` and put this class inside it. Below is an example of the `ChunkWiseCSVDataset` implementation: +The `ChunkWiseCSVDataset` is a variant of the `pandas.CSVDataSet` where the main change is to the `_save` method that appends data instead of overwriting it. You need to create a file `src//chunkwise.py` and put this class inside it. Below is an example of the `ChunkWiseCSVDataset` implementation: ```python import pandas as pd @@ -295,10 +295,10 @@ import pandas as pd from kedro.io.core import ( get_filepath_str, ) -from kedro.extras.datasets.pandas import CSVDataset +from kedro.extras.datasets.pandas import CSVDataSet -class ChunkWiseCSVDataset(CSVDataset): +class ChunkWiseCSVDataset(CSVDataSet): """``ChunkWiseCSVDataset`` loads/saves data from/to a CSV file using an underlying filesystem. It uses pandas to handle the CSV file. """ @@ -319,20 +319,20 @@ After that, you need to update the `catalog.yml` to use this new dataset. ```diff + y_pred: -+ type: .chunkwise.ChunkWiseCSVDataSet ++ type: .chunkwise.ChunkWiseCSVDataset + filepath: data/07_model_output/y_pred.csv ``` -With these changes, when you run `kedro run` in your terminal, you should see `y_pred`` being saved multiple times in the logs as the generator lazily processes and saves the data in smaller chunks. +With these changes, when you run `kedro run` in your terminal, you should see `y_pred` being saved multiple times in the logs as the generator lazily processes and saves the data in smaller chunks. ``` ... INFO Loading data from 'y_train' (MemoryDataset)... data_catalog.py:475 INFO Running node: make_predictions: make_predictions([X_train,X_test,y_train]) -> [y_pred] node.py:331 - INFO Saving data to 'y_pred' (ChunkWiseCSVDataSet)... data_catalog.py:514 - INFO Saving data to 'y_pred' (ChunkWiseCSVDataSet)... data_catalog.py:514 - INFO Saving data to 'y_pred' (ChunkWiseCSVDataSet)... data_catalog.py:514 + INFO Saving data to 'y_pred' (ChunkWiseCSVDataset)... data_catalog.py:514 + INFO Saving data to 'y_pred' (ChunkWiseCSVDataset)... data_catalog.py:514 + INFO Saving data to 'y_pred' (ChunkWiseCSVDataset)... data_catalog.py:514 INFO Completed 2 out of 3 tasks sequential_runner.py:85 - INFO Loading data from 'y_pred' (ChunkWiseCSVDataSet)... data_catalog.py:475 + INFO Loading data from 'y_pred' (ChunkWiseCSVDataset)... data_catalog.py:475 ... runner.py:105 ``` diff --git a/docs/source/nodes_and_pipelines/run_a_pipeline.md b/docs/source/nodes_and_pipelines/run_a_pipeline.md index 417510fe8e..2d7af412ad 100644 --- a/docs/source/nodes_and_pipelines/run_a_pipeline.md +++ b/docs/source/nodes_and_pipelines/run_a_pipeline.md @@ -57,7 +57,7 @@ If the built-in Kedro runners do not meet your requirements, you can also define ```python # in src//runner.py -from kedro.io import AbstractDataSet, DataCatalog, MemoryDataSet +from kedro.io import AbstractDataset, DataCatalog, MemoryDataSet from kedro.pipeline import Pipeline from kedro.runner.runner import AbstractRunner from pluggy import PluginManager @@ -69,13 +69,13 @@ class DryRunner(AbstractRunner): neccessary data exists. """ - def create_default_data_set(self, ds_name: str) -> AbstractDataSet: + def create_default_data_set(self, ds_name: str) -> AbstractDataset: """Factory method for creating the default data set for the runner. Args: ds_name: Name of the missing data set Returns: - An instance of an implementation of AbstractDataSet to be used + An instance of an implementation of AbstractDataset to be used for all unregistered data sets. """ diff --git a/kedro/extras/datasets/README.md b/kedro/extras/datasets/README.md index 3058ac4ab2..bd93acd6be 100644 --- a/kedro/extras/datasets/README.md +++ b/kedro/extras/datasets/README.md @@ -4,9 +4,9 @@ > `kedro.extras.datasets` is deprecated and will be removed in Kedro 0.19, > install `kedro-datasets` instead by running `pip install kedro-datasets`. -Welcome to `kedro.extras.datasets`, the home of Kedro's data connectors. Here you will find `AbstractDataSet` implementations created by QuantumBlack and external contributors. +Welcome to `kedro.extras.datasets`, the home of Kedro's data connectors. Here you will find `AbstractDataset` implementations created by QuantumBlack and external contributors. -## What `AbstractDataSet` implementations are supported? +## What `AbstractDataset` implementations are supported? We support a range of data descriptions, including CSV, Excel, Parquet, Feather, HDF5, JSON, Pickle, SQL Tables, SQL Queries, Spark DataFrames and more. We even allow support for working with images. @@ -16,7 +16,7 @@ These data descriptions are supported with the APIs of `pandas`, `spark`, `netwo Here is a full list of [supported data descriptions and APIs](https://kedro.readthedocs.io/en/stable/kedro.extras.datasets.html). -## How can I create my own `AbstractDataSet` implementation? +## How can I create my own `AbstractDataset` implementation? -Take a look at our [instructions on how to create your own `AbstractDataSet` implementation](https://kedro.readthedocs.io/en/stable/extend_kedro/custom_datasets.html). +Take a look at our [instructions on how to create your own `AbstractDataset` implementation](https://kedro.readthedocs.io/en/stable/extend_kedro/custom_datasets.html). diff --git a/kedro/extras/datasets/__init__.py b/kedro/extras/datasets/__init__.py index 5397e3da98..3eec3e3fe1 100644 --- a/kedro/extras/datasets/__init__.py +++ b/kedro/extras/datasets/__init__.py @@ -1,5 +1,5 @@ """``kedro.extras.datasets`` is where you can find all of Kedro's data connectors. -These data connectors are implementations of the ``AbstractDataSet``. +These data connectors are implementations of the ``AbstractDataset``. .. warning:: diff --git a/kedro/extras/datasets/api/api_dataset.py b/kedro/extras/datasets/api/api_dataset.py index cdfaa93c83..f288c96814 100644 --- a/kedro/extras/datasets/api/api_dataset.py +++ b/kedro/extras/datasets/api/api_dataset.py @@ -6,14 +6,14 @@ import requests from requests.auth import AuthBase -from kedro.io.core import AbstractDataSet, DatasetError +from kedro.io.core import AbstractDataset, DatasetError # NOTE: kedro.extras.datasets will be removed in Kedro 0.19.0. # Any contribution to datasets should be made in kedro-datasets # in kedro-plugins (https://github.com/kedro-org/kedro-plugins) -class APIDataSet(AbstractDataSet[None, requests.Response]): +class APIDataSet(AbstractDataset[None, requests.Response]): """``APIDataSet`` loads the data from HTTP(S) APIs. It uses the python requests library: https://requests.readthedocs.io/en/latest/ diff --git a/kedro/extras/datasets/biosequence/__init__.py b/kedro/extras/datasets/biosequence/__init__.py index 9f2f1a2a2e..d806e3ca33 100644 --- a/kedro/extras/datasets/biosequence/__init__.py +++ b/kedro/extras/datasets/biosequence/__init__.py @@ -1,4 +1,4 @@ -"""``AbstractDataSet`` implementation to read/write from/to a sequence file.""" +"""``AbstractDataset`` implementation to read/write from/to a sequence file.""" __all__ = ["BioSequenceDataSet"] diff --git a/kedro/extras/datasets/biosequence/biosequence_dataset.py b/kedro/extras/datasets/biosequence/biosequence_dataset.py index 4888158774..ac0770aa68 100644 --- a/kedro/extras/datasets/biosequence/biosequence_dataset.py +++ b/kedro/extras/datasets/biosequence/biosequence_dataset.py @@ -8,14 +8,14 @@ import fsspec from Bio import SeqIO -from kedro.io.core import AbstractDataSet, get_filepath_str, get_protocol_and_path +from kedro.io.core import AbstractDataset, get_filepath_str, get_protocol_and_path # NOTE: kedro.extras.datasets will be removed in Kedro 0.19.0. # Any contribution to datasets should be made in kedro-datasets # in kedro-plugins (https://github.com/kedro-org/kedro-plugins) -class BioSequenceDataSet(AbstractDataSet[List, List]): +class BioSequenceDataSet(AbstractDataset[List, List]): r"""``BioSequenceDataSet`` loads and saves data to a sequence file. Example: diff --git a/kedro/extras/datasets/dask/parquet_dataset.py b/kedro/extras/datasets/dask/parquet_dataset.py index 08c93b1d49..23dc7a701b 100644 --- a/kedro/extras/datasets/dask/parquet_dataset.py +++ b/kedro/extras/datasets/dask/parquet_dataset.py @@ -8,14 +8,14 @@ import fsspec import triad -from kedro.io.core import AbstractDataSet, get_protocol_and_path +from kedro.io.core import AbstractDataset, get_protocol_and_path # NOTE: kedro.extras.datasets will be removed in Kedro 0.19.0. # Any contribution to datasets should be made in kedro-datasets # in kedro-plugins (https://github.com/kedro-org/kedro-plugins) -class ParquetDataSet(AbstractDataSet[dd.DataFrame, dd.DataFrame]): +class ParquetDataSet(AbstractDataset[dd.DataFrame, dd.DataFrame]): """``ParquetDataSet`` loads and saves data to parquet file(s). It uses Dask remote data services to handle the corresponding load and save operations: https://docs.dask.org/en/latest/how-to/connect-to-remote-data.html diff --git a/kedro/extras/datasets/email/__init__.py b/kedro/extras/datasets/email/__init__.py index 97aa7a3455..ba7873cbf2 100644 --- a/kedro/extras/datasets/email/__init__.py +++ b/kedro/extras/datasets/email/__init__.py @@ -1,4 +1,4 @@ -"""``AbstractDataSet`` implementations for managing email messages.""" +"""``AbstractDataset`` implementations for managing email messages.""" __all__ = ["EmailMessageDataSet"] diff --git a/kedro/extras/datasets/email/message_dataset.py b/kedro/extras/datasets/email/message_dataset.py index 8a725540c2..695d93cbbe 100644 --- a/kedro/extras/datasets/email/message_dataset.py +++ b/kedro/extras/datasets/email/message_dataset.py @@ -13,7 +13,7 @@ import fsspec from kedro.io.core import ( - AbstractVersionedDataSet, + AbstractVersionedDataset, DatasetError, Version, get_filepath_str, @@ -26,7 +26,7 @@ class EmailMessageDataSet( - AbstractVersionedDataSet[Message, Message] + AbstractVersionedDataset[Message, Message] ): # pylint: disable=too-many-instance-attributes """``EmailMessageDataSet`` loads/saves an email message from/to a file using an underlying filesystem (e.g.: local, S3, GCS). It uses the diff --git a/kedro/extras/datasets/geopandas/__init__.py b/kedro/extras/datasets/geopandas/__init__.py index 966577fc37..bee7462a83 100644 --- a/kedro/extras/datasets/geopandas/__init__.py +++ b/kedro/extras/datasets/geopandas/__init__.py @@ -1,4 +1,4 @@ -"""``GeoJSONDataSet`` is an ``AbstractVersionedDataSet`` to save and load GeoJSON files. +"""``GeoJSONDataSet`` is an ``AbstractVersionedDataset`` to save and load GeoJSON files. """ __all__ = ["GeoJSONDataSet"] diff --git a/kedro/extras/datasets/geopandas/geojson_dataset.py b/kedro/extras/datasets/geopandas/geojson_dataset.py index 88cce18dee..5beba29d57 100644 --- a/kedro/extras/datasets/geopandas/geojson_dataset.py +++ b/kedro/extras/datasets/geopandas/geojson_dataset.py @@ -10,7 +10,7 @@ import geopandas as gpd from kedro.io.core import ( - AbstractVersionedDataSet, + AbstractVersionedDataset, DatasetError, Version, get_filepath_str, @@ -23,7 +23,7 @@ class GeoJSONDataSet( - AbstractVersionedDataSet[ + AbstractVersionedDataset[ gpd.GeoDataFrame, Union[gpd.GeoDataFrame, Dict[str, gpd.GeoDataFrame]] ] ): diff --git a/kedro/extras/datasets/holoviews/__init__.py b/kedro/extras/datasets/holoviews/__init__.py index c97bd72a6d..f50db9b823 100644 --- a/kedro/extras/datasets/holoviews/__init__.py +++ b/kedro/extras/datasets/holoviews/__init__.py @@ -1,4 +1,4 @@ -"""``AbstractDataSet`` implementation to save Holoviews objects as image files.""" +"""``AbstractDataset`` implementation to save Holoviews objects as image files.""" __all__ = ["HoloviewsWriter"] diff --git a/kedro/extras/datasets/holoviews/holoviews_writer.py b/kedro/extras/datasets/holoviews/holoviews_writer.py index 2ed30f7156..34daeb1769 100644 --- a/kedro/extras/datasets/holoviews/holoviews_writer.py +++ b/kedro/extras/datasets/holoviews/holoviews_writer.py @@ -10,7 +10,7 @@ import holoviews as hv from kedro.io.core import ( - AbstractVersionedDataSet, + AbstractVersionedDataset, DatasetError, Version, get_filepath_str, @@ -25,7 +25,7 @@ HoloViews = TypeVar("HoloViews") -class HoloviewsWriter(AbstractVersionedDataSet[HoloViews, NoReturn]): +class HoloviewsWriter(AbstractVersionedDataset[HoloViews, NoReturn]): """``HoloviewsWriter`` saves Holoviews objects to image file(s) in an underlying filesystem (e.g. local, S3, GCS). diff --git a/kedro/extras/datasets/json/__init__.py b/kedro/extras/datasets/json/__init__.py index 5f023b35f4..887f7cd72f 100644 --- a/kedro/extras/datasets/json/__init__.py +++ b/kedro/extras/datasets/json/__init__.py @@ -1,4 +1,4 @@ -"""``AbstractDataSet`` implementation to load/save data from/to a JSON file.""" +"""``AbstractDataset`` implementation to load/save data from/to a JSON file.""" __all__ = ["JSONDataSet"] diff --git a/kedro/extras/datasets/json/json_dataset.py b/kedro/extras/datasets/json/json_dataset.py index 17cc2cf69e..5e05dd46ed 100644 --- a/kedro/extras/datasets/json/json_dataset.py +++ b/kedro/extras/datasets/json/json_dataset.py @@ -9,7 +9,7 @@ import fsspec from kedro.io.core import ( - AbstractVersionedDataSet, + AbstractVersionedDataset, DatasetError, Version, get_filepath_str, @@ -21,7 +21,7 @@ # in kedro-plugins (https://github.com/kedro-org/kedro-plugins) -class JSONDataSet(AbstractVersionedDataSet[Any, Any]): +class JSONDataSet(AbstractVersionedDataset[Any, Any]): """``JSONDataSet`` loads/saves data from/to a JSON file using an underlying filesystem (e.g.: local, S3, GCS). It uses native json to handle the JSON file. diff --git a/kedro/extras/datasets/matplotlib/__init__.py b/kedro/extras/datasets/matplotlib/__init__.py index ee2bc06466..eabd8fc517 100644 --- a/kedro/extras/datasets/matplotlib/__init__.py +++ b/kedro/extras/datasets/matplotlib/__init__.py @@ -1,4 +1,4 @@ -"""``AbstractDataSet`` implementation to save matplotlib objects as image files.""" +"""``AbstractDataset`` implementation to save matplotlib objects as image files.""" __all__ = ["MatplotlibWriter"] diff --git a/kedro/extras/datasets/matplotlib/matplotlib_writer.py b/kedro/extras/datasets/matplotlib/matplotlib_writer.py index 00a365f2ec..204e4673c5 100644 --- a/kedro/extras/datasets/matplotlib/matplotlib_writer.py +++ b/kedro/extras/datasets/matplotlib/matplotlib_writer.py @@ -11,7 +11,7 @@ import matplotlib.pyplot as plt from kedro.io.core import ( - AbstractVersionedDataSet, + AbstractVersionedDataset, DatasetError, Version, get_filepath_str, @@ -24,7 +24,7 @@ class MatplotlibWriter( - AbstractVersionedDataSet[ + AbstractVersionedDataset[ Union[plt.figure, List[plt.figure], Dict[str, plt.figure]], NoReturn ] ): diff --git a/kedro/extras/datasets/networkx/__init__.py b/kedro/extras/datasets/networkx/__init__.py index 73674c81fe..ece1b98f9c 100644 --- a/kedro/extras/datasets/networkx/__init__.py +++ b/kedro/extras/datasets/networkx/__init__.py @@ -1,4 +1,4 @@ -"""``AbstractDataSet`` implementation to save and load NetworkX graphs in JSON +"""``AbstractDataset`` implementation to save and load NetworkX graphs in JSON , GraphML and GML formats using ``NetworkX``.""" __all__ = ["GMLDataSet", "GraphMLDataSet", "JSONDataSet"] diff --git a/kedro/extras/datasets/networkx/gml_dataset.py b/kedro/extras/datasets/networkx/gml_dataset.py index d48f7d37e2..a56ddbe7ba 100644 --- a/kedro/extras/datasets/networkx/gml_dataset.py +++ b/kedro/extras/datasets/networkx/gml_dataset.py @@ -11,7 +11,7 @@ import networkx from kedro.io.core import ( - AbstractVersionedDataSet, + AbstractVersionedDataset, Version, get_filepath_str, get_protocol_and_path, @@ -22,7 +22,7 @@ # in kedro-plugins (https://github.com/kedro-org/kedro-plugins) -class GMLDataSet(AbstractVersionedDataSet[networkx.Graph, networkx.Graph]): +class GMLDataSet(AbstractVersionedDataset[networkx.Graph, networkx.Graph]): """``GMLDataSet`` loads and saves graphs to a GML file using an underlying filesystem (e.g.: local, S3, GCS). ``NetworkX`` is used to create GML data. diff --git a/kedro/extras/datasets/networkx/graphml_dataset.py b/kedro/extras/datasets/networkx/graphml_dataset.py index 54f5d496f7..368459958f 100644 --- a/kedro/extras/datasets/networkx/graphml_dataset.py +++ b/kedro/extras/datasets/networkx/graphml_dataset.py @@ -10,7 +10,7 @@ import networkx from kedro.io.core import ( - AbstractVersionedDataSet, + AbstractVersionedDataset, Version, get_filepath_str, get_protocol_and_path, @@ -21,7 +21,7 @@ # in kedro-plugins (https://github.com/kedro-org/kedro-plugins) -class GraphMLDataSet(AbstractVersionedDataSet[networkx.Graph, networkx.Graph]): +class GraphMLDataSet(AbstractVersionedDataset[networkx.Graph, networkx.Graph]): """``GraphMLDataSet`` loads and saves graphs to a GraphML file using an underlying filesystem (e.g.: local, S3, GCS). ``NetworkX`` is used to create GraphML data. diff --git a/kedro/extras/datasets/networkx/json_dataset.py b/kedro/extras/datasets/networkx/json_dataset.py index 4ae9940601..60db837a91 100644 --- a/kedro/extras/datasets/networkx/json_dataset.py +++ b/kedro/extras/datasets/networkx/json_dataset.py @@ -11,7 +11,7 @@ import networkx from kedro.io.core import ( - AbstractVersionedDataSet, + AbstractVersionedDataset, Version, get_filepath_str, get_protocol_and_path, @@ -22,7 +22,7 @@ # in kedro-plugins (https://github.com/kedro-org/kedro-plugins) -class JSONDataSet(AbstractVersionedDataSet[networkx.Graph, networkx.Graph]): +class JSONDataSet(AbstractVersionedDataset[networkx.Graph, networkx.Graph]): """NetworkX ``JSONDataSet`` loads and saves graphs to a JSON file using an underlying filesystem (e.g.: local, S3, GCS). ``NetworkX`` is used to create JSON data. diff --git a/kedro/extras/datasets/pandas/__init__.py b/kedro/extras/datasets/pandas/__init__.py index b84015d1d9..2a8ba76371 100644 --- a/kedro/extras/datasets/pandas/__init__.py +++ b/kedro/extras/datasets/pandas/__init__.py @@ -1,4 +1,4 @@ -"""``AbstractDataSet`` implementations that produce pandas DataFrames.""" +"""``AbstractDataset`` implementations that produce pandas DataFrames.""" __all__ = [ "CSVDataSet", diff --git a/kedro/extras/datasets/pandas/csv_dataset.py b/kedro/extras/datasets/pandas/csv_dataset.py index 597d03ecf9..01b044969c 100644 --- a/kedro/extras/datasets/pandas/csv_dataset.py +++ b/kedro/extras/datasets/pandas/csv_dataset.py @@ -12,7 +12,7 @@ from kedro.io.core import ( PROTOCOL_DELIMITER, - AbstractVersionedDataSet, + AbstractVersionedDataset, DatasetError, Version, get_filepath_str, @@ -26,7 +26,7 @@ # in kedro-plugins (https://github.com/kedro-org/kedro-plugins) -class CSVDataSet(AbstractVersionedDataSet[pd.DataFrame, pd.DataFrame]): +class CSVDataSet(AbstractVersionedDataset[pd.DataFrame, pd.DataFrame]): """``CSVDataSet`` loads/saves data from/to a CSV file using an underlying filesystem (e.g.: local, S3, GCS). It uses pandas to handle the CSV file. diff --git a/kedro/extras/datasets/pandas/excel_dataset.py b/kedro/extras/datasets/pandas/excel_dataset.py index 05c1144721..21139c7ca9 100644 --- a/kedro/extras/datasets/pandas/excel_dataset.py +++ b/kedro/extras/datasets/pandas/excel_dataset.py @@ -12,7 +12,7 @@ from kedro.io.core import ( PROTOCOL_DELIMITER, - AbstractVersionedDataSet, + AbstractVersionedDataset, DatasetError, Version, get_filepath_str, @@ -27,7 +27,7 @@ class ExcelDataSet( - AbstractVersionedDataSet[ + AbstractVersionedDataset[ Union[pd.DataFrame, Dict[str, pd.DataFrame]], Union[pd.DataFrame, Dict[str, pd.DataFrame]], ] diff --git a/kedro/extras/datasets/pandas/feather_dataset.py b/kedro/extras/datasets/pandas/feather_dataset.py index 534d84d9bf..b43ecc1814 100644 --- a/kedro/extras/datasets/pandas/feather_dataset.py +++ b/kedro/extras/datasets/pandas/feather_dataset.py @@ -13,7 +13,7 @@ from kedro.io.core import ( PROTOCOL_DELIMITER, - AbstractVersionedDataSet, + AbstractVersionedDataset, Version, get_filepath_str, get_protocol_and_path, @@ -26,7 +26,7 @@ # in kedro-plugins (https://github.com/kedro-org/kedro-plugins) -class FeatherDataSet(AbstractVersionedDataSet[pd.DataFrame, pd.DataFrame]): +class FeatherDataSet(AbstractVersionedDataset[pd.DataFrame, pd.DataFrame]): """``FeatherDataSet`` loads and saves data to a feather file using an underlying filesystem (e.g.: local, S3, GCS). The underlying functionality is supported by pandas, so it supports all allowed pandas options diff --git a/kedro/extras/datasets/pandas/gbq_dataset.py b/kedro/extras/datasets/pandas/gbq_dataset.py index dda5cf9d35..16cea01213 100644 --- a/kedro/extras/datasets/pandas/gbq_dataset.py +++ b/kedro/extras/datasets/pandas/gbq_dataset.py @@ -13,7 +13,7 @@ from google.oauth2.credentials import Credentials from kedro.io.core import ( - AbstractDataSet, + AbstractDataset, DatasetError, get_filepath_str, get_protocol_and_path, @@ -25,7 +25,7 @@ # in kedro-plugins (https://github.com/kedro-org/kedro-plugins) -class GBQTableDataSet(AbstractDataSet[None, pd.DataFrame]): +class GBQTableDataSet(AbstractDataset[None, pd.DataFrame]): """``GBQTableDataSet`` loads and saves data from/to Google BigQuery. It uses pandas-gbq to read and write from/to BigQuery table. @@ -175,7 +175,7 @@ def _validate_location(self): ) -class GBQQueryDataSet(AbstractDataSet[None, pd.DataFrame]): +class GBQQueryDataSet(AbstractDataset[None, pd.DataFrame]): """``GBQQueryDataSet`` loads data from a provided SQL query from Google BigQuery. It uses ``pandas.read_gbq`` which itself uses ``pandas-gbq`` internally to read from BigQuery table. Therefore it supports all allowed diff --git a/kedro/extras/datasets/pandas/generic_dataset.py b/kedro/extras/datasets/pandas/generic_dataset.py index bf44694a26..7212310e8f 100644 --- a/kedro/extras/datasets/pandas/generic_dataset.py +++ b/kedro/extras/datasets/pandas/generic_dataset.py @@ -10,7 +10,7 @@ import pandas as pd from kedro.io.core import ( - AbstractVersionedDataSet, + AbstractVersionedDataset, DatasetError, Version, get_filepath_str, @@ -34,7 +34,7 @@ ] -class GenericDataSet(AbstractVersionedDataSet[pd.DataFrame, pd.DataFrame]): +class GenericDataSet(AbstractVersionedDataset[pd.DataFrame, pd.DataFrame]): """`pandas.GenericDataSet` loads/saves data from/to a data file using an underlying filesystem (e.g.: local, S3, GCS). It uses pandas to dynamically select the appropriate type of read/write target on a best effort basis. diff --git a/kedro/extras/datasets/pandas/hdf_dataset.py b/kedro/extras/datasets/pandas/hdf_dataset.py index d60161d095..0d337af42d 100644 --- a/kedro/extras/datasets/pandas/hdf_dataset.py +++ b/kedro/extras/datasets/pandas/hdf_dataset.py @@ -10,7 +10,7 @@ import pandas as pd from kedro.io.core import ( - AbstractVersionedDataSet, + AbstractVersionedDataset, DatasetError, Version, get_filepath_str, @@ -24,7 +24,7 @@ # in kedro-plugins (https://github.com/kedro-org/kedro-plugins) -class HDFDataSet(AbstractVersionedDataSet[pd.DataFrame, pd.DataFrame]): +class HDFDataSet(AbstractVersionedDataset[pd.DataFrame, pd.DataFrame]): """``HDFDataSet`` loads/saves data from/to a hdf file using an underlying filesystem (e.g. local, S3, GCS). It uses pandas.HDFStore to handle the hdf file. diff --git a/kedro/extras/datasets/pandas/json_dataset.py b/kedro/extras/datasets/pandas/json_dataset.py index 1d5e3cb2d1..8148d325c5 100644 --- a/kedro/extras/datasets/pandas/json_dataset.py +++ b/kedro/extras/datasets/pandas/json_dataset.py @@ -12,7 +12,7 @@ from kedro.io.core import ( PROTOCOL_DELIMITER, - AbstractVersionedDataSet, + AbstractVersionedDataset, DatasetError, Version, get_filepath_str, @@ -26,7 +26,7 @@ # in kedro-plugins (https://github.com/kedro-org/kedro-plugins) -class JSONDataSet(AbstractVersionedDataSet[pd.DataFrame, pd.DataFrame]): +class JSONDataSet(AbstractVersionedDataset[pd.DataFrame, pd.DataFrame]): """``JSONDataSet`` loads/saves data from/to a JSON file using an underlying filesystem (e.g.: local, S3, GCS). It uses pandas to handle the json file. diff --git a/kedro/extras/datasets/pandas/parquet_dataset.py b/kedro/extras/datasets/pandas/parquet_dataset.py index bf03f97ccd..4bdba28772 100644 --- a/kedro/extras/datasets/pandas/parquet_dataset.py +++ b/kedro/extras/datasets/pandas/parquet_dataset.py @@ -13,7 +13,7 @@ from kedro.io.core import ( PROTOCOL_DELIMITER, - AbstractVersionedDataSet, + AbstractVersionedDataset, DatasetError, Version, get_filepath_str, @@ -27,7 +27,7 @@ # in kedro-plugins (https://github.com/kedro-org/kedro-plugins) -class ParquetDataSet(AbstractVersionedDataSet[pd.DataFrame, pd.DataFrame]): +class ParquetDataSet(AbstractVersionedDataset[pd.DataFrame, pd.DataFrame]): """``ParquetDataSet`` loads/saves data from/to a Parquet file using an underlying filesystem (e.g.: local, S3, GCS). It uses pandas to handle the Parquet file. diff --git a/kedro/extras/datasets/pandas/sql_dataset.py b/kedro/extras/datasets/pandas/sql_dataset.py index 7c084cb82e..373663ce84 100644 --- a/kedro/extras/datasets/pandas/sql_dataset.py +++ b/kedro/extras/datasets/pandas/sql_dataset.py @@ -11,7 +11,7 @@ from sqlalchemy.exc import NoSuchModuleError from kedro.io.core import ( - AbstractDataSet, + AbstractDataset, DatasetError, get_filepath_str, get_protocol_and_path, @@ -92,7 +92,7 @@ def _get_sql_alchemy_missing_error() -> DatasetError: ) -class SQLTableDataSet(AbstractDataSet[pd.DataFrame, pd.DataFrame]): +class SQLTableDataSet(AbstractDataset[pd.DataFrame, pd.DataFrame]): """``SQLTableDataSet`` loads data from a SQL table and saves a pandas dataframe to a table. It uses ``pandas.DataFrame`` internally, so it supports all allowed pandas options on ``read_sql_table`` and @@ -264,7 +264,7 @@ def _exists(self) -> bool: return exists -class SQLQueryDataSet(AbstractDataSet[None, pd.DataFrame]): +class SQLQueryDataSet(AbstractDataset[None, pd.DataFrame]): """``SQLQueryDataSet`` loads data from a provided SQL query. It uses ``pandas.DataFrame`` internally, so it supports all allowed pandas options on ``read_sql_query``. Since Pandas uses SQLAlchemy behind diff --git a/kedro/extras/datasets/pandas/xml_dataset.py b/kedro/extras/datasets/pandas/xml_dataset.py index 9433ae238d..ad91b4ad4b 100644 --- a/kedro/extras/datasets/pandas/xml_dataset.py +++ b/kedro/extras/datasets/pandas/xml_dataset.py @@ -12,7 +12,7 @@ from kedro.io.core import ( PROTOCOL_DELIMITER, - AbstractVersionedDataSet, + AbstractVersionedDataset, DatasetError, Version, get_filepath_str, @@ -26,7 +26,7 @@ # in kedro-plugins (https://github.com/kedro-org/kedro-plugins) -class XMLDataSet(AbstractVersionedDataSet[pd.DataFrame, pd.DataFrame]): +class XMLDataSet(AbstractVersionedDataset[pd.DataFrame, pd.DataFrame]): """``XMLDataSet`` loads/saves data from/to a XML file using an underlying filesystem (e.g.: local, S3, GCS). It uses pandas to handle the XML file. diff --git a/kedro/extras/datasets/pickle/__init__.py b/kedro/extras/datasets/pickle/__init__.py index 8e6707d450..40b898eb07 100644 --- a/kedro/extras/datasets/pickle/__init__.py +++ b/kedro/extras/datasets/pickle/__init__.py @@ -1,4 +1,4 @@ -"""``AbstractDataSet`` implementation to load/save data from/to a Pickle file.""" +"""``AbstractDataset`` implementation to load/save data from/to a Pickle file.""" __all__ = ["PickleDataSet"] diff --git a/kedro/extras/datasets/pickle/pickle_dataset.py b/kedro/extras/datasets/pickle/pickle_dataset.py index eb9fb55594..19bda78f96 100644 --- a/kedro/extras/datasets/pickle/pickle_dataset.py +++ b/kedro/extras/datasets/pickle/pickle_dataset.py @@ -11,7 +11,7 @@ import fsspec from kedro.io.core import ( - AbstractVersionedDataSet, + AbstractVersionedDataset, DatasetError, Version, get_filepath_str, @@ -23,7 +23,7 @@ # in kedro-plugins (https://github.com/kedro-org/kedro-plugins) -class PickleDataSet(AbstractVersionedDataSet[Any, Any]): +class PickleDataSet(AbstractVersionedDataset[Any, Any]): """``PickleDataSet`` loads/saves data from/to a Pickle file using an underlying filesystem (e.g.: local, S3, GCS). The underlying functionality is supported by the specified backend library passed in (defaults to the ``pickle`` library), so it diff --git a/kedro/extras/datasets/pillow/__init__.py b/kedro/extras/datasets/pillow/__init__.py index bd68c032c3..03df85f3ee 100644 --- a/kedro/extras/datasets/pillow/__init__.py +++ b/kedro/extras/datasets/pillow/__init__.py @@ -1,4 +1,4 @@ -"""``AbstractDataSet`` implementation to load/save image data.""" +"""``AbstractDataset`` implementation to load/save image data.""" __all__ = ["ImageDataSet"] diff --git a/kedro/extras/datasets/pillow/image_dataset.py b/kedro/extras/datasets/pillow/image_dataset.py index 35c84995f4..1244035df1 100644 --- a/kedro/extras/datasets/pillow/image_dataset.py +++ b/kedro/extras/datasets/pillow/image_dataset.py @@ -9,7 +9,7 @@ from PIL import Image from kedro.io.core import ( - AbstractVersionedDataSet, + AbstractVersionedDataset, DatasetError, Version, get_filepath_str, @@ -21,7 +21,7 @@ # in kedro-plugins (https://github.com/kedro-org/kedro-plugins) -class ImageDataSet(AbstractVersionedDataSet[Image.Image, Image.Image]): +class ImageDataSet(AbstractVersionedDataset[Image.Image, Image.Image]): """``ImageDataSet`` loads/saves image data as `numpy` from an underlying filesystem (e.g.: local, S3, GCS). It uses Pillow to handle image file. diff --git a/kedro/extras/datasets/plotly/__init__.py b/kedro/extras/datasets/plotly/__init__.py index f864ea6dbe..c2851bb000 100644 --- a/kedro/extras/datasets/plotly/__init__.py +++ b/kedro/extras/datasets/plotly/__init__.py @@ -1,4 +1,4 @@ -"""``AbstractDataSet`` implementations to load/save a plotly figure from/to a JSON +"""``AbstractDataset`` implementations to load/save a plotly figure from/to a JSON file.""" __all__ = ["PlotlyDataSet", "JSONDataSet"] diff --git a/kedro/extras/datasets/plotly/json_dataset.py b/kedro/extras/datasets/plotly/json_dataset.py index a03ee5b812..5fa555d665 100644 --- a/kedro/extras/datasets/plotly/json_dataset.py +++ b/kedro/extras/datasets/plotly/json_dataset.py @@ -10,7 +10,7 @@ from plotly import graph_objects as go from kedro.io.core import ( - AbstractVersionedDataSet, + AbstractVersionedDataset, Version, get_filepath_str, get_protocol_and_path, @@ -22,7 +22,7 @@ class JSONDataSet( - AbstractVersionedDataSet[go.Figure, Union[go.Figure, go.FigureWidget]] + AbstractVersionedDataset[go.Figure, Union[go.Figure, go.FigureWidget]] ): """``JSONDataSet`` loads/saves a plotly figure from/to a JSON file using an underlying filesystem (e.g.: local, S3, GCS). diff --git a/kedro/extras/datasets/redis/__init__.py b/kedro/extras/datasets/redis/__init__.py index ba56e1fb85..f3c553ec3b 100644 --- a/kedro/extras/datasets/redis/__init__.py +++ b/kedro/extras/datasets/redis/__init__.py @@ -1,4 +1,4 @@ -"""``AbstractDataSet`` implementation to load/save data from/to a redis db.""" +"""``AbstractDataset`` implementation to load/save data from/to a redis db.""" __all__ = ["PickleDataSet"] diff --git a/kedro/extras/datasets/redis/redis_dataset.py b/kedro/extras/datasets/redis/redis_dataset.py index c2bb2ca660..bac3a15b65 100644 --- a/kedro/extras/datasets/redis/redis_dataset.py +++ b/kedro/extras/datasets/redis/redis_dataset.py @@ -9,14 +9,14 @@ import redis -from kedro.io.core import AbstractDataSet, DatasetError +from kedro.io.core import AbstractDataset, DatasetError # NOTE: kedro.extras.datasets will be removed in Kedro 0.19.0. # Any contribution to datasets should be made in kedro-datasets # in kedro-plugins (https://github.com/kedro-org/kedro-plugins) -class PickleDataSet(AbstractDataSet[Any, Any]): +class PickleDataSet(AbstractDataset[Any, Any]): """``PickleDataSet`` loads/saves data from/to a Redis database. The underlying functionality is supported by the redis library, so it supports all allowed options for instantiating the redis app ``from_url`` and setting diff --git a/kedro/extras/datasets/spark/deltatable_dataset.py b/kedro/extras/datasets/spark/deltatable_dataset.py index fc6c1d5d97..0f6655ac8c 100644 --- a/kedro/extras/datasets/spark/deltatable_dataset.py +++ b/kedro/extras/datasets/spark/deltatable_dataset.py @@ -1,4 +1,4 @@ -"""``AbstractDataSet`` implementation to access DeltaTables using +"""``AbstractDataset`` implementation to access DeltaTables using ``delta-spark`` """ from pathlib import PurePosixPath @@ -12,14 +12,14 @@ _split_filepath, _strip_dbfs_prefix, ) -from kedro.io.core import AbstractDataSet, DatasetError +from kedro.io.core import AbstractDataset, DatasetError # NOTE: kedro.extras.datasets will be removed in Kedro 0.19.0. # Any contribution to datasets should be made in kedro-datasets # in kedro-plugins (https://github.com/kedro-org/kedro-plugins) -class DeltaTableDataSet(AbstractDataSet[None, DeltaTable]): +class DeltaTableDataSet(AbstractDataset[None, DeltaTable]): """``DeltaTableDataSet`` loads data into DeltaTable objects. Example usage for the diff --git a/kedro/extras/datasets/spark/spark_dataset.py b/kedro/extras/datasets/spark/spark_dataset.py index 0d60d943ac..317e173d24 100644 --- a/kedro/extras/datasets/spark/spark_dataset.py +++ b/kedro/extras/datasets/spark/spark_dataset.py @@ -1,4 +1,4 @@ -"""``AbstractVersionedDataSet`` implementation to access Spark dataframes using +"""``AbstractVersionedDataset`` implementation to access Spark dataframes using ``pyspark`` """ import json @@ -17,7 +17,7 @@ from s3fs import S3FileSystem from kedro.io.core import ( - AbstractVersionedDataSet, + AbstractVersionedDataset, DatasetError, Version, get_filepath_str, @@ -162,7 +162,7 @@ def hdfs_glob(self, pattern: str) -> List[str]: return sorted(matched) -class SparkDataSet(AbstractVersionedDataSet[DataFrame, DataFrame]): +class SparkDataSet(AbstractVersionedDataset[DataFrame, DataFrame]): """``SparkDataSet`` loads and saves Spark dataframes. Example usage for the diff --git a/kedro/extras/datasets/spark/spark_hive_dataset.py b/kedro/extras/datasets/spark/spark_hive_dataset.py index 81f09b9daa..2abbd1f166 100644 --- a/kedro/extras/datasets/spark/spark_hive_dataset.py +++ b/kedro/extras/datasets/spark/spark_hive_dataset.py @@ -1,4 +1,4 @@ -"""``AbstractDataSet`` implementation to access Spark dataframes using +"""``AbstractDataset`` implementation to access Spark dataframes using ``pyspark`` on Apache Hive. """ import pickle @@ -8,15 +8,15 @@ from pyspark.sql import DataFrame, SparkSession, Window from pyspark.sql.functions import col, lit, row_number -from kedro.io.core import AbstractDataSet, DatasetError +from kedro.io.core import AbstractDataset, DatasetError # NOTE: kedro.extras.datasets will be removed in Kedro 0.19.0. # Any contribution to datasets should be made in kedro-datasets # in kedro-plugins (https://github.com/kedro-org/kedro-plugins) -# noqa: too-many-instance-attributes -class SparkHiveDataSet(AbstractDataSet[DataFrame, DataFrame]): +# pylint:disable=too-many-instance-attributes +class SparkHiveDataSet(AbstractDataset[DataFrame, DataFrame]): """``SparkHiveDataSet`` loads and saves Spark dataframes stored on Hive. This data set also handles some incompatible file types such as using partitioned parquet on hive which will not normally allow upserts to existing data without a complete replacement diff --git a/kedro/extras/datasets/spark/spark_jdbc_dataset.py b/kedro/extras/datasets/spark/spark_jdbc_dataset.py index 15e01c4468..3abeeb312a 100644 --- a/kedro/extras/datasets/spark/spark_jdbc_dataset.py +++ b/kedro/extras/datasets/spark/spark_jdbc_dataset.py @@ -5,7 +5,7 @@ from pyspark.sql import DataFrame, SparkSession -from kedro.io.core import AbstractDataSet, DatasetError +from kedro.io.core import AbstractDataset, DatasetError __all__ = ["SparkJDBCDataSet"] @@ -14,7 +14,7 @@ # in kedro-plugins (https://github.com/kedro-org/kedro-plugins) -class SparkJDBCDataSet(AbstractDataSet[DataFrame, DataFrame]): +class SparkJDBCDataSet(AbstractDataset[DataFrame, DataFrame]): """``SparkJDBCDataSet`` loads data from a database table accessible via JDBC URL url and connection properties and saves the content of a PySpark DataFrame to an external database table via JDBC. It uses diff --git a/kedro/extras/datasets/svmlight/__init__.py b/kedro/extras/datasets/svmlight/__init__.py index 4ea2429612..4b77f3dfde 100644 --- a/kedro/extras/datasets/svmlight/__init__.py +++ b/kedro/extras/datasets/svmlight/__init__.py @@ -1,4 +1,4 @@ -"""``AbstractDataSet`` implementation to load/save data from/to a svmlight/ +"""``AbstractDataset`` implementation to load/save data from/to a svmlight/ libsvm sparse data file.""" __all__ = ["SVMLightDataSet"] diff --git a/kedro/extras/datasets/svmlight/svmlight_dataset.py b/kedro/extras/datasets/svmlight/svmlight_dataset.py index f8820b036f..697253ef2a 100644 --- a/kedro/extras/datasets/svmlight/svmlight_dataset.py +++ b/kedro/extras/datasets/svmlight/svmlight_dataset.py @@ -12,7 +12,7 @@ from sklearn.datasets import dump_svmlight_file, load_svmlight_file from kedro.io.core import ( - AbstractVersionedDataSet, + AbstractVersionedDataset, DatasetError, Version, get_filepath_str, @@ -29,7 +29,7 @@ _DO = Tuple[csr_matrix, ndarray] -class SVMLightDataSet(AbstractVersionedDataSet[_DI, _DO]): +class SVMLightDataSet(AbstractVersionedDataset[_DI, _DO]): """``SVMLightDataSet`` loads/saves data from/to a svmlight/libsvm file using an underlying filesystem (e.g.: local, S3, GCS). It uses sklearn functions ``dump_svmlight_file`` to save and ``load_svmlight_file`` to load a file. diff --git a/kedro/extras/datasets/tensorflow/tensorflow_model_dataset.py b/kedro/extras/datasets/tensorflow/tensorflow_model_dataset.py index e1b35e6620..c0e916d01f 100644 --- a/kedro/extras/datasets/tensorflow/tensorflow_model_dataset.py +++ b/kedro/extras/datasets/tensorflow/tensorflow_model_dataset.py @@ -10,7 +10,7 @@ import tensorflow as tf from kedro.io.core import ( - AbstractVersionedDataSet, + AbstractVersionedDataset, DatasetError, Version, get_filepath_str, @@ -24,7 +24,7 @@ # in kedro-plugins (https://github.com/kedro-org/kedro-plugins) -class TensorFlowModelDataset(AbstractVersionedDataSet[tf.keras.Model, tf.keras.Model]): +class TensorFlowModelDataset(AbstractVersionedDataset[tf.keras.Model, tf.keras.Model]): """``TensorflowModelDataset`` loads and saves TensorFlow models. The underlying functionality is supported by, and passes input arguments through to, TensorFlow 2.X load_model and save_model methods. diff --git a/kedro/extras/datasets/text/__init__.py b/kedro/extras/datasets/text/__init__.py index fab08acea4..9ed2c37c0e 100644 --- a/kedro/extras/datasets/text/__init__.py +++ b/kedro/extras/datasets/text/__init__.py @@ -1,4 +1,4 @@ -"""``AbstractDataSet`` implementation to load/save data from/to a text file.""" +"""``AbstractDataset`` implementation to load/save data from/to a text file.""" __all__ = ["TextDataSet"] diff --git a/kedro/extras/datasets/text/text_dataset.py b/kedro/extras/datasets/text/text_dataset.py index 2b02bfba3d..3c8a859445 100644 --- a/kedro/extras/datasets/text/text_dataset.py +++ b/kedro/extras/datasets/text/text_dataset.py @@ -8,7 +8,7 @@ import fsspec from kedro.io.core import ( - AbstractVersionedDataSet, + AbstractVersionedDataset, DatasetError, Version, get_filepath_str, @@ -20,7 +20,7 @@ # in kedro-plugins (https://github.com/kedro-org/kedro-plugins) -class TextDataSet(AbstractVersionedDataSet[str, str]): +class TextDataSet(AbstractVersionedDataset[str, str]): """``TextDataSet`` loads/saves data from/to a text file using an underlying filesystem (e.g.: local, S3, GCS) diff --git a/kedro/extras/datasets/video/video_dataset.py b/kedro/extras/datasets/video/video_dataset.py index 4aba723afa..4f5e793f29 100644 --- a/kedro/extras/datasets/video/video_dataset.py +++ b/kedro/extras/datasets/video/video_dataset.py @@ -14,7 +14,7 @@ import numpy as np import PIL.Image -from kedro.io.core import AbstractDataSet, get_protocol_and_path +from kedro.io.core import AbstractDataset, get_protocol_and_path class SlicedVideo: @@ -192,7 +192,7 @@ def __iter__(self): return self -class VideoDataSet(AbstractDataSet[AbstractVideo, AbstractVideo]): +class VideoDataSet(AbstractDataset[AbstractVideo, AbstractVideo]): """``VideoDataSet`` loads / save video data from a given filepath as sequence of PIL.Image.Image using OpenCV. diff --git a/kedro/extras/datasets/yaml/__init__.py b/kedro/extras/datasets/yaml/__init__.py index b3780de3a6..07abbaf4a5 100644 --- a/kedro/extras/datasets/yaml/__init__.py +++ b/kedro/extras/datasets/yaml/__init__.py @@ -1,4 +1,4 @@ -"""``AbstractDataSet`` implementation to load/save data from/to a YAML file.""" +"""``AbstractDataset`` implementation to load/save data from/to a YAML file.""" __all__ = ["YAMLDataSet"] diff --git a/kedro/extras/datasets/yaml/yaml_dataset.py b/kedro/extras/datasets/yaml/yaml_dataset.py index 91c6b474cf..7ea2760cdf 100644 --- a/kedro/extras/datasets/yaml/yaml_dataset.py +++ b/kedro/extras/datasets/yaml/yaml_dataset.py @@ -9,7 +9,7 @@ import yaml from kedro.io.core import ( - AbstractVersionedDataSet, + AbstractVersionedDataset, DatasetError, Version, get_filepath_str, @@ -21,7 +21,7 @@ # in kedro-plugins (https://github.com/kedro-org/kedro-plugins) -class YAMLDataSet(AbstractVersionedDataSet[Dict, Dict]): +class YAMLDataSet(AbstractVersionedDataset[Dict, Dict]): """``YAMLDataSet`` loads/saves data from/to a YAML file using an underlying filesystem (e.g.: local, S3, GCS). It uses PyYAML to handle the YAML file. diff --git a/kedro/io/__init__.py b/kedro/io/__init__.py index 0755af906c..26d4c3619c 100644 --- a/kedro/io/__init__.py +++ b/kedro/io/__init__.py @@ -1,12 +1,12 @@ """``kedro.io`` provides functionality to read and write to a -number of data sets. At the core of the library is the ``AbstractDataSet`` class. +number of data sets. At the core of the library is the ``AbstractDataset`` class. """ from __future__ import annotations from .cached_dataset import CachedDataSet, CachedDataset from .core import ( - AbstractDataSet, - AbstractVersionedDataSet, + AbstractDataset, + AbstractVersionedDataset, DatasetAlreadyExistsError, DatasetError, DatasetNotFoundError, @@ -26,19 +26,23 @@ DataSetError: type[DatasetError] DataSetNotFoundError: type[DatasetNotFoundError] DataSetAlreadyExistsError: type[DatasetAlreadyExistsError] +AbstractDataSet: type[AbstractDataset] +AbstractVersionedDataSet: type[AbstractVersionedDataset] def __getattr__(name): import kedro.io.core # noqa: import-outside-toplevel - if name in (kedro.io.core._DEPRECATED_ERROR_CLASSES): # noqa: protected-access + if name in (kedro.io.core._DEPRECATED_CLASSES): # noqa: protected-access return getattr(kedro.io.core, name) raise AttributeError(f"module {repr(__name__)} has no attribute {repr(name)}") __all__ = [ "AbstractDataSet", + "AbstractDataset", "AbstractVersionedDataSet", + "AbstractVersionedDataset", "CachedDataSet", "CachedDataset", "DataCatalog", diff --git a/kedro/io/cached_dataset.py b/kedro/io/cached_dataset.py index d3aee1a39e..6ec2a59fb7 100644 --- a/kedro/io/cached_dataset.py +++ b/kedro/io/cached_dataset.py @@ -8,14 +8,14 @@ import warnings from typing import Any -from kedro.io.core import VERSIONED_FLAG_KEY, AbstractDataSet, Version +from kedro.io.core import VERSIONED_FLAG_KEY, AbstractDataset, Version from kedro.io.memory_dataset import MemoryDataset # https://github.com/pylint-dev/pylint/issues/4300#issuecomment-1043601901 CachedDataSet: type[CachedDataset] -class CachedDataset(AbstractDataSet): +class CachedDataset(AbstractDataset): """``CachedDataset`` is a dataset wrapper which caches in memory the data saved, so that the user avoids io operations with slow storage media. @@ -40,7 +40,7 @@ class as shown above. def __init__( self, - dataset: AbstractDataSet | dict, + dataset: AbstractDataset | dict, version: Version = None, copy_mode: str = None, metadata: dict[str, Any] = None, @@ -66,7 +66,7 @@ def __init__( """ if isinstance(dataset, dict): self._dataset = self._from_config(dataset, version) - elif isinstance(dataset, AbstractDataSet): + elif isinstance(dataset, AbstractDataset): self._dataset = dataset else: raise ValueError( @@ -89,10 +89,10 @@ def _from_config(config, version): ) if version: config[VERSIONED_FLAG_KEY] = True - return AbstractDataSet.from_config( + return AbstractDataset.from_config( "_cached", config, version.load, version.save ) - return AbstractDataSet.from_config("_cached", config) + return AbstractDataset.from_config("_cached", config) def _describe(self) -> dict[str, Any]: return { diff --git a/kedro/io/core.py b/kedro/io/core.py index f608f10840..6a097d7058 100644 --- a/kedro/io/core.py +++ b/kedro/io/core.py @@ -33,13 +33,15 @@ DataSetError: type[DatasetError] DataSetNotFoundError: type[DatasetNotFoundError] DataSetAlreadyExistsError: type[DatasetAlreadyExistsError] +AbstractDataSet: type[AbstractDataset] +AbstractVersionedDataSet: type[AbstractVersionedDataset] class DatasetError(Exception): - """``DatasetError`` raised by ``AbstractDataSet`` implementations + """``DatasetError`` raised by ``AbstractDataset`` implementations in case of failure of input/output methods. - ``AbstractDataSet`` implementations should provide instructive + ``AbstractDataset`` implementations should provide instructive information in case of failure. """ @@ -62,28 +64,8 @@ class DatasetAlreadyExistsError(DatasetError): pass -_DEPRECATED_ERROR_CLASSES = { - "DataSetError": DatasetError, - "DataSetNotFoundError": DatasetNotFoundError, - "DataSetAlreadyExistsError": DatasetAlreadyExistsError, -} - - -def __getattr__(name): - if name in _DEPRECATED_ERROR_CLASSES: - alias = _DEPRECATED_ERROR_CLASSES[name] - warnings.warn( - f"{repr(name)} has been renamed to {repr(alias.__name__)}, " - f"and the alias will be removed in Kedro 0.19.0", - DeprecationWarning, - stacklevel=2, - ) - return alias - raise AttributeError(f"module {repr(__name__)} has no attribute {repr(name)}") - - class VersionNotFoundError(DatasetError): - """``VersionNotFoundError`` raised by ``AbstractVersionedDataSet`` implementations + """``VersionNotFoundError`` raised by ``AbstractVersionedDataset`` implementations in case of no load versions available for the data set. """ @@ -94,8 +76,8 @@ class VersionNotFoundError(DatasetError): _DO = TypeVar("_DO") -class AbstractDataSet(abc.ABC, Generic[_DI, _DO]): - """``AbstractDataSet`` is the base class for all data set implementations. +class AbstractDataset(abc.ABC, Generic[_DI, _DO]): + """``AbstractDataset`` is the base class for all data set implementations. All data set implementations should extend this abstract class and implement the methods marked as abstract. If a specific dataset implementation cannot be used in conjunction with @@ -106,10 +88,10 @@ class AbstractDataSet(abc.ABC, Generic[_DI, _DO]): >>> from pathlib import Path, PurePosixPath >>> import pandas as pd - >>> from kedro.io import AbstractDataSet + >>> from kedro.io import AbstractDataset >>> >>> - >>> class MyOwnDataset(AbstractDataSet[pd.DataFrame, pd.DataFrame]): + >>> class MyOwnDataset(AbstractDataset[pd.DataFrame, pd.DataFrame]): >>> def __init__(self, filepath, param1, param2=True): >>> self._filepath = PurePosixPath(filepath) >>> self._param1 = param1 @@ -144,7 +126,7 @@ def from_config( config: dict[str, Any], load_version: str = None, save_version: str = None, - ) -> AbstractDataSet: + ) -> AbstractDataset: """Create a data set instance using the configuration provided. Args: @@ -158,7 +140,7 @@ def from_config( if versioning was not enabled. Returns: - An instance of an ``AbstractDataSet`` subclass. + An instance of an ``AbstractDataset`` subclass. Raises: DatasetError: When the function fails to create the data set @@ -274,21 +256,21 @@ def _to_str(obj, is_root=False): @abc.abstractmethod def _load(self) -> _DO: raise NotImplementedError( - f"'{self.__class__.__name__}' is a subclass of AbstractDataSet and " + f"'{self.__class__.__name__}' is a subclass of AbstractDataset and " f"it must implement the '_load' method" ) @abc.abstractmethod def _save(self, data: _DI) -> None: raise NotImplementedError( - f"'{self.__class__.__name__}' is a subclass of AbstractDataSet and " + f"'{self.__class__.__name__}' is a subclass of AbstractDataset and " f"it must implement the '_save' method" ) @abc.abstractmethod def _describe(self) -> dict[str, Any]: raise NotImplementedError( - f"'{self.__class__.__name__}' is a subclass of AbstractDataSet and " + f"'{self.__class__.__name__}' is a subclass of AbstractDataset and " f"it must implement the '_describe' method" ) @@ -336,7 +318,7 @@ def release(self) -> None: def _release(self) -> None: pass - def _copy(self, **overwrite_params) -> AbstractDataSet: + def _copy(self, **overwrite_params) -> AbstractDataset: dataset_copy = copy.deepcopy(self) for name, value in overwrite_params.items(): setattr(dataset_copy, name, value) @@ -379,7 +361,7 @@ class Version(namedtuple("Version", ["load", "save"])): def parse_dataset_definition( config: dict[str, Any], load_version: str = None, save_version: str = None -) -> tuple[type[AbstractDataSet], dict[str, Any]]: +) -> tuple[type[AbstractDataset], dict[str, Any]]: """Parse and instantiate a dataset class using the configuration provided. Args: @@ -422,10 +404,10 @@ def parse_dataset_definition( f"has not been installed." ) from exc - if not issubclass(class_obj, AbstractDataSet): + if not issubclass(class_obj, AbstractDataset): raise DatasetError( f"Dataset type '{class_obj.__module__}.{class_obj.__qualname__}' " - f"is invalid: all data set types must extend 'AbstractDataSet'." + f"is invalid: all data set types must extend 'AbstractDataset'." ) if VERSION_KEY in config: @@ -481,9 +463,9 @@ def _local_exists(filepath: str) -> bool: # SKIP_IF_NO_SPARK return filepath.exists() or any(par.is_file() for par in filepath.parents) -class AbstractVersionedDataSet(AbstractDataSet[_DI, _DO], abc.ABC): +class AbstractVersionedDataset(AbstractDataset[_DI, _DO], abc.ABC): """ - ``AbstractVersionedDataSet`` is the base class for all versioned data set + ``AbstractVersionedDataset`` is the base class for all versioned data set implementations. All data sets that implement versioning should extend this abstract class and implement the methods marked as abstract. @@ -492,10 +474,10 @@ class AbstractVersionedDataSet(AbstractDataSet[_DI, _DO], abc.ABC): >>> from pathlib import Path, PurePosixPath >>> import pandas as pd - >>> from kedro.io import AbstractVersionedDataSet + >>> from kedro.io import AbstractVersionedDataset >>> >>> - >>> class MyOwnDataset(AbstractVersionedDataSet): + >>> class MyOwnDataset(AbstractVersionedDataset): >>> def __init__(self, filepath, version, param1, param2=True): >>> super().__init__(PurePosixPath(filepath), version) >>> self._param1 = param1 @@ -534,7 +516,7 @@ def __init__( exists_function: Callable[[str], bool] = None, glob_function: Callable[[str], list[str]] = None, ): - """Creates a new instance of ``AbstractVersionedDataSet``. + """Creates a new instance of ``AbstractVersionedDataset``. Args: filepath: Filepath in POSIX format to a file. @@ -778,3 +760,25 @@ def validate_on_forbidden_chars(**kwargs): raise DatasetError( f"Neither white-space nor semicolon are allowed in '{key}'." ) + + +_DEPRECATED_CLASSES = { + "DataSetError": DatasetError, + "DataSetNotFoundError": DatasetNotFoundError, + "DataSetAlreadyExistsError": DatasetAlreadyExistsError, + "AbstractDataSet": AbstractDataset, + "AbstractVersionedDataSet": AbstractVersionedDataset, +} + + +def __getattr__(name): + if name in _DEPRECATED_CLASSES: + alias = _DEPRECATED_CLASSES[name] + warnings.warn( + f"{repr(name)} has been renamed to {repr(alias.__name__)}, " + f"and the alias will be removed in Kedro 0.19.0", + DeprecationWarning, + stacklevel=2, + ) + return alias + raise AttributeError(f"module {repr(__name__)} has no attribute {repr(name)}") diff --git a/kedro/io/data_catalog.py b/kedro/io/data_catalog.py index 98e8d8f289..156be2130f 100644 --- a/kedro/io/data_catalog.py +++ b/kedro/io/data_catalog.py @@ -1,4 +1,4 @@ -"""``DataCatalog`` stores instances of ``AbstractDataSet`` implementations to +"""``DataCatalog`` stores instances of ``AbstractDataset`` implementations to provide ``load`` and ``save`` capabilities from anywhere in the program. To use a ``DataCatalog``, you need to instantiate it with a dictionary of data sets. Then it will act as a single point of reference for your calls, @@ -16,8 +16,8 @@ from parse import parse from kedro.io.core import ( - AbstractDataSet, - AbstractVersionedDataSet, + AbstractDataset, + AbstractVersionedDataset, DatasetAlreadyExistsError, DatasetError, DatasetNotFoundError, @@ -103,7 +103,7 @@ class _FrozenDatasets: def __init__( self, - *datasets_collections: _FrozenDatasets | dict[str, AbstractDataSet], + *datasets_collections: _FrozenDatasets | dict[str, AbstractDataset], ): """Return a _FrozenDatasets instance from some datasets collections. Each collection could either be another _FrozenDatasets or a dictionary. @@ -132,7 +132,7 @@ def __setattr__(self, key, value): class DataCatalog: - """``DataCatalog`` stores instances of ``AbstractDataSet`` implementations + """``DataCatalog`` stores instances of ``AbstractDataset`` implementations to provide ``load`` and ``save`` capabilities from anywhere in the program. To use a ``DataCatalog``, you need to instantiate it with a dictionary of data sets. Then it will act as a single point of reference @@ -142,14 +142,14 @@ class DataCatalog: def __init__( # noqa: too-many-arguments self, - data_sets: dict[str, AbstractDataSet] = None, + data_sets: dict[str, AbstractDataset] = None, feed_dict: dict[str, Any] = None, layers: dict[str, set[str]] = None, dataset_patterns: Patterns = None, load_versions: dict[str, str] = None, save_version: str = None, ) -> None: - """``DataCatalog`` stores instances of ``AbstractDataSet`` + """``DataCatalog`` stores instances of ``AbstractDataset`` implementations to provide ``load`` and ``save`` capabilities from anywhere in the program. To use a ``DataCatalog``, you need to instantiate it with a dictionary of data sets. Then it will act as a @@ -214,13 +214,13 @@ def from_config( Args: catalog: A dictionary whose keys are the data set names and the values are dictionaries with the constructor arguments - for classes implementing ``AbstractDataSet``. The data set + for classes implementing ``AbstractDataset``. The data set class to be loaded is specified with the key ``type`` and their fully qualified class name. All ``kedro.io`` data set can be specified by their class name only, i.e. their module name can be omitted. credentials: A dictionary containing credentials for different - data sets. Use the ``credentials`` key in a ``AbstractDataSet`` + data sets. Use the ``credentials`` key in a ``AbstractDataset`` to refer to the appropriate credentials as shown in the example below. load_versions: A mapping between dataset names and versions @@ -296,7 +296,7 @@ class to be loaded is specified with the key ``type`` and their ds_layer = ds_config.pop("layer", None) if ds_layer is not None: layers[ds_layer].add(ds_name) - data_sets[ds_name] = AbstractDataSet.from_config( + data_sets[ds_name] = AbstractDataset.from_config( ds_name, ds_config, load_versions.get(ds_name), save_version ) dataset_layers = layers or None @@ -327,7 +327,7 @@ def _is_pattern(pattern: str): @staticmethod def _match_pattern(data_set_patterns: Patterns, data_set_name: str) -> str | None: - """Match a dataset name against patterns in a dictionary containing patterns""" + """Match a dataset name against patterns in a dictionary.""" matches = ( pattern for pattern in data_set_patterns.keys() @@ -337,7 +337,10 @@ def _match_pattern(data_set_patterns: Patterns, data_set_name: str) -> str | Non @classmethod def _sort_patterns(cls, data_set_patterns: Patterns) -> dict[str, dict[str, Any]]: - """Sort a dictionary of dataset patterns according to parsing rules - + """Sort a dictionary of dataset patterns according to parsing rules. + + In order: + 1. Decreasing specificity (number of characters outside the curly brackets) 2. Decreasing number of placeholders (number of curly bracket pairs) 3. Alphabetically @@ -354,11 +357,14 @@ def _sort_patterns(cls, data_set_patterns: Patterns) -> dict[str, dict[str, Any] @staticmethod def _specificity(pattern: str) -> int: - """Helper function to check the length of exactly matched characters not inside brackets - Example - - specificity("{namespace}.companies") = 10 - specificity("{namespace}.{dataset}") = 1 - specificity("france.companies") = 16 + """Helper function to check the length of exactly matched characters not inside brackets. + + Example: + :: + + >>> specificity("{namespace}.companies") = 10 + >>> specificity("{namespace}.{dataset}") = 1 + >>> specificity("france.companies") = 16 """ # Remove all the placeholders from the pattern and count the number of remaining chars result = re.sub(r"\{.*?\}", "", pattern) @@ -366,7 +372,7 @@ def _specificity(pattern: str) -> int: def _get_dataset( self, data_set_name: str, version: Version = None, suggest: bool = True - ) -> AbstractDataSet: + ) -> AbstractDataset: matched_pattern = self._match_pattern(self._dataset_patterns, data_set_name) if data_set_name not in self._data_sets and matched_pattern: # If the dataset is a patterned dataset, materialise it and add it to @@ -376,7 +382,7 @@ def _get_dataset( if ds_layer: self.layers = self.layers or {} self.layers.setdefault(ds_layer, set()).add(data_set_name) - data_set = AbstractDataSet.from_config( + data_set = AbstractDataset.from_config( data_set_name, data_set_config, self._load_versions.get(data_set_name), @@ -405,7 +411,7 @@ def _get_dataset( error_msg += f" - did you mean one of these instead: {suggestions}" raise DatasetNotFoundError(error_msg) data_set = self._data_sets[data_set_name] - if version and isinstance(data_set, AbstractVersionedDataSet): + if version and isinstance(data_set, AbstractVersionedDataset): # we only want to return a similar-looking dataset, # not modify the one stored in the current catalog data_set = data_set._copy(_version=version) # noqa: protected-access @@ -424,7 +430,7 @@ def _resolve_config( data_set_name: str, matched_pattern: str, ) -> dict[str, Any]: - """Get resolved AbstractDataSet from a factory config""" + """Get resolved AbstractDataset from a factory config""" result = parse(matched_pattern, data_set_name) config_copy = copy.deepcopy(self._dataset_patterns[matched_pattern]) # Resolve the factory config for the dataset @@ -547,9 +553,9 @@ def release(self, name: str): dataset.release() def add( - self, data_set_name: str, data_set: AbstractDataSet, replace: bool = False + self, data_set_name: str, data_set: AbstractDataset, replace: bool = False ) -> None: - """Adds a new ``AbstractDataSet`` object to the ``DataCatalog``. + """Adds a new ``AbstractDataset`` object to the ``DataCatalog``. Args: data_set_name: A unique data set name which has not been @@ -585,7 +591,7 @@ def add( self.datasets = _FrozenDatasets(self.datasets, {data_set_name: data_set}) def add_all( - self, data_sets: dict[str, AbstractDataSet], replace: bool = False + self, data_sets: dict[str, AbstractDataset], replace: bool = False ) -> None: """Adds a group of new data sets to the ``DataCatalog``. @@ -645,7 +651,7 @@ def add_feed_dict(self, feed_dict: dict[str, Any], replace: bool = False) -> Non >>> assert io.load("data").equals(df) """ for data_set_name in feed_dict: - if isinstance(feed_dict[data_set_name], AbstractDataSet): + if isinstance(feed_dict[data_set_name], AbstractDataset): data_set = feed_dict[data_set_name] else: data_set = MemoryDataset(data=feed_dict[data_set_name]) diff --git a/kedro/io/lambda_dataset.py b/kedro/io/lambda_dataset.py index b2cca48921..68d7161b11 100644 --- a/kedro/io/lambda_dataset.py +++ b/kedro/io/lambda_dataset.py @@ -1,19 +1,19 @@ -"""``LambdaDataset`` is an implementation of ``AbstractDataSet`` which allows for +"""``LambdaDataset`` is an implementation of ``AbstractDataset`` which allows for providing custom load, save, and exists methods without extending -``AbstractDataSet``. +``AbstractDataset``. """ from __future__ import annotations import warnings from typing import Any, Callable -from kedro.io.core import AbstractDataSet, DatasetError +from kedro.io.core import AbstractDataset, DatasetError # https://github.com/pylint-dev/pylint/issues/4300#issuecomment-1043601901 LambdaDataSet: type[LambdaDataset] -class LambdaDataset(AbstractDataSet): +class LambdaDataset(AbstractDataset): """``LambdaDataset`` loads and saves data to a data set. It relies on delegating to specific implementation such as csv, sql, etc. diff --git a/kedro/io/memory_dataset.py b/kedro/io/memory_dataset.py index 1dc5ded1b0..7cab3f4d3d 100644 --- a/kedro/io/memory_dataset.py +++ b/kedro/io/memory_dataset.py @@ -6,7 +6,7 @@ import warnings from typing import Any -from kedro.io.core import AbstractDataSet, DatasetError +from kedro.io.core import AbstractDataset, DatasetError _EMPTY = object() @@ -14,7 +14,7 @@ MemoryDataSet: type[MemoryDataset] -class MemoryDataset(AbstractDataSet): +class MemoryDataset(AbstractDataset): """``MemoryDataset`` loads and saves data from/to an in-memory Python object. diff --git a/kedro/io/partitioned_dataset.py b/kedro/io/partitioned_dataset.py index 1501fc4a04..66df5294a8 100644 --- a/kedro/io/partitioned_dataset.py +++ b/kedro/io/partitioned_dataset.py @@ -14,7 +14,7 @@ from kedro.io.core import ( VERSION_KEY, VERSIONED_FLAG_KEY, - AbstractDataSet, + AbstractDataset, DatasetError, parse_dataset_definition, ) @@ -36,7 +36,7 @@ IncrementalDataSet: type[IncrementalDataset] -class PartitionedDataset(AbstractDataSet): +class PartitionedDataset(AbstractDataset): # noqa: too-many-instance-attributes,protected-access """``PartitionedDataset`` loads and saves partitioned file-like data using the underlying dataset definition. For filesystem level operations it uses `fsspec`: @@ -138,7 +138,7 @@ class PartitionedDataset(AbstractDataSet): def __init__( # noqa: too-many-arguments self, path: str, - dataset: str | type[AbstractDataSet] | dict[str, Any], + dataset: str | type[AbstractDataset] | dict[str, Any], filepath_arg: str = "filepath", filename_suffix: str = "", credentials: dict[str, Any] = None, @@ -161,7 +161,7 @@ def __init__( # noqa: too-many-arguments dataset: Underlying dataset definition. This is used to instantiate the dataset for each file located inside the ``path``. Accepted formats are: - a) object of a class that inherits from ``AbstractDataSet`` + a) object of a class that inherits from ``AbstractDataset`` b) a string representing a fully qualified class name to such class c) a dictionary with ``type`` key pointing to a string from b), other keys are passed to the Dataset initializer. @@ -384,7 +384,7 @@ class IncrementalDataset(PartitionedDataset): def __init__( # noqa: too-many-arguments self, path: str, - dataset: str | type[AbstractDataSet] | dict[str, Any], + dataset: str | type[AbstractDataset] | dict[str, Any], checkpoint: str | dict[str, Any] | None = None, filepath_arg: str = "filepath", filename_suffix: str = "", @@ -408,7 +408,7 @@ def __init__( # noqa: too-many-arguments dataset: Underlying dataset definition. This is used to instantiate the dataset for each file located inside the ``path``. Accepted formats are: - a) object of a class that inherits from ``AbstractDataSet`` + a) object of a class that inherits from ``AbstractDataset`` b) a string representing a fully qualified class name to such class c) a dictionary with ``type`` key pointing to a string from b), other keys are passed to the Dataset initializer. @@ -521,7 +521,7 @@ def _is_valid_partition(partition) -> bool: ) @property - def _checkpoint(self) -> AbstractDataSet: + def _checkpoint(self) -> AbstractDataset: type_, kwargs = parse_dataset_definition(self._checkpoint_config) return type_(**kwargs) # type: ignore @@ -555,15 +555,15 @@ def confirm(self) -> None: self._checkpoint.save(partition_ids[-1]) # checkpoint to last partition -_DEPRECATED_ERROR_CLASSES = { +_DEPRECATED_CLASSES = { "PartitionedDataSet": PartitionedDataset, "IncrementalDataSet": IncrementalDataset, } def __getattr__(name): - if name in _DEPRECATED_ERROR_CLASSES: - alias = _DEPRECATED_ERROR_CLASSES[name] + if name in _DEPRECATED_CLASSES: + alias = _DEPRECATED_CLASSES[name] warnings.warn( f"{repr(name)} has been renamed to {repr(alias.__name__)}, " f"and the alias will be removed in Kedro 0.19.0", diff --git a/kedro/runner/parallel_runner.py b/kedro/runner/parallel_runner.py index b9a45792da..860cefed6a 100644 --- a/kedro/runner/parallel_runner.py +++ b/kedro/runner/parallel_runner.py @@ -38,7 +38,7 @@ class _SharedMemoryDataset: """``_SharedMemoryDataset`` is a wrapper class for a shared MemoryDataset in SyncManager. - It is not inherited from AbstractDataSet class. + It is not inherited from AbstractDataset class. """ def __init__(self, manager: SyncManager): diff --git a/kedro/runner/runner.py b/kedro/runner/runner.py index be379ace71..084843124e 100644 --- a/kedro/runner/runner.py +++ b/kedro/runner/runner.py @@ -21,7 +21,7 @@ from pluggy import PluginManager from kedro.framework.hooks.manager import _NullPluginManager -from kedro.io import AbstractDataSet, DataCatalog, MemoryDataset +from kedro.io import AbstractDataset, DataCatalog, MemoryDataset from kedro.pipeline import Pipeline from kedro.pipeline.node import Node @@ -164,14 +164,14 @@ def _run( pass @abstractmethod # pragma: no cover - def create_default_data_set(self, ds_name: str) -> AbstractDataSet: + def create_default_data_set(self, ds_name: str) -> AbstractDataset: """Factory method for creating the default dataset for the runner. Args: ds_name: Name of the missing dataset. Returns: - An instance of an implementation of ``AbstractDataSet`` to be + An instance of an implementation of ``AbstractDataset`` to be used for all unregistered datasets. """ pass diff --git a/kedro/runner/sequential_runner.py b/kedro/runner/sequential_runner.py index 59f53e7b7a..e944f8af09 100644 --- a/kedro/runner/sequential_runner.py +++ b/kedro/runner/sequential_runner.py @@ -8,7 +8,7 @@ from pluggy import PluginManager -from kedro.io import AbstractDataSet, DataCatalog, MemoryDataset +from kedro.io import AbstractDataset, DataCatalog, MemoryDataset from kedro.pipeline import Pipeline from kedro.runner.runner import AbstractRunner, run_node @@ -29,14 +29,14 @@ def __init__(self, is_async: bool = False): """ super().__init__(is_async=is_async) - def create_default_data_set(self, ds_name: str) -> AbstractDataSet: + def create_default_data_set(self, ds_name: str) -> AbstractDataset: """Factory method for creating the default data set for the runner. Args: ds_name: Name of the missing data set Returns: - An instance of an implementation of AbstractDataSet to be used + An instance of an implementation of AbstractDataset to be used for all unregistered data sets. """ diff --git a/tests/io/test_core.py b/tests/io/test_core.py index 05a3204639..7274a0cd32 100644 --- a/tests/io/test_core.py +++ b/tests/io/test_core.py @@ -9,8 +9,8 @@ import pytest from kedro.io.core import ( - _DEPRECATED_ERROR_CLASSES, - AbstractDataSet, + _DEPRECATED_CLASSES, + AbstractDataset, _parse_filepath, get_filepath_str, ) @@ -34,13 +34,13 @@ @pytest.mark.parametrize("module_name", ["kedro.io", "kedro.io.core"]) -@pytest.mark.parametrize("class_name", _DEPRECATED_ERROR_CLASSES) +@pytest.mark.parametrize("class_name", _DEPRECATED_CLASSES) def test_deprecation(module_name, class_name): with pytest.warns(DeprecationWarning, match=f"{repr(class_name)} has been renamed"): getattr(importlib.import_module(module_name), class_name) -class MyDataSet(AbstractDataSet): +class MyDataSet(AbstractDataset): def __init__(self, var=None): self.var = var diff --git a/tests/io/test_data_catalog.py b/tests/io/test_data_catalog.py index 9c61a9d3ec..9273fa5200 100644 --- a/tests/io/test_data_catalog.py +++ b/tests/io/test_data_catalog.py @@ -11,7 +11,7 @@ from kedro.extras.datasets.pandas import CSVDataSet, ParquetDataSet from kedro.io import ( - AbstractDataSet, + AbstractDataset, DataCatalog, DatasetAlreadyExistsError, DatasetError, @@ -175,7 +175,7 @@ def conflicting_feed_dict(): return {"ds1": ds1, "ds3": 1} -class BadDataset(AbstractDataSet): # pragma: no cover +class BadDataset(AbstractDataset): # pragma: no cover def __init__(self, filepath): self.filepath = filepath raise Exception("Naughty!") # pylint: disable=broad-exception-raised @@ -477,7 +477,7 @@ def test_config_invalid_data_set(self, sane_config): pattern = ( "An exception occurred when parsing config for dataset 'boats':\n" "Dataset type 'kedro.io.data_catalog.DataCatalog' is invalid: " - "all data set types must extend 'AbstractDataSet'" + "all data set types must extend 'AbstractDataset'" ) with pytest.raises(DatasetError, match=re.escape(pattern)): DataCatalog.from_config(**sane_config) diff --git a/tests/io/test_incremental_dataset.py b/tests/io/test_incremental_dataset.py index b1dd974f28..76218b6324 100644 --- a/tests/io/test_incremental_dataset.py +++ b/tests/io/test_incremental_dataset.py @@ -13,7 +13,7 @@ from kedro.extras.datasets.pickle import PickleDataSet from kedro.extras.datasets.text import TextDataSet -from kedro.io import AbstractDataSet, DatasetError, IncrementalDataset +from kedro.io import AbstractDataset, DatasetError, IncrementalDataset from kedro.io.data_catalog import CREDENTIALS_KEY DATASET = "kedro.extras.datasets.pandas.CSVDataSet" @@ -41,7 +41,7 @@ def local_csvs(tmp_path, partitioned_data_pandas): return local_dir -class DummyDataset(AbstractDataSet): # pragma: no cover +class DummyDataset(AbstractDataset): # pragma: no cover def __init__(self, filepath): pass diff --git a/tests/io/test_partitioned_dataset.py b/tests/io/test_partitioned_dataset.py index 97735a7380..453ff1781e 100644 --- a/tests/io/test_partitioned_dataset.py +++ b/tests/io/test_partitioned_dataset.py @@ -266,7 +266,7 @@ def test_invalid_dataset(self, dataset, local_csvs): ( FakeDataset, r"Dataset type 'tests\.io\.test_partitioned_dataset\.FakeDataset' " - r"is invalid\: all data set types must extend 'AbstractDataSet'", + r"is invalid\: all data set types must extend 'AbstractDataset'", ), ({}, "'type' is missing from dataset catalog configuration"), ], diff --git a/tests/runner/test_parallel_runner.py b/tests/runner/test_parallel_runner.py index a74cff8d53..8c301b4216 100644 --- a/tests/runner/test_parallel_runner.py +++ b/tests/runner/test_parallel_runner.py @@ -9,7 +9,7 @@ from kedro.framework.hooks import _create_hook_manager from kedro.io import ( - AbstractDataSet, + AbstractDataset, DataCatalog, DatasetError, LambdaDataset, @@ -228,7 +228,7 @@ def test_unable_to_schedule_all_nodes( runner.run(fan_out_fan_in, catalog) -class LoggingDataset(AbstractDataSet): +class LoggingDataset(AbstractDataset): def __init__(self, log, name, value=None): self.log = log self.name = name diff --git a/tests/runner/test_sequential_runner.py b/tests/runner/test_sequential_runner.py index cf91b76c49..36d2a7f6ac 100644 --- a/tests/runner/test_sequential_runner.py +++ b/tests/runner/test_sequential_runner.py @@ -7,7 +7,7 @@ import pytest from kedro.framework.hooks import _create_hook_manager -from kedro.io import AbstractDataSet, DataCatalog, DatasetError, LambdaDataset +from kedro.io import AbstractDataset, DataCatalog, DatasetError, LambdaDataset from kedro.pipeline import node from kedro.pipeline.modular_pipeline import pipeline as modular_pipeline from kedro.runner import SequentialRunner @@ -125,7 +125,7 @@ def test_unsatisfied_inputs(self, is_async, unfinished_outputs_pipeline, catalog ) -class LoggingDataset(AbstractDataSet): +class LoggingDataset(AbstractDataset): def __init__(self, log, name, value=None): self.log = log self.name = name diff --git a/tests/runner/test_thread_runner.py b/tests/runner/test_thread_runner.py index a9348548a7..a95b9294c8 100644 --- a/tests/runner/test_thread_runner.py +++ b/tests/runner/test_thread_runner.py @@ -6,7 +6,7 @@ import pytest from kedro.framework.hooks import _create_hook_manager -from kedro.io import AbstractDataSet, DataCatalog, DatasetError, MemoryDataset +from kedro.io import AbstractDataset, DataCatalog, DatasetError, MemoryDataset from kedro.pipeline import node from kedro.pipeline.modular_pipeline import pipeline as modular_pipeline from kedro.runner import ThreadRunner @@ -111,7 +111,7 @@ def test_node_returning_none(self): ThreadRunner().run(pipeline, catalog) -class LoggingDataset(AbstractDataSet): +class LoggingDataset(AbstractDataset): def __init__(self, log, name, value=None): self.log = log self.name = name From 9bc7a462d660d226f1778923f0ca96bea353f401 Mon Sep 17 00:00:00 2001 From: Jo Stichbury Date: Fri, 18 Aug 2023 13:12:01 +0100 Subject: [PATCH 26/70] Reorganise and improve the data catalog documentation (#2888) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * First drop of newly organised data catalog docs Signed-off-by: Jo Stichbury * linter Signed-off-by: Jo Stichbury * Added to-do notes Signed-off-by: Jo Stichbury * Afternoon's work in rewriting/reorganising content Signed-off-by: Jo Stichbury * More changes Signed-off-by: Jo Stichbury * Further changes Signed-off-by: Jo Stichbury * Another chunk of changes Signed-off-by: Jo Stichbury * Final changes Signed-off-by: Jo Stichbury * Revise ordering of pages Signed-off-by: Jo Stichbury * Add new CLI commands to dataset factory docs (#2935) * Add changes from #2930 Signed-off-by: Ahdra Merali * Lint Signed-off-by: Ahdra Merali * Apply suggestions from code review Co-authored-by: Jo Stichbury * Make code snippets collapsable Signed-off-by: Ahdra Merali --------- Signed-off-by: Ahdra Merali Co-authored-by: Jo Stichbury * Bunch of changes from feedback Signed-off-by: Jo Stichbury * A few more tweaks Signed-off-by: Jo Stichbury * Update h1,h2,h3 font sizes Signed-off-by: Tynan DeBold * Add code snippet for using DataCatalog with Kedro config Signed-off-by: Ankita Katiyar * Few more tweaks Signed-off-by: Jo Stichbury * Update docs/source/data/data_catalog.md * Upgrade kedro-datasets for docs Signed-off-by: Juan Luis Cano Rodríguez * Improve prose Signed-off-by: Juan Luis Cano Rodríguez Co-authored-by: Jo Stichbury --------- Signed-off-by: Jo Stichbury Signed-off-by: Ahdra Merali Signed-off-by: Tynan DeBold Signed-off-by: Ankita Katiyar Signed-off-by: Juan Luis Cano Rodríguez Co-authored-by: Ahdra Merali <90615669+AhdraMeraliQB@users.noreply.github.com> Co-authored-by: Tynan DeBold Co-authored-by: Ankita Katiyar Co-authored-by: Juan Luis Cano Rodríguez Signed-off-by: Jason Hite --- RELEASE.md | 2 + docs/source/_static/css/qb1-sphinx-rtd.css | 6 +- docs/source/configuration/credentials.md | 2 +- .../data/advanced_data_catalog_usage.md | 225 +++++ docs/source/data/data_catalog.md | 819 ++---------------- .../source/data/data_catalog_yaml_examples.md | 408 +++++++++ .../how_to_create_a_custom_dataset.md} | 21 +- docs/source/data/index.md | 45 +- docs/source/data/kedro_dataset_factories.md | 385 ++++++++ ...> partitioned_and_incremental_datasets.md} | 274 +----- docs/source/deployment/argo.md | 2 +- docs/source/deployment/aws_batch.md | 2 +- .../databricks_deployment_workflow.md | 2 +- .../databricks_ide_development_workflow.md | 2 +- docs/source/development/commands_reference.md | 2 +- docs/source/experiment_tracking/index.md | 2 +- docs/source/extend_kedro/common_use_cases.md | 2 +- docs/source/extend_kedro/index.md | 1 - docs/source/faq/faq.md | 3 - docs/source/nodes_and_pipelines/nodes.md | 2 +- .../kedro_and_notebooks.md | 2 +- docs/source/tutorial/add_another_pipeline.md | 2 +- docs/source/tutorial/set_up_data.md | 2 +- setup.py | 2 +- 24 files changed, 1187 insertions(+), 1028 deletions(-) create mode 100644 docs/source/data/advanced_data_catalog_usage.md create mode 100644 docs/source/data/data_catalog_yaml_examples.md rename docs/source/{extend_kedro/custom_datasets.md => data/how_to_create_a_custom_dataset.md} (93%) create mode 100644 docs/source/data/kedro_dataset_factories.md rename docs/source/data/{kedro_io.md => partitioned_and_incremental_datasets.md} (62%) diff --git a/RELEASE.md b/RELEASE.md index ea0fce323a..603cb61f46 100644 --- a/RELEASE.md +++ b/RELEASE.md @@ -18,6 +18,8 @@ * Updated `kedro pipeline create` and `kedro catalog create` to use new `/conf` file structure. ## Documentation changes +* Revised the `data` section to restructure beginner and advanced pages about the Data Catalog and datasets. +* Moved contributor documentation to the [GitHub wiki](https://github.com/kedro-org/kedro/wiki/Contribute-to-Kedro). * Update example of using generator functions in nodes. * Added migration guide from the `ConfigLoader` to the `OmegaConfigLoader`. The `ConfigLoader` is deprecated and will be removed in the `0.19.0` release. diff --git a/docs/source/_static/css/qb1-sphinx-rtd.css b/docs/source/_static/css/qb1-sphinx-rtd.css index 3f11d0ceee..fa58317d22 100644 --- a/docs/source/_static/css/qb1-sphinx-rtd.css +++ b/docs/source/_static/css/qb1-sphinx-rtd.css @@ -321,16 +321,16 @@ h1, h2, .rst-content .toctree-wrapper p.caption, h3, h4, h5, h6, legend { } .wy-body-for-nav h1 { - font-size: 2.6rem; + font-size: 2.6rem !important; letter-spacing: -0.3px; } .wy-body-for-nav h2 { - font-size: 2.3rem; + font-size: 2rem; } .wy-body-for-nav h3 { - font-size: 2.1rem; + font-size: 2rem; } .wy-body-for-nav h4 { diff --git a/docs/source/configuration/credentials.md b/docs/source/configuration/credentials.md index 620fb569ac..0d91da9cbc 100644 --- a/docs/source/configuration/credentials.md +++ b/docs/source/configuration/credentials.md @@ -3,7 +3,7 @@ For security reasons, we strongly recommend that you *do not* commit any credentials or other secrets to version control. Kedro is set up so that, by default, if a file inside the `conf` folder (and its subfolders) contains `credentials` in its name, it will be ignored by git. -Credentials configuration can be used on its own directly in code or [fed into the `DataCatalog`](../data/data_catalog.md#feeding-in-credentials). +Credentials configuration can be used on its own directly in code or [fed into the `DataCatalog`](../data/data_catalog.md#dataset-access-credentials). If you would rather store your credentials in environment variables instead of a file, you can use the `OmegaConfigLoader` [to load credentials from environment variables](advanced_configuration.md#how-to-load-credentials-through-environment-variables) as described in the advanced configuration chapter. ## How to load credentials in code diff --git a/docs/source/data/advanced_data_catalog_usage.md b/docs/source/data/advanced_data_catalog_usage.md new file mode 100644 index 0000000000..03670eaac7 --- /dev/null +++ b/docs/source/data/advanced_data_catalog_usage.md @@ -0,0 +1,225 @@ +# Advanced: Access the Data Catalog in code + +You can define a Data Catalog in two ways. Most use cases can be through a YAML configuration file as [illustrated previously](./data_catalog.md), but it is possible to access the Data Catalog programmatically through [`kedro.io.DataCatalog`](/kedro.io.DataCatalog) using an API that allows you to configure data sources in code and use the IO module within notebooks. + +## How to configure the Data Catalog + +To use the `DataCatalog` API, construct a `DataCatalog` object programmatically in a file like `catalog.py`. + +In the following, we are using several pre-built data loaders documented in the [API reference documentation](/kedro_datasets). + +```python +from kedro.io import DataCatalog +from kedro_datasets.pandas import ( + CSVDataSet, + SQLTableDataSet, + SQLQueryDataSet, + ParquetDataSet, +) + +io = DataCatalog( + { + "bikes": CSVDataSet(filepath="../data/01_raw/bikes.csv"), + "cars": CSVDataSet(filepath="../data/01_raw/cars.csv", load_args=dict(sep=",")), + "cars_table": SQLTableDataSet( + table_name="cars", credentials=dict(con="sqlite:///kedro.db") + ), + "scooters_query": SQLQueryDataSet( + sql="select * from cars where gear=4", + credentials=dict(con="sqlite:///kedro.db"), + ), + "ranked": ParquetDataSet(filepath="ranked.parquet"), + } +) +``` + +When using `SQLTableDataSet` or `SQLQueryDataSet` you must provide a `con` key containing [SQLAlchemy compatible](https://docs.sqlalchemy.org/en/13/core/engines.html#database-urls) database connection string. In the example above we pass it as part of `credentials` argument. Alternative to `credentials` is to put `con` into `load_args` and `save_args` (`SQLTableDataSet` only). + +## How to view the available data sources + +To review the `DataCatalog`: + +```python +io.list() +``` + +## How to load datasets programmatically + +To access each dataset by its name: + +```python +cars = io.load("cars") # data is now loaded as a DataFrame in 'cars' +gear = cars["gear"].values +``` + +The following steps happened behind the scenes when `load` was called: + +- The value `cars` was located in the Data Catalog +- The corresponding `AbstractDataSet` object was retrieved +- The `load` method of this dataset was called +- This `load` method delegated the loading to the underlying pandas `read_csv` function + +## How to save data programmatically + +```{warning} +This pattern is not recommended unless you are using platform notebook environments (Sagemaker, Databricks etc) or writing unit/integration tests for your Kedro pipeline. Use the YAML approach in preference. +``` + +### How to save data to memory + +To save data using an API similar to that used to load data: + +```python +from kedro.io import MemoryDataSet + +memory = MemoryDataSet(data=None) +io.add("cars_cache", memory) +io.save("cars_cache", "Memory can store anything.") +io.load("cars_cache") +``` + +### How to save data to a SQL database for querying + +To put the data in a SQLite database: + +```python +import os + +# This cleans up the database in case it exists at this point +try: + os.remove("kedro.db") +except FileNotFoundError: + pass + +io.save("cars_table", cars) + +# rank scooters by their mpg +ranked = io.load("scooters_query")[["brand", "mpg"]] +``` + +### How to save data in Parquet + +To save the processed data in Parquet format: + +```python +io.save("ranked", ranked) +``` + +```{warning} +Saving `None` to a dataset is not allowed! +``` + +## How to access a dataset with credentials +Before instantiating the `DataCatalog`, Kedro will first attempt to read [the credentials from the project configuration](../configuration/credentials.md). The resulting dictionary is then passed into `DataCatalog.from_config()` as the `credentials` argument. + +Let's assume that the project contains the file `conf/local/credentials.yml` with the following contents: + +```yaml +dev_s3: + client_kwargs: + aws_access_key_id: key + aws_secret_access_key: secret + +scooters_credentials: + con: sqlite:///kedro.db + +my_gcp_credentials: + id_token: key +``` + +Your code will look as follows: + +```python +CSVDataSet( + filepath="s3://test_bucket/data/02_intermediate/company/motorbikes.csv", + load_args=dict(sep=",", skiprows=5, skipfooter=1, na_values=["#NA", "NA"]), + credentials=dict(key="token", secret="key"), +) +``` + +## How to version a dataset using the Code API + +In an earlier section of the documentation we described how [Kedro enables dataset and ML model versioning](./data_catalog.md/#dataset-versioning). + +If you require programmatic control over load and save versions of a specific dataset, you can instantiate `Version` and pass it as a parameter to the dataset initialisation: + +```python +from kedro.io import DataCatalog, Version +from kedro_datasets.pandas import CSVDataSet +import pandas as pd + +data1 = pd.DataFrame({"col1": [1, 2], "col2": [4, 5], "col3": [5, 6]}) +data2 = pd.DataFrame({"col1": [7], "col2": [8], "col3": [9]}) +version = Version( + load=None, # load the latest available version + save=None, # generate save version automatically on each save operation +) + +test_data_set = CSVDataSet( + filepath="data/01_raw/test.csv", save_args={"index": False}, version=version +) +io = DataCatalog({"test_data_set": test_data_set}) + +# save the dataset to data/01_raw/test.csv//test.csv +io.save("test_data_set", data1) +# save the dataset into a new file data/01_raw/test.csv//test.csv +io.save("test_data_set", data2) + +# load the latest version from data/test.csv/*/test.csv +reloaded = io.load("test_data_set") +assert data2.equals(reloaded) +``` + +In the example above, we do not fix any versions. The behaviour of load and save operations becomes slightly different when we set a version: + + +```python +version = Version( + load="my_exact_version", # load exact version + save="my_exact_version", # save to exact version +) + +test_data_set = CSVDataSet( + filepath="data/01_raw/test.csv", save_args={"index": False}, version=version +) +io = DataCatalog({"test_data_set": test_data_set}) + +# save the dataset to data/01_raw/test.csv/my_exact_version/test.csv +io.save("test_data_set", data1) +# load from data/01_raw/test.csv/my_exact_version/test.csv +reloaded = io.load("test_data_set") +assert data1.equals(reloaded) + +# raises DataSetError since the path +# data/01_raw/test.csv/my_exact_version/test.csv already exists +io.save("test_data_set", data2) +``` + +We do not recommend passing exact load and/or save versions, since it might lead to inconsistencies between operations. For example, if versions for load and save operations do not match, a save operation would result in a `UserWarning`. + +Imagine a simple pipeline with two nodes, where B takes the output from A. If you specify the load-version of the data for B to be `my_data_2023_08_16.csv`, the data that A produces (`my_data_20230818.csv`) is not used. + +```text +Node_A -> my_data_20230818.csv +my_data_2023_08_16.csv -> Node B +``` + +In code: + +```python +version = Version( + load="my_data_2023_08_16.csv", # load exact version + save="my_data_20230818.csv", # save to exact version +) + +test_data_set = CSVDataSet( + filepath="data/01_raw/test.csv", save_args={"index": False}, version=version +) +io = DataCatalog({"test_data_set": test_data_set}) + +io.save("test_data_set", data1) # emits a UserWarning due to version inconsistency + +# raises DataSetError since the data/01_raw/test.csv/exact_load_version/test.csv +# file does not exist +reloaded = io.load("test_data_set") +``` diff --git a/docs/source/data/data_catalog.md b/docs/source/data/data_catalog.md index fb1f7ac3dc..680db626f7 100644 --- a/docs/source/data/data_catalog.md +++ b/docs/source/data/data_catalog.md @@ -1,31 +1,38 @@ -# The Data Catalog +# Introduction to the Data Catalog -This section introduces `catalog.yml`, the project-shareable Data Catalog. The file is located in `conf/base` and is a registry of all data sources available for use by a project; it manages loading and saving of data. +In a Kedro project, the Data Catalog is a registry of all data sources available for use by the project. It is specified with a YAML catalog file that maps the names of node inputs and outputs as keys in the `DataCatalog` class. -All supported data connectors are available in [`kedro-datasets`](/kedro_datasets). +This page introduces the basic sections of `catalog.yml`, which is the file used to register data sources for a Kedro project. -## Use the Data Catalog within Kedro configuration +## The basics of `catalog.yml` +A separate page of [Data Catalog YAML examples](./data_catalog_yaml_examples.md) gives further examples of how to work with `catalog.yml`, but here we revisit the [basic `catalog.yml` introduced by the spaceflights tutorial](../tutorial/set_up_data.md). -Kedro uses configuration to make your code reproducible when it has to reference datasets in different locations and/or in different environments. +The example below registers two `csv` datasets, and an `xlsx` dataset. The minimum details needed to load and save a file within a local file system are the key, which is name of the dataset, the type of data to indicate the dataset to use (`type`) and the file's location (`filepath`). -You can copy this file and reference additional locations for the same datasets. For instance, you can use the `catalog.yml` file in `conf/base/` to register the locations of datasets that would run in production, while copying and updating a second version of `catalog.yml` in `conf/local/` to register the locations of sample datasets that you are using for prototyping your data pipeline(s). +```yaml +companies: + type: pandas.CSVDataSet + filepath: data/01_raw/companies.csv -Built-in functionality for `conf/local/` to overwrite `conf/base/` is [described in the documentation about configuration](../configuration/configuration_basics.md). This means that a dataset called `cars` could exist in the `catalog.yml` files in `conf/base/` and `conf/local/`. In code, in `src`, you would only call a dataset named `cars` and Kedro would detect which definition of `cars` dataset to use to run your pipeline - `cars` definition from `conf/local/catalog.yml` would take precedence in this case. +reviews: + type: pandas.CSVDataSet + filepath: data/01_raw/reviews.csv -The Data Catalog also works with the `credentials.yml` file in `conf/local/`, allowing you to specify usernames and passwords required to load certain datasets. +shuttles: + type: pandas.ExcelDataSet + filepath: data/01_raw/shuttles.xlsx + load_args: + engine: openpyxl # Use modern Excel engine (the default since Kedro 0.18.0) +``` +### Dataset `type` -You can define a Data Catalog in two ways - through YAML configuration, or programmatically using an API. Both methods allow you to specify: +Kedro offers a range of datasets, including CSV, Excel, Parquet, Feather, HDF5, JSON, Pickle, SQL Tables, SQL Queries, Spark DataFrames and more. They are supported with the APIs of pandas, spark, networkx, matplotlib, yaml and more. - - Dataset name - - Dataset type - - Location of the dataset using `fsspec`, detailed in the next section - - Credentials needed to access the dataset - - Load and saving arguments - - Whether you want a [dataset or ML model to be versioned](kedro_io.md#versioning) when you run your data pipeline +[The `kedro-datasets` package documentation](/kedro_datasets) contains a comprehensive list of all available file types. -## Specify the location of the dataset +### Dataset `filepath` -Kedro relies on [`fsspec`](https://filesystem-spec.readthedocs.io/en/latest/) to read and save data from a variety of data stores including local file systems, network file systems, cloud object stores, and Hadoop. When specifying a storage location in `filepath:`, you should provide a URL using the general form `protocol://path/to/data`. If no protocol is provided, the local file system is assumed (same as ``file://``). +Kedro relies on [`fsspec`](https://filesystem-spec.readthedocs.io/en/latest/) to read and save data from a variety of data stores including local file systems, network file systems, cloud object stores, and Hadoop. When specifying a storage location in `filepath:`, you should provide a URL using the general form `protocol://path/to/data`. If no protocol is provided, the local file system is assumed (which is the same as ``file://``). The following prepends are available: @@ -41,65 +48,17 @@ The following prepends are available: `fsspec` also provides other file systems, such as SSH, FTP and WebHDFS. [See the fsspec documentation for more information](https://filesystem-spec.readthedocs.io/en/latest/api.html#implementations). -## Data Catalog `*_args` parameters - -Data Catalog accepts two different groups of `*_args` parameters that serve different purposes: -- `fs_args` -- `load_args` and `save_args` - -The `fs_args` is used to configure the interaction with a filesystem. -All the top-level parameters of `fs_args` (except `open_args_load` and `open_args_save`) will be passed in an underlying filesystem class. - -### Example 1: Provide the `project` value to the underlying filesystem class (`GCSFileSystem`) to interact with Google Cloud Storage (GCS) - -```yaml -test_dataset: - type: ... - fs_args: - project: test_project -``` -The `open_args_load` and `open_args_save` parameters are passed to the filesystem's `open` method to configure how a dataset file (on a specific filesystem) is opened during a load or save operation, respectively. +## Additional settings in `catalog.yml` -### Example 2: Load data from a local binary file using `utf-8` encoding +This section explains the additional settings available within `catalog.yml`. -```yaml -test_dataset: - type: ... - fs_args: - open_args_load: - mode: "rb" - encoding: "utf-8" -``` +### Load and save arguments +The Kedro Data Catalog also accepts two different groups of `*_args` parameters that serve different purposes: -`load_args` and `save_args` configure how a third-party library (e.g. `pandas` for `CSVDataSet`) loads/saves data from/to a file. +* **`load_args` and `save_args`**: Configures how a third-party library loads/saves data from/to a file. In the spaceflights example above, `load_args`, is passed to the excel file read method (`pd.read_excel`) as a [keyword argument](https://pandas.pydata.org/pandas-docs/stable/reference/api/pandas.read_excel.html). Although not specified here, the equivalent output is `save_args` and the value would be passed to [`pd.DataFrame.to_excel` method](https://pandas.pydata.org/pandas-docs/stable/reference/api/pandas.DataFrame.to_excel.html). -### Example 3: Save data to a CSV file without row names (index) using `utf-8` encoding - -```yaml -test_dataset: - type: pandas.CSVDataSet - ... - save_args: - index: False - encoding: "utf-8" -``` - -## Use the Data Catalog with the YAML API - -The YAML API allows you to configure your datasets in a YAML configuration file, `conf/base/catalog.yml` or `conf/local/catalog.yml`. - -Here are some examples of data configuration in a `catalog.yml`: - -### Example 1: Loads / saves a CSV file from / to a local file system - -```yaml -bikes: - type: pandas.CSVDataSet - filepath: data/01_raw/bikes.csv -``` - -### Example 2: Loads and saves a CSV on a local file system, using specified load and save arguments +For example, to load or save a CSV on a local file system, using specified load/save arguments: ```yaml cars: @@ -111,270 +70,35 @@ cars: index: False date_format: '%Y-%m-%d %H:%M' decimal: . - ``` -### Example 3: Loads and saves a compressed CSV on a local file system +* **`fs_args`**: Configures the interaction with a filesystem. +All the top-level parameters of `fs_args` (except `open_args_load` and `open_args_save`) will be passed to an underlying filesystem class. -```yaml -boats: - type: pandas.CSVDataSet - filepath: data/01_raw/company/boats.csv.gz - load_args: - sep: ',' - compression: 'gzip' - fs_args: - open_args_load: - mode: 'rb' -``` - -### Example 4: Loads a CSV file from a specific S3 bucket, using credentials and load arguments +For example, to provide the `project` value to the underlying filesystem class (`GCSFileSystem`) to interact with Google Cloud Storage: ```yaml -motorbikes: - type: pandas.CSVDataSet - filepath: s3://your_bucket/data/02_intermediate/company/motorbikes.csv - credentials: dev_s3 - load_args: - sep: ',' - skiprows: 5 - skipfooter: 1 - na_values: ['#NA', NA] -``` - -### Example 5: Loads / saves a pickle file from / to a local file system - -```yaml -airplanes: - type: pickle.PickleDataSet - filepath: data/06_models/airplanes.pkl - backend: pickle -``` - -### Example 6: Loads an Excel file from Google Cloud Storage - -```yaml -rockets: - type: pandas.ExcelDataSet - filepath: gcs://your_bucket/data/02_intermediate/company/motorbikes.xlsx +test_dataset: + type: ... fs_args: - project: my-project - credentials: my_gcp_credentials - save_args: - sheet_name: Sheet1 + project: test_project ``` -### Example 7: Loads a multi-sheet Excel file from a local file system - -```yaml -trains: - type: pandas.ExcelDataSet - filepath: data/02_intermediate/company/trains.xlsx - load_args: - sheet_name: [Sheet1, Sheet2, Sheet3] -``` +The `open_args_load` and `open_args_save` parameters are passed to the filesystem's `open` method to configure how a dataset file (on a specific filesystem) is opened during a load or save operation, respectively. -### Example 8: Saves an image created with Matplotlib on Google Cloud Storage +For example, to load data from a local binary file using `utf-8` encoding: ```yaml -results_plot: - type: matplotlib.MatplotlibWriter - filepath: gcs://your_bucket/data/08_results/plots/output_1.jpeg +test_dataset: + type: ... fs_args: - project: my-project - credentials: my_gcp_credentials -``` - - -### Example 9: Loads / saves an HDF file on local file system storage, using specified load and save arguments - -```yaml -skateboards: - type: pandas.HDFDataSet - filepath: data/02_intermediate/skateboards.hdf - key: name - load_args: - columns: [brand, length] - save_args: - mode: w # Overwrite even when the file already exists - dropna: True -``` - -### Example 10: Loads / saves a parquet file on local file system storage, using specified load and save arguments - -```yaml -trucks: - type: pandas.ParquetDataSet - filepath: data/02_intermediate/trucks.parquet - load_args: - columns: [name, gear, disp, wt] - categories: list - index: name - save_args: - compression: GZIP - file_scheme: hive - has_nulls: False - partition_on: [name] -``` - - -### Example 11: Loads / saves a Spark table on S3, using specified load and save arguments - -```yaml -weather: - type: spark.SparkDataSet - filepath: s3a://your_bucket/data/01_raw/weather* - credentials: dev_s3 - file_format: csv - load_args: - header: True - inferSchema: True - save_args: - sep: '|' - header: True -``` - - -### Example 12: Loads / saves a SQL table using credentials, a database connection, using specified load and save arguments - -```yaml -scooters: - type: pandas.SQLTableDataSet - credentials: scooters_credentials - table_name: scooters - load_args: - index_col: [name] - columns: [name, gear] - save_args: - if_exists: replace -``` - -### Example 13: Loads an SQL table with credentials, a database connection, and applies a SQL query to the table - - -```yaml -scooters_query: - type: pandas.SQLQueryDataSet - credentials: scooters_credentials - sql: select * from cars where gear=4 - load_args: - index_col: [name] -``` - -When you use [`pandas.SQLTableDataSet`](/kedro_datasets.pandas.SQLTableDataSet) or [`pandas.SQLQueryDataSet`](/kedro_datasets.pandas.SQLQueryDataSet), you must provide a database connection string. In the above example, we pass it using the `scooters_credentials` key from the credentials (see the details in the [Feeding in credentials](#feeding-in-credentials) section below). `scooters_credentials` must have a top-level key `con` containing a [SQLAlchemy compatible](https://docs.sqlalchemy.org/en/13/core/engines.html#database-urls) connection string. As an alternative to credentials, you could explicitly put `con` into `load_args` and `save_args` (`pandas.SQLTableDataSet` only). - - -### Example 14: Loads data from an API endpoint, example US corn yield data from USDA - -```yaml -us_corn_yield_data: - type: api.APIDataSet - url: https://quickstats.nass.usda.gov - credentials: usda_credentials - params: - key: SOME_TOKEN - format: JSON - commodity_desc: CORN - statisticcat_des: YIELD - agg_level_desc: STATE - year: 2000 -``` - -Note that `usda_credientials` will be passed as the `auth` argument in the `requests` library. Specify the username and password as a list in your `credentials.yml` file as follows: - -```yaml -usda_credentials: - - username - - password -``` - - -### Example 15: Loads data from Minio (S3 API Compatible Storage) - - -```yaml -test: - type: pandas.CSVDataSet - filepath: s3://your_bucket/test.csv # assume `test.csv` is uploaded to the Minio server. - credentials: dev_minio -``` -In `credentials.yml`, define the `key`, `secret` and the `endpoint_url` as follows: - -```yaml -dev_minio: - key: token - secret: key - client_kwargs: - endpoint_url : 'http://localhost:9000' -``` - -```{note} -The easiest way to setup MinIO is to run a Docker image. After the following command, you can access the Minio server with `http://localhost:9000` and create a bucket and add files as if it is on S3. -``` - -`docker run -p 9000:9000 -e "MINIO_ACCESS_KEY=token" -e "MINIO_SECRET_KEY=key" minio/minio server /data` - - -### Example 16: Loads a model saved as a pickle from Azure Blob Storage - -```yaml -ml_model: - type: pickle.PickleDataSet - filepath: "abfs://models/ml_models.pickle" - versioned: True - credentials: dev_abs -``` -In the `credentials.yml` file, define the `account_name` and `account_key`: - -```yaml -dev_abs: - account_name: accountname - account_key: key -``` - - -### Example 17: Loads a CSV file stored in a remote location through SSH - -```{note} -This example requires [Paramiko](https://www.paramiko.org) to be installed (`pip install paramiko`). -``` -```yaml -cool_dataset: - type: pandas.CSVDataSet - filepath: "sftp:///path/to/remote_cluster/cool_data.csv" - credentials: cluster_credentials -``` -All parameters required to establish the SFTP connection can be defined through `fs_args` or in the `credentials.yml` file as follows: - -```yaml -cluster_credentials: - username: my_username - host: host_address - port: 22 - password: password -``` -The list of all available parameters is given in the [Paramiko documentation](https://docs.paramiko.org/en/2.4/api/client.html#paramiko.client.SSHClient.connect). - -## Create a Data Catalog YAML configuration file via CLI - -You can use the [`kedro catalog create` command to create a Data Catalog YAML configuration](../development/commands_reference.md#create-a-data-catalog-yaml-configuration-file). - -This creates a `//catalog_.yml` configuration file with `MemoryDataSet` datasets for each dataset in a registered pipeline if it is missing from the `DataCatalog`. - -```yaml -# //catalog_.yml -rockets: - type: MemoryDataSet -scooters: - type: MemoryDataSet + open_args_load: + mode: "rb" + encoding: "utf-8" ``` -## Adding parameters - -You can [configure parameters](../configuration/parameters.md) for your project and [reference them](../configuration/parameters.md#how-to-use-parameters) in your nodes. To do this, use the `add_feed_dict()` method ([API documentation](/kedro.io.DataCatalog)). You can use this method to add any other entry or metadata you wish on the `DataCatalog`. - - -## Feeding in credentials +### Dataset access credentials +The Data Catalog also works with the `credentials.yml` file in `conf/local/`, allowing you to specify usernames and passwords required to load certain datasets. Before instantiating the `DataCatalog`, Kedro will first attempt to read [the credentials from the project configuration](../configuration/credentials.md). The resulting dictionary is then passed into `DataCatalog.from_config()` as the `credentials` argument. @@ -385,333 +109,24 @@ dev_s3: client_kwargs: aws_access_key_id: key aws_secret_access_key: secret - -scooters_credentials: - con: sqlite:///kedro.db - -my_gcp_credentials: - id_token: key ``` -In the example above, the `catalog.yml` file contains references to credentials keys `dev_s3` and `scooters_credentials`. This means that when it instantiates the `motorbikes` dataset, for example, the `DataCatalog` will attempt to read top-level key `dev_s3` from the received `credentials` dictionary, and then will pass its values into the dataset `__init__` as a `credentials` argument. This is essentially equivalent to calling this: - -```python -CSVDataSet( - filepath="s3://test_bucket/data/02_intermediate/company/motorbikes.csv", - load_args=dict(sep=",", skiprows=5, skipfooter=1, na_values=["#NA", "NA"]), - credentials=dict(key="token", secret="key"), -) -``` - - -## Load multiple datasets with similar configuration using YAML anchors - -Different datasets might use the same file format, load and save arguments, and be stored in the same folder. [YAML has a built-in syntax](https://yaml.org/spec/1.2.1/#Syntax) for factorising parts of a YAML file, which means that you can decide what is generalisable across your datasets, so that you need not spend time copying and pasting dataset configurations in the `catalog.yml` file. - -You can see this in the following example: +and the Data Catalog is specified in `catalog.yml` as follows: ```yaml -_csv: &csv - type: spark.SparkDataSet - file_format: csv +motorbikes: + type: pandas.CSVDataSet + filepath: s3://your_bucket/data/02_intermediate/company/motorbikes.csv + credentials: dev_s3 load_args: sep: ',' - na_values: ['#NA', NA] - header: True - inferSchema: False - -cars: - <<: *csv - filepath: s3a://data/01_raw/cars.csv - -trucks: - <<: *csv - filepath: s3a://data/01_raw/trucks.csv - -bikes: - <<: *csv - filepath: s3a://data/01_raw/bikes.csv - load_args: - header: False -``` - -The syntax `&csv` names the following block `csv` and the syntax `<<: *csv` inserts the contents of the block named `csv`. Locally declared keys entirely override inserted ones as seen in `bikes`. - -```{note} -It's important that the name of the template entry starts with a `_` so Kedro knows not to try and instantiate it as a dataset. ``` +In the example above, the `catalog.yml` file contains references to credentials keys `dev_s3`. The Data Catalog first reads `dev_s3` from the received `credentials` dictionary, and then passes its values into the dataset as a `credentials` argument to `__init__`. -You can also nest reuseable YAML syntax: - -```yaml -_csv: &csv - type: spark.SparkDataSet - file_format: csv - load_args: &csv_load_args - header: True - inferSchema: False - -airplanes: - <<: *csv - filepath: s3a://data/01_raw/airplanes.csv - load_args: - <<: *csv_load_args - sep: ; -``` -In this example, the default `csv` configuration is inserted into `airplanes` and then the `load_args` block is overridden. Normally, that would replace the whole dictionary. In order to extend `load_args`, the defaults for that block are then re-inserted. +### Dataset versioning -## Load multiple datasets with similar configuration using dataset factories -For catalog entries that share configuration details, you can also use the dataset factories introduced in Kedro 0.18.12. This syntax allows you to generalise the configuration and -reduce the number of similar catalog entries by matching datasets used in your project's pipelines to dataset factory patterns. - -### Example 1: Generalise datasets with similar names and types into one dataset factory -Consider the following catalog entries: -```yaml -factory_data: - type: pandas.CSVDataSet - filepath: data/01_raw/factory_data.csv - - -process_data: - type: pandas.CSVDataSet - filepath: data/01_raw/process_data.csv -``` -The datasets in this catalog can be generalised to the following dataset factory: -```yaml -"{name}_data": - type: pandas.CSVDataSet - filepath: data/01_raw/{name}_data.csv -``` -When `factory_data` or `process_data` is used in your pipeline, it is matched to the factory pattern `{name}_data`. The factory pattern must always be enclosed in -quotes to avoid YAML parsing errors. - - -### Example 2: Generalise datasets of the same type into one dataset factory -You can also combine all the datasets with the same type and configuration details. For example, consider the following -catalog with three datasets named `boats`, `cars` and `planes` of the type `pandas.CSVDataSet`: -```yaml -boats: - type: pandas.CSVDataSet - filepath: data/01_raw/shuttles.csv - -cars: - type: pandas.CSVDataSet - filepath: data/01_raw/reviews.csv - -planes: - type: pandas.CSVDataSet - filepath: data/01_raw/companies.csv -``` -These datasets can be combined into the following dataset factory: -```yaml -"{dataset_name}#csv": - type: pandas.CSVDataSet - filepath: data/01_raw/{dataset_name}.csv -``` -You will then have to update the pipelines in your project located at `src///pipeline.py` to refer to these datasets as `boats#csv`, -`cars#csv` and `planes#csv`. Adding a suffix or a prefix to the dataset names and the dataset factory patterns, like `#csv` here, ensures that the dataset -names are matched with the intended pattern. -```python -from .nodes import create_model_input_table, preprocess_companies, preprocess_shuttles - - -def create_pipeline(**kwargs) -> Pipeline: - return pipeline( - [ - node( - func=preprocess_boats, - inputs="boats#csv", - outputs="preprocessed_boats", - name="preprocess_boats_node", - ), - node( - func=preprocess_cars, - inputs="cars#csv", - outputs="preprocessed_cars", - name="preprocess_cars_node", - ), - node( - func=preprocess_planes, - inputs="planes#csv", - outputs="preprocessed_planes", - name="preprocess_planes_node", - ), - node( - func=create_model_input_table, - inputs=[ - "preprocessed_boats", - "preprocessed_planes", - "preprocessed_cars", - ], - outputs="model_input_table", - name="create_model_input_table_node", - ), - ] - ) -``` -### Example 3: Generalise datasets using namespaces into one dataset factory -You can also generalise the catalog entries for datasets belonging to namespaced modular pipelines. Consider the -following pipeline which takes in a `model_input_table` and outputs two regressors belonging to the -`active_modelling_pipeline` and the `candidate_modelling_pipeline` namespaces: -```python -from kedro.pipeline import Pipeline, node -from kedro.pipeline.modular_pipeline import pipeline - -from .nodes import evaluate_model, split_data, train_model - - -def create_pipeline(**kwargs) -> Pipeline: - pipeline_instance = pipeline( - [ - node( - func=split_data, - inputs=["model_input_table", "params:model_options"], - outputs=["X_train", "y_train"], - name="split_data_node", - ), - node( - func=train_model, - inputs=["X_train", "y_train"], - outputs="regressor", - name="train_model_node", - ), - ] - ) - ds_pipeline_1 = pipeline( - pipe=pipeline_instance, - inputs="model_input_table", - namespace="active_modelling_pipeline", - ) - ds_pipeline_2 = pipeline( - pipe=pipeline_instance, - inputs="model_input_table", - namespace="candidate_modelling_pipeline", - ) - - return ds_pipeline_1 + ds_pipeline_2 -``` -You can now have one dataset factory pattern in your catalog instead of two separate entries for `active_modelling_pipeline.regressor` -and `candidate_modelling_pipeline.regressor` as below: -```yaml -{namespace}.regressor: - type: pickle.PickleDataSet - filepath: data/06_models/regressor_{namespace}.pkl - versioned: true -``` -### Example 4: Generalise datasets of the same type in different layers into one dataset factory with multiple placeholders - -You can use multiple placeholders in the same pattern. For example, consider the following catalog where the dataset -entries share `type`, `file_format` and `save_args`: -```yaml -processing.factory_data: - type: spark.SparkDataSet - filepath: data/processing/factory_data.pq - file_format: parquet - save_args: - mode: overwrite - -processing.process_data: - type: spark.SparkDataSet - filepath: data/processing/process_data.pq - file_format: parquet - save_args: - mode: overwrite - -modelling.metrics: - type: spark.SparkDataSet - filepath: data/modelling/factory_data.pq - file_format: parquet - save_args: - mode: overwrite -``` -This could be generalised to the following pattern: -```yaml -"{layer}.{dataset_name}": - type: spark.SparkDataSet - filepath: data/{layer}/{dataset_name}.pq - file_format: parquet - save_args: - mode: overwrite -``` -All the placeholders used in the catalog entry body must exist in the factory pattern name. - -### Example 5: Generalise datasets using multiple dataset factories -You can have multiple dataset factories in your catalog. For example: -```yaml -"{namespace}.{dataset_name}@spark": - type: spark.SparkDataSet - filepath: data/{namespace}/{dataset_name}.pq - file_format: parquet - -"{dataset_name}@csv": - type: pandas.CSVDataSet - filepath: data/01_raw/{dataset_name}.csv -``` - -Having multiple dataset factories in your catalog can lead to a situation where a dataset name from your pipeline might -match multiple patterns. To overcome this, Kedro sorts all the potential matches for the dataset name in the pipeline and picks the best match. -The matches are ranked according to the following criteria : -1. Number of exact character matches between the dataset name and the factory pattern. For example, a dataset named `factory_data$csv` would match `{dataset}_data$csv` over `{dataset_name}$csv`. -2. Number of placeholders. For example, the dataset `preprocessing.shuttles+csv` would match `{namespace}.{dataset}+csv` over `{dataset}+csv`. -3. Alphabetical order - -### Example 6: Generalise all datasets with a catch-all dataset factory to overwrite the default `MemoryDataSet` -You can use dataset factories to define a catch-all pattern which will overwrite the default `MemoryDataSet` creation. -```yaml -"{default_dataset}": - type: pandas.CSVDataSet - filepath: data/{default_dataset}.csv - -``` -Kedro will now treat all the datasets mentioned in your project's pipelines that do not appear as specific patterns or explicit entries in your catalog -as `pandas.CSVDataSet`. - -## Transcode datasets - -You might come across a situation where you would like to read the same file using two different dataset implementations. Use transcoding when you want to load and save the same file, via its specified `filepath`, using different `DataSet` implementations. - -### A typical example of transcoding - -For instance, parquet files can not only be loaded via the `ParquetDataSet` using `pandas`, but also directly by `SparkDataSet`. This conversion is typical when coordinating a `Spark` to `pandas` workflow. - -To enable transcoding, define two `DataCatalog` entries for the same dataset in a common format (Parquet, JSON, CSV, etc.) in your `conf/base/catalog.yml`: - -```yaml -my_dataframe@spark: - type: spark.SparkDataSet - filepath: data/02_intermediate/data.parquet - file_format: parquet - -my_dataframe@pandas: - type: pandas.ParquetDataSet - filepath: data/02_intermediate/data.parquet -``` - -These entries are used in the pipeline like this: - -```python -pipeline( - [ - node(func=my_func1, inputs="spark_input", outputs="my_dataframe@spark"), - node(func=my_func2, inputs="my_dataframe@pandas", outputs="pipeline_output"), - ] -) -``` - -### How does transcoding work? - -In this example, Kedro understands that `my_dataframe` is the same dataset in its `spark.SparkDataSet` and `pandas.ParquetDataSet` formats and helps resolve the node execution order. - -In the pipeline, Kedro uses the `spark.SparkDataSet` implementation for saving and `pandas.ParquetDataSet` -for loading, so the first node should output a `pyspark.sql.DataFrame`, while the second node would receive a `pandas.Dataframe`. - - -## Version datasets and ML models - -Making a simple addition to your Data Catalog allows you to perform versioning of datasets and machine learning models. - -Consider the following versioned dataset defined in the `catalog.yml`: +Kedro enables dataset and ML model versioning through the `versioned` definition. For example: ```yaml cars: @@ -720,125 +135,41 @@ cars: versioned: True ``` -The `DataCatalog` will create a versioned `CSVDataSet` called `cars`. The actual csv file location will look like `data/01_raw/company/cars.csv//cars.csv`, where `` corresponds to a global save version string formatted as `YYYY-MM-DDThh.mm.ss.sssZ`. +In this example, `filepath` is used as the basis of a folder that stores versions of the `cars` dataset. Each time a new version is created by a pipeline run it is stored within `data/01_raw/company/cars.csv//cars.csv`, where `` corresponds to a version string formatted as `YYYY-MM-DDThh.mm.ss.sssZ`. -You can run the pipeline with a particular versioned data set with `--load-version` flag as follows: +By default, `kedro run` loads the latest version of the dataset. However, you can also specify a particular versioned data set with `--load-version` flag as follows: ```bash kedro run --load-version=cars:YYYY-MM-DDThh.mm.ss.sssZ ``` where `--load-version` is dataset name and version timestamp separated by `:`. -This section shows just the very basics of versioning, which is described further in [the documentation about Kedro IO](../data/kedro_io.md#versioning). - -## Use the Data Catalog with the Code API - -The code API allows you to: - -* configure data sources in code -* operate the IO module within notebooks - -### Configure a Data Catalog - -In a file like `catalog.py`, you can construct a `DataCatalog` object programmatically. In the following, we are using several pre-built data loaders documented in the [API reference documentation](/kedro_datasets). - -```python -from kedro.io import DataCatalog -from kedro_datasets.pandas import ( - CSVDataSet, - SQLTableDataSet, - SQLQueryDataSet, - ParquetDataSet, -) - -io = DataCatalog( - { - "bikes": CSVDataSet(filepath="../data/01_raw/bikes.csv"), - "cars": CSVDataSet(filepath="../data/01_raw/cars.csv", load_args=dict(sep=",")), - "cars_table": SQLTableDataSet( - table_name="cars", credentials=dict(con="sqlite:///kedro.db") - ), - "scooters_query": SQLQueryDataSet( - sql="select * from cars where gear=4", - credentials=dict(con="sqlite:///kedro.db"), - ), - "ranked": ParquetDataSet(filepath="ranked.parquet"), - } -) -``` - -When using `SQLTableDataSet` or `SQLQueryDataSet` you must provide a `con` key containing [SQLAlchemy compatible](https://docs.sqlalchemy.org/en/13/core/engines.html#database-urls) database connection string. In the example above we pass it as part of `credentials` argument. Alternative to `credentials` is to put `con` into `load_args` and `save_args` (`SQLTableDataSet` only). - -### Load datasets - -You can access each dataset by its name. - -```python -cars = io.load("cars") # data is now loaded as a DataFrame in 'cars' -gear = cars["gear"].values -``` - -#### Behind the scenes - -The following steps happened behind the scenes when `load` was called: - -- The value `cars` was located in the Data Catalog -- The corresponding `AbstractDataset` object was retrieved -- The `load` method of this dataset was called -- This `load` method delegated the loading to the underlying pandas `read_csv` function - -### View the available data sources - -If you forget what data was assigned, you can always review the `DataCatalog`. - -```python -io.list() -``` - -### Save data +A dataset offers versioning support if it extends the [`AbstractVersionedDataSet`](/kedro.io.AbstractVersionedDataset) class to accept a version keyword argument as part of the constructor and adapt the `_save` and `_load` method to use the versioned data path obtained from `_get_save_path` and `_get_load_path` respectively. -You can save data using an API similar to that used to load data. +To verify whether a dataset can undergo versioning, you should examine the dataset class code to inspect its inheritance [(you can find contributed datasets within the `kedro-datasets` repository)](https://github.com/kedro-org/kedro-plugins/tree/main/kedro-datasets/kedro_datasets). Check if the dataset class inherits from the `AbstractVersionedDataSet`. For instance, if you encounter a class like `CSVDataSet(AbstractVersionedDataSet[pd.DataFrame, pd.DataFrame])`, this indicates that the dataset is set up to support versioning. -```{warning} -This use is not recommended unless you are prototyping in notebooks. -``` - -#### Save data to memory - -```python -from kedro.io import MemoryDataSet - -memory = MemoryDataSet(data=None) -io.add("cars_cache", memory) -io.save("cars_cache", "Memory can store anything.") -io.load("cars_cache") +```{note} +Note that HTTP(S) is a supported file system in the dataset implementations, but if you it, you can't also use versioning. ``` -#### Save data to a SQL database for querying - -We might now want to put the data in a SQLite database to run queries on it. Let's use that to rank scooters by their mpg. - -```python -import os - -# This cleans up the database in case it exists at this point -try: - os.remove("kedro.db") -except FileNotFoundError: - pass - -io.save("cars_table", cars) -ranked = io.load("scooters_query")[["brand", "mpg"]] -``` +## Use the Data Catalog within Kedro configuration -#### Save data in Parquet +Kedro configuration enables you to organise your project for different stages of your data pipeline. For example, you might need different Data Catalog settings for development, testing, and production environments. -Finally, we can save the processed data in Parquet format. +By default, Kedro has a `base` and a `local` folder for configuration. The Data Catalog configuration is loaded using a configuration loader class which recursively scans for configuration files inside the `conf` folder, firstly in `conf/base` and then in `conf/local` (which is the designated overriding environment). Kedro merges the configuration information and returns a configuration dictionary according to rules set out in the [configuration documentation](../configuration/configuration_basics.md). -```python -io.save("ranked", ranked) -``` +In summary, if you need to configure your datasets for different environments, you can create both `conf/base/catalog.yml` and `conf/local/catalog.yml`. For instance, you can use the `catalog.yml` file in `conf/base/` to register the locations of datasets that would run in production, while adding a second version of `catalog.yml` in `conf/local/` to register the locations of sample datasets while you are using them for prototyping data pipeline(s). -```{warning} -Saving `None` to a dataset is not allowed! +To illustrate this, consider the following catalog entry for a dataset named `cars` in `conf/base/catalog.yml`, which points to a csv file stored in a bucket on AWS S3: +```yaml +cars: + filepath: s3://my_bucket/cars.csv + type: pandas.CSVDataSet + ``` +You can overwrite this catalog entry in `conf/local/catalog.yml` to point to a locally stored file instead: +```yaml +cars: + filepath: data/01_raw/cars.csv + type: pandas.CSVDataSet ``` +In your pipeline code, when the `cars` dataset is used, it will use the overwritten catalog entry from `conf/local/catalog.yml` and rely on Kedro to detect which definition of `cars` dataset to use in your pipeline. diff --git a/docs/source/data/data_catalog_yaml_examples.md b/docs/source/data/data_catalog_yaml_examples.md new file mode 100644 index 0000000000..0570aa0f2c --- /dev/null +++ b/docs/source/data/data_catalog_yaml_examples.md @@ -0,0 +1,408 @@ +# Data Catalog YAML examples + +This page contains a set of examples to help you structure your YAML configuration file in `conf/base/catalog.yml` or `conf/local/catalog.yml`. + +```{contents} Table of Contents +:depth: 3 +``` + +## Load data from a local binary file using `utf-8` encoding + +The `open_args_load` and `open_args_save` parameters are passed to the filesystem's `open` method to configure how a dataset file (on a specific filesystem) is opened during a load or save operation, respectively. + +```yaml +test_dataset: + type: ... + fs_args: + open_args_load: + mode: "rb" + encoding: "utf-8" +``` + +`load_args` and `save_args` configure how a third-party library (e.g. `pandas` for `CSVDataSet`) loads/saves data from/to a file. + +## Save data to a CSV file without row names (index) using `utf-8` encoding + +```yaml +test_dataset: + type: pandas.CSVDataSet + ... + save_args: + index: False + encoding: "utf-8" +``` + +## Load/save a CSV file from/to a local file system + +```yaml +bikes: + type: pandas.CSVDataSet + filepath: data/01_raw/bikes.csv +``` + +## Load/save a CSV on a local file system, using specified load/save arguments + +```yaml +cars: + type: pandas.CSVDataSet + filepath: data/01_raw/company/cars.csv + load_args: + sep: ',' + save_args: + index: False + date_format: '%Y-%m-%d %H:%M' + decimal: . + +``` + +## Load/save a compressed CSV on a local file system + +```yaml +boats: + type: pandas.CSVDataSet + filepath: data/01_raw/company/boats.csv.gz + load_args: + sep: ',' + compression: 'gzip' + fs_args: + open_args_load: + mode: 'rb' +``` + +## Load a CSV file from a specific S3 bucket, using credentials and load arguments + +```yaml +motorbikes: + type: pandas.CSVDataSet + filepath: s3://your_bucket/data/02_intermediate/company/motorbikes.csv + credentials: dev_s3 + load_args: + sep: ',' + skiprows: 5 + skipfooter: 1 + na_values: ['#NA', NA] +``` + +## Load/save a pickle file from/to a local file system + +```yaml +airplanes: + type: pickle.PickleDataSet + filepath: data/06_models/airplanes.pkl + backend: pickle +``` + +## Load an Excel file from Google Cloud Storage + +The example includes the `project` value for the underlying filesystem class (`GCSFileSystem`) within Google Cloud Storage (GCS) + +```yaml +rockets: + type: pandas.ExcelDataSet + filepath: gcs://your_bucket/data/02_intermediate/company/motorbikes.xlsx + fs_args: + project: my-project + credentials: my_gcp_credentials + save_args: + sheet_name: Sheet1 +``` + + +## Load a multi-sheet Excel file from a local file system + +```yaml +trains: + type: pandas.ExcelDataSet + filepath: data/02_intermediate/company/trains.xlsx + load_args: + sheet_name: [Sheet1, Sheet2, Sheet3] +``` + +## Save an image created with Matplotlib on Google Cloud Storage + +```yaml +results_plot: + type: matplotlib.MatplotlibWriter + filepath: gcs://your_bucket/data/08_results/plots/output_1.jpeg + fs_args: + project: my-project + credentials: my_gcp_credentials +``` + + +## Load/save an HDF file on local file system storage, using specified load/save arguments + +```yaml +skateboards: + type: pandas.HDFDataSet + filepath: data/02_intermediate/skateboards.hdf + key: name + load_args: + columns: [brand, length] + save_args: + mode: w # Overwrite even when the file already exists + dropna: True +``` + +## Load/save a parquet file on local file system storage, using specified load/save arguments + +```yaml +trucks: + type: pandas.ParquetDataSet + filepath: data/02_intermediate/trucks.parquet + load_args: + columns: [name, gear, disp, wt] + categories: list + index: name + save_args: + compression: GZIP + file_scheme: hive + has_nulls: False + partition_on: [name] +``` + + +## Load/save a Spark table on S3, using specified load/save arguments + +```yaml +weather: + type: spark.SparkDataSet + filepath: s3a://your_bucket/data/01_raw/weather* + credentials: dev_s3 + file_format: csv + load_args: + header: True + inferSchema: True + save_args: + sep: '|' + header: True +``` + + +## Load/save a SQL table using credentials, a database connection, and specified load/save arguments + +```yaml +scooters: + type: pandas.SQLTableDataSet + credentials: scooters_credentials + table_name: scooters + load_args: + index_col: [name] + columns: [name, gear] + save_args: + if_exists: replace +``` + +## Load a SQL table with credentials and a database connection, and apply a SQL query to the table + + +```yaml +scooters_query: + type: pandas.SQLQueryDataSet + credentials: scooters_credentials + sql: select * from cars where gear=4 + load_args: + index_col: [name] +``` + +When you use [`pandas.SQLTableDataSet`](/kedro_datasets.pandas.SQLTableDataSet) or [`pandas.SQLQueryDataSet`](/kedro_datasets.pandas.SQLQueryDataSet), you must provide a database connection string. In the above example, we pass it using the `scooters_credentials` key from the credentials. + +Note that `scooters_credentials` must have a top-level key `con` containing a [SQLAlchemy compatible](https://docs.sqlalchemy.org/en/13/core/engines.html#database-urls) connection string. As an alternative to credentials, you could explicitly put `con` into `load_args` and `save_args` (`pandas.SQLTableDataSet` only). + + +## Load data from an API endpoint + +This example uses US corn yield data from USDA. + +```yaml +us_corn_yield_data: + type: api.APIDataSet + url: https://quickstats.nass.usda.gov + credentials: usda_credentials + params: + key: SOME_TOKEN + format: JSON + commodity_desc: CORN + statisticcat_des: YIELD + agg_level_desc: STATE + year: 2000 +``` + +Note that `usda_credientials` will be passed as the `auth` argument in the `requests` library. Specify the username and password as a list in your `credentials.yml` file as follows: + +```yaml +usda_credentials: + - username + - password +``` + + +## Load data from Minio (S3 API Compatible Storage) + + +```yaml +test: + type: pandas.CSVDataSet + filepath: s3://your_bucket/test.csv # assume `test.csv` is uploaded to the Minio server. + credentials: dev_minio +``` +In `credentials.yml`, define the `key`, `secret` and the `endpoint_url` as follows: + +```yaml +dev_minio: + key: token + secret: key + client_kwargs: + endpoint_url : 'http://localhost:9000' +``` + +```{note} +The easiest way to setup MinIO is to run a Docker image. After the following command, you can access the Minio server with `http://localhost:9000` and create a bucket and add files as if it is on S3. +``` + +`docker run -p 9000:9000 -e "MINIO_ACCESS_KEY=token" -e "MINIO_SECRET_KEY=key" minio/minio server /data` + + +## Load a model saved as a pickle from Azure Blob Storage + +```yaml +ml_model: + type: pickle.PickleDataSet + filepath: "abfs://models/ml_models.pickle" + versioned: True + credentials: dev_abs +``` +In the `credentials.yml` file, define the `account_name` and `account_key`: + +```yaml +dev_abs: + account_name: accountname + account_key: key +``` + + +## Load a CSV file stored in a remote location through SSH + +```{note} +This example requires [Paramiko](https://www.paramiko.org) to be installed (`pip install paramiko`). +``` +```yaml +cool_dataset: + type: pandas.CSVDataSet + filepath: "sftp:///path/to/remote_cluster/cool_data.csv" + credentials: cluster_credentials +``` +All parameters required to establish the SFTP connection can be defined through `fs_args` or in the `credentials.yml` file as follows: + +```yaml +cluster_credentials: + username: my_username + host: host_address + port: 22 + password: password +``` +The list of all available parameters is given in the [Paramiko documentation](https://docs.paramiko.org/en/2.4/api/client.html#paramiko.client.SSHClient.connect). + +## Load multiple datasets with similar configuration using YAML anchors + +Different datasets might use the same file format, load and save arguments, and be stored in the same folder. [YAML has a built-in syntax](https://yaml.org/spec/1.2.1/#Syntax) for factorising parts of a YAML file, which means that you can decide what is generalisable across your datasets, so that you need not spend time copying and pasting dataset configurations in the `catalog.yml` file. + +You can see this in the following example: + +```yaml +_csv: &csv + type: spark.SparkDataSet + file_format: csv + load_args: + sep: ',' + na_values: ['#NA', NA] + header: True + inferSchema: False + +cars: + <<: *csv + filepath: s3a://data/01_raw/cars.csv + +trucks: + <<: *csv + filepath: s3a://data/01_raw/trucks.csv + +bikes: + <<: *csv + filepath: s3a://data/01_raw/bikes.csv + load_args: + header: False +``` + +The syntax `&csv` names the following block `csv` and the syntax `<<: *csv` inserts the contents of the block named `csv`. Locally declared keys entirely override inserted ones as seen in `bikes`. + +```{note} +It's important that the name of the template entry starts with a `_` so Kedro knows not to try and instantiate it as a dataset. +``` + +You can also nest reuseable YAML syntax: + +```yaml +_csv: &csv + type: spark.SparkDataSet + file_format: csv + load_args: &csv_load_args + header: True + inferSchema: False + +airplanes: + <<: *csv + filepath: s3a://data/01_raw/airplanes.csv + load_args: + <<: *csv_load_args + sep: ; +``` + +In this example, the default `csv` configuration is inserted into `airplanes` and then the `load_args` block is overridden. Normally, that would replace the whole dictionary. In order to extend `load_args`, the defaults for that block are then re-inserted. + +## Read the same file using two different datasets + +You might come across a situation where you would like to read the same file using two different dataset implementations (known as transcoding). For example, Parquet files can not only be loaded via the `ParquetDataSet` using `pandas`, but also directly by `SparkDataSet`. This conversion is typical when coordinating a `Spark` to `pandas` workflow. + +Define two `DataCatalog` entries for the same dataset in a common format (Parquet, JSON, CSV, etc.) in your `conf/base/catalog.yml`: + +```yaml +my_dataframe@spark: + type: spark.SparkDataSet + filepath: data/02_intermediate/data.parquet + file_format: parquet + +my_dataframe@pandas: + type: pandas.ParquetDataSet + filepath: data/02_intermediate/data.parquet +``` + +These entries are used in the pipeline like this: + +```python +pipeline( + [ + node(func=my_func1, inputs="spark_input", outputs="my_dataframe@spark"), + node(func=my_func2, inputs="my_dataframe@pandas", outputs="pipeline_output"), + ] +) +``` + +In this example, Kedro understands that `my_dataframe` is the same dataset in its `spark.SparkDataSet` and `pandas.ParquetDataSet` formats and resolves the node execution order. + +In the pipeline, Kedro uses the `spark.SparkDataSet` implementation for saving and `pandas.ParquetDataSet` +for loading, so the first node outputs a `pyspark.sql.DataFrame`, while the second node receives a `pandas.Dataframe`. + +## Create a Data Catalog YAML configuration file via the CLI + +You can use the [`kedro catalog create` command to create a Data Catalog YAML configuration](../development/commands_reference.md#create-a-data-catalog-yaml-configuration-file). + +This creates a `//catalog/.yml` configuration file with `MemoryDataSet` datasets for each dataset in a registered pipeline if it is missing from the `DataCatalog`. + +```yaml +# //catalog/.yml +rockets: + type: MemoryDataSet +scooters: + type: MemoryDataSet +``` diff --git a/docs/source/extend_kedro/custom_datasets.md b/docs/source/data/how_to_create_a_custom_dataset.md similarity index 93% rename from docs/source/extend_kedro/custom_datasets.md rename to docs/source/data/how_to_create_a_custom_dataset.md index c0aad914da..86010b4f18 100644 --- a/docs/source/extend_kedro/custom_datasets.md +++ b/docs/source/data/how_to_create_a_custom_dataset.md @@ -1,7 +1,12 @@ -# Custom datasets +# Advanced: Tutorial to create a custom dataset [Kedro supports many datasets](/kedro_datasets) out of the box, but you may find that you need to create a custom dataset. For example, you may need to handle a proprietary data format or filesystem in your pipeline, or perhaps you have found a particular use case for a dataset that Kedro does not support. This tutorial explains how to create a custom dataset to read and save image data. +## AbstractDataSet + +For contributors, if you would like to submit a new dataset, you must extend the [`AbstractDataSet` interface](/kedro.io.AbstractDataset) or [`AbstractVersionedDataSet` interface](/kedro.io.AbstractVersionedDataset) if you plan to support versioning. It requires subclasses to override the `_load` and `_save` and provides `load` and `save` methods that enrich the corresponding private methods with uniform error handling. It also requires subclasses to override `_describe`, which is used in logging the internal information about the instances of your custom `AbstractDataSet` implementation. + + ## Scenario In this example, we use a [Kaggle dataset of Pokémon images and types](https://www.kaggle.com/vishalsubbiah/pokemon-images-and-types) to train a model to classify the type of a given [Pokémon](https://en.wikipedia.org/wiki/Pok%C3%A9mon), e.g. Water, Fire, Bug, etc., based on its appearance. To train the model, we read the Pokémon images from PNG files into `numpy` arrays before further manipulation in the Kedro pipeline. To work with PNG images out of the box, in this example we create an `ImageDataSet` to read and save image data. @@ -93,7 +98,7 @@ src/kedro_pokemon/extras ## Implement the `_load` method with `fsspec` -Many of the built-in Kedro datasets rely on [fsspec](https://filesystem-spec.readthedocs.io/en/latest/) as a consistent interface to different data sources, as described earlier in the section about the [Data Catalog](../data/data_catalog.md#specify-the-location-of-the-dataset). In this example, it's particularly convenient to use `fsspec` in conjunction with `Pillow` to read image data, since it allows the dataset to work flexibly with different image locations and formats. +Many of the built-in Kedro datasets rely on [fsspec](https://filesystem-spec.readthedocs.io/en/latest/) as a consistent interface to different data sources, as described earlier in the section about the [Data Catalog](../data/data_catalog.md#dataset-filepath). In this example, it's particularly convenient to use `fsspec` in conjunction with `Pillow` to read image data, since it allows the dataset to work flexibly with different image locations and formats. Here is the implementation of the `_load` method using `fsspec` and `Pillow` to read the data of a single image into a `numpy` array: @@ -266,7 +271,7 @@ class ImageDataSet(AbstractDataset[np.ndarray, np.ndarray]): Currently, the `ImageDataSet` only works with a single image, but this example needs to load all Pokemon images from the raw data directory for further processing. -Kedro's [`PartitionedDataSet`](../data/kedro_io.md#partitioned-dataset) is a convenient way to load multiple separate data files of the same underlying dataset type into a directory. +Kedro's [`PartitionedDataSet`](./partitioned_and_incremental_datasets.md) is a convenient way to load multiple separate data files of the same underlying dataset type into a directory. To use `PartitionedDataSet` with `ImageDataSet` to load all Pokemon PNG images, add this to the data catalog YAML so that `PartitionedDataSet` loads all PNG files from the data directory using `ImageDataSet`: @@ -297,11 +302,14 @@ $ ls -la data/01_raw/pokemon-images-and-types/images/images/*.png | wc -l ## Versioning +### How to implement versioning in your dataset + ```{note} Versioning doesn't work with `PartitionedDataSet`. You can't use both of them at the same time. ``` -To add [Versioning](../data/kedro_io.md#versioning) support to the new dataset we need to extend the - [AbstractVersionedDataset](/kedro.io.AbstractVersionedDataset) to: + +To add versioning support to the new dataset we need to extend the + [AbstractVersionedDataSet](/kedro.io.AbstractVersionedDataset) to: * Accept a `version` keyword argument as part of the constructor * Adapt the `_save` and `_load` method to use the versioned data path obtained from `_get_save_path` and `_get_load_path` respectively @@ -498,7 +506,6 @@ In [2]: context.catalog.save('pikachu', data=img) Inspect the content of the data directory to find a new version of the data, written by `save`. -You may also want to consult the [in-depth documentation about the Versioning API](../data/kedro_io.md#versioning). ## Thread-safety @@ -562,7 +569,7 @@ class ImageDataSet(AbstractVersionedDataset): ... ``` -We provide additional examples of [how to use parameters through the data catalog's YAML API](../data/data_catalog.md#use-the-data-catalog-with-the-yaml-api). For an example of how to use these parameters in your dataset's constructor, please see the [SparkDataSet](/kedro_datasets.spark.SparkDataSet)'s implementation. +We provide additional examples of [how to use parameters through the data catalog's YAML API](./data_catalog_yaml_examples.md). For an example of how to use these parameters in your dataset's constructor, please see the [SparkDataSet](/kedro_datasets.spark.SparkDataSet)'s implementation. ## How to contribute a custom dataset implementation diff --git a/docs/source/data/index.md b/docs/source/data/index.md index 00c05353fc..b90a3d9961 100644 --- a/docs/source/data/index.md +++ b/docs/source/data/index.md @@ -1,8 +1,49 @@ -# Data Catalog + +# The Kedro Data Catalog + +In a Kedro project, the Data Catalog is a registry of all data sources available for use by the project. The catalog is stored in a YAML file (`catalog.yml`) that maps the names of node inputs and outputs as keys in the `DataCatalog` class. + +[Kedro provides different built-in datasets in the `kedro-datasets` package](/kedro_datasets) for numerous file types and file systems, so you don’t have to write any of the logic for reading/writing data. + + +We first introduce the basic sections of `catalog.yml`, which is the file used to register data sources for a Kedro project. ```{toctree} :maxdepth: 1 data_catalog -kedro_io +``` + +The following page offers a range of examples of YAML specification for various Data Catalog use cases: + +```{toctree} +:maxdepth: 1 + +data_catalog_yaml_examples +``` + +Once you are familiar with the format of `catalog.yml`, you may find your catalog gets repetitive if you need to load multiple datasets with similar configuration. From Kedro 0.18.12 you can use dataset factories to generalise the configuration and reduce the number of similar catalog entries. This works by by matching datasets used in your project’s pipelines to dataset factory patterns and is explained in a new page about Kedro dataset factories: + + +```{toctree} +:maxdepth: 1 + +kedro_dataset_factories +``` + +Further pages describe more advanced concepts: + +```{toctree} +:maxdepth: 1 + +advanced_data_catalog_usage +partitioned_and_incremental_datasets +``` + +This section on handing data with Kedro concludes with an advanced use case, illustrated with a tutorial that explains how to create your own custom dataset: + +```{toctree} +:maxdepth: 1 + +how_to_create_a_custom_dataset ``` diff --git a/docs/source/data/kedro_dataset_factories.md b/docs/source/data/kedro_dataset_factories.md new file mode 100644 index 0000000000..693272c013 --- /dev/null +++ b/docs/source/data/kedro_dataset_factories.md @@ -0,0 +1,385 @@ +# Kedro dataset factories +You can load multiple datasets with similar configuration using dataset factories, introduced in Kedro 0.18.12. + +The syntax allows you to generalise the configuration and reduce the number of similar catalog entries by matching datasets used in your project's pipelines to dataset factory patterns. + +## How to generalise datasets with similar names and types + +Consider the following catalog entries: + +```yaml +factory_data: + type: pandas.CSVDataSet + filepath: data/01_raw/factory_data.csv + + +process_data: + type: pandas.CSVDataSet + filepath: data/01_raw/process_data.csv +``` + +The datasets in this catalog can be generalised to the following dataset factory: + +```yaml +"{name}_data": + type: pandas.CSVDataSet + filepath: data/01_raw/{name}_data.csv +``` + +When `factory_data` or `process_data` is used in your pipeline, it is matched to the factory pattern `{name}_data`. The factory pattern must always be enclosed in +quotes to avoid YAML parsing errors. + + +## How to generalise datasets of the same type + +You can also combine all the datasets with the same type and configuration details. For example, consider the following +catalog with three datasets named `boats`, `cars` and `planes` of the type `pandas.CSVDataSet`: + +```yaml +boats: + type: pandas.CSVDataSet + filepath: data/01_raw/shuttles.csv + +cars: + type: pandas.CSVDataSet + filepath: data/01_raw/reviews.csv + +planes: + type: pandas.CSVDataSet + filepath: data/01_raw/companies.csv +``` + +These datasets can be combined into the following dataset factory: + +```yaml +"{dataset_name}#csv": + type: pandas.CSVDataSet + filepath: data/01_raw/{dataset_name}.csv +``` + +You will then have to update the pipelines in your project located at `src///pipeline.py` to refer to these datasets as `boats#csv`, +`cars#csv` and `planes#csv`. Adding a suffix or a prefix to the dataset names and the dataset factory patterns, like `#csv` here, ensures that the dataset +names are matched with the intended pattern. + +```python +from .nodes import create_model_input_table, preprocess_companies, preprocess_shuttles + + +def create_pipeline(**kwargs) -> Pipeline: + return pipeline( + [ + node( + func=preprocess_boats, + inputs="boats#csv", + outputs="preprocessed_boats", + name="preprocess_boats_node", + ), + node( + func=preprocess_cars, + inputs="cars#csv", + outputs="preprocessed_cars", + name="preprocess_cars_node", + ), + node( + func=preprocess_planes, + inputs="planes#csv", + outputs="preprocessed_planes", + name="preprocess_planes_node", + ), + node( + func=create_model_input_table, + inputs=[ + "preprocessed_boats", + "preprocessed_planes", + "preprocessed_cars", + ], + outputs="model_input_table", + name="create_model_input_table_node", + ), + ] + ) +``` +## How to generalise datasets using namespaces + +You can also generalise the catalog entries for datasets belonging to namespaced modular pipelines. Consider the +following pipeline which takes in a `model_input_table` and outputs two regressors belonging to the +`active_modelling_pipeline` and the `candidate_modelling_pipeline` namespaces: + +```python +from kedro.pipeline import Pipeline, node +from kedro.pipeline.modular_pipeline import pipeline + +from .nodes import evaluate_model, split_data, train_model + + +def create_pipeline(**kwargs) -> Pipeline: + pipeline_instance = pipeline( + [ + node( + func=split_data, + inputs=["model_input_table", "params:model_options"], + outputs=["X_train", "y_train"], + name="split_data_node", + ), + node( + func=train_model, + inputs=["X_train", "y_train"], + outputs="regressor", + name="train_model_node", + ), + ] + ) + ds_pipeline_1 = pipeline( + pipe=pipeline_instance, + inputs="model_input_table", + namespace="active_modelling_pipeline", + ) + ds_pipeline_2 = pipeline( + pipe=pipeline_instance, + inputs="model_input_table", + namespace="candidate_modelling_pipeline", + ) + + return ds_pipeline_1 + ds_pipeline_2 +``` +You can now have one dataset factory pattern in your catalog instead of two separate entries for `active_modelling_pipeline.regressor` +and `candidate_modelling_pipeline.regressor` as below: + +```yaml +{namespace}.regressor: + type: pickle.PickleDataSet + filepath: data/06_models/regressor_{namespace}.pkl + versioned: true +``` +## How to generalise datasets of the same type in different layers + +You can use multiple placeholders in the same pattern. For example, consider the following catalog where the dataset +entries share `type`, `file_format` and `save_args`: + +```yaml +processing.factory_data: + type: spark.SparkDataSet + filepath: data/processing/factory_data.pq + file_format: parquet + save_args: + mode: overwrite + +processing.process_data: + type: spark.SparkDataSet + filepath: data/processing/process_data.pq + file_format: parquet + save_args: + mode: overwrite + +modelling.metrics: + type: spark.SparkDataSet + filepath: data/modelling/factory_data.pq + file_format: parquet + save_args: + mode: overwrite +``` + +This could be generalised to the following pattern: + +```yaml +"{layer}.{dataset_name}": + type: spark.SparkDataSet + filepath: data/{layer}/{dataset_name}.pq + file_format: parquet + save_args: + mode: overwrite +``` +All the placeholders used in the catalog entry body must exist in the factory pattern name. + +## How to generalise datasets using multiple dataset factories +You can have multiple dataset factories in your catalog. For example: + +```yaml +"{namespace}.{dataset_name}@spark": + type: spark.SparkDataSet + filepath: data/{namespace}/{dataset_name}.pq + file_format: parquet + +"{dataset_name}@csv": + type: pandas.CSVDataSet + filepath: data/01_raw/{dataset_name}.csv +``` + +Having multiple dataset factories in your catalog can lead to a situation where a dataset name from your pipeline might +match multiple patterns. To overcome this, Kedro sorts all the potential matches for the dataset name in the pipeline and picks the best match. +The matches are ranked according to the following criteria: + +1. Number of exact character matches between the dataset name and the factory pattern. For example, a dataset named `factory_data$csv` would match `{dataset}_data$csv` over `{dataset_name}$csv`. +2. Number of placeholders. For example, the dataset `preprocessing.shuttles+csv` would match `{namespace}.{dataset}+csv` over `{dataset}+csv`. +3. Alphabetical order + +## How to override the default dataset creation with dataset factories + +You can use dataset factories to define a catch-all pattern which will overwrite the default [`MemoryDataSet`](/kedro.io.MemoryDataset) creation. + +```yaml +"{default_dataset}": + type: pandas.CSVDataSet + filepath: data/{default_dataset}.csv + +``` +Kedro will now treat all the datasets mentioned in your project's pipelines that do not appear as specific patterns or explicit entries in your catalog +as `pandas.CSVDataSet`. + +## CLI commands for dataset factories + +To manage your dataset factories, two new commands have been added to the Kedro CLI: `kedro catalog rank` (0.18.12) and `kedro catalog resolve` (0.18.13). + +### How to use `kedro catalog rank` + +This command outputs a list of all dataset factories in the catalog, ranked in the order by which pipeline datasets are matched against them. The ordering is determined by the following criteria: + +1. The number of non-placeholder characters in the pattern +2. The number of placeholders in the pattern +3. Alphabetic ordering + +Consider a catalog file with the following patterns: + +
+Click to expand + +```yaml +"{layer}.{dataset_name}": + type: pandas.CSVDataSet + filepath: data/{layer}/{dataset_name}.csv + +preprocessed_{dataset_name}: + type: pandas.ParquetDataSet + filepath: data/02_intermediate/preprocessed_{dataset_name}.pq + +processed_{dataset_name}: + type: pandas.ParquetDataSet + filepath: data/03_primary/processed_{dataset_name}.pq + +"{dataset_name}_csv": + type: pandas.CSVDataSet + filepath: data/03_primary/{dataset_name}.csv + +"{namespace}.{dataset_name}_pq": + type: pandas.ParquetDataSet + filepath: data/03_primary/{dataset_name}_{namespace}.pq + +"{default_dataset}": + type: pickle.PickleDataSet + filepath: data/01_raw/{default_dataset}.pickle +``` +
+ +Running `kedro catalog rank` will result in the following output: + +``` +- preprocessed_{dataset_name} +- processed_{dataset_name} +- '{namespace}.{dataset_name}_pq' +- '{dataset_name}_csv' +- '{layer}.{dataset_name}' +- '{default_dataset}' +``` + +As we can see, the entries are ranked firstly by how many non-placeholders are in the pattern, in descending order. Where two entries have the same number of non-placeholder characters, `{namespace}.{dataset_name}_pq` and `{dataset_name}_csv` with four each, they are then ranked by the number of placeholders, also in decreasing order. `{default_dataset}` is the least specific pattern possible, and will always be matched against last. + +### How to use `kedro catalog resolve` + +This command resolves dataset patterns in the catalog against any explicit dataset entries in the project pipeline. The resulting output contains all explicit dataset entries in the catalog and any dataset in the default pipeline that resolves some dataset pattern. + +To illustrate this, consider the following catalog file: + +
+Click to expand + +```yaml +companies: + type: pandas.CSVDataSet + filepath: data/01_raw/companies.csv + +reviews: + type: pandas.CSVDataSet + filepath: data/01_raw/reviews.csv + +shuttles: + type: pandas.ExcelDataSet + filepath: data/01_raw/shuttles.xlsx + load_args: + engine: openpyxl # Use modern Excel engine, it is the default since Kedro 0.18.0 + +preprocessed_{name}: + type: pandas.ParquetDataSet + filepath: data/02_intermediate/preprocessed_{name}.pq + +"{default}": + type: pandas.ParquetDataSet + filepath: data/03_primary/{default}.pq +``` +
+ +and the following pipeline in `pipeline.py`: + +
+Click to expand + +```python +def create_pipeline(**kwargs) -> Pipeline: + return pipeline( + [ + node( + func=preprocess_companies, + inputs="companies", + outputs="preprocessed_companies", + name="preprocess_companies_node", + ), + node( + func=preprocess_shuttles, + inputs="shuttles", + outputs="preprocessed_shuttles", + name="preprocess_shuttles_node", + ), + node( + func=create_model_input_table, + inputs=["preprocessed_shuttles", "preprocessed_companies", "reviews"], + outputs="model_input_table", + name="create_model_input_table_node", + ), + ] + ) +``` +
+ +The resolved catalog output by the command will be as follows: + +
+Click to expand + +```yaml +companies: + filepath: data/01_raw/companies.csv + type: pandas.CSVDataSet +model_input_table: + filepath: data/03_primary/model_input_table.pq + type: pandas.ParquetDataSet +preprocessed_companies: + filepath: data/02_intermediate/preprocessed_companies.pq + type: pandas.ParquetDataSet +preprocessed_shuttles: + filepath: data/02_intermediate/preprocessed_shuttles.pq + type: pandas.ParquetDataSet +reviews: + filepath: data/01_raw/reviews.csv + type: pandas.CSVDataSet +shuttles: + filepath: data/01_raw/shuttles.xlsx + load_args: + engine: openpyxl + type: pandas.ExcelDataSet +``` +
+ +By default this is output to the terminal. However, if you wish to output the resolved catalog to a specific file, you can use the redirection operator `>`: + +```bash +kedro catalog resolve > output_file.yaml +``` diff --git a/docs/source/data/kedro_io.md b/docs/source/data/partitioned_and_incremental_datasets.md similarity index 62% rename from docs/source/data/kedro_io.md rename to docs/source/data/partitioned_and_incremental_datasets.md index a38ea97fcb..7e48c23137 100644 --- a/docs/source/data/kedro_io.md +++ b/docs/source/data/partitioned_and_incremental_datasets.md @@ -1,245 +1,9 @@ -# Kedro IO +# Advanced: Partitioned and incremental datasets +## Partitioned datasets -In this tutorial, we cover advanced uses of [the Kedro IO module](/kedro.io) to understand the underlying implementation. The relevant API documentation is [kedro.io.AbstractDataset](/kedro.io.AbstractDataset) and [kedro.io.DataSetError](/kedro.io.DataSetError). +Distributed systems play an increasingly important role in ETL data pipelines. They significantly increase the processing throughput, enabling us to work with much larger volumes of input data. However, these benefits sometimes come at a cost. When dealing with the input data generated by such distributed systems, you might encounter a situation where your Kedro node needs to read the data from a directory full of uniform files of the same type (e.g. JSON, CSV, Parquet, etc.) rather than from a single file. Tools like `PySpark` and the corresponding [SparkDataSet](/kedro_datasets.spark.SparkDataSet) cater for such use cases, but the use of Spark is not always feasible. -## Error handling - -We have custom exceptions for the main classes of errors that you can handle to deal with failures. - -```python -from kedro.io import * -``` - -```python -io = DataCatalog(data_sets=dict()) # empty catalog - -try: - cars_df = io.load("cars") -except DataSetError: - print("Error raised.") -``` - - -## AbstractDataset - -To understand what is going on behind the scenes, you should study the [AbstractDataset interface](/kedro.io.AbstractDataset). `AbstractDataset` is the underlying interface that all datasets extend. It requires subclasses to override the `_load` and `_save` and provides `load` and `save` methods that enrich the corresponding private methods with uniform error handling. It also requires subclasses to override `_describe`, which is used in logging the internal information about the instances of your custom `AbstractDataset` implementation. - -If you have a dataset called `parts`, you can make direct calls to it like so: - -```python -parts_df = parts.load() -``` - -We recommend using a `DataCatalog` instead (for more details, see [the `DataCatalog` documentation](../data/data_catalog.md)) as it has been designed to make all datasets available to project members. - -For contributors, if you would like to submit a new dataset, you must extend the `AbstractDataset`. For a complete guide, please read [the section on custom datasets](../extend_kedro/custom_datasets.md). - - -## Versioning - -In order to enable versioning, you need to update the `catalog.yml` config file and set the `versioned` attribute to `true` for the given dataset. If this is a custom dataset, the implementation must also: - 1. extend `kedro.io.core.AbstractVersionedDataset` AND - 2. add `version` namedtuple as an argument to its `__init__` method AND - 3. call `super().__init__()` with positional arguments `filepath`, `version`, and, optionally, with `glob` and `exists` functions if it uses a non-local filesystem (see [kedro_datasets.pandas.CSVDataSet](/kedro_datasets.pandas.CSVDataSet) as an example) AND - 4. modify its `_describe`, `_load` and `_save` methods respectively to support versioning (see [`kedro_datasets.pandas.CSVDataSet`](/kedro_datasets.pandas.CSVDataSet) for an example implementation) - -```{note} -If a new version of a dataset is created mid-run, for instance by an external system adding new files, it will not interfere in the current run, i.e. the load version stays the same throughout subsequent loads. -``` - -An example dataset could look similar to the below: - -```python -from pathlib import Path, PurePosixPath - -import pandas as pd - -from kedro.io import AbstractVersionedDataset - - -class MyOwnDataSet(AbstractVersionedDataset): - def __init__(self, filepath, version, param1, param2=True): - super().__init__(PurePosixPath(filepath), version) - self._param1 = param1 - self._param2 = param2 - - def _load(self) -> pd.DataFrame: - load_path = self._get_load_path() - return pd.read_csv(load_path) - - def _save(self, df: pd.DataFrame) -> None: - save_path = self._get_save_path() - df.to_csv(save_path) - - def _exists(self) -> bool: - path = self._get_load_path() - return Path(path).exists() - - def _describe(self): - return dict(version=self._version, param1=self._param1, param2=self._param2) -``` - -With `catalog.yml` specifying: - -```yaml -my_dataset: - type: .MyOwnDataSet - filepath: data/01_raw/my_data.csv - versioned: true - param1: # param1 is a required argument - # param2 will be True by default -``` - -### `version` namedtuple - -Versioned dataset `__init__` method must have an optional argument called `version` with a default value of `None`. If provided, this argument must be an instance of [`kedro.io.core.Version`](/kedro.io.Version). Its `load` and `save` attributes must either be `None` or contain string values representing exact load and save versions: - -* If `version` is `None`, then the dataset is considered *not versioned*. -* If `version.load` is `None`, then the latest available version will be used to load the dataset, otherwise a string representing exact load version must be provided. -* If `version.save` is `None`, then a new save version string will be generated by calling `kedro.io.core.generate_timestamp()`, otherwise a string representing the exact save version must be provided. - -### Versioning using the YAML API - -The easiest way to version a specific dataset is to change the corresponding entry in the `catalog.yml` file. For example, if the following dataset was defined in the `catalog.yml` file: - -```yaml -cars: - type: pandas.CSVDataSet - filepath: data/01_raw/company/car_data.csv - versioned: true -``` - -The `DataCatalog` will create a versioned `CSVDataSet` called `cars`. The actual csv file location will look like `data/01_raw/company/car_data.csv//car_data.csv`, where `` corresponds to a global save version string formatted as `YYYY-MM-DDThh.mm.ss.sssZ`. Every time the `DataCatalog` is instantiated, it generates a new global save version, which is propagated to all versioned datasets it contains. - -The `catalog.yml` file only allows you to version your datasets, but does not allow you to choose which version to load or save. This is deliberate because we have chosen to separate the data catalog from any runtime configuration. If you need to pin a dataset version, you can either [specify the versions in a separate `yml` file and call it at runtime](../nodes_and_pipelines/run_a_pipeline.md#configure-kedro-run-arguments) or [instantiate your versioned datasets using Code API and define a version parameter explicitly](#versioning-using-the-code-api). - -By default, the `DataCatalog` will load the latest version of the dataset. However, you can also specify an exact load version. In order to do that, pass a dictionary with exact load versions to `DataCatalog.from_config`: - -```python -load_versions = {"cars": "2019-02-13T14.35.36.518Z"} -io = DataCatalog.from_config(catalog_config, credentials, load_versions=load_versions) -cars = io.load("cars") -``` - -The last row in the example above would attempt to load a CSV file from `data/01_raw/company/car_data.csv/2019-02-13T14.35.36.518Z/car_data.csv`: - -* `load_versions` configuration has an effect only if a dataset versioning has been enabled in the catalog config file - see the example above. - -* We recommend that you do not override `save_version` argument in `DataCatalog.from_config` unless strongly required to do so, since it may lead to inconsistencies between loaded and saved versions of the versioned datasets. - -```{warning} -The `DataCatalog` does not re-generate save versions between instantiations. Therefore, if you call `catalog.save('cars', some_data)` twice, then the second call will fail, since it tries to overwrite a versioned dataset using the same save version. To mitigate this, reload your data catalog by calling `%reload_kedro` line magic. This limitation does not apply to `load` operation. -``` - -### Versioning using the Code API - -Although we recommend enabling versioning using the `catalog.yml` config file as described in the section above, you might require more control over load and save versions of a specific dataset. To achieve this, you can instantiate `Version` and pass it as a parameter to the dataset initialisation: - -```python -from kedro.io import DataCatalog, Version -from kedro_datasets.pandas import CSVDataSet -import pandas as pd - -data1 = pd.DataFrame({"col1": [1, 2], "col2": [4, 5], "col3": [5, 6]}) -data2 = pd.DataFrame({"col1": [7], "col2": [8], "col3": [9]}) -version = Version( - load=None, # load the latest available version - save=None, # generate save version automatically on each save operation -) - -test_data_set = CSVDataSet( - filepath="data/01_raw/test.csv", save_args={"index": False}, version=version -) -io = DataCatalog({"test_data_set": test_data_set}) - -# save the dataset to data/01_raw/test.csv//test.csv -io.save("test_data_set", data1) -# save the dataset into a new file data/01_raw/test.csv//test.csv -io.save("test_data_set", data2) - -# load the latest version from data/test.csv/*/test.csv -reloaded = io.load("test_data_set") -assert data2.equals(reloaded) -``` - -```{note} -In the example above, we did not fix any versions. If we do, then the behaviour of load and save operations becomes slightly different: -``` - -```python -version = Version( - load="my_exact_version", # load exact version - save="my_exact_version", # save to exact version -) - -test_data_set = CSVDataSet( - filepath="data/01_raw/test.csv", save_args={"index": False}, version=version -) -io = DataCatalog({"test_data_set": test_data_set}) - -# save the dataset to data/01_raw/test.csv/my_exact_version/test.csv -io.save("test_data_set", data1) -# load from data/01_raw/test.csv/my_exact_version/test.csv -reloaded = io.load("test_data_set") -assert data1.equals(reloaded) - -# raises DataSetError since the path -# data/01_raw/test.csv/my_exact_version/test.csv already exists -io.save("test_data_set", data2) -``` - -```{warning} -We do not recommend passing exact load and/or save versions, since it might lead to inconsistencies between operations. For example, if versions for load and save operations do not match, a save operation would result in a `UserWarning` indicating that save and load versions do not match. Load after save might also return an error if the corresponding load version is not found: -``` - -```python -version = Version( - load="exact_load_version", # load exact version - save="exact_save_version", # save to exact version -) - -test_data_set = CSVDataSet( - filepath="data/01_raw/test.csv", save_args={"index": False}, version=version -) -io = DataCatalog({"test_data_set": test_data_set}) - -io.save("test_data_set", data1) # emits a UserWarning due to version inconsistency - -# raises DataSetError since the data/01_raw/test.csv/exact_load_version/test.csv -# file does not exist -reloaded = io.load("test_data_set") -``` - -### Supported datasets - -Currently, the following datasets support versioning: - -- `kedro_datasets.matplotlib.MatplotlibWriter` -- `kedro_datasets.holoviews.HoloviewsWriter` -- `kedro_datasets.networkx.NetworkXDataSet` -- `kedro_datasets.pandas.CSVDataSet` -- `kedro_datasets.pandas.ExcelDataSet` -- `kedro_datasets.pandas.FeatherDataSet` -- `kedro_datasets.pandas.HDFDataSet` -- `kedro_datasets.pandas.JSONDataSet` -- `kedro_datasets.pandas.ParquetDataSet` -- `kedro_datasets.pickle.PickleDataSet` -- `kedro_datasets.pillow.ImageDataSet` -- `kedro_datasets.text.TextDataSet` -- `kedro_datasets.spark.SparkDataSet` -- `kedro_datasets.yaml.YAMLDataSet` -- `kedro_datasets.api.APIDataSet` -- `kedro_datasets.tensorflow.TensorFlowModelDataSet` -- `kedro_datasets.json.JSONDataSet` - -```{note} -Although HTTP(S) is a supported file system in the dataset implementations, it does not support versioning. -``` - -## Partitioned dataset - -These days, distributed systems play an increasingly important role in ETL data pipelines. They significantly increase the processing throughput, enabling us to work with much larger volumes of input data. However, these benefits sometimes come at a cost. When dealing with the input data generated by such distributed systems, you might encounter a situation where your Kedro node needs to read the data from a directory full of uniform files of the same type (e.g. JSON, CSV, Parquet, etc.) rather than from a single file. Tools like `PySpark` and the corresponding [SparkDataSet](/kedro_datasets.spark.SparkDataSet) cater for such use cases, but the use of Spark is not always feasible. This is why Kedro provides a built-in [PartitionedDataSet](/kedro.io.PartitionedDataSet), with the following features: @@ -252,9 +16,9 @@ This is why Kedro provides a built-in [PartitionedDataSet](/kedro.io.Partitioned In this section, each individual file inside a given location is called a partition. ``` -### Partitioned dataset definition +### How to use `PartitionedDataSet` -`PartitionedDataSet` definition can be put in your `catalog.yml` file like any other regular dataset definition. The definition represents the following structure: +You can use a `PartitionedDataSet` in `catalog.yml` file like any other regular dataset definition: ```yaml # conf/base/catalog.yml @@ -320,22 +84,22 @@ Here is an exhaustive list of the arguments supported by `PartitionedDataSet`: | `filepath_arg` | No | `str` (defaults to `filepath`) | Argument name of the underlying dataset initializer that will contain a path to an individual partition | | `filename_suffix` | No | `str` (defaults to an empty string) | If specified, partitions that don't end with this string will be ignored | -#### Dataset definition +### Dataset definition -Dataset definition should be passed into the `dataset` argument of the `PartitionedDataSet`. The dataset definition is used to instantiate a new dataset object for each individual partition, and use that dataset object for load and save operations. Dataset definition supports shorthand and full notations. +The dataset definition should be passed into the `dataset` argument of the `PartitionedDataSet`. The dataset definition is used to instantiate a new dataset object for each individual partition, and use that dataset object for load and save operations. Dataset definition supports shorthand and full notations. -##### Shorthand notation +#### Shorthand notation Requires you only to specify a class of the underlying dataset either as a string (e.g. `pandas.CSVDataSet` or a fully qualified class path like `kedro_datasets.pandas.CSVDataSet`) or as a class object that is a subclass of the [AbstractDataset](/kedro.io.AbstractDataset). -##### Full notation +#### Full notation Full notation allows you to specify a dictionary with the full underlying dataset definition _except_ the following arguments: * The argument that receives the partition path (`filepath` by default) - if specified, a `UserWarning` will be emitted stating that this value will be overridden by individual partition paths * `credentials` key - specifying it will result in a `DataSetError` being raised; dataset credentials should be passed into the `credentials` argument of the `PartitionedDataSet` rather than the underlying dataset definition - see the section below on [partitioned dataset credentials](#partitioned-dataset-credentials) for details * `versioned` flag - specifying it will result in a `DataSetError` being raised; versioning cannot be enabled for the underlying datasets -#### Partitioned dataset credentials +### Partitioned dataset credentials ```{note} Support for `dataset_credentials` key in the credentials for `PartitionedDataSet` is now deprecated. The dataset credentials should be specified explicitly inside the dataset config. @@ -414,7 +178,7 @@ new_partitioned_dataset: filename_suffix: ".csv" ``` -node definition: +Here is the node definition: ```python from kedro.pipeline import node @@ -422,7 +186,7 @@ from kedro.pipeline import node node(create_partitions, inputs=None, outputs="new_partitioned_dataset") ``` -and underlying node function `create_partitions`: +The underlying node function is as follows in `create_partitions`: ```python from typing import Any, Dict @@ -449,6 +213,7 @@ Writing to an existing partition may result in its data being overwritten, if th ### Partitioned dataset lazy saving `PartitionedDataSet` also supports lazy saving, where the partition's data is not materialised until it is time to write. + To use this, simply return `Callable` types in the dictionary: ```python @@ -473,8 +238,7 @@ def create_partitions() -> Dict[str, Callable[[], Any]]: ```{note} When using lazy saving, the dataset will be written _after_ the `after_node_run` [hook](../hooks/introduction). ``` - -### Incremental loads with `IncrementalDataSet` +## Incremental datasets [IncrementalDataSet](/kedro.io.IncrementalDataSet) is a subclass of `PartitionedDataSet`, which stores the information about the last processed partition in the so-called `checkpoint`. `IncrementalDataSet` addresses the use case when partitions have to be processed incrementally, i.e. each subsequent pipeline run should only process the partitions which were not processed by the previous runs. @@ -482,17 +246,17 @@ This checkpoint, by default, is persisted to the location of the data partitions The checkpoint file is only created _after_ [the partitioned dataset is explicitly confirmed](#incremental-dataset-confirm). -#### Incremental dataset load +### Incremental dataset loads Loading `IncrementalDataSet` works similarly to [`PartitionedDataSet`](#partitioned-dataset-load) with several exceptions: 1. `IncrementalDataSet` loads the data _eagerly_, so the values in the returned dictionary represent the actual data stored in the corresponding partition, rather than a pointer to the load function. `IncrementalDataSet` considers a partition relevant for processing if its ID satisfies the comparison function, given the checkpoint value. 2. `IncrementalDataSet` _does not_ raise a `DataSetError` if load finds no partitions to return - an empty dictionary is returned instead. An empty list of available partitions is part of a normal workflow for `IncrementalDataSet`. -#### Incremental dataset save +### Incremental dataset save The `IncrementalDataSet` save operation is identical to the [save operation of the `PartitionedDataSet`](#partitioned-dataset-save). -#### Incremental dataset confirm +### Incremental dataset confirm ```{note} The checkpoint value *is not* automatically updated when a new set of partitions is successfully loaded or saved. @@ -549,7 +313,7 @@ Important notes about the confirmation operation: * A pipeline cannot contain more than one node confirming the same dataset. -#### Checkpoint configuration +### Checkpoint configuration `IncrementalDataSet` does not require explicit configuration of the checkpoint unless there is a need to deviate from the defaults. To update the checkpoint configuration, add a `checkpoint` key containing the valid dataset configuration. This may be required if, say, the pipeline has read-only permissions to the location of partitions (or write operations are undesirable for any other reason). In such cases, `IncrementalDataSet` can be configured to save the checkpoint elsewhere. The `checkpoint` key also supports partial config updates where only some checkpoint attributes are overwritten, while the defaults are kept for the rest: @@ -565,7 +329,7 @@ my_partitioned_dataset: k1: v1 ``` -#### Special checkpoint config keys +### Special checkpoint config keys Along with the standard dataset attributes, `checkpoint` config also accepts two special optional keys: * `comparison_func` (defaults to `operator.gt`) - a fully qualified import path to the function that will be used to compare a partition ID with the checkpoint value, to determine whether a partition should be processed. Such functions must accept two positional string arguments - partition ID and checkpoint value - and return `True` if such partition is considered to be past the checkpoint. It might be useful to specify your own `comparison_func` if you need to customise the checkpoint filtration mechanism - for example, you might want to implement windowed loading, where you always want to load the partitions representing the last calendar month. See the example config specifying a custom comparison function: diff --git a/docs/source/deployment/argo.md b/docs/source/deployment/argo.md index f66b809b0e..9207debe3d 100644 --- a/docs/source/deployment/argo.md +++ b/docs/source/deployment/argo.md @@ -24,7 +24,7 @@ To use Argo Workflows, ensure you have the following prerequisites in place: - [Argo Workflows is installed](https://github.com/argoproj/argo/blob/master/README.md#quickstart) on your Kubernetes cluster - [Argo CLI is installed](https://github.com/argoproj/argo/releases) on your machine - A `name` attribute is set for each [Kedro node](/kedro.pipeline.node) since it is used to build a DAG -- [All node input/output DataSets must be configured in `catalog.yml`](../data/data_catalog.md#use-the-data-catalog-with-the-yaml-api) and refer to an external location (e.g. AWS S3); you cannot use the `MemoryDataSet` in your workflow +- [All node input/output DataSets must be configured in `catalog.yml`](../data/data_catalog_yaml_examples.md) and refer to an external location (e.g. AWS S3); you cannot use the `MemoryDataSet` in your workflow ```{note} Each node will run in its own container. diff --git a/docs/source/deployment/aws_batch.md b/docs/source/deployment/aws_batch.md index 976d5e9e5a..c83b58f8ea 100644 --- a/docs/source/deployment/aws_batch.md +++ b/docs/source/deployment/aws_batch.md @@ -18,7 +18,7 @@ To use AWS Batch, ensure you have the following prerequisites in place: - An [AWS account set up](https://aws.amazon.com/premiumsupport/knowledge-center/create-and-activate-aws-account/). - A `name` attribute is set for each [Kedro node](/kedro.pipeline.node). Each node will run in its own Batch job, so having sensible node names will make it easier to `kedro run --node=`. -- [All node input/output `DataSets` must be configured in `catalog.yml`](../data/data_catalog.md#use-the-data-catalog-with-the-yaml-api) and refer to an external location (e.g. AWS S3). A clean way to do this is to create a new configuration environment `conf/aws_batch` containing a `catalog.yml` file with the appropriate configuration, as illustrated below. +- [All node input/output `DataSets` must be configured in `catalog.yml`](../data/data_catalog_yaml_examples.md) and refer to an external location (e.g. AWS S3). A clean way to do this is to create a new configuration environment `conf/aws_batch` containing a `catalog.yml` file with the appropriate configuration, as illustrated below.
Click to expand diff --git a/docs/source/deployment/databricks/databricks_deployment_workflow.md b/docs/source/deployment/databricks/databricks_deployment_workflow.md index 799a5044c1..245708e6bf 100644 --- a/docs/source/deployment/databricks/databricks_deployment_workflow.md +++ b/docs/source/deployment/databricks/databricks_deployment_workflow.md @@ -170,7 +170,7 @@ A Kedro project's configuration and data do not get included when it is packaged Your packaged Kedro project needs access to data and configuration in order to run. Therefore, you will need to upload your project's data and configuration to a location accessible to Databricks. In this guide, we will store the data on the Databricks File System (DBFS). -The `databricks-iris` starter contains a [catalog](../../data/data_catalog.md#the-data-catalog) that is set up to access data stored in DBFS (`/conf/`). You will point your project to use configuration stored on DBFS using the `--conf-source` option when you create your job on Databricks. +The `databricks-iris` starter contains a [catalog](../../data/data_catalog.md) that is set up to access data stored in DBFS (`/conf/`). You will point your project to use configuration stored on DBFS using the `--conf-source` option when you create your job on Databricks. There are several ways to upload data to DBFS: you can use the [DBFS API](https://learn.microsoft.com/en-us/azure/databricks/dev-tools/api/latest/dbfs), the [`dbutils` module](https://docs.databricks.com/dev-tools/databricks-utils.html) in a Databricks notebook or the [Databricks CLI](https://docs.databricks.com/dev-tools/cli/dbfs-cli.html). In this guide, it is recommended to use the Databricks CLI because of the convenience it offers. diff --git a/docs/source/deployment/databricks/databricks_ide_development_workflow.md b/docs/source/deployment/databricks/databricks_ide_development_workflow.md index dc723189c9..2cf8f40ca2 100644 --- a/docs/source/deployment/databricks/databricks_ide_development_workflow.md +++ b/docs/source/deployment/databricks/databricks_ide_development_workflow.md @@ -142,7 +142,7 @@ Name the new folder `local`. In this guide, we have no local credentials to stor When run on Databricks, Kedro cannot access data stored in your project's directory. Therefore, you will need to upload your project's data to an accessible location. In this guide, we will store the data on the Databricks File System (DBFS). -The `databricks-iris` starter contains a [catalog](../../data/data_catalog.md#the-data-catalog) that is set up to access data stored in DBFS (`/conf/`). You will point your project to use configuration stored on DBFS using the `--conf-source` option when you create your job on Databricks. +The `databricks-iris` starter contains a [catalog](../../data/data_catalog.md) that is set up to access data stored in DBFS (`/conf/`). You will point your project to use configuration stored on DBFS using the `--conf-source` option when you create your job on Databricks. There are several ways to upload data to DBFS. In this guide, it is recommended to use [Databricks CLI](https://docs.databricks.com/dev-tools/cli/dbfs-cli.html) because of the convenience it offers. At the command line in your local environment, use the following Databricks CLI command to upload your locally stored data to DBFS: diff --git a/docs/source/development/commands_reference.md b/docs/source/development/commands_reference.md index 45801ea112..815bae91f8 100644 --- a/docs/source/development/commands_reference.md +++ b/docs/source/development/commands_reference.md @@ -498,7 +498,7 @@ kedro catalog list --pipeline=ds,de kedro catalog rank ``` -The output includes a list of any [dataset factories](../data/data_catalog.md#load-multiple-datasets-with-similar-configuration-using-dataset-factories) in the catalog, ranked by the priority on which they are matched against. +The output includes a list of any [dataset factories](../data/kedro_dataset_factories.md) in the catalog, ranked by the priority on which they are matched against. #### Data Catalog diff --git a/docs/source/experiment_tracking/index.md b/docs/source/experiment_tracking/index.md index a8e94dd05b..31bff89ee2 100644 --- a/docs/source/experiment_tracking/index.md +++ b/docs/source/experiment_tracking/index.md @@ -19,7 +19,7 @@ Kedro's [experiment tracking demo](https://demo.kedro.org/experiment-tracking) e ![](../meta/images/experiment-tracking_demo.gif) ## Kedro versions supporting experiment tracking -Kedro has always supported parameter versioning (as part of your codebase with a version control system like `git`) and Kedro’s dataset versioning capabilities enabled you to [snapshot models, datasets and plots](../data/data_catalog.md#version-datasets-and-ml-models). +Kedro has always supported parameter versioning (as part of your codebase with a version control system like `git`) and Kedro’s dataset versioning capabilities enabled you to [snapshot models, datasets and plots](../data/data_catalog.md#dataset-versioning). Kedro-Viz version 4.1.1 introduced metadata capture, visualisation, discovery and comparison, enabling you to access, edit and [compare your experiments](#access-run-data-and-compare-runs) and additionally [track how your metrics change over time](#view-and-compare-metrics-data). diff --git a/docs/source/extend_kedro/common_use_cases.md b/docs/source/extend_kedro/common_use_cases.md index 04b36d6ca5..9f8d32dc9f 100644 --- a/docs/source/extend_kedro/common_use_cases.md +++ b/docs/source/extend_kedro/common_use_cases.md @@ -12,7 +12,7 @@ This can now achieved by using [Hooks](../hooks/introduction.md), to define the ## Use Case 2: How to integrate Kedro with additional data sources -You can use [DataSets](/kedro_datasets) to interface with various different data sources. If the data source you plan to use is not supported out of the box by Kedro, you can [create a custom dataset](custom_datasets.md). +You can use [DataSets](/kedro_datasets) to interface with various different data sources. If the data source you plan to use is not supported out of the box by Kedro, you can [create a custom dataset](../data/how_to_create_a_custom_dataset.md). ## Use Case 3: How to add or modify CLI commands diff --git a/docs/source/extend_kedro/index.md b/docs/source/extend_kedro/index.md index f368ac9a73..fefa8e21f9 100644 --- a/docs/source/extend_kedro/index.md +++ b/docs/source/extend_kedro/index.md @@ -4,6 +4,5 @@ :maxdepth: 1 common_use_cases -custom_datasets plugins ``` diff --git a/docs/source/faq/faq.md b/docs/source/faq/faq.md index 75790690a9..23cfa6b094 100644 --- a/docs/source/faq/faq.md +++ b/docs/source/faq/faq.md @@ -39,9 +39,6 @@ This is a growing set of technical FAQs. The [product FAQs on the Kedro website] * [How do I use resolvers in the `OmegaConfigLoader`](../configuration/advanced_configuration.md#how-to-use-resolvers-in-the-omegaconfigloader)? * [How do I load credentials through environment variables](../configuration/advanced_configuration.md#how-to-load-credentials-through-environment-variables)? -## Datasets and the Data Catalog - -* [Can I read the same data file using two different dataset implementations](../data/data_catalog.md#transcode-datasets)? ## Nodes and pipelines diff --git a/docs/source/nodes_and_pipelines/nodes.md b/docs/source/nodes_and_pipelines/nodes.md index a41f147244..70835183dc 100644 --- a/docs/source/nodes_and_pipelines/nodes.md +++ b/docs/source/nodes_and_pipelines/nodes.md @@ -213,7 +213,7 @@ With `pandas` built-in support, you can use the `chunksize` argument to read dat ### Saving data with Generators To use generators to save data lazily, you need do three things: - Update the `make_prediction` function definition to use `return` instead of `yield`. -- Create a [custom dataset](../extend_kedro/custom_datasets.md) called `ChunkWiseCSVDataset` +- Create a [custom dataset](../data/how_to_create_a_custom_dataset.md) called `ChunkWiseCSVDataset` - Update `catalog.yml` to use a newly created `ChunkWiseCSVDataset`. Copy the following code to `nodes.py`. The main change is to use a new model `DecisionTreeClassifier` to make prediction by chunks in `make_predictions`. diff --git a/docs/source/notebooks_and_ipython/kedro_and_notebooks.md b/docs/source/notebooks_and_ipython/kedro_and_notebooks.md index d32139b2f8..8344b1346f 100644 --- a/docs/source/notebooks_and_ipython/kedro_and_notebooks.md +++ b/docs/source/notebooks_and_ipython/kedro_and_notebooks.md @@ -101,7 +101,7 @@ INFO Loading data from 'parameters' (MemoryDataSet)... ``` ```{note} -If you enable [versioning](../data/data_catalog.md#version-datasets-and-ml-models) you can load a particular version of a dataset, e.g. `catalog.load("example_train_x", version="2021-12-13T15.08.09.255Z")`. +If you enable [versioning](../data/data_catalog.md#dataset-versioning) you can load a particular version of a dataset, e.g. `catalog.load("example_train_x", version="2021-12-13T15.08.09.255Z")`. ``` ### `context` diff --git a/docs/source/tutorial/add_another_pipeline.md b/docs/source/tutorial/add_another_pipeline.md index 95093b5d0b..1ceba96edc 100644 --- a/docs/source/tutorial/add_another_pipeline.md +++ b/docs/source/tutorial/add_another_pipeline.md @@ -125,7 +125,7 @@ regressor: versioned: true ``` -By setting `versioned` to `true`, versioning is enabled for `regressor`. This means that the pickled output of the `regressor` is saved every time the pipeline runs, which stores the history of the models built using this pipeline. You can learn more in the [Versioning section](../data/kedro_io.md#versioning). +By setting `versioned` to `true`, versioning is enabled for `regressor`. This means that the pickled output of the `regressor` is saved every time the pipeline runs, which stores the history of the models built using this pipeline. You can learn more in the [later section about dataset and ML model versioning](../data/data_catalog.md#dataset-versioning). ## Data science pipeline diff --git a/docs/source/tutorial/set_up_data.md b/docs/source/tutorial/set_up_data.md index 364818b3a1..2315f04068 100644 --- a/docs/source/tutorial/set_up_data.md +++ b/docs/source/tutorial/set_up_data.md @@ -120,7 +120,7 @@ When you have finished, close `ipython` session with `exit()`. [Kedro supports numerous datasets](/kedro_datasets) out of the box, but you can also add support for any proprietary data format or filesystem. -You can find further information about [how to add support for custom datasets](../extend_kedro/custom_datasets.md) in specific documentation covering advanced usage. +You can find further information about [how to add support for custom datasets](../data/how_to_create_a_custom_dataset.md) in specific documentation covering advanced usage. ### Supported data locations diff --git a/setup.py b/setup.py index e78ea817a7..8d94b9c965 100644 --- a/setup.py +++ b/setup.py @@ -97,7 +97,7 @@ def _collect_requirements(requires): "sphinxcontrib-mermaid~=0.7.1", "myst-parser~=1.0.0", "Jinja2<3.1.0", - "kedro-datasets[all,pandas-deltatabledataset]~=1.5.1", + "kedro-datasets[all]~=1.5.3", ], "geopandas": _collect_requirements(geopandas_require), "matplotlib": _collect_requirements(matplotlib_require), From 9e2095b49c38253cc75c4f3783c12f71e32aa861 Mon Sep 17 00:00:00 2001 From: Tynan DeBold Date: Fri, 18 Aug 2023 14:46:13 +0200 Subject: [PATCH 27/70] Add line about Viz to PR template (#2929) Signed-off-by: Jason Hite --- .github/PULL_REQUEST_TEMPLATE.md | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index d8da8611d3..157613b425 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -14,3 +14,4 @@ - [ ] Updated the documentation to reflect the code changes - [ ] Added a description of this change in the [`RELEASE.md`](https://github.com/kedro-org/kedro/blob/main/RELEASE.md) file - [ ] Added tests to cover my changes +- [ ] Checked if this change will affect Kedro-Viz, and if so, communicated that with the Viz team From 1e55b01652354ba21878524dc17e03744f04276b Mon Sep 17 00:00:00 2001 From: Jo Stichbury Date: Fri, 18 Aug 2023 14:52:50 +0100 Subject: [PATCH 28/70] Add architecture graphic back to docs with revisions (#2916) * Revise FAQs and README Signed-off-by: Jo Stichbury * Add back the data layers FAQ as I've no idea where else it fits Signed-off-by: Jo Stichbury * minor changes from review Signed-off-by: Jo Stichbury * Re-add kedro arch diagram, with revised graphic Signed-off-by: Jo Stichbury * fix broken anchor Signed-off-by: Jo Stichbury * fix broken anchor Signed-off-by: Jo Stichbury * Update docs/source/extend_kedro/architecture_overview.md Co-authored-by: Nok Lam Chan * Update docs/source/extend_kedro/architecture_overview.md Co-authored-by: Nok Lam Chan * Update docs/source/extend_kedro/architecture_overview.md Co-authored-by: Nok Lam Chan * Update docs/source/extend_kedro/architecture_overview.md Co-authored-by: Nok Lam Chan * Changes to architecture page following review Signed-off-by: Jo Stichbury * Change diagram following reivew * Add links to API docs Signed-off-by: Jo Stichbury * Added in type of users * Fix linting error Signed-off-by: Jo Stichbury --------- Signed-off-by: Jo Stichbury Co-authored-by: Nok Lam Chan Co-authored-by: Yetunde Dada <43755008+yetudada@users.noreply.github.com> Signed-off-by: Jason Hite --- CONTRIBUTING.md | 2 +- .../extend_kedro/architecture_overview.md | 55 ++++ docs/source/extend_kedro/index.md | 1 + .../meta/images/KedroArchitecture.drawio | 306 +++++++++++++++++- .../source/meta/images/kedro_architecture.png | Bin 132332 -> 125633 bytes 5 files changed, 362 insertions(+), 2 deletions(-) create mode 100644 docs/source/extend_kedro/architecture_overview.md diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index bbc850442d..6eeb6532ba 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -10,7 +10,7 @@ We also curate a [GitHub repo that lists content created by the Kedro community] ## Contribute to the project -There are quite a few ways to contribute to Kedro, sich as answering questions about Kedro to help others, fixing a typo on the documentation, reporting a bug, reviewing pull requests or adding a feature. +There are quite a few ways to contribute to Kedro, such as answering questions about Kedro to help others, fixing a typo on the documentation, reporting a bug, reviewing pull requests or adding a feature. Take a look at some of our [contribution suggestions on the Kedro GitHub Wiki](https://github.com/kedro-org/kedro/wiki/Contribute-to-Kedro)! diff --git a/docs/source/extend_kedro/architecture_overview.md b/docs/source/extend_kedro/architecture_overview.md new file mode 100644 index 0000000000..272fcef572 --- /dev/null +++ b/docs/source/extend_kedro/architecture_overview.md @@ -0,0 +1,55 @@ +# Kedro architecture overview + +There are different ways to leverage Kedro in your work, you can: + + - Commit to using all of Kedro (framework, project, starters and library); which is preferable to take advantage of the full value proposition of Kedro + - You can leverage parts of Kedro, like the DataCatalog (I/O), ConfigLoader, Pipelines and Runner, by using it as a Python libary; this best supports a workflow where you don't want to adopt the Kedro project template + - Or, you can develop extensions for Kedro e.g. custom starters, plugins, Hooks and more + +At a high level, Kedro consists of five main parts: + +![Kedro architecture diagram](../meta/images/kedro_architecture.png) + + +## Kedro project + +As a data pipeline developer, you will interact with a Kedro project, which consists of: + +* The **`conf/`** directory, which contains configuration for the project, such as data catalog configuration, parameters, etc. +* The **`src`** directory, which contains the source code for the project, including: + * The **`pipelines`** directory, which contains the source code for your pipelines. + * **`settings.py`** file contains the settings for the project, such as library component registration, custom hooks registration, etc. All the available settings are listed and explained in the [project settings chapter](../kedro_project_setup/settings.md). + * **`pipeline_registry.py`** file defines the project pipelines, i.e. pipelines that can be run using `kedro run --pipeline`. + * **`__main__.py`** file serves as the main entry point of the project in [package mode](../tutorial/package_a_project.md#package-a-kedro-project). +* **`pyproject.toml`** identifies the project root by providing project metadata, including: + * `package_name`: A valid Python package name for your project package. + * `project_name`: A human readable name for your project. + * `kedro_init_version`: Kedro version with which the project was generated. + +## Kedro framework + +Kedro framework serves as the interface between a Kedro project and Kedro library components. The major building blocks of the Kedro framework include: + +* **[`Session`](/kedro.framework.session)** is responsible for managing the lifecycle of a Kedro run. +* **[`Context`](/kedro.framework.context)** holds the configuration and Kedro's main functionality, and also serves as the main entry point for interactions with core library components. +* **[`Hooks`](/kedro.framework.hooks)** defines all hook specifications available to extend Kedro. +* **[`CLI`](/kedro.framework.cli)** defines built-in Kedro CLI commands and utilities to load custom CLI commands from plugins. + +## Kedro starter + +You can use a [Kedro starter](../kedro_project_setup/starters.md) to generate a Kedro project that contains boilerplate code. We maintain a set of [official starters](https://github.com/kedro-org/kedro-starters/) but you can also use a custom starter of your choice. + +## Kedro library + +Kedro library consists of independent units, each responsible for one aspect of computation in a data pipeline: + +* **[`ConfigLoader`](/kedro.config.ConfigLoader)** provides utility to parse and load configuration defined in a Kedro project. +* **[`Pipeline`](/kedro.pipeline)** provides a collection of abstractions to model data pipelines. +* **[`Runner`](/kedro.runner)** provides an abstraction for different execution strategy of a data pipeline. +* **[`I/O`](/kedro.io)** provides a collection of abstractions to handle I/O in a project, including `DataCatalog` and many `Dataset` implementations. + +## Kedro extension + +You can also extend Kedro behaviour in your project using a Kedro extension, which can be a custom starter, a Python library with extra hooks implementations, extra CLI commands such as [Kedro-Viz](https://github.com/kedro-org/kedro-viz) or a custom library component implementation. + +If you create a Kedro extension, we welcome all kinds of contributions. Check out our [guide to contributing to Kedro](https://github.com/kedro-org/kedro/wiki/Contribute-to-Kedro). Dataset contributions to [`kedro-datasets`](https://github.com/kedro-org/kedro-plugins/tree/main/kedro-datasets) are the most frequently accepted, since they do not require any changes to the framework itself. However, we do not discourage contributions to any of the other [`kedro-plugins`](https://github.com/kedro-org/kedro-plugins). diff --git a/docs/source/extend_kedro/index.md b/docs/source/extend_kedro/index.md index fefa8e21f9..5671a2c786 100644 --- a/docs/source/extend_kedro/index.md +++ b/docs/source/extend_kedro/index.md @@ -5,4 +5,5 @@ common_use_cases plugins +architecture_overview ``` diff --git a/docs/source/meta/images/KedroArchitecture.drawio b/docs/source/meta/images/KedroArchitecture.drawio index 0d32eeeebd..8256737edf 100644 --- a/docs/source/meta/images/KedroArchitecture.drawio +++ b/docs/source/meta/images/KedroArchitecture.drawio @@ -1 +1,305 @@ -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 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/docs/source/meta/images/kedro_architecture.png b/docs/source/meta/images/kedro_architecture.png index 7db347260ebb6aad173d7825cb9ca0f13b251a3c..0d7fd896baa04b92cb3b6b559d4e069284af24ba 100644 GIT binary patch literal 125633 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/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 D)kan7 literal 132332 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 Date: Fri, 18 Aug 2023 15:32:55 +0100 Subject: [PATCH 29/70] Add kedro catalog resolve command (#2891) * Implement working draft Signed-off-by: Ahdra Merali * Remove comments Signed-off-by: Ahdra Merali * Remove raw_config Signed-off-by: Ahdra Merali * Use config loader to access catalog config Signed-off-by: Ahdra Merali * Use dict comprehension Signed-off-by: Ahdra Merali * Remove pipeline filtering Signed-off-by: Ahdra Merali * Prevent overwrite or param inclusion Signed-off-by: Ahdra Merali * Trim filepath Signed-off-by: Ahdra Merali * Appease linter Signed-off-by: Ahdra Merali * Add test for resolve Signed-off-by: Ahdra Merali * Add test to exclude params Signed-off-by: Ahdra Merali * Add test for overwrite (not working) Signed-off-by: Ahdra Merali * Fix test Signed-off-by: Ahdra Merali * Remove print Signed-off-by: Ahdra Merali * Add changes to RELEASE.md Signed-off-by: Ahdra Merali * Trim unreachable code Signed-off-by: Ahdra Merali * Make helper function private Signed-off-by: Ahdra Merali * Refactor Signed-off-by: Ahdra Merali * Add changes to the docs Signed-off-by: Ahdra Merali * Refactor 2 Signed-off-by: Ahdra Merali * Fix docs build Signed-off-by: Ahdra Merali * Add suggestions from code review (docs) Signed-off-by: Ahdra Merali * Add suggestions from code review (code) Signed-off-by: Ahdra Merali * Rename catalog_config variable to explicit_datasets Signed-off-by: Ahdra Merali * Apply suggestions from code review Co-authored-by: Merel Theisen <49397448+merelcht@users.noreply.github.com> * Fix mocking in test Signed-off-by: Ahdra Merali * Change test fixture name Signed-off-by: Ahdra Merali * Lint Signed-off-by: Ahdra Merali --------- Signed-off-by: Ahdra Merali Co-authored-by: Merel Theisen <49397448+merelcht@users.noreply.github.com> Signed-off-by: Jason Hite --- RELEASE.md | 1 + docs/source/development/commands_reference.md | 9 ++ kedro/framework/cli/catalog.py | 48 ++++++ tests/framework/cli/test_catalog.py | 152 ++++++++++++++++++ 4 files changed, 210 insertions(+) diff --git a/RELEASE.md b/RELEASE.md index 603cb61f46..169485d03e 100644 --- a/RELEASE.md +++ b/RELEASE.md @@ -13,6 +13,7 @@ ## Major features and improvements * Allowed registering of custom resolvers to `OmegaConfigLoader` through `CONFIG_LOADER_ARGS`. * Added support for Python 3.11. This includes tackling challenges like dependency pinning and test adjustments to ensure a smooth experience. Detailed migration tips are provided below for further context. +* Added `kedro catalog resolve` CLI command that resolves dataset factories in the catalog with any explicit entries in the project pipeline. ## Bug fixes and other changes * Updated `kedro pipeline create` and `kedro catalog create` to use new `/conf` file structure. diff --git a/docs/source/development/commands_reference.md b/docs/source/development/commands_reference.md index 815bae91f8..1c99dae715 100644 --- a/docs/source/development/commands_reference.md +++ b/docs/source/development/commands_reference.md @@ -62,6 +62,7 @@ Here is a list of Kedro CLI commands, as a shortcut to the descriptions below. P * [`kedro build-docs`](#build-the-project-documentation) (deprecated from version 0.19.0) * [`kedro build-reqs`](#build-the-projects-dependency-tree) (deprecated from version 0.19.0) * [`kedro catalog list`](#list-datasets-per-pipeline-per-type) + * [`kedro catalog resolve`](#resolve-dataset-factories-in-the-catalog) * [`kedro catalog rank`](#rank-dataset-factories-in-the-catalog) * [`kedro catalog create`](#create-a-data-catalog-yaml-configuration-file) * [`kedro ipython`](#notebooks) @@ -492,6 +493,14 @@ The command also accepts an optional `--pipeline` argument that allows you to sp kedro catalog list --pipeline=ds,de ``` +##### Resolve dataset factories in the catalog + +```bash +kedro catalog resolve +``` + +This command resolves dataset factories in the catalog file with any explicit entries in the pipeline. The output includes datasets explicitly mentioned in your catalog files and any datasets mentioned in the project's pipelines that match a dataset factory. + ##### Rank dataset factories in the catalog ```bash diff --git a/kedro/framework/cli/catalog.py b/kedro/framework/cli/catalog.py index 7bfa5f54cb..39da4dd979 100644 --- a/kedro/framework/cli/catalog.py +++ b/kedro/framework/cli/catalog.py @@ -207,3 +207,51 @@ def rank_catalog_factories(metadata: ProjectMetadata, env): click.echo(yaml.dump(list(catalog_factories.keys()))) else: click.echo("There are no dataset factories in the catalog.") + + +@catalog.command("resolve") +@env_option +@click.pass_obj +def resolve_patterns(metadata: ProjectMetadata, env): + """Resolve catalog factories against pipeline datasets""" + + session = _create_session(metadata.package_name, env=env) + context = session.load_context() + + data_catalog = context.catalog + catalog_config = context.config_loader["catalog"] + + explicit_datasets = { + ds_name: ds_config + for ds_name, ds_config in catalog_config.items() + if not data_catalog._is_pattern(ds_name) + } + + target_pipelines = pipelines.keys() + datasets = set() + + for pipe in target_pipelines: + pl_obj = pipelines.get(pipe) + if pl_obj: + datasets.update(pl_obj.data_sets()) + + for ds_name in datasets: + is_param = ds_name.startswith("params:") or ds_name == "parameters" + if ds_name in explicit_datasets or is_param: + continue + + matched_pattern = data_catalog._match_pattern( + data_catalog._dataset_patterns, ds_name + ) + if matched_pattern: + ds_config = data_catalog._resolve_config(ds_name, matched_pattern) + ds_config["filepath"] = _trim_filepath( + str(context.project_path) + "/", ds_config["filepath"] + ) + explicit_datasets[ds_name] = ds_config + + secho(yaml.dump(explicit_datasets)) + + +def _trim_filepath(project_path: str, file_path: str): + return file_path.replace(project_path, "", 1) diff --git a/tests/framework/cli/test_catalog.py b/tests/framework/cli/test_catalog.py index a0ee78662b..a0dba38635 100644 --- a/tests/framework/cli/test_catalog.py +++ b/tests/framework/cli/test_catalog.py @@ -67,6 +67,23 @@ def fake_catalog_with_overlapping_factories(): return config +@pytest.fixture +def fake_catalog_config_with_resolvable_dataset(): + config = { + "parquet_{factory_pattern}": { + "type": "pandas.ParquetDataSet", + "filepath": "test.pq", + }, + "csv_{factory_pattern}": {"type": "pandas.CSVDataSet", "filepath": "test.csv"}, + "explicit_ds": {"type": "pandas.CSVDataSet", "filepath": "test.csv"}, + "{factory_pattern}_ds": { + "type": "pandas.ParquetDataSet", + "filepath": "test.pq", + }, + } + return config + + @pytest.mark.usefixtures( "chdir_to_dummy_project", "fake_load_context", "mock_pipelines" ) @@ -441,3 +458,138 @@ def test_rank_catalog_factories_with_no_factories( assert not result.exit_code expected_output = "There are no dataset factories in the catalog." assert expected_output in result.output + + +@pytest.mark.usefixtures( + "chdir_to_dummy_project", "fake_load_context", "mock_pipelines" +) +def test_catalog_resolve( + fake_project_cli, + fake_metadata, + fake_load_context, + mocker, + mock_pipelines, + fake_catalog_config, +): + """Test that datasets factories are correctly resolved to the explicit datasets in the pipeline.""" + yaml_dump_mock = mocker.patch("yaml.dump", return_value="Result YAML") + mocked_context = fake_load_context.return_value + mocked_context.catalog = DataCatalog.from_config(fake_catalog_config) + + placeholder_ds = mocked_context.catalog._data_sets.keys() + explicit_ds = {"csv_example", "parquet_example"} + + mocker.patch.object( + mock_pipelines[PIPELINE_NAME], + "data_sets", + return_value=explicit_ds, + ) + + result = CliRunner().invoke( + fake_project_cli, ["catalog", "resolve"], obj=fake_metadata + ) + + assert not result.exit_code + assert yaml_dump_mock.call_count == 1 + + output = yaml_dump_mock.call_args[0][0] + + for ds in placeholder_ds: + assert ds not in output + + for ds in explicit_ds: + assert ds in output + + +@pytest.mark.usefixtures( + "chdir_to_dummy_project", "fake_load_context", "mock_pipelines" +) +def test_no_overwrite( + fake_project_cli, + fake_metadata, + fake_load_context, + mocker, + mock_pipelines, + fake_catalog_config_with_resolvable_dataset, +): + """Test that explicit catalog entries are not overwritten by factory config.""" + yaml_dump_mock = mocker.patch("yaml.dump", return_value="Result YAML") + mocked_context = fake_load_context.return_value + + mocked_context.config_loader = { + "catalog": fake_catalog_config_with_resolvable_dataset + } + mocked_context.catalog = DataCatalog.from_config( + fake_catalog_config_with_resolvable_dataset + ) + + mocker.patch.object( + mock_pipelines[PIPELINE_NAME], + "data_sets", + return_value=mocked_context.catalog._data_sets.keys() + | {"csv_example", "parquet_example"}, + ) + + result = CliRunner().invoke( + fake_project_cli, ["catalog", "resolve"], obj=fake_metadata + ) + + assert not result.exit_code + assert yaml_dump_mock.call_count == 1 + + assert ( + yaml_dump_mock.call_args[0][0]["explicit_ds"] + == fake_catalog_config_with_resolvable_dataset["explicit_ds"] + ) + + +@pytest.mark.usefixtures( + "chdir_to_dummy_project", "fake_load_context", "mock_pipelines" +) +def test_no_param_datasets_in_resolve( + fake_project_cli, fake_metadata, fake_load_context, mocker, mock_pipelines +): + + yaml_dump_mock = mocker.patch("yaml.dump", return_value="Result YAML") + mocked_context = fake_load_context.return_value + + catalog_config = { + "iris_data": { + "type": "pandas.CSVDataSet", + "filepath": "test.csv", + }, + "intermediate": {"type": "MemoryDataSet"}, + } + + catalog_datasets = { + "iris_data": CSVDataSet("test.csv"), + "intermediate": MemoryDataset(), + "parameters": MemoryDataset(), + "params:data_ratio": MemoryDataset(), + } + + mocked_context.config_loader = {"catalog": catalog_config} + mocked_context.catalog = DataCatalog(data_sets=catalog_datasets) + + mocker.patch.object( + mock_pipelines[PIPELINE_NAME], + "data_sets", + return_value=catalog_datasets.keys(), + ) + + result = CliRunner().invoke( + fake_project_cli, + ["catalog", "resolve"], + obj=fake_metadata, + ) + + assert not result.exit_code + assert yaml_dump_mock.call_count == 1 + + # 'parameters' and 'params:data_ratio' should not appear in the output + output = yaml_dump_mock.call_args[0][0] + + assert "parameters" not in output.keys() + assert "params:data_ratio" not in output.keys() + assert "iris_data" in output.keys() + assert "intermediate" in output.keys() From ffb46b55ece98f0531b168c41b2d9bd888e0cc35 Mon Sep 17 00:00:00 2001 From: Deepyaman Datta Date: Fri, 18 Aug 2023 09:45:10 -0500 Subject: [PATCH 30/70] Replace "DataSet" with "Dataset" in Markdown files (#2735) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * LambdaDataSet->LambdaDataset in .md files Signed-off-by: Deepyaman Datta * MemoryDataSet->MemoryDataset in .md files Signed-off-by: Deepyaman Datta * PartitionedDataSet->PartitionedDataset in .md files Signed-off-by: Deepyaman Datta * IncrementalDataSet->IncrementalDataset in .md files Signed-off-by: Deepyaman Datta * CachedDataSet->CachedDataset in .md files Signed-off-by: Deepyaman Datta * DataSetError->DatasetError in .md files Signed-off-by: Deepyaman Datta * DataSetNotFoundError->DatasetNotFoundError in .md files Signed-off-by: Deepyaman Datta * Replace "DataSet" with "Dataset" in Markdown files * Update RELEASE.md * Fix remaining instance of "*DataSet*"->"*Dataset*" Signed-off-by: Deepyaman Datta * `find . -name '*.md' -print0 | xargs -0 sed -i "" "s/\([^A-Za-z]\)DataSet/\1Dataset/g"` Signed-off-by: Deepyaman Datta * Change non-class instances of Dataset to dataset * Replace any remaining instances of DataSet in docs * Fix a broken link to docs for `PartitionedDataset` --------- Signed-off-by: Deepyaman Datta Co-authored-by: Jo Stichbury Co-authored-by: Juan Luis Cano Rodríguez Signed-off-by: Jason Hite --- .../configuration/advanced_configuration.md | 6 +- docs/source/configuration/parameters.md | 2 +- .../data/advanced_data_catalog_usage.md | 10 +-- docs/source/data/data_catalog.md | 5 +- .../source/data/data_catalog_yaml_examples.md | 6 +- .../data/how_to_create_a_custom_dataset.md | 16 ++-- docs/source/data/kedro_dataset_factories.md | 2 +- .../partitioned_and_incremental_datasets.md | 88 +++++++++---------- docs/source/deployment/argo.md | 4 +- docs/source/deployment/aws_batch.md | 2 +- docs/source/deployment/aws_step_functions.md | 2 +- docs/source/deployment/prefect.md | 4 +- docs/source/development/commands_reference.md | 2 +- docs/source/extend_kedro/common_use_cases.md | 4 +- docs/source/hooks/examples.md | 16 ++-- .../integrations/pyspark_integration.md | 12 +-- .../nodes_and_pipelines/run_a_pipeline.md | 14 +-- .../nodes_and_pipelines/slice_a_pipeline.md | 4 +- .../kedro_and_notebooks.md | 2 +- docs/source/resources/glossary.md | 2 +- docs/source/tutorial/add_another_pipeline.md | 66 +++++++------- docs/source/tutorial/create_a_pipeline.md | 26 +++--- .../tutorial/spaceflights_tutorial_faqs.md | 18 ++-- 23 files changed, 157 insertions(+), 156 deletions(-) diff --git a/docs/source/configuration/advanced_configuration.md b/docs/source/configuration/advanced_configuration.md index ecfe45a71f..8e703b5bc2 100644 --- a/docs/source/configuration/advanced_configuration.md +++ b/docs/source/configuration/advanced_configuration.md @@ -176,7 +176,7 @@ From version 0.17.0, `TemplatedConfigLoader` also supports the [Jinja2](https:// ``` {% for speed in ['fast', 'slow'] %} {{ speed }}-trains: - type: MemoryDataSet + type: MemoryDataset {{ speed }}-cars: type: pandas.CSVDataSet @@ -197,13 +197,13 @@ The output Python dictionary will look as follows: ```python { - "fast-trains": {"type": "MemoryDataSet"}, + "fast-trains": {"type": "MemoryDataset"}, "fast-cars": { "type": "pandas.CSVDataSet", "filepath": "s3://my_s3_bucket/fast-cars.csv", "save_args": {"index": True}, }, - "slow-trains": {"type": "MemoryDataSet"}, + "slow-trains": {"type": "MemoryDataset"}, "slow-cars": { "type": "pandas.CSVDataSet", "filepath": "s3://my_s3_bucket/slow-cars.csv", diff --git a/docs/source/configuration/parameters.md b/docs/source/configuration/parameters.md index 60de2d4da4..61c6ff0e9c 100644 --- a/docs/source/configuration/parameters.md +++ b/docs/source/configuration/parameters.md @@ -66,7 +66,7 @@ node( ) ``` -In both cases, under the hood parameters are added to the Data Catalog through the method `add_feed_dict()` in [`DataCatalog`](/kedro.io.DataCatalog), where they live as `MemoryDataSet`s. This method is also what the `KedroContext` class uses when instantiating the catalog. +In both cases, under the hood parameters are added to the Data Catalog through the method `add_feed_dict()` in [`DataCatalog`](/kedro.io.DataCatalog), where they live as `MemoryDataset`s. This method is also what the `KedroContext` class uses when instantiating the catalog. ```{note} You can use `add_feed_dict()` to inject any other entries into your `DataCatalog` as per your use case. diff --git a/docs/source/data/advanced_data_catalog_usage.md b/docs/source/data/advanced_data_catalog_usage.md index 03670eaac7..1906500d35 100644 --- a/docs/source/data/advanced_data_catalog_usage.md +++ b/docs/source/data/advanced_data_catalog_usage.md @@ -55,7 +55,7 @@ gear = cars["gear"].values The following steps happened behind the scenes when `load` was called: - The value `cars` was located in the Data Catalog -- The corresponding `AbstractDataSet` object was retrieved +- The corresponding `AbstractDataset` object was retrieved - The `load` method of this dataset was called - This `load` method delegated the loading to the underlying pandas `read_csv` function @@ -70,9 +70,9 @@ This pattern is not recommended unless you are using platform notebook environme To save data using an API similar to that used to load data: ```python -from kedro.io import MemoryDataSet +from kedro.io import MemoryDataset -memory = MemoryDataSet(data=None) +memory = MemoryDataset(data=None) io.add("cars_cache", memory) io.save("cars_cache", "Memory can store anything.") io.load("cars_cache") @@ -190,7 +190,7 @@ io.save("test_data_set", data1) reloaded = io.load("test_data_set") assert data1.equals(reloaded) -# raises DataSetError since the path +# raises DatasetError since the path # data/01_raw/test.csv/my_exact_version/test.csv already exists io.save("test_data_set", data2) ``` @@ -219,7 +219,7 @@ io = DataCatalog({"test_data_set": test_data_set}) io.save("test_data_set", data1) # emits a UserWarning due to version inconsistency -# raises DataSetError since the data/01_raw/test.csv/exact_load_version/test.csv +# raises DatasetError since the data/01_raw/test.csv/exact_load_version/test.csv # file does not exist reloaded = io.load("test_data_set") ``` diff --git a/docs/source/data/data_catalog.md b/docs/source/data/data_catalog.md index 680db626f7..241e339635 100644 --- a/docs/source/data/data_catalog.md +++ b/docs/source/data/data_catalog.md @@ -126,6 +126,7 @@ In the example above, the `catalog.yml` file contains references to credentials ### Dataset versioning + Kedro enables dataset and ML model versioning through the `versioned` definition. For example: ```yaml @@ -144,9 +145,9 @@ kedro run --load-version=cars:YYYY-MM-DDThh.mm.ss.sssZ ``` where `--load-version` is dataset name and version timestamp separated by `:`. -A dataset offers versioning support if it extends the [`AbstractVersionedDataSet`](/kedro.io.AbstractVersionedDataset) class to accept a version keyword argument as part of the constructor and adapt the `_save` and `_load` method to use the versioned data path obtained from `_get_save_path` and `_get_load_path` respectively. +A dataset offers versioning support if it extends the [`AbstractVersionedDataset`](/kedro.io.AbstractVersionedDataset) class to accept a version keyword argument as part of the constructor and adapt the `_save` and `_load` method to use the versioned data path obtained from `_get_save_path` and `_get_load_path` respectively. -To verify whether a dataset can undergo versioning, you should examine the dataset class code to inspect its inheritance [(you can find contributed datasets within the `kedro-datasets` repository)](https://github.com/kedro-org/kedro-plugins/tree/main/kedro-datasets/kedro_datasets). Check if the dataset class inherits from the `AbstractVersionedDataSet`. For instance, if you encounter a class like `CSVDataSet(AbstractVersionedDataSet[pd.DataFrame, pd.DataFrame])`, this indicates that the dataset is set up to support versioning. +To verify whether a dataset can undergo versioning, you should examine the dataset class code to inspect its inheritance [(you can find contributed datasets within the `kedro-datasets` repository)](https://github.com/kedro-org/kedro-plugins/tree/main/kedro-datasets/kedro_datasets). Check if the dataset class inherits from the `AbstractVersionedDataset`. For instance, if you encounter a class like `CSVDataSet(AbstractVersionedDataset[pd.DataFrame, pd.DataFrame])`, this indicates that the dataset is set up to support versioning. ```{note} Note that HTTP(S) is a supported file system in the dataset implementations, but if you it, you can't also use versioning. diff --git a/docs/source/data/data_catalog_yaml_examples.md b/docs/source/data/data_catalog_yaml_examples.md index 0570aa0f2c..f27981600d 100644 --- a/docs/source/data/data_catalog_yaml_examples.md +++ b/docs/source/data/data_catalog_yaml_examples.md @@ -397,12 +397,12 @@ for loading, so the first node outputs a `pyspark.sql.DataFrame`, while the seco You can use the [`kedro catalog create` command to create a Data Catalog YAML configuration](../development/commands_reference.md#create-a-data-catalog-yaml-configuration-file). -This creates a `//catalog/.yml` configuration file with `MemoryDataSet` datasets for each dataset in a registered pipeline if it is missing from the `DataCatalog`. +This creates a `//catalog/.yml` configuration file with `MemoryDataset` datasets for each dataset in a registered pipeline if it is missing from the `DataCatalog`. ```yaml # //catalog/.yml rockets: - type: MemoryDataSet + type: MemoryDataset scooters: - type: MemoryDataSet + type: MemoryDataset ``` diff --git a/docs/source/data/how_to_create_a_custom_dataset.md b/docs/source/data/how_to_create_a_custom_dataset.md index 86010b4f18..46364031a0 100644 --- a/docs/source/data/how_to_create_a_custom_dataset.md +++ b/docs/source/data/how_to_create_a_custom_dataset.md @@ -2,9 +2,9 @@ [Kedro supports many datasets](/kedro_datasets) out of the box, but you may find that you need to create a custom dataset. For example, you may need to handle a proprietary data format or filesystem in your pipeline, or perhaps you have found a particular use case for a dataset that Kedro does not support. This tutorial explains how to create a custom dataset to read and save image data. -## AbstractDataSet +## AbstractDataset -For contributors, if you would like to submit a new dataset, you must extend the [`AbstractDataSet` interface](/kedro.io.AbstractDataset) or [`AbstractVersionedDataSet` interface](/kedro.io.AbstractVersionedDataset) if you plan to support versioning. It requires subclasses to override the `_load` and `_save` and provides `load` and `save` methods that enrich the corresponding private methods with uniform error handling. It also requires subclasses to override `_describe`, which is used in logging the internal information about the instances of your custom `AbstractDataSet` implementation. +For contributors, if you would like to submit a new dataset, you must extend the [`AbstractDataset` interface](/kedro.io.AbstractDataset) or [`AbstractVersionedDataset` interface](/kedro.io.AbstractVersionedDataset) if you plan to support versioning. It requires subclasses to override the `_load` and `_save` and provides `load` and `save` methods that enrich the corresponding private methods with uniform error handling. It also requires subclasses to override `_describe`, which is used in logging the internal information about the instances of your custom `AbstractDataset` implementation. ## Scenario @@ -267,19 +267,19 @@ class ImageDataSet(AbstractDataset[np.ndarray, np.ndarray]): ```

-## Integration with `PartitionedDataSet` +## Integration with `PartitionedDataset` Currently, the `ImageDataSet` only works with a single image, but this example needs to load all Pokemon images from the raw data directory for further processing. -Kedro's [`PartitionedDataSet`](./partitioned_and_incremental_datasets.md) is a convenient way to load multiple separate data files of the same underlying dataset type into a directory. +Kedro's [`PartitionedDataset`](/kedro.io.PartitionedDataset) is a convenient way to load multiple separate data files of the same underlying dataset type into a directory. -To use `PartitionedDataSet` with `ImageDataSet` to load all Pokemon PNG images, add this to the data catalog YAML so that `PartitionedDataSet` loads all PNG files from the data directory using `ImageDataSet`: +To use `PartitionedDataset` with `ImageDataSet` to load all Pokemon PNG images, add this to the data catalog YAML so that `PartitionedDataset` loads all PNG files from the data directory using `ImageDataSet`: ```yaml # in conf/base/catalog.yml pokemon: - type: PartitionedDataSet + type: PartitionedDataset dataset: kedro_pokemon.extras.datasets.image_dataset.ImageDataSet path: data/01_raw/pokemon-images-and-types/images/images filename_suffix: ".png" @@ -305,11 +305,11 @@ $ ls -la data/01_raw/pokemon-images-and-types/images/images/*.png | wc -l ### How to implement versioning in your dataset ```{note} -Versioning doesn't work with `PartitionedDataSet`. You can't use both of them at the same time. +Versioning doesn't work with `PartitionedDataset`. You can't use both of them at the same time. ``` To add versioning support to the new dataset we need to extend the - [AbstractVersionedDataSet](/kedro.io.AbstractVersionedDataset) to: + [AbstractVersionedDataset](/kedro.io.AbstractVersionedDataset) to: * Accept a `version` keyword argument as part of the constructor * Adapt the `_save` and `_load` method to use the versioned data path obtained from `_get_save_path` and `_get_load_path` respectively diff --git a/docs/source/data/kedro_dataset_factories.md b/docs/source/data/kedro_dataset_factories.md index 693272c013..2a65b4359e 100644 --- a/docs/source/data/kedro_dataset_factories.md +++ b/docs/source/data/kedro_dataset_factories.md @@ -215,7 +215,7 @@ The matches are ranked according to the following criteria: ## How to override the default dataset creation with dataset factories -You can use dataset factories to define a catch-all pattern which will overwrite the default [`MemoryDataSet`](/kedro.io.MemoryDataset) creation. +You can use dataset factories to define a catch-all pattern which will overwrite the default [`MemoryDataset`](/kedro.io.MemoryDataset) creation. ```yaml "{default_dataset}": diff --git a/docs/source/data/partitioned_and_incremental_datasets.md b/docs/source/data/partitioned_and_incremental_datasets.md index 7e48c23137..fde9dfd90a 100644 --- a/docs/source/data/partitioned_and_incremental_datasets.md +++ b/docs/source/data/partitioned_and_incremental_datasets.md @@ -4,10 +4,9 @@ Distributed systems play an increasingly important role in ETL data pipelines. They significantly increase the processing throughput, enabling us to work with much larger volumes of input data. However, these benefits sometimes come at a cost. When dealing with the input data generated by such distributed systems, you might encounter a situation where your Kedro node needs to read the data from a directory full of uniform files of the same type (e.g. JSON, CSV, Parquet, etc.) rather than from a single file. Tools like `PySpark` and the corresponding [SparkDataSet](/kedro_datasets.spark.SparkDataSet) cater for such use cases, but the use of Spark is not always feasible. +This is why Kedro provides a built-in [PartitionedDataset](/kedro.io.PartitionedDataset), with the following features: -This is why Kedro provides a built-in [PartitionedDataSet](/kedro.io.PartitionedDataSet), with the following features: - -* `PartitionedDataSet` can recursively load/save all or specific files from a given location. +* `PartitionedDataset` can recursively load/save all or specific files from a given location. * It is platform agnostic, and can work with any filesystem implementation supported by [fsspec](https://filesystem-spec.readthedocs.io/) including local, S3, GCS, and many more. * It implements a [lazy loading](https://en.wikipedia.org/wiki/Lazy_loading) approach, and does not attempt to load any partition data until a processing node explicitly requests it. * It supports lazy saving by using `Callable`s. @@ -16,15 +15,15 @@ This is why Kedro provides a built-in [PartitionedDataSet](/kedro.io.Partitioned In this section, each individual file inside a given location is called a partition. ``` -### How to use `PartitionedDataSet` +### How to use `PartitionedDataset` -You can use a `PartitionedDataSet` in `catalog.yml` file like any other regular dataset definition: +You can use a `PartitionedDataset` in `catalog.yml` file like any other regular dataset definition: ```yaml # conf/base/catalog.yml my_partitioned_dataset: - type: PartitionedDataSet + type: PartitionedDataset path: s3://my-bucket-name/path/to/folder # path to the location of partitions dataset: pandas.CSVDataSet # shorthand notation for the dataset which will handle individual partitions credentials: my_credentials @@ -34,16 +33,16 @@ my_partitioned_dataset: ``` ```{note} -Like any other dataset, `PartitionedDataSet` can also be instantiated programmatically in Python: +Like any other dataset, `PartitionedDataset` can also be instantiated programmatically in Python: ``` ```python from kedro_datasets.pandas import CSVDataSet -from kedro.io import PartitionedDataSet +from kedro.io import PartitionedDataset my_credentials = {...} # credentials dictionary -my_partitioned_dataset = PartitionedDataSet( +my_partitioned_dataset = PartitionedDataset( path="s3://my-bucket-name/path/to/folder", dataset=CSVDataSet, credentials=my_credentials, @@ -57,7 +56,7 @@ Alternatively, if you need more granular configuration of the underlying dataset # conf/base/catalog.yml my_partitioned_dataset: - type: PartitionedDataSet + type: PartitionedDataset path: s3://my-bucket-name/path/to/folder dataset: # full dataset config notation type: pandas.CSVDataSet @@ -73,7 +72,7 @@ my_partitioned_dataset: filename_suffix: ".csv" ``` -Here is an exhaustive list of the arguments supported by `PartitionedDataSet`: +Here is an exhaustive list of the arguments supported by `PartitionedDataset`: | Argument | Required | Supported types | Description | | ----------------- | ------------------------------ | ------------------------------------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | @@ -86,7 +85,7 @@ Here is an exhaustive list of the arguments supported by `PartitionedDataSet`: ### Dataset definition -The dataset definition should be passed into the `dataset` argument of the `PartitionedDataSet`. The dataset definition is used to instantiate a new dataset object for each individual partition, and use that dataset object for load and save operations. Dataset definition supports shorthand and full notations. +The dataset definition should be passed into the `dataset` argument of the `PartitionedDataset`. The dataset definition is used to instantiate a new dataset object for each individual partition, and use that dataset object for load and save operations. Dataset definition supports shorthand and full notations. #### Shorthand notation @@ -96,26 +95,26 @@ Requires you only to specify a class of the underlying dataset either as a strin Full notation allows you to specify a dictionary with the full underlying dataset definition _except_ the following arguments: * The argument that receives the partition path (`filepath` by default) - if specified, a `UserWarning` will be emitted stating that this value will be overridden by individual partition paths -* `credentials` key - specifying it will result in a `DataSetError` being raised; dataset credentials should be passed into the `credentials` argument of the `PartitionedDataSet` rather than the underlying dataset definition - see the section below on [partitioned dataset credentials](#partitioned-dataset-credentials) for details -* `versioned` flag - specifying it will result in a `DataSetError` being raised; versioning cannot be enabled for the underlying datasets +* `credentials` key - specifying it will result in a `DatasetError` being raised; dataset credentials should be passed into the `credentials` argument of the `PartitionedDataset` rather than the underlying dataset definition - see the section below on [partitioned dataset credentials](#partitioned-dataset-credentials) for details +* `versioned` flag - specifying it will result in a `DatasetError` being raised; versioning cannot be enabled for the underlying datasets ### Partitioned dataset credentials ```{note} -Support for `dataset_credentials` key in the credentials for `PartitionedDataSet` is now deprecated. The dataset credentials should be specified explicitly inside the dataset config. +Support for `dataset_credentials` key in the credentials for `PartitionedDataset` is now deprecated. The dataset credentials should be specified explicitly inside the dataset config. ``` -Credentials management for `PartitionedDataSet` is somewhat special, because it might contain credentials for both `PartitionedDataSet` itself _and_ the underlying dataset that is used for partition load and save. Top-level credentials are passed to the underlying dataset config (unless such config already has credentials configured), but not the other way around - dataset credentials are never propagated to the filesystem. +Credentials management for `PartitionedDataset` is somewhat special, because it might contain credentials for both `PartitionedDataset` itself _and_ the underlying dataset that is used for partition load and save. Top-level credentials are passed to the underlying dataset config (unless such config already has credentials configured), but not the other way around - dataset credentials are never propagated to the filesystem. Here is the full list of possible scenarios: -| Top-level credentials | Underlying dataset credentials | Example `PartitionedDataSet` definition | Description | +| Top-level credentials | Underlying dataset credentials | Example `PartitionedDataset` definition | Description | | --------------------- | ------------------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| Undefined | Undefined | `PartitionedDataSet(path="s3://bucket-name/path/to/folder", dataset="pandas.CSVDataSet")` | Credentials are not passed to the underlying dataset or the filesystem | -| Undefined | Specified | `PartitionedDataSet(path="s3://bucket-name/path/to/folder", dataset={"type": "pandas.CSVDataSet", "credentials": {"secret": True}})` | Underlying dataset credentials are passed to the `CSVDataSet` constructor, filesystem is instantiated without credentials | -| Specified | Undefined | `PartitionedDataSet(path="s3://bucket-name/path/to/folder", dataset="pandas.CSVDataSet", credentials={"secret": True})` | Top-level credentials are passed to the underlying `CSVDataSet` constructor and the filesystem | -| Specified | `None` | `PartitionedDataSet(path="s3://bucket-name/path/to/folder", dataset={"type": "pandas.CSVDataSet", "credentials": None}, credentials={"dataset_secret": True})` | Top-level credentials are passed to the filesystem, `CSVDataSet` is instantiated without credentials - this way you can stop the top-level credentials from propagating into the dataset config | -| Specified | Specified | `PartitionedDataSet(path="s3://bucket-name/path/to/folder", dataset={"type": "pandas.CSVDataSet", "credentials": {"dataset_secret": True}}, credentials={"secret": True})` | Top-level credentials are passed to the filesystem, underlying dataset credentials are passed to the `CSVDataSet` constructor | +| Undefined | Undefined | `PartitionedDataset(path="s3://bucket-name/path/to/folder", dataset="pandas.CSVDataSet")` | Credentials are not passed to the underlying dataset or the filesystem | +| Undefined | Specified | `PartitionedDataset(path="s3://bucket-name/path/to/folder", dataset={"type": "pandas.CSVDataSet", "credentials": {"secret": True}})` | Underlying dataset credentials are passed to the `CSVDataSet` constructor, filesystem is instantiated without credentials | +| Specified | Undefined | `PartitionedDataset(path="s3://bucket-name/path/to/folder", dataset="pandas.CSVDataSet", credentials={"secret": True})` | Top-level credentials are passed to the underlying `CSVDataSet` constructor and the filesystem | +| Specified | `None` | `PartitionedDataset(path="s3://bucket-name/path/to/folder", dataset={"type": "pandas.CSVDataSet", "credentials": None}, credentials={"dataset_secret": True})` | Top-level credentials are passed to the filesystem, `CSVDataSet` is instantiated without credentials - this way you can stop the top-level credentials from propagating into the dataset config | +| Specified | Specified | `PartitionedDataset(path="s3://bucket-name/path/to/folder", dataset={"type": "pandas.CSVDataSet", "credentials": {"dataset_secret": True}}, credentials={"secret": True})` | Top-level credentials are passed to the filesystem, underlying dataset credentials are passed to the `CSVDataSet` constructor | ### Partitioned dataset load @@ -153,7 +152,7 @@ def concat_partitions(partitioned_input: Dict[str, Callable[[], Any]]) -> pd.Dat return result ``` -As you can see from the above example, on load `PartitionedDataSet` _does not_ automatically load the data from the located partitions. Instead, `PartitionedDataSet` returns a dictionary with partition IDs as keys and the corresponding load functions as values. It allows the node that consumes the `PartitionedDataSet` to implement the logic that defines what partitions need to be loaded, and how this data is going to be processed. +As you can see from the above example, on load `PartitionedDataset` _does not_ automatically load the data from the located partitions. Instead, `PartitionedDataset` returns a dictionary with partition IDs as keys and the corresponding load functions as values. It allows the node that consumes the `PartitionedDataset` to implement the logic that defines what partitions need to be loaded, and how this data is going to be processed. Partition ID _does not_ represent the whole partition path, but only a part of it that is unique for a given partition _and_ filename suffix: @@ -162,17 +161,17 @@ Partition ID _does not_ represent the whole partition path, but only a part of i * Example 2: if `path=s3://my-bucket-name/folder` and `filename_suffix=".csv"` and partition is stored in `s3://my-bucket-name/folder/2019-12-04/data.csv`, then its Partition ID is `2019-12-04/data`. -`PartitionedDataSet` implements caching on load operation, which means that if multiple nodes consume the same `PartitionedDataSet`, they will all receive the same partition dictionary even if some new partitions were added to the folder after the first load has been completed. This is done deliberately to guarantee the consistency of load operations between the nodes and avoid race conditions. To reset the cache, call the `release()` method of the partitioned dataset object. +`PartitionedDataset` implements caching on load operation, which means that if multiple nodes consume the same `PartitionedDataset`, they will all receive the same partition dictionary even if some new partitions were added to the folder after the first load has been completed. This is done deliberately to guarantee the consistency of load operations between the nodes and avoid race conditions. To reset the cache, call the `release()` method of the partitioned dataset object. ### Partitioned dataset save -`PartitionedDataSet` also supports a save operation. Let's assume the following configuration: +`PartitionedDataset` also supports a save operation. Let's assume the following configuration: ```yaml # conf/base/catalog.yml new_partitioned_dataset: - type: PartitionedDataSet + type: PartitionedDataset path: s3://my-bucket-name dataset: pandas.CSVDataSet filename_suffix: ".csv" @@ -194,7 +193,7 @@ import pandas as pd def create_partitions() -> Dict[str, Any]: - """Create new partitions and save using PartitionedDataSet. + """Create new partitions and save using PartitionedDataset. Returns: Dictionary with the partitions to create. @@ -208,11 +207,11 @@ def create_partitions() -> Dict[str, Any]: ``` ```{note} -Writing to an existing partition may result in its data being overwritten, if this case is not specifically handled by the underlying dataset implementation. You should implement your own checks to ensure that no existing data is lost when writing to a `PartitionedDataSet`. The simplest safety mechanism could be to use partition IDs with a high chance of uniqueness: for example, the current timestamp. +Writing to an existing partition may result in its data being overwritten, if this case is not specifically handled by the underlying dataset implementation. You should implement your own checks to ensure that no existing data is lost when writing to a `PartitionedDataset`. The simplest safety mechanism could be to use partition IDs with a high chance of uniqueness: for example, the current timestamp. ``` ### Partitioned dataset lazy saving -`PartitionedDataSet` also supports lazy saving, where the partition's data is not materialised until it is time to write. +`PartitionedDataset` also supports lazy saving, where the partition's data is not materialised until it is time to write. To use this, simply return `Callable` types in the dictionary: @@ -222,7 +221,7 @@ import pandas as pd def create_partitions() -> Dict[str, Callable[[], Any]]: - """Create new partitions and save using PartitionedDataSet. + """Create new partitions and save using PartitionedDataset. Returns: Dictionary of the partitions to create to a function that creates them. @@ -238,23 +237,24 @@ def create_partitions() -> Dict[str, Callable[[], Any]]: ```{note} When using lazy saving, the dataset will be written _after_ the `after_node_run` [hook](../hooks/introduction). ``` + ## Incremental datasets -[IncrementalDataSet](/kedro.io.IncrementalDataSet) is a subclass of `PartitionedDataSet`, which stores the information about the last processed partition in the so-called `checkpoint`. `IncrementalDataSet` addresses the use case when partitions have to be processed incrementally, i.e. each subsequent pipeline run should only process the partitions which were not processed by the previous runs. +[IncrementalDataset](/kedro.io.IncrementalDataset) is a subclass of `PartitionedDataset`, which stores the information about the last processed partition in the so-called `checkpoint`. `IncrementalDataset` addresses the use case when partitions have to be processed incrementally, i.e. each subsequent pipeline run should only process the partitions which were not processed by the previous runs. -This checkpoint, by default, is persisted to the location of the data partitions. For example, for `IncrementalDataSet` instantiated with path `s3://my-bucket-name/path/to/folder`, the checkpoint will be saved to `s3://my-bucket-name/path/to/folder/CHECKPOINT`, unless [the checkpoint configuration is explicitly overwritten](#checkpoint-configuration). +This checkpoint, by default, is persisted to the location of the data partitions. For example, for `IncrementalDataset` instantiated with path `s3://my-bucket-name/path/to/folder`, the checkpoint will be saved to `s3://my-bucket-name/path/to/folder/CHECKPOINT`, unless [the checkpoint configuration is explicitly overwritten](#checkpoint-configuration). The checkpoint file is only created _after_ [the partitioned dataset is explicitly confirmed](#incremental-dataset-confirm). ### Incremental dataset loads -Loading `IncrementalDataSet` works similarly to [`PartitionedDataSet`](#partitioned-dataset-load) with several exceptions: -1. `IncrementalDataSet` loads the data _eagerly_, so the values in the returned dictionary represent the actual data stored in the corresponding partition, rather than a pointer to the load function. `IncrementalDataSet` considers a partition relevant for processing if its ID satisfies the comparison function, given the checkpoint value. -2. `IncrementalDataSet` _does not_ raise a `DataSetError` if load finds no partitions to return - an empty dictionary is returned instead. An empty list of available partitions is part of a normal workflow for `IncrementalDataSet`. +Loading `IncrementalDataset` works similarly to [`PartitionedDataset`](#partitioned-dataset-load) with several exceptions: +1. `IncrementalDataset` loads the data _eagerly_, so the values in the returned dictionary represent the actual data stored in the corresponding partition, rather than a pointer to the load function. `IncrementalDataset` considers a partition relevant for processing if its ID satisfies the comparison function, given the checkpoint value. +2. `IncrementalDataset` _does not_ raise a `DatasetError` if load finds no partitions to return - an empty dictionary is returned instead. An empty list of available partitions is part of a normal workflow for `IncrementalDataset`. ### Incremental dataset save -The `IncrementalDataSet` save operation is identical to the [save operation of the `PartitionedDataSet`](#partitioned-dataset-save). +The `IncrementalDataset` save operation is identical to the [save operation of the `PartitionedDataset`](#partitioned-dataset-save). ### Incremental dataset confirm @@ -267,7 +267,7 @@ Partitioned dataset checkpoint update is triggered by an explicit `confirms` ins ```python from kedro.pipeline import node -# process and then confirm `IncrementalDataSet` within the same node +# process and then confirm `IncrementalDataset` within the same node node( process_partitions, inputs="my_partitioned_dataset", @@ -309,17 +309,17 @@ pipeline( Important notes about the confirmation operation: -* Confirming a partitioned dataset does not affect any subsequent loads within the same run. All downstream nodes that input the same partitioned dataset as input will all receive the _same_ partitions. Partitions that are created externally during the run will also not affect the dataset loads and won't appear in the list of loaded partitions until the next run or until the [`release()`](/kedro.io.IncrementalDataSet) method is called on the dataset object. +* Confirming a partitioned dataset does not affect any subsequent loads within the same run. All downstream nodes that input the same partitioned dataset as input will all receive the _same_ partitions. Partitions that are created externally during the run will also not affect the dataset loads and won't appear in the list of loaded partitions until the next run or until the [`release()`](/kedro.io.IncrementalDataset) method is called on the dataset object. * A pipeline cannot contain more than one node confirming the same dataset. ### Checkpoint configuration -`IncrementalDataSet` does not require explicit configuration of the checkpoint unless there is a need to deviate from the defaults. To update the checkpoint configuration, add a `checkpoint` key containing the valid dataset configuration. This may be required if, say, the pipeline has read-only permissions to the location of partitions (or write operations are undesirable for any other reason). In such cases, `IncrementalDataSet` can be configured to save the checkpoint elsewhere. The `checkpoint` key also supports partial config updates where only some checkpoint attributes are overwritten, while the defaults are kept for the rest: +`IncrementalDataset` does not require explicit configuration of the checkpoint unless there is a need to deviate from the defaults. To update the checkpoint configuration, add a `checkpoint` key containing the valid dataset configuration. This may be required if, say, the pipeline has read-only permissions to the location of partitions (or write operations are undesirable for any other reason). In such cases, `IncrementalDataset` can be configured to save the checkpoint elsewhere. The `checkpoint` key also supports partial config updates where only some checkpoint attributes are overwritten, while the defaults are kept for the rest: ```yaml my_partitioned_dataset: - type: IncrementalDataSet + type: IncrementalDataset path: s3://my-bucket-name/path/to/folder dataset: pandas.CSVDataSet checkpoint: @@ -336,7 +336,7 @@ Along with the standard dataset attributes, `checkpoint` config also accepts two ```yaml my_partitioned_dataset: - type: IncrementalDataSet + type: IncrementalDataset path: s3://my-bucket-name/path/to/folder dataset: pandas.CSVDataSet checkpoint: @@ -347,7 +347,7 @@ my_partitioned_dataset: ```yaml my_partitioned_dataset: - type: IncrementalDataSet + type: IncrementalDataset path: s3://my-bucket-name/path/to/folder dataset: pandas.CSVDataSet checkpoint: @@ -360,7 +360,7 @@ Specification of `force_checkpoint` is also supported via the shorthand notation ```yaml my_partitioned_dataset: - type: IncrementalDataSet + type: IncrementalDataset path: s3://my-bucket-name/path/to/folder dataset: pandas.CSVDataSet checkpoint: 2020-01-01/data.csv @@ -372,7 +372,7 @@ If you need to force the partitioned dataset to load all available partitions, s ```yaml my_partitioned_dataset: - type: IncrementalDataSet + type: IncrementalDataset path: s3://my-bucket-name/path/to/folder dataset: pandas.CSVDataSet checkpoint: "" diff --git a/docs/source/deployment/argo.md b/docs/source/deployment/argo.md index 9207debe3d..3aa86b8213 100644 --- a/docs/source/deployment/argo.md +++ b/docs/source/deployment/argo.md @@ -24,7 +24,7 @@ To use Argo Workflows, ensure you have the following prerequisites in place: - [Argo Workflows is installed](https://github.com/argoproj/argo/blob/master/README.md#quickstart) on your Kubernetes cluster - [Argo CLI is installed](https://github.com/argoproj/argo/releases) on your machine - A `name` attribute is set for each [Kedro node](/kedro.pipeline.node) since it is used to build a DAG -- [All node input/output DataSets must be configured in `catalog.yml`](../data/data_catalog_yaml_examples.md) and refer to an external location (e.g. AWS S3); you cannot use the `MemoryDataSet` in your workflow +- [All node input/output datasets must be configured in `catalog.yml`](../data/data_catalog_yaml_examples.md) and refer to an external location (e.g. AWS S3); you cannot use the `MemoryDataset` in your workflow ```{note} Each node will run in its own container. @@ -174,7 +174,7 @@ spec: The Argo Workflows is defined as the dependencies between tasks using a directed-acyclic graph (DAG). ``` -For the purpose of this walk-through, we will use an AWS S3 bucket for DataSets; therefore `AWS_ACCESS_KEY_ID` and `AWS_SECRET_ACCESS_KEY` environment variables must be set to have an ability to communicate with S3. The `AWS_ACCESS_KEY_ID` and `AWS_SECRET_ACCESS_KEY` values should be stored in [Kubernetes Secrets](https://kubernetes.io/docs/concepts/configuration/secret/) (an example [Kubernetes Secrets spec is given below](#submit-argo-workflows-spec-to-kubernetes)). +For the purpose of this walk-through, we will use an AWS S3 bucket for datasets; therefore `AWS_ACCESS_KEY_ID` and `AWS_SECRET_ACCESS_KEY` environment variables must be set to have an ability to communicate with S3. The `AWS_ACCESS_KEY_ID` and `AWS_SECRET_ACCESS_KEY` values should be stored in [Kubernetes Secrets](https://kubernetes.io/docs/concepts/configuration/secret/) (an example [Kubernetes Secrets spec is given below](#submit-argo-workflows-spec-to-kubernetes)). The spec template is written with the [Jinja templating language](https://jinja.palletsprojects.com/en/2.11.x/), so you must install the Jinja Python package: diff --git a/docs/source/deployment/aws_batch.md b/docs/source/deployment/aws_batch.md index c83b58f8ea..5856701608 100644 --- a/docs/source/deployment/aws_batch.md +++ b/docs/source/deployment/aws_batch.md @@ -18,7 +18,7 @@ To use AWS Batch, ensure you have the following prerequisites in place: - An [AWS account set up](https://aws.amazon.com/premiumsupport/knowledge-center/create-and-activate-aws-account/). - A `name` attribute is set for each [Kedro node](/kedro.pipeline.node). Each node will run in its own Batch job, so having sensible node names will make it easier to `kedro run --node=`. -- [All node input/output `DataSets` must be configured in `catalog.yml`](../data/data_catalog_yaml_examples.md) and refer to an external location (e.g. AWS S3). A clean way to do this is to create a new configuration environment `conf/aws_batch` containing a `catalog.yml` file with the appropriate configuration, as illustrated below. +- [All node input/output datasets must be configured in `catalog.yml`](../data/data_catalog_yaml_examples.md) and refer to an external location (e.g. AWS S3). A clean way to do this is to create a new configuration environment `conf/aws_batch` containing a `catalog.yml` file with the appropriate configuration, as illustrated below.
Click to expand diff --git a/docs/source/deployment/aws_step_functions.md b/docs/source/deployment/aws_step_functions.md index 380f303067..7a08ba3416 100644 --- a/docs/source/deployment/aws_step_functions.md +++ b/docs/source/deployment/aws_step_functions.md @@ -40,7 +40,7 @@ $ cdk -h The deployment process for a Kedro pipeline on AWS Step Functions consists of the following steps: * Develop the Kedro pipeline locally as normal -* Create a new configuration environment in which we ensure all nodes' inputs and outputs have a persistent location on S3, since `MemoryDataSet` can't be shared between AWS Lambda functions +* Create a new configuration environment in which we ensure all nodes' inputs and outputs have a persistent location on S3, since `MemoryDataset` can't be shared between AWS Lambda functions * Package the Kedro pipeline as an [AWS Lambda-compliant Docker image](https://docs.aws.amazon.com/lambda/latest/dg/lambda-images.html) * Write a script to convert and deploy each Kedro node as an AWS Lambda function. Each function will use the same pipeline Docker image created in the previous step and run a single Kedro node associated with it. This follows the principles laid out in our [distributed deployment guide](distributed). * The script above will also convert and deploy the entire Kedro pipeline as an AWS Step Functions State Machine. diff --git a/docs/source/deployment/prefect.md b/docs/source/deployment/prefect.md index b602b499ec..4021fab3fa 100644 --- a/docs/source/deployment/prefect.md +++ b/docs/source/deployment/prefect.md @@ -69,7 +69,7 @@ from kedro.framework.hooks.manager import _create_hook_manager from kedro.framework.project import pipelines from kedro.framework.session import KedroSession from kedro.framework.startup import bootstrap_project -from kedro.io import DataCatalog, MemoryDataSet +from kedro.io import DataCatalog, MemoryDataset from kedro.pipeline.node import Node from kedro.runner import run_node @@ -165,7 +165,7 @@ def kedro_init( logger.info("Registering datasets...") unregistered_ds = pipeline.data_sets() - set(catalog.list()) # NOQA for ds_name in unregistered_ds: - catalog.add(ds_name, MemoryDataSet()) + catalog.add(ds_name, MemoryDataset()) return {"catalog": catalog, "sess_id": session.session_id} diff --git a/docs/source/development/commands_reference.md b/docs/source/development/commands_reference.md index 1c99dae715..adf3db84c3 100644 --- a/docs/source/development/commands_reference.md +++ b/docs/source/development/commands_reference.md @@ -513,7 +513,7 @@ The output includes a list of any [dataset factories](../data/kedro_dataset_fact ##### Create a Data Catalog YAML configuration file -The following command creates a Data Catalog YAML configuration file with `MemoryDataSet` datasets for each dataset in a registered pipeline, if it is missing from the `DataCatalog`. +The following command creates a Data Catalog YAML configuration file with `MemoryDataset` datasets for each dataset in a registered pipeline, if it is missing from the `DataCatalog`. ```bash kedro catalog create --pipeline= diff --git a/docs/source/extend_kedro/common_use_cases.md b/docs/source/extend_kedro/common_use_cases.md index 9f8d32dc9f..5eb2541673 100644 --- a/docs/source/extend_kedro/common_use_cases.md +++ b/docs/source/extend_kedro/common_use_cases.md @@ -4,7 +4,7 @@ Kedro has a few built-in mechanisms for you to extend its behaviour. This docume ## Use Case 1: How to add extra behaviour to Kedro's execution timeline -The execution timeline of a Kedro pipeline can be thought of as a sequence of actions performed by various Kedro library components, such as the [DataSets](/kedro_datasets), [DataCatalog](/kedro.io.DataCatalog), [Pipeline](/kedro.pipeline.Pipeline), [Node](/kedro.pipeline.node.Node) and [KedroContext](/kedro.framework.context.KedroContext). +The execution timeline of a Kedro pipeline can be thought of as a sequence of actions performed by various Kedro library components, such as the [datasets](/kedro_datasets), [DataCatalog](/kedro.io.DataCatalog), [Pipeline](/kedro.pipeline.Pipeline), [Node](/kedro.pipeline.node.Node) and [KedroContext](/kedro.framework.context.KedroContext). At different points in the lifecycle of these components, you might want to add extra behaviour: for example, you could add extra computation for profiling purposes _before_ and _after_ a node runs, or _before_ and _after_ the I/O actions of a dataset, namely the `load` and `save` actions. @@ -12,7 +12,7 @@ This can now achieved by using [Hooks](../hooks/introduction.md), to define the ## Use Case 2: How to integrate Kedro with additional data sources -You can use [DataSets](/kedro_datasets) to interface with various different data sources. If the data source you plan to use is not supported out of the box by Kedro, you can [create a custom dataset](../data/how_to_create_a_custom_dataset.md). +You can use [datasets](/kedro_datasets) to interface with various different data sources. If the data source you plan to use is not supported out of the box by Kedro, you can [create a custom dataset](../data/how_to_create_a_custom_dataset.md). ## Use Case 3: How to add or modify CLI commands diff --git a/docs/source/hooks/examples.md b/docs/source/hooks/examples.md index f556879319..9a293e56a0 100644 --- a/docs/source/hooks/examples.md +++ b/docs/source/hooks/examples.md @@ -78,17 +78,17 @@ The output should look similar to the following: ... [01/25/23 21:38:23] INFO Loading data from 'example_iris_data' (CSVDataSet)... data_catalog.py:343 INFO Loading example_iris_data consumed 0.99MiB memory hooks.py:67 - INFO Loading data from 'parameters' (MemoryDataSet)... data_catalog.py:343 + INFO Loading data from 'parameters' (MemoryDataset)... data_catalog.py:343 INFO Loading parameters consumed 0.48MiB memory hooks.py:67 INFO Running node: split: split_data([example_iris_data,parameters]) -> [X_train,X_test,y_train,y_test] node.py:327 - INFO Saving data to 'X_train' (MemoryDataSet)... data_catalog.py:382 - INFO Saving data to 'X_test' (MemoryDataSet)... data_catalog.py:382 - INFO Saving data to 'y_train' (MemoryDataSet)... data_catalog.py:382 - INFO Saving data to 'y_test' (MemoryDataSet)... data_catalog.py:382 + INFO Saving data to 'X_train' (MemoryDataset)... data_catalog.py:382 + INFO Saving data to 'X_test' (MemoryDataset)... data_catalog.py:382 + INFO Saving data to 'y_train' (MemoryDataset)... data_catalog.py:382 + INFO Saving data to 'y_test' (MemoryDataset)... data_catalog.py:382 INFO Completed 1 out of 3 tasks sequential_runner.py:85 - INFO Loading data from 'X_train' (MemoryDataSet)... data_catalog.py:343 + INFO Loading data from 'X_train' (MemoryDataset)... data_catalog.py:343 INFO Loading X_train consumed 0.49MiB memory hooks.py:67 - INFO Loading data from 'X_test' (MemoryDataSet)... + INFO Loading data from 'X_test' (MemoryDataset)... ... ``` @@ -264,7 +264,7 @@ This example adds observability to your pipeline using [statsd](https://statsd.r pip install statsd ``` -* Implement `before_node_run` and `after_node_run` Hooks to collect metrics (DataSet size and node execution time): +* Implement `before_node_run` and `after_node_run` Hooks to collect metrics (Dataset size and node execution time): ```python # src//hooks.py diff --git a/docs/source/integrations/pyspark_integration.md b/docs/source/integrations/pyspark_integration.md index 3afaf084c7..c0e5cec08b 100644 --- a/docs/source/integrations/pyspark_integration.md +++ b/docs/source/integrations/pyspark_integration.md @@ -169,7 +169,7 @@ pipeline( ) ``` -`first_operation_complete` is a `MemoryDataSet` and it signals that any Delta operations which occur "outside" the Kedro DAG are complete. This can be used as input to a downstream node, to preserve the shape of the DAG. Otherwise, if no downstream nodes need to run after this, the node can simply not return anything: +`first_operation_complete` is a `MemoryDataset` and it signals that any Delta operations which occur "outside" the Kedro DAG are complete. This can be used as input to a downstream node, to preserve the shape of the DAG. Otherwise, if no downstream nodes need to run after this, the node can simply not return anything: ```python pipeline( @@ -188,11 +188,11 @@ The following diagram is the visual representation of the workflow explained abo This pattern of creating "dummy" datasets to preserve the data flow also applies to other "out of DAG" execution operations such as SQL operations within a node. ``` -## Use `MemoryDataSet` for intermediary `DataFrame` +## Use `MemoryDataset` for intermediary `DataFrame` -For nodes operating on `DataFrame` that doesn't need to perform Spark actions such as writing the `DataFrame` to storage, we recommend using the default `MemoryDataSet` to hold the `DataFrame`. In other words, there is no need to specify it in the `DataCatalog` or `catalog.yml`. This allows you to take advantage of Spark's optimiser and lazy evaluation. +For nodes operating on `DataFrame` that doesn't need to perform Spark actions such as writing the `DataFrame` to storage, we recommend using the default `MemoryDataset` to hold the `DataFrame`. In other words, there is no need to specify it in the `DataCatalog` or `catalog.yml`. This allows you to take advantage of Spark's optimiser and lazy evaluation. -## Use `MemoryDataSet` with `copy_mode="assign"` for non-`DataFrame` Spark objects +## Use `MemoryDataset` with `copy_mode="assign"` for non-`DataFrame` Spark objects Sometimes, you might want to use Spark objects that aren't `DataFrame` as inputs and outputs in your pipeline. For example, suppose you have a `train_model` node to train a classifier using Spark ML's [`RandomForrestClassifier`](https://spark.apache.org/docs/latest/ml-classification-regression.html#random-forest-classifier) and a `predict` node to make predictions using this classifier. In this scenario, the `train_model` node will output a `RandomForestClassifier` object, which then becomes the input for the `predict` node. Below is the code for this pipeline: @@ -233,11 +233,11 @@ To make the pipeline work, you will need to specify `example_classifier` as foll ```yaml example_classifier: - type: MemoryDataSet + type: MemoryDataset copy_mode: assign ``` -The `assign` copy mode ensures that the `MemoryDataSet` will be assigned the Spark object itself, not a [deep copy](https://docs.python.org/3/library/copy.html) version of it, since deep copy doesn't work with Spark object generally. +The `assign` copy mode ensures that the `MemoryDataset` will be assigned the Spark object itself, not a [deep copy](https://docs.python.org/3/library/copy.html) version of it, since deep copy doesn't work with Spark object generally. ## Tips for maximising concurrency using `ThreadRunner` diff --git a/docs/source/nodes_and_pipelines/run_a_pipeline.md b/docs/source/nodes_and_pipelines/run_a_pipeline.md index 2d7af412ad..e23d2ceaa3 100644 --- a/docs/source/nodes_and_pipelines/run_a_pipeline.md +++ b/docs/source/nodes_and_pipelines/run_a_pipeline.md @@ -57,7 +57,7 @@ If the built-in Kedro runners do not meet your requirements, you can also define ```python # in src//runner.py -from kedro.io import AbstractDataset, DataCatalog, MemoryDataSet +from kedro.io import AbstractDataset, DataCatalog, MemoryDataset from kedro.pipeline import Pipeline from kedro.runner.runner import AbstractRunner from pluggy import PluginManager @@ -79,7 +79,7 @@ class DryRunner(AbstractRunner): for all unregistered data sets. """ - return MemoryDataSet() + return MemoryDataset() def _run( self, @@ -198,14 +198,14 @@ By using `DataCatalog` from the IO module we are still able to write pure functi Through `DataCatalog`, we can control where inputs are loaded from, where intermediate variables get persisted and ultimately the location to which output variables are written. -In a simple example, we define a `MemoryDataSet` called `xs` to store our inputs, save our input list `[1, 2, 3]` into `xs`, then instantiate `SequentialRunner` and call its `run` method with the pipeline and data catalog instances: +In a simple example, we define a `MemoryDataset` called `xs` to store our inputs, save our input list `[1, 2, 3]` into `xs`, then instantiate `SequentialRunner` and call its `run` method with the pipeline and data catalog instances:
Click to expand ```python -io = DataCatalog(dict(xs=MemoryDataSet())) +io = DataCatalog(dict(xs=MemoryDataset())) ``` ```python @@ -237,7 +237,7 @@ Out[11]: {'v': 0.666666666666667} ## Output to a file -We can also use IO to save outputs to a file. In this example, we define a custom `LambdaDataSet` that would serialise the output to a file locally: +We can also use IO to save outputs to a file. In this example, we define a custom `LambdaDataset` that would serialise the output to a file locally:
Click to expand @@ -254,14 +254,14 @@ def load(): return pickle.load(f) -pickler = LambdaDataSet(load=load, save=save) +pickler = LambdaDataset(load=load, save=save) io.add("v", pickler) ```
It is important to make sure that the data catalog variable name `v` matches the name `v` in the pipeline definition. -Next we can confirm that this `LambdaDataSet` behaves correctly: +Next we can confirm that this `LambdaDataset` behaves correctly:
Click to expand diff --git a/docs/source/nodes_and_pipelines/slice_a_pipeline.md b/docs/source/nodes_and_pipelines/slice_a_pipeline.md index f4f4bccf0d..2ed8ee4b3a 100644 --- a/docs/source/nodes_and_pipelines/slice_a_pipeline.md +++ b/docs/source/nodes_and_pipelines/slice_a_pipeline.md @@ -303,10 +303,10 @@ To demonstrate this, let us save the intermediate output `n` using a `JSONDataSe ```python from kedro_datasets.pandas import JSONDataSet -from kedro.io import DataCatalog, MemoryDataSet +from kedro.io import DataCatalog, MemoryDataset n_json = JSONDataSet(filepath="./data/07_model_output/len.json") -io = DataCatalog(dict(xs=MemoryDataSet([1, 2, 3]), n=n_json)) +io = DataCatalog(dict(xs=MemoryDataset([1, 2, 3]), n=n_json)) ```
diff --git a/docs/source/notebooks_and_ipython/kedro_and_notebooks.md b/docs/source/notebooks_and_ipython/kedro_and_notebooks.md index 8344b1346f..0cd509b32c 100644 --- a/docs/source/notebooks_and_ipython/kedro_and_notebooks.md +++ b/docs/source/notebooks_and_ipython/kedro_and_notebooks.md @@ -93,7 +93,7 @@ catalog.load("parameters") You should see the following: ```ipython -INFO Loading data from 'parameters' (MemoryDataSet)... +INFO Loading data from 'parameters' (MemoryDataset)... {'example_test_data_ratio': 0.2, 'example_num_train_iter': 10000, diff --git a/docs/source/resources/glossary.md b/docs/source/resources/glossary.md index 55f841c8e7..4f382d9b78 100644 --- a/docs/source/resources/glossary.md +++ b/docs/source/resources/glossary.md @@ -2,7 +2,7 @@ ## Data Catalog - The Data Catalog is Kedro's registry of all data sources available for use in the data pipeline. It manages loading and saving of data. The Data Catalog maps the names of node inputs and outputs as keys in a Kedro `DataSet`, which can be specialised for different types of data storage. + The Data Catalog is Kedro's registry of all data sources available for use in the data pipeline. It manages loading and saving of data. The Data Catalog maps the names of node inputs and outputs as keys in a Kedro dataset, which can be specialised for different types of data storage. [Further information about the Data Catalog](../data/data_catalog.md) diff --git a/docs/source/tutorial/add_another_pipeline.md b/docs/source/tutorial/add_another_pipeline.md index 1ceba96edc..b6324f3669 100644 --- a/docs/source/tutorial/add_another_pipeline.md +++ b/docs/source/tutorial/add_another_pipeline.md @@ -187,40 +187,40 @@ You should see output similar to the following: INFO Loading data from 'companies' (CSVDataSet)... data_catalog.py:343 INFO Running node: preprocess_companies_node: node.py:327 preprocess_companies([companies]) -> [preprocessed_companies] - INFO Saving data to 'preprocessed_companies' (MemoryDataSet)... data_catalog.py:382 + INFO Saving data to 'preprocessed_companies' (MemoryDataset)... data_catalog.py:382 INFO Completed 1 out of 6 tasks sequential_runner.py:85 INFO Loading data from 'shuttles' (ExcelDataSet)... data_catalog.py:343 [08/09/22 16:56:15] INFO Running node: preprocess_shuttles_node: preprocess_shuttles([shuttles]) node.py:327 -> [preprocessed_shuttles] - INFO Saving data to 'preprocessed_shuttles' (MemoryDataSet)... data_catalog.py:382 + INFO Saving data to 'preprocessed_shuttles' (MemoryDataset)... data_catalog.py:382 INFO Completed 2 out of 6 tasks sequential_runner.py:85 - INFO Loading data from 'preprocessed_shuttles' (MemoryDataSet)... data_catalog.py:343 - INFO Loading data from 'preprocessed_companies' (MemoryDataSet)... data_catalog.py:343 + INFO Loading data from 'preprocessed_shuttles' (MemoryDataset)... data_catalog.py:343 + INFO Loading data from 'preprocessed_companies' (MemoryDataset)... data_catalog.py:343 INFO Loading data from 'reviews' (CSVDataSet)... data_catalog.py:343 INFO Running node: create_model_input_table_node: node.py:327 create_model_input_table([preprocessed_shuttles,preprocessed_companies, reviews]) -> [model_input_table] -[08/09/22 16:56:18] INFO Saving data to 'model_input_table' (MemoryDataSet)... data_catalog.py:382 +[08/09/22 16:56:18] INFO Saving data to 'model_input_table' (MemoryDataset)... data_catalog.py:382 [08/09/22 16:56:19] INFO Completed 3 out of 6 tasks sequential_runner.py:85 - INFO Loading data from 'model_input_table' (MemoryDataSet)... data_catalog.py:343 - INFO Loading data from 'params:model_options' (MemoryDataSet)... data_catalog.py:343 + INFO Loading data from 'model_input_table' (MemoryDataset)... data_catalog.py:343 + INFO Loading data from 'params:model_options' (MemoryDataset)... data_catalog.py:343 INFO Running node: split_data_node: node.py:327 split_data([model_input_table,params:model_options]) -> [X_train,X_test,y_train,y_test] - INFO Saving data to 'X_train' (MemoryDataSet)... data_catalog.py:382 - INFO Saving data to 'X_test' (MemoryDataSet)... data_catalog.py:382 - INFO Saving data to 'y_train' (MemoryDataSet)... data_catalog.py:382 - INFO Saving data to 'y_test' (MemoryDataSet)... data_catalog.py:382 + INFO Saving data to 'X_train' (MemoryDataset)... data_catalog.py:382 + INFO Saving data to 'X_test' (MemoryDataset)... data_catalog.py:382 + INFO Saving data to 'y_train' (MemoryDataset)... data_catalog.py:382 + INFO Saving data to 'y_test' (MemoryDataset)... data_catalog.py:382 INFO Completed 4 out of 6 tasks sequential_runner.py:85 - INFO Loading data from 'X_train' (MemoryDataSet)... data_catalog.py:343 - INFO Loading data from 'y_train' (MemoryDataSet)... data_catalog.py:343 + INFO Loading data from 'X_train' (MemoryDataset)... data_catalog.py:343 + INFO Loading data from 'y_train' (MemoryDataset)... data_catalog.py:343 INFO Running node: train_model_node: train_model([X_train,y_train]) -> node.py:327 [regressor] [08/09/22 16:56:20] INFO Saving data to 'regressor' (PickleDataSet)... data_catalog.py:382 INFO Completed 5 out of 6 tasks sequential_runner.py:85 INFO Loading data from 'regressor' (PickleDataSet)... data_catalog.py:343 - INFO Loading data from 'X_test' (MemoryDataSet)... data_catalog.py:343 - INFO Loading data from 'y_test' (MemoryDataSet)... data_catalog.py:343 + INFO Loading data from 'X_test' (MemoryDataset)... data_catalog.py:343 + INFO Loading data from 'y_test' (MemoryDataset)... data_catalog.py:343 INFO Running node: evaluate_model_node: node.py:327 evaluate_model([regressor,X_test,y_test]) -> None INFO Model has a coefficient R^2 of 0.462 on test data. nodes.py:55 @@ -384,52 +384,52 @@ def create_pipeline(**kwargs) -> Pipeline: ^[[B[11/02/22 10:41:14] INFO Saving data to 'model_input_table' (ParquetDataSet)... data_catalog.py:382 [11/02/22 10:41:15] INFO Completed 3 out of 9 tasks sequential_runner.py:85 INFO Loading data from 'model_input_table' (ParquetDataSet)... data_catalog.py:343 - INFO Loading data from 'params:active_modelling_pipeline.model_options' (MemoryDataSet)... data_catalog.py:343 + INFO Loading data from 'params:active_modelling_pipeline.model_options' (MemoryDataset)... data_catalog.py:343 INFO Running node: split_data_node: node.py:327 split_data([model_input_table,params:active_modelling_pipeline.model_options]) -> [active_modelling_pipeline.X_train,active_modelling_pipeline.X_test,active_modelling_pipeline.y_t rain,active_modelling_pipeline.y_test] - INFO Saving data to 'active_modelling_pipeline.X_train' (MemoryDataSet)... data_catalog.py:382 - INFO Saving data to 'active_modelling_pipeline.X_test' (MemoryDataSet)... data_catalog.py:382 - INFO Saving data to 'active_modelling_pipeline.y_train' (MemoryDataSet)... data_catalog.py:382 - INFO Saving data to 'active_modelling_pipeline.y_test' (MemoryDataSet)... data_catalog.py:382 + INFO Saving data to 'active_modelling_pipeline.X_train' (MemoryDataset)... data_catalog.py:382 + INFO Saving data to 'active_modelling_pipeline.X_test' (MemoryDataset)... data_catalog.py:382 + INFO Saving data to 'active_modelling_pipeline.y_train' (MemoryDataset)... data_catalog.py:382 + INFO Saving data to 'active_modelling_pipeline.y_test' (MemoryDataset)... data_catalog.py:382 INFO Completed 4 out of 9 tasks sequential_runner.py:85 INFO Loading data from 'model_input_table' (ParquetDataSet)... data_catalog.py:343 - INFO Loading data from 'params:candidate_modelling_pipeline.model_options' (MemoryDataSet)... data_catalog.py:343 + INFO Loading data from 'params:candidate_modelling_pipeline.model_options' (MemoryDataset)... data_catalog.py:343 INFO Running node: split_data_node: node.py:327 split_data([model_input_table,params:candidate_modelling_pipeline.model_options]) -> [candidate_modelling_pipeline.X_train,candidate_modelling_pipeline.X_test,candidate_modelling_pip eline.y_train,candidate_modelling_pipeline.y_test] - INFO Saving data to 'candidate_modelling_pipeline.X_train' (MemoryDataSet)... data_catalog.py:382 - INFO Saving data to 'candidate_modelling_pipeline.X_test' (MemoryDataSet)... data_catalog.py:382 - INFO Saving data to 'candidate_modelling_pipeline.y_train' (MemoryDataSet)... data_catalog.py:382 - INFO Saving data to 'candidate_modelling_pipeline.y_test' (MemoryDataSet)... data_catalog.py:382 + INFO Saving data to 'candidate_modelling_pipeline.X_train' (MemoryDataset)... data_catalog.py:382 + INFO Saving data to 'candidate_modelling_pipeline.X_test' (MemoryDataset)... data_catalog.py:382 + INFO Saving data to 'candidate_modelling_pipeline.y_train' (MemoryDataset)... data_catalog.py:382 + INFO Saving data to 'candidate_modelling_pipeline.y_test' (MemoryDataset)... data_catalog.py:382 INFO Completed 5 out of 9 tasks sequential_runner.py:85 - INFO Loading data from 'active_modelling_pipeline.X_train' (MemoryDataSet)... data_catalog.py:343 - INFO Loading data from 'active_modelling_pipeline.y_train' (MemoryDataSet)... data_catalog.py:343 + INFO Loading data from 'active_modelling_pipeline.X_train' (MemoryDataset)... data_catalog.py:343 + INFO Loading data from 'active_modelling_pipeline.y_train' (MemoryDataset)... data_catalog.py:343 INFO Running node: train_model_node: node.py:327 train_model([active_modelling_pipeline.X_train,active_modelling_pipeline.y_train]) -> [active_modelling_pipeline.regressor] INFO Saving data to 'active_modelling_pipeline.regressor' (PickleDataSet)... data_catalog.py:382 INFO Completed 6 out of 9 tasks sequential_runner.py:85 - INFO Loading data from 'candidate_modelling_pipeline.X_train' (MemoryDataSet)... data_catalog.py:343 - INFO Loading data from 'candidate_modelling_pipeline.y_train' (MemoryDataSet)... data_catalog.py:343 + INFO Loading data from 'candidate_modelling_pipeline.X_train' (MemoryDataset)... data_catalog.py:343 + INFO Loading data from 'candidate_modelling_pipeline.y_train' (MemoryDataset)... data_catalog.py:343 INFO Running node: train_model_node: node.py:327 train_model([candidate_modelling_pipeline.X_train,candidate_modelling_pipeline.y_train]) -> [candidate_modelling_pipeline.regressor] INFO Saving data to 'candidate_modelling_pipeline.regressor' (PickleDataSet)... data_catalog.py:382 INFO Completed 7 out of 9 tasks sequential_runner.py:85 INFO Loading data from 'active_modelling_pipeline.regressor' (PickleDataSet)... data_catalog.py:343 - INFO Loading data from 'active_modelling_pipeline.X_test' (MemoryDataSet)... data_catalog.py:343 - INFO Loading data from 'active_modelling_pipeline.y_test' (MemoryDataSet)... data_catalog.py:343 + INFO Loading data from 'active_modelling_pipeline.X_test' (MemoryDataset)... data_catalog.py:343 + INFO Loading data from 'active_modelling_pipeline.y_test' (MemoryDataset)... data_catalog.py:343 INFO Running node: evaluate_model_node: node.py:327 evaluate_model([active_modelling_pipeline.regressor,active_modelling_pipeline.X_test,active_model ling_pipeline.y_test]) -> None INFO Model has a coefficient R^2 of 0.462 on test data. nodes.py:60 INFO Completed 8 out of 9 tasks sequential_runner.py:85 INFO Loading data from 'candidate_modelling_pipeline.regressor' (PickleDataSet)... data_catalog.py:343 - INFO Loading data from 'candidate_modelling_pipeline.X_test' (MemoryDataSet)... data_catalog.py:343 - INFO Loading data from 'candidate_modelling_pipeline.y_test' (MemoryDataSet)... data_catalog.py:343 + INFO Loading data from 'candidate_modelling_pipeline.X_test' (MemoryDataset)... data_catalog.py:343 + INFO Loading data from 'candidate_modelling_pipeline.y_test' (MemoryDataset)... data_catalog.py:343 INFO Running node: evaluate_model_node: node.py:327 evaluate_model([candidate_modelling_pipeline.regressor,candidate_modelling_pipeline.X_test,candid ate_modelling_pipeline.y_test]) -> None diff --git a/docs/source/tutorial/create_a_pipeline.md b/docs/source/tutorial/create_a_pipeline.md index 1828c41929..d0e0db222e 100644 --- a/docs/source/tutorial/create_a_pipeline.md +++ b/docs/source/tutorial/create_a_pipeline.md @@ -138,10 +138,10 @@ You should see output similar to the below: [08/09/22 16:43:11] INFO Loading data from 'companies' (CSVDataSet)... data_catalog.py:343 INFO Running node: preprocess_companies_node: node.py:327 preprocess_companies([companies]) -> [preprocessed_companies] - INFO Saving data to 'preprocessed_companies' (MemoryDataSet)... data_catalog.py:382 + INFO Saving data to 'preprocessed_companies' (MemoryDataset)... data_catalog.py:382 INFO Completed 1 out of 1 tasks sequential_runner.py:85 INFO Pipeline execution completed successfully. runner.py:89 - INFO Loading data from 'preprocessed_companies' (MemoryDataSet)... data_catalog.py:343 + INFO Loading data from 'preprocessed_companies' (MemoryDataset)... data_catalog.py:343 ```
@@ -161,16 +161,16 @@ You should see output similar to the following: INFO Loading data from 'companies' (CSVDataSet)... data_catalog.py:343 INFO Running node: preprocess_companies_node: node.py:327 preprocess_companies([companies]) -> [preprocessed_companies] - INFO Saving data to 'preprocessed_companies' (MemoryDataSet)... data_catalog.py:382 + INFO Saving data to 'preprocessed_companies' (MemoryDataset)... data_catalog.py:382 INFO Completed 1 out of 2 tasks sequential_runner.py:85 INFO Loading data from 'shuttles' (ExcelDataSet)... data_catalog.py:343 [08/09/22 16:46:08] INFO Running node: preprocess_shuttles_node: preprocess_shuttles([shuttles]) node.py:327 -> [preprocessed_shuttles] - INFO Saving data to 'preprocessed_shuttles' (MemoryDataSet)... data_catalog.py:382 + INFO Saving data to 'preprocessed_shuttles' (MemoryDataset)... data_catalog.py:382 INFO Completed 2 out of 2 tasks sequential_runner.py:85 INFO Pipeline execution completed successfully. runner.py:89 - INFO Loading data from 'preprocessed_companies' (MemoryDataSet)... data_catalog.py:343 - INFO Loading data from 'preprocessed_shuttles' (MemoryDataSet)... data_catalog.py:343 + INFO Loading data from 'preprocessed_companies' (MemoryDataset)... data_catalog.py:343 + INFO Loading data from 'preprocessed_shuttles' (MemoryDataset)... data_catalog.py:343 ```
@@ -193,7 +193,7 @@ preprocessed_shuttles: ``` -If you remove these lines from `catalog.yml`, Kedro still runs the pipeline successfully and automatically stores the preprocessed data, in memory, as temporary Python objects of the [MemoryDataSet](/kedro.io.MemoryDataSet) class. Once all nodes that depend on a temporary dataset have executed, Kedro clears the dataset and the Python garbage collector releases the memory. +If you remove these lines from `catalog.yml`, Kedro still runs the pipeline successfully and automatically stores the preprocessed data, in memory, as temporary Python objects of the [MemoryDataset](/kedro.io.MemoryDataset) class. Once all nodes that depend on a temporary dataset have executed, Kedro clears the dataset and the Python garbage collector releases the memory. ## Create a table for model input @@ -295,24 +295,24 @@ You should see output similar to the following: INFO Loading data from 'companies' (CSVDataSet)... data_catalog.py:343 INFO Running node: preprocess_companies_node: node.py:327 preprocess_companies([companies]) -> [preprocessed_companies] - INFO Saving data to 'preprocessed_companies' (MemoryDataSet)... data_catalog.py:382 + INFO Saving data to 'preprocessed_companies' (MemoryDataset)... data_catalog.py:382 INFO Completed 1 out of 3 tasks sequential_runner.py:85 INFO Loading data from 'shuttles' (ExcelDataSet)... data_catalog.py:343 [08/09/22 17:01:25] INFO Running node: preprocess_shuttles_node: preprocess_shuttles([shuttles]) node.py:327 -> [preprocessed_shuttles] - INFO Saving data to 'preprocessed_shuttles' (MemoryDataSet)... data_catalog.py:382 + INFO Saving data to 'preprocessed_shuttles' (MemoryDataset)... data_catalog.py:382 INFO Completed 2 out of 3 tasks sequential_runner.py:85 - INFO Loading data from 'preprocessed_shuttles' (MemoryDataSet)... data_catalog.py:343 - INFO Loading data from 'preprocessed_companies' (MemoryDataSet)... data_catalog.py:343 + INFO Loading data from 'preprocessed_shuttles' (MemoryDataset)... data_catalog.py:343 + INFO Loading data from 'preprocessed_companies' (MemoryDataset)... data_catalog.py:343 INFO Loading data from 'reviews' (CSVDataSet)... data_catalog.py:343 INFO Running node: create_model_input_table_node: node.py:327 create_model_input_table([preprocessed_shuttles,preprocessed_companies, reviews]) -> [model_input_table] -[08/09/22 17:01:28] INFO Saving data to 'model_input_table' (MemoryDataSet)... data_catalog.py:382 +[08/09/22 17:01:28] INFO Saving data to 'model_input_table' (MemoryDataset)... data_catalog.py:382 [08/09/22 17:01:29] INFO Completed 3 out of 3 tasks sequential_runner.py:85 INFO Pipeline execution completed successfully. runner.py:89 - INFO Loading data from 'model_input_table' (MemoryDataSet)... data_catalog.py:343 + INFO Loading data from 'model_input_table' (MemoryDataset)... data_catalog.py:343 ``` diff --git a/docs/source/tutorial/spaceflights_tutorial_faqs.md b/docs/source/tutorial/spaceflights_tutorial_faqs.md index 92d873dcb9..0645c40f11 100644 --- a/docs/source/tutorial/spaceflights_tutorial_faqs.md +++ b/docs/source/tutorial/spaceflights_tutorial_faqs.md @@ -6,12 +6,12 @@ If you can't find the answer you need here, [ask the Kedro community for help](h ## How do I resolve these common errors? -### DataSet errors -#### DataSetError: Failed while loading data from data set +### Dataset errors +#### DatasetError: Failed while loading data from data set You're [testing whether Kedro can load the raw test data](./set_up_data.md#test-that-kedro-can-load-the-data) and see the following: ```python -DataSetError: Failed while loading data from data set +DatasetError: Failed while loading data from data set CSVDataSet(filepath=...). [Errno 2] No such file or directory: '.../companies.csv' ``` @@ -20,12 +20,12 @@ or a similar error for the `shuttles` or `reviews` data. Are the [three sample data files](./set_up_data.md#project-datasets) stored in the `data/raw` folder? -#### DataSetNotFoundError: DataSet not found in the catalog +#### DatasetNotFoundError: Dataset not found in the catalog You see an error such as the following: ```python -DataSetNotFoundError: DataSet 'companies' not found in the catalog +DatasetNotFoundError: Dataset 'companies' not found in the catalog ``` Has something changed in your `catalog.yml` from the version generated by the spaceflights starter? Take a look at the [data specification](./set_up_data.md#dataset-registration) to ensure it is valid. @@ -34,12 +34,12 @@ Has something changed in your `catalog.yml` from the version generated by the sp Call `exit()` within the IPython session and restart `kedro ipython` (or type `@kedro_reload` into the IPython console to reload Kedro into the session without restarting). Then try again. -#### DataSetError: An exception occurred when parsing config for DataSet +#### DatasetError: An exception occurred when parsing config for Dataset Are you seeing a message saying that an exception occurred? ```bash -DataSetError: An exception occurred when parsing config for DataSet +DatasetError: An exception occurred when parsing config for Dataset 'data_processing.preprocessed_companies': Object 'ParquetDataSet' cannot be loaded from 'kedro_datasets.pandas'. Please see the documentation on how to install relevant dependencies for kedro_datasets.pandas.ParquetDataSet: @@ -70,7 +70,7 @@ The above exception was the direct cause of the following exception: Traceback (most recent call last): ... - raise DataSetError(message) from exc -kedro.io.core.DataSetError: Failed while loading data from data set CSVDataSet(filepath=data/03_primary/model_input_table.csv, save_args={'index': False}). + raise DatasetError(message) from exc +kedro.io.core.DatasetError: Failed while loading data from data set CSVDataSet(filepath=data/03_primary/model_input_table.csv, save_args={'index': False}). [Errno 2] File b'data/03_primary/model_input_table.csv' does not exist: b'data/03_primary/model_input_table.csv' ``` From 0111685c3be56035b13805f36259d814a95e8093 Mon Sep 17 00:00:00 2001 From: Tynan DeBold Date: Fri, 18 Aug 2023 17:06:07 +0200 Subject: [PATCH 31/70] Update h1,h2,h3 font sizes in the docs pages (#2938) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Update h1,h2,h3 font sizes Signed-off-by: Tynan DeBold * Update h2 font size Signed-off-by: Tynan DeBold --------- Signed-off-by: Tynan DeBold Co-authored-by: Jo Stichbury Co-authored-by: Juan Luis Cano Rodríguez Signed-off-by: Jason Hite From 53cc179263e846afa4a4acd909217648c7165c85 Mon Sep 17 00:00:00 2001 From: Ankita Katiyar <110245118+ankatiyar@users.noreply.github.com> Date: Fri, 18 Aug 2023 16:57:18 +0100 Subject: [PATCH 32/70] Automatically trigger `kedro-starters` release on the release of `kedro` (#2889) * Add GA for triggering starters release Signed-off-by: Ankita Katiyar * Update token name Signed-off-by: Ankita Katiyar --------- Signed-off-by: Ankita Katiyar Signed-off-by: Jason Hite --- .github/workflows/release-starters.yml | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) create mode 100644 .github/workflows/release-starters.yml diff --git a/.github/workflows/release-starters.yml b/.github/workflows/release-starters.yml new file mode 100644 index 0000000000..36526bf4bd --- /dev/null +++ b/.github/workflows/release-starters.yml @@ -0,0 +1,22 @@ +name: Trigger kedro-starters Release + +on: + release: + types: [published] + +jobs: + trigger-release: + runs-on: ubuntu-latest + + steps: + - name: Get Latest Release Version + id: get_version + run: echo "version=${GITHUB_REF#refs/tags/}" >> $GITHUB_OUTPUT + + - name: Trigger release in kedro-starters + uses: peter-evans/repository-dispatch@v2 + with: + token: ${{ secrets.GA_STARTERS_RELEASE }} + repository: kedro-org/kedro-starters + event-type: release-triggered + client-payload: '{"version": "${{ steps.get_version.outputs.version }}"}' From 4559ddf95e29ae896ea407fe92352543db10bd6b Mon Sep 17 00:00:00 2001 From: Nok Lam Chan Date: Fri, 18 Aug 2023 17:46:52 +0100 Subject: [PATCH 33/70] Create issues_metrics.yml (#2814) * Create issues_metrics.yml * Update issues_metrics.yml Signed-off-by: Jason Hite --- .github/workflows/issues_metrics.yml | 50 ++++++++++++++++++++++++++++ 1 file changed, 50 insertions(+) create mode 100644 .github/workflows/issues_metrics.yml diff --git a/.github/workflows/issues_metrics.yml b/.github/workflows/issues_metrics.yml new file mode 100644 index 0000000000..f1b0889fd7 --- /dev/null +++ b/.github/workflows/issues_metrics.yml @@ -0,0 +1,50 @@ +name: Monthly issue metrics +on: + workflow_dispatch: + schedule: + - cron: '3 2 1 * *' + +permissions: + issues: write + +jobs: + build: + name: issue metrics + runs-on: ubuntu-latest + steps: + + - name: Get dates for last month + shell: bash + run: | + # Get the current date + current_date=$(date +'%Y-%m-%d') + + # Calculate the previous month + previous_date=$(date -d "$current_date -1 month" +'%Y-%m-%d') + + # Extract the year and month from the previous date + previous_year=$(date -d "$previous_date" +'%Y') + previous_month=$(date -d "$previous_date" +'%m') + + # Calculate the first day of the previous month + first_day=$(date -d "$previous_year-$previous_month-01" +'%Y-%m-%d') + + # Calculate the last day of the previous month + last_day=$(date -d "$first_day +1 month -1 day" +'%Y-%m-%d') + + echo "$first_day..$last_day" + echo "last_month=$first_day..$last_day" >> "$GITHUB_ENV" + + - name: Run issue-metrics tool + uses: github/issue-metrics@v2 + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + SEARCH_QUERY: 'repo:kedro-org/kedro is:issue created:${{ env.last_month }} -reason:"not planned"' + + - name: Create issue + uses: peter-evans/create-issue-from-file@v4 + with: + title: Monthly issue metrics report + token: ${{ secrets.GITHUB_TOKEN }} + content-filepath: ./issue_metrics.md + assignees: noklam From 6b12c69d2f37f6c49fa5a262116d14d70fcb3f28 Mon Sep 17 00:00:00 2001 From: "L. R. Couto" <57910428+lrcouto@users.noreply.github.com> Date: Fri, 18 Aug 2023 20:17:20 -0300 Subject: [PATCH 34/70] Clean up setuptools and wheel requirements to align with PEP-518 (#2927) Signed-off-by: Jason Hite --- .circleci/continue_config.yml | 3 --- .github/workflows/e2e-tests.yml | 1 - .github/workflows/pip-compile.yml | 1 - .github/workflows/unit-tests.yml | 1 - Makefile | 3 --- features/environment.py | 2 -- tools/circleci/requirements.txt | 1 - 7 files changed, 12 deletions(-) diff --git a/.circleci/continue_config.yml b/.circleci/continue_config.yml index c83d6615cb..c26fb2fbc0 100644 --- a/.circleci/continue_config.yml +++ b/.circleci/continue_config.yml @@ -48,9 +48,6 @@ commands: setup_requirements: steps: - - run: - name: Install pip setuptools - command: make install-pip-setuptools - run: name: Install venv for some pre-commit hooks command: conda install -y virtualenv diff --git a/.github/workflows/e2e-tests.yml b/.github/workflows/e2e-tests.yml index 0369e4b134..911498a102 100644 --- a/.github/workflows/e2e-tests.yml +++ b/.github/workflows/e2e-tests.yml @@ -22,7 +22,6 @@ jobs: uses: actions/setup-python@v3 with: python-version: ${{inputs.python-version}} - - run: make install-pip-setuptools - name: Cache python packages for Linux if: inputs.os == 'ubuntu-latest' uses: actions/cache@v3 diff --git a/.github/workflows/pip-compile.yml b/.github/workflows/pip-compile.yml index b5b1453782..fb30b49b1d 100644 --- a/.github/workflows/pip-compile.yml +++ b/.github/workflows/pip-compile.yml @@ -18,7 +18,6 @@ jobs: uses: actions/setup-python@v3 with: python-version: ${{inputs.python-version}} - - run: make install-pip-setuptools - name: Cache python packages for Linux if: inputs.os == 'ubuntu-latest' uses: actions/cache@v3 diff --git a/.github/workflows/unit-tests.yml b/.github/workflows/unit-tests.yml index c56a67c707..347940ea86 100644 --- a/.github/workflows/unit-tests.yml +++ b/.github/workflows/unit-tests.yml @@ -17,7 +17,6 @@ jobs: uses: actions/setup-python@v3 with: python-version: ${{inputs.python-version}} - - run: make install-pip-setuptools - name: Cache python packages for Linux if: inputs.os == 'ubuntu-latest' uses: actions/cache@v3 diff --git a/Makefile b/Makefile index 2e8436390b..1036151fe8 100644 --- a/Makefile +++ b/Makefile @@ -7,9 +7,6 @@ clean: find . -regex ".*\.egg-info" -exec rm -rf {} + pre-commit clean || true -install-pip-setuptools: - python -m pip install -U "pip>=21.2, <23.2" "setuptools>=65.5.1" wheel - lint: pre-commit run -a --hook-stage manual $(hook) test: diff --git a/features/environment.py b/features/environment.py index 172dfd006a..95cc11cee2 100644 --- a/features/environment.py +++ b/features/environment.py @@ -106,8 +106,6 @@ def _setup_minimal_env(context): # pip==23.2 breaks pip-tools<7.0, and pip-tools>=7.0 does not support Python 3.7 "pip>=21.2,<23.2; python_version < '3.8'", "pip>=21.2; python_version >= '3.8'", - "setuptools>=65.5.1", - "wheel", ], env=context.env, ) diff --git a/tools/circleci/requirements.txt b/tools/circleci/requirements.txt index 224e670115..4317e05e69 100644 --- a/tools/circleci/requirements.txt +++ b/tools/circleci/requirements.txt @@ -1,3 +1,2 @@ pip>=21.2 -setuptools>=65.5.1 twine~=3.0 From fa97e35a9786cb823eabdca81bd9ee5d75834aca Mon Sep 17 00:00:00 2001 From: Nok Lam Chan Date: Mon, 21 Aug 2023 13:04:13 +0100 Subject: [PATCH 35/70] Clean up `kedro pipeline create` outdated docs (#2945) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * remove the README template generate by kedro pipeline create Signed-off-by: Nok * fix docstring for kedro catalog create Signed-off-by: Nok * Fix broken link Signed-off-by: Nok * fix broken link Signed-off-by: Nok --------- Signed-off-by: Nok Co-authored-by: Juan Luis Cano Rodríguez Signed-off-by: Jason Hite --- kedro/framework/cli/catalog.py | 2 +- .../README.md | 21 ------------------- ...eters_{{ cookiecutter.pipeline_name }}.yml | 2 +- .../cli/micropkg/test_micropkg_package.py | 2 -- .../cli/micropkg/test_micropkg_pull.py | 1 - 5 files changed, 2 insertions(+), 26 deletions(-) delete mode 100644 kedro/templates/pipeline/{{ cookiecutter.pipeline_name }}/README.md diff --git a/kedro/framework/cli/catalog.py b/kedro/framework/cli/catalog.py index 39da4dd979..b8849b5843 100644 --- a/kedro/framework/cli/catalog.py +++ b/kedro/framework/cli/catalog.py @@ -137,7 +137,7 @@ def create_catalog(metadata: ProjectMetadata, pipeline_name, env): the ``DataCatalog``. The catalog configuration will be saved to - `//catalog/.yml` file. + `//catalog_.yml` file. """ env = env or "base" session = _create_session(metadata.package_name, env=env) diff --git a/kedro/templates/pipeline/{{ cookiecutter.pipeline_name }}/README.md b/kedro/templates/pipeline/{{ cookiecutter.pipeline_name }}/README.md deleted file mode 100644 index 2ed430c018..0000000000 --- a/kedro/templates/pipeline/{{ cookiecutter.pipeline_name }}/README.md +++ /dev/null @@ -1,21 +0,0 @@ -# Pipeline {{ cookiecutter.pipeline_name }} - -> *Note:* This is a `README.md` boilerplate generated using `Kedro {{ cookiecutter.kedro_version }}`. - -## Overview - - - -## Pipeline inputs - - - -## Pipeline outputs - - diff --git a/kedro/templates/pipeline/{{ cookiecutter.pipeline_name }}/config/parameters_{{ cookiecutter.pipeline_name }}.yml b/kedro/templates/pipeline/{{ cookiecutter.pipeline_name }}/config/parameters_{{ cookiecutter.pipeline_name }}.yml index c4242b73e6..cf8f0d70d6 100644 --- a/kedro/templates/pipeline/{{ cookiecutter.pipeline_name }}/config/parameters_{{ cookiecutter.pipeline_name }}.yml +++ b/kedro/templates/pipeline/{{ cookiecutter.pipeline_name }}/config/parameters_{{ cookiecutter.pipeline_name }}.yml @@ -2,4 +2,4 @@ # using Kedro {{ cookiecutter.kedro_version }}. # # Documentation for this file format can be found in "Parameters" -# Link: https://docs.kedro.org/en/{{ cookiecutter.kedro_version }}/kedro_project_setup/configuration.html#parameters +# Link: https://docs.kedro.org/en/{{ cookiecutter.kedro_version }}/configuration/parameters.html diff --git a/tests/framework/cli/micropkg/test_micropkg_package.py b/tests/framework/cli/micropkg/test_micropkg_package.py index 3207bb15f5..7c0674c6e1 100644 --- a/tests/framework/cli/micropkg/test_micropkg_package.py +++ b/tests/framework/cli/micropkg/test_micropkg_package.py @@ -30,7 +30,6 @@ def assert_sdist_contents_correct( expected_files = { f"{package_name}-{version}/{package_name}/__init__.py", - f"{package_name}-{version}/{package_name}/README.md", f"{package_name}-{version}/{package_name}/nodes.py", f"{package_name}-{version}/{package_name}/pipeline.py", f"{package_name}-{version}/{package_name}/config/parameters_{package_name}.yml", @@ -264,7 +263,6 @@ def test_package_micropkg_no_config( expected_files = { f"{PIPELINE_NAME}-{version}/{PIPELINE_NAME}/__init__.py", - f"{PIPELINE_NAME}-{version}/{PIPELINE_NAME}/README.md", f"{PIPELINE_NAME}-{version}/{PIPELINE_NAME}/nodes.py", f"{PIPELINE_NAME}-{version}/{PIPELINE_NAME}/pipeline.py", f"{PIPELINE_NAME}-{version}/tests/__init__.py", diff --git a/tests/framework/cli/micropkg/test_micropkg_pull.py b/tests/framework/cli/micropkg/test_micropkg_pull.py index 6a9a4073ae..3e40843449 100644 --- a/tests/framework/cli/micropkg/test_micropkg_pull.py +++ b/tests/framework/cli/micropkg/test_micropkg_pull.py @@ -51,7 +51,6 @@ def assert_package_files_exist(self, source_path): "__init__.py", "nodes.py", "pipeline.py", - "README.md", } @pytest.mark.parametrize("env", [None, "local"]) From 786c7d6e1bbfce8ca0219f74c535eb2d68b1afa4 Mon Sep 17 00:00:00 2001 From: Ankita Katiyar <110245118+ankatiyar@users.noreply.github.com> Date: Mon, 21 Aug 2023 16:06:15 +0100 Subject: [PATCH 36/70] Add globals feature for `OmegaConfigLoader` using a globals resolver (#2921) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Refactor load_and_merge_dir() Signed-off-by: Ankita Katiyar * Try adding globals resolver Signed-off-by: Ankita Katiyar * Minor change Signed-off-by: Ankita Katiyar * Add globals resolver Signed-off-by: Ankita Katiyar * Revert refactoring Signed-off-by: Ankita Katiyar * Add test + remove self.globals Signed-off-by: Ankita Katiyar * Allow for nested variables in globals Signed-off-by: Ankita Katiyar * Add documentation Signed-off-by: Ankita Katiyar * Typo Signed-off-by: Ankita Katiyar * Add error message + test Signed-off-by: Ankita Katiyar * Apply suggestions from code review Co-authored-by: Merel Theisen <49397448+merelcht@users.noreply.github.com> * Split test into multiple tests Signed-off-by: Ankita Katiyar * Restrict the globals config_patterns Signed-off-by: Ankita Katiyar * Release notes Signed-off-by: Ankita Katiyar * Update docs/source/configuration/advanced_configuration.md Co-authored-by: Merel Theisen <49397448+merelcht@users.noreply.github.com> * Add helpful error message for keys starting with _ Signed-off-by: Ankita Katiyar * Enable setting default value for globals resolver Signed-off-by: Ankita Katiyar * Typo Signed-off-by: Ankita Katiyar * Move test for keys starting with _ to the top Signed-off-by: Ankita Katiyar * Fix cross ref link in docs Signed-off-by: Ankita Katiyar --------- Signed-off-by: Ankita Katiyar Co-authored-by: Merel Theisen <49397448+merelcht@users.noreply.github.com> Co-authored-by: Juan Luis Cano Rodríguez Co-authored-by: Nok Lam Chan Signed-off-by: Jason Hite --- RELEASE.md | 2 + .../configuration/advanced_configuration.md | 35 ++++- .../configuration/configuration_basics.md | 5 +- docs/source/faq/faq.md | 1 + kedro/config/omegaconf_config.py | 37 ++++- tests/config/test_omegaconf_config.py | 126 ++++++++++++++++++ 6 files changed, 201 insertions(+), 5 deletions(-) diff --git a/RELEASE.md b/RELEASE.md index 169485d03e..bd338addd5 100644 --- a/RELEASE.md +++ b/RELEASE.md @@ -14,6 +14,8 @@ * Allowed registering of custom resolvers to `OmegaConfigLoader` through `CONFIG_LOADER_ARGS`. * Added support for Python 3.11. This includes tackling challenges like dependency pinning and test adjustments to ensure a smooth experience. Detailed migration tips are provided below for further context. * Added `kedro catalog resolve` CLI command that resolves dataset factories in the catalog with any explicit entries in the project pipeline. +* Added support for global variables to `OmegaConfigLoader`. + ## Bug fixes and other changes * Updated `kedro pipeline create` and `kedro catalog create` to use new `/conf` file structure. diff --git a/docs/source/configuration/advanced_configuration.md b/docs/source/configuration/advanced_configuration.md index 8e703b5bc2..6bf78d487e 100644 --- a/docs/source/configuration/advanced_configuration.md +++ b/docs/source/configuration/advanced_configuration.md @@ -34,7 +34,7 @@ folders: fea: "04_feature" ``` -To point your `TemplatedConfigLoader` to the globals file, add it to the the `CONFIG_LOADER_ARGS` variable in [`src//settings.py`](../kedro_project_setup/settings.md): +To point your `TemplatedConfigLoader` to the globals file, add it to the `CONFIG_LOADER_ARGS` variable in [`src//settings.py`](../kedro_project_setup/settings.md): ```python CONFIG_LOADER_ARGS = {"globals_pattern": "*globals.yml"} @@ -124,6 +124,7 @@ This section contains a set of guidance for advanced configuration requirements * [How to bypass the configuration loading rules](#how-to-bypass-the-configuration-loading-rules) * [How to use Jinja2 syntax in configuration](#how-to-use-jinja2-syntax-in-configuration) * [How to do templating with the `OmegaConfigLoader`](#how-to-do-templating-with-the-omegaconfigloader) +* [How to use global variables with the `OmegaConfigLoader`](#how-to-use-global-variables-with-the-omegaconfigloader) * [How to use resolvers in the `OmegaConfigLoader`](#how-to-use-resolvers-in-the-omegaconfigloader) * [How to load credentials through environment variables](#how-to-load-credentials-through-environment-variables) @@ -262,6 +263,38 @@ Since both of the file names (`catalog.yml` and `catalog_globals.yml`) match the #### Other configuration files It's also possible to use variable interpolation in configuration files other than parameters and catalog, such as custom spark or mlflow configuration. This works in the same way as variable interpolation in parameter files. You can still use the underscore for the templated values if you want, but it's not mandatory like it is for catalog files. +### How to use global variables with the `OmegaConfigLoader` +From Kedro `0.18.13`, you can use variable interpolation in your configurations using "globals" with `OmegaConfigLoader`. +The benefit of using globals over regular variable interpolation is that the global variables are shared across different configuration types, such as catalog and parameters. +By default, these global variables are assumed to be in files called `globals.yml` in any of your environments. If you want to configure the naming patterns for the files that contain your global variables, +you can do so [by overwriting the `globals` key in `config_patterns`](#how-to-change-which-configuration-files-are-loaded). You can also [bypass the configuration loading](#how-to-bypass-the-configuration-loading-rules) +to directly set the global variables in `OmegaConfigLoader`. + +Suppose you have global variables located in the file `conf/base/globals.yml`: +```yaml +my_global_value: 45 +dataset_type: + csv: pandas.CSVDataSet +``` +You can access these global variables in your catalog or parameters config files with a `globals` resolver like this: +`conf/base/parameters.yml`: +```yaml +my_param : "${globals:my_global_value}" +``` +`conf/base/catalog.yml`: +```yaml +companies: + filepath: data/01_raw/companies.csv + type: "${globals:dataset_type.csv}" +``` +You can also provide a default value to be used in case the global variable does not exist: +```yaml +my_param: "${globals: nonexistent_global, 23}" +``` +If there are duplicate keys in the globals files in your base and run time environments, the values in the run time environment +will overwrite the values in your base environment. + + ### How to use resolvers in the `OmegaConfigLoader` Instead of hard-coding values in your configuration files, you can also dynamically compute them using [`OmegaConf`'s resolvers functionality](https://omegaconf.readthedocs.io/en/2.3_branch/custom_resolvers.html#resolvers). You use resolvers to define custom diff --git a/docs/source/configuration/configuration_basics.md b/docs/source/configuration/configuration_basics.md index 2e964b512f..9e133f0e5e 100644 --- a/docs/source/configuration/configuration_basics.md +++ b/docs/source/configuration/configuration_basics.md @@ -61,7 +61,7 @@ Configuration files will be matched according to file name and type rules. Suppo ### Configuration patterns Under the hood, the Kedro configuration loader loads files based on regex patterns that specify the naming convention for configuration files. These patterns are specified by `config_patterns` in the configuration loader classes. -By default those patterns are set as follows for the configuration of catalog, parameters, logging and credentials: +By default those patterns are set as follows for the configuration of catalog, parameters, logging, credentials, and globals: ```python config_patterns = { @@ -69,10 +69,11 @@ config_patterns = { "parameters": ["parameters*", "parameters*/**", "**/parameters*"], "credentials": ["credentials*", "credentials*/**", "**/credentials*"], "logging": ["logging*", "logging*/**", "**/logging*"], + "globals": ["globals*", "globals*/**", "**/globals*"], } ``` -If you want to change change the way configuration is loaded, you can either [customise the config patterns](advanced_configuration.md#how-to-change-which-configuration-files-are-loaded) or [bypass the configuration loading](advanced_configuration.md#how-to-bypass-the-configuration-loading-rules) as described in the advanced configuration chapter. +If you want to change the way configuration is loaded, you can either [customise the config patterns](advanced_configuration.md#how-to-change-which-configuration-files-are-loaded) or [bypass the configuration loading](advanced_configuration.md#how-to-bypass-the-configuration-loading-rules) as described in the advanced configuration chapter. ## How to use Kedro configuration diff --git a/docs/source/faq/faq.md b/docs/source/faq/faq.md index 23cfa6b094..30bd2a1929 100644 --- a/docs/source/faq/faq.md +++ b/docs/source/faq/faq.md @@ -36,6 +36,7 @@ This is a growing set of technical FAQs. The [product FAQs on the Kedro website] * [How do I bypass the configuration loading rules](../configuration/advanced_configuration.md#how-to-bypass-the-configuration-loading-rules)? * [How do I use Jinja2 syntax in configuration](../configuration/advanced_configuration.md#how-to-use-jinja2-syntax-in-configuration)? * [How do I do templating with the `OmegaConfigLoader`](../configuration/advanced_configuration.md#how-to-do-templating-with-the-omegaconfigloader)? +* [How to use global variables with the `OmegaConfigLoader`](../configuration/advanced_configuration.md#how-to-use-global-variables-with-the-omegaconfigloader)? * [How do I use resolvers in the `OmegaConfigLoader`](../configuration/advanced_configuration.md#how-to-use-resolvers-in-the-omegaconfigloader)? * [How do I load credentials through environment variables](../configuration/advanced_configuration.md#how-to-load-credentials-through-environment-variables)? diff --git a/kedro/config/omegaconf_config.py b/kedro/config/omegaconf_config.py index 4d2ace59d4..0d7b5d740c 100644 --- a/kedro/config/omegaconf_config.py +++ b/kedro/config/omegaconf_config.py @@ -11,6 +11,7 @@ import fsspec from omegaconf import OmegaConf +from omegaconf.errors import InterpolationResolutionError from omegaconf.resolvers import oc from yaml.parser import ParserError from yaml.scanner import ScannerError @@ -109,6 +110,7 @@ def __init__( # noqa: too-many-arguments "parameters": ["parameters*", "parameters*/**", "**/parameters*"], "credentials": ["credentials*", "credentials*/**", "**/credentials*"], "logging": ["logging*", "logging*/**", "**/logging*"], + "globals": ["globals.yml"], } self.config_patterns.update(config_patterns or {}) @@ -117,7 +119,8 @@ def __init__( # noqa: too-many-arguments # Register user provided custom resolvers if custom_resolvers: self._register_new_resolvers(custom_resolvers) - + # Register globals resolver + self._register_globals_resolver() file_mimetype, _ = mimetypes.guess_type(conf_source) if file_mimetype == "application/x-tar": self._protocol = "tar" @@ -199,7 +202,7 @@ def __getitem__(self, key) -> dict[str, Any]: config.update(env_config) - if not processed_files: + if not processed_files and key != "globals": raise MissingConfigException( f"No files of YAML or JSON format found in {base_path} or {env_path} matching" f" the glob pattern(s): {[*self.config_patterns[key]]}" @@ -308,6 +311,36 @@ def _is_valid_config_path(self, path): ".json", ] + def _register_globals_resolver(self): + """Register the globals resolver""" + OmegaConf.register_new_resolver( + "globals", + lambda variable, default_value=None: self._get_globals_value( + variable, default_value + ), + replace=True, + ) + + def _get_globals_value(self, variable, default_value): + """Return the globals values to the resolver""" + if variable.startswith("_"): + raise InterpolationResolutionError( + "Keys starting with '_' are not supported for globals." + ) + keys = variable.split(".") + value = self["globals"] + for k in keys: + value = value.get(k) + if not value: + if default_value: + _config_logger.debug( + f"Using the default value for the global variable {variable}." + ) + return default_value + msg = f"Globals key '{variable}' not found and no default value provided. " + raise InterpolationResolutionError(msg) + return value + @staticmethod def _register_new_resolvers(resolvers: dict[str, Callable]): """Register custom resolvers""" diff --git a/tests/config/test_omegaconf_config.py b/tests/config/test_omegaconf_config.py index af57b52224..948aac7372 100644 --- a/tests/config/test_omegaconf_config.py +++ b/tests/config/test_omegaconf_config.py @@ -12,6 +12,7 @@ import pytest import yaml from omegaconf import OmegaConf, errors +from omegaconf.errors import InterpolationResolutionError from omegaconf.resolvers import oc from yaml.parser import ParserError @@ -671,3 +672,128 @@ def test_custom_resolvers(self, tmp_path): assert conf["parameters"]["model_options"]["param1"] == 7 assert conf["parameters"]["model_options"]["param2"] == 3 assert conf["parameters"]["model_options"]["param3"] == "my_env_variable" + + def test_globals(self, tmp_path): + globals_params = tmp_path / _BASE_ENV / "globals.yml" + globals_config = { + "x": 34, + } + _write_yaml(globals_params, globals_config) + conf = OmegaConfigLoader(tmp_path, default_run_env="") + # OmegaConfigLoader has globals resolver + assert OmegaConf.has_resolver("globals") + # Globals is readable in a dict way + assert conf["globals"] == globals_config + + def test_globals_resolution(self, tmp_path): + base_params = tmp_path / _BASE_ENV / "parameters.yml" + base_catalog = tmp_path / _BASE_ENV / "catalog.yml" + globals_params = tmp_path / _BASE_ENV / "globals.yml" + param_config = { + "my_param": "${globals:x}", + "my_param_default": "${globals:y,34}", # y does not exist in globals + } + catalog_config = { + "companies": { + "type": "${globals:dataset_type}", + "filepath": "data/01_raw/companies.csv", + }, + } + globals_config = {"x": 34, "dataset_type": "pandas.CSVDataSet"} + _write_yaml(base_params, param_config) + _write_yaml(globals_params, globals_config) + _write_yaml(base_catalog, catalog_config) + conf = OmegaConfigLoader(tmp_path, default_run_env="") + assert OmegaConf.has_resolver("globals") + # Globals are resolved correctly in parameter + assert conf["parameters"]["my_param"] == globals_config["x"] + # The default value is used if the key does not exist + assert conf["parameters"]["my_param_default"] == 34 + # Globals are resolved correctly in catalog + assert conf["catalog"]["companies"]["type"] == globals_config["dataset_type"] + + def test_globals_nested(self, tmp_path): + base_params = tmp_path / _BASE_ENV / "parameters.yml" + globals_params = tmp_path / _BASE_ENV / "globals.yml" + param_config = { + "my_param": "${globals:x}", + "my_nested_param": "${globals:nested.y}", + } + globals_config = { + "x": 34, + "nested": { + "y": 42, + }, + } + _write_yaml(base_params, param_config) + _write_yaml(globals_params, globals_config) + conf = OmegaConfigLoader(tmp_path, default_run_env="") + assert conf["parameters"]["my_param"] == globals_config["x"] + # Nested globals are accessible with dot notation + assert conf["parameters"]["my_nested_param"] == globals_config["nested"]["y"] + + def test_globals_across_env(self, tmp_path): + base_params = tmp_path / _BASE_ENV / "parameters.yml" + local_params = tmp_path / _DEFAULT_RUN_ENV / "parameters.yml" + base_globals = tmp_path / _BASE_ENV / "globals.yml" + local_globals = tmp_path / _DEFAULT_RUN_ENV / "globals.yml" + base_param_config = { + "param1": "${globals:y}", + } + local_param_config = { + "param2": "${globals:x}", + } + base_globals_config = { + "x": 34, + "y": 25, + } + local_globals_config = { + "y": 99, + } + _write_yaml(base_params, base_param_config) + _write_yaml(local_params, local_param_config) + _write_yaml(base_globals, base_globals_config) + _write_yaml(local_globals, local_globals_config) + conf = OmegaConfigLoader(tmp_path) + # Local global overwrites the base global value + assert conf["parameters"]["param1"] == local_globals_config["y"] + # Base global value is accessible to local params + assert conf["parameters"]["param2"] == base_globals_config["x"] + + def test_bad_globals(self, tmp_path): + base_params = tmp_path / _BASE_ENV / "parameters.yml" + base_globals = tmp_path / _BASE_ENV / "globals.yml" + base_param_config = { + "param1": "${globals:x.y}", + } + base_globals_config = { + "x": { + "z": 23, + }, + } + _write_yaml(base_params, base_param_config) + _write_yaml(base_globals, base_globals_config) + conf = OmegaConfigLoader(tmp_path, default_run_env="") + with pytest.raises( + InterpolationResolutionError, + match=r"Globals key 'x.y' not found and no default value provided.", + ): + conf["parameters"]["param1"] + + def test_bad_globals_underscore(self, tmp_path): + base_params = tmp_path / _BASE_ENV / "parameters.yml" + base_globals = tmp_path / _BASE_ENV / "globals.yml" + base_param_config = { + "param2": "${globals:_ignore}", + } + base_globals_config = { + "_ignore": 45, + } + _write_yaml(base_params, base_param_config) + _write_yaml(base_globals, base_globals_config) + conf = OmegaConfigLoader(tmp_path, default_run_env="") + with pytest.raises( + InterpolationResolutionError, + match=r"Keys starting with '_' are not supported for globals.", + ): + conf["parameters"]["param2"] From 87b568fdb38134f874dbdc83f950810cc4f9f7d8 Mon Sep 17 00:00:00 2001 From: Deepyaman Datta Date: Tue, 22 Aug 2023 03:06:45 -0600 Subject: [PATCH 37/70] Consolidate two `ruff-pre-commit` entries into one (#2881) * Clean up Ruff configuration in base pyproject.toml Signed-off-by: Deepyaman Datta * Consolidate two `ruff-pre-commit` entries into one Signed-off-by: Deepyaman Datta * Move Ruff hook to the top, because it's aggressive Signed-off-by: Deepyaman Datta * Add `kedro` as `known-first-party` for `isort` run Signed-off-by: Deepyaman Datta * Add noqa in docs/source/conf.py Signed-off-by: Deepyaman Datta * Fix ruff linter precommit config Signed-off-by: Nok --------- Signed-off-by: Deepyaman Datta Signed-off-by: Nok Co-authored-by: Nok Signed-off-by: Jason Hite --- .pre-commit-config.yaml | 29 +++++++++-------------------- docs/source/conf.py | 2 +- kedro/framework/cli/micropkg.py | 2 +- pyproject.toml | 20 ++++++++++++-------- 4 files changed, 23 insertions(+), 30 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 57bafd2416..ba9bf87df2 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -4,6 +4,15 @@ default_stages: [commit, manual] repos: + - repo: https://github.com/astral-sh/ruff-pre-commit + # Ruff version. + rev: v0.0.277 + hooks: + - id: ruff + name: "ruff on kedro/, tests/ and docs/" + args: ["--fix", "--exit-non-zero-on-fix"] + exclude: "^kedro/templates/|^features/steps/test_starter/" + - repo: https://github.com/pre-commit/pre-commit-hooks rev: v2.2.3 hooks: @@ -48,26 +57,6 @@ repos: additional_dependencies: [black~=22.0] entry: blacken-docs --skip-errors - - repo: https://github.com/astral-sh/ruff-pre-commit - # Ruff version. - rev: v0.0.277 - hooks: - - id: ruff - name: "ruff on kedro/" - args: ["--fix", "--show-fixes", "--exit-non-zero-on-fix"] - exclude: "^kedro/templates/|^features/steps/test_starter/|tests|docs" - - repo: https://github.com/astral-sh/ruff-pre-commit - # Ruff version. - rev: v0.0.277 - hooks: - - id: ruff - name: "ruff on tests/ and docs/" - # PLR2004: Magic value used - # PLR0913: Too many arguments - args: ["--fix", "--show-fixes", "--exit-non-zero-on-fix", "--ignore=PLR2004,PLR0913"] - # include: "tests" - exclude: "^kedro/templates/|^features/steps/test_starter/|kedro" - - repo: local hooks: - id: black diff --git a/docs/source/conf.py b/docs/source/conf.py index 804bbbbfa9..4519143968 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -464,7 +464,7 @@ def autolink_classes_and_methods(lines): lines[i] = re.sub(existing, rf"{replacement}", lines[i]) -def autodoc_process_docstring(app, what, name, obj, options, lines): +def autodoc_process_docstring(app, what, name, obj, options, lines): # noqa: PLR0913 try: # guarded method to make sure build never fails log_suggestions(lines, name) diff --git a/kedro/framework/cli/micropkg.py b/kedro/framework/cli/micropkg.py index 36d103a332..f42ea0edbf 100644 --- a/kedro/framework/cli/micropkg.py +++ b/kedro/framework/cli/micropkg.py @@ -1,5 +1,5 @@ """A collection of CLI commands for working with Kedro micro-packages.""" -# ruff: noqa: I001 # https://github.com/kedro-org/kedro/pull/2634 +# ruff: noqa: I001 from __future__ import annotations import logging diff --git a/pyproject.toml b/pyproject.toml index df3bf4a2fe..0421e10093 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -206,14 +206,18 @@ ignore_imports = [ [tool.ruff] line-length = 88 show-fixes = true -# select = ["A", "B", "C", "E", "F", "G", "I", "N", "Q", "S", "T", "W", "ARG", "BLE", "COM", "DJ", "DTZ", "EM", "ERA", "EXE", "FBT", "ICN", "INP", "ISC", "NPY", "PD", "PGH", "PIE", "PL", "PT", "PTH", "PYI", "RET", "RSE", "RUF", "SIM", "SLF", "TCH", "TID", "TRY", "UP", "YTT"] select = [ - "F", # Pyflakes - "E", # Pycodestyle - "W", # Pycodestyle + "F", # Pyflakes + "W", # pycodestyle + "E", # pycodestyle + "I", # isort "UP", # pyupgrade - "I", # isort - "PL", # Pylint + "PL", # Pylint ] -ignore = ["E501"] # Black take care off line-too-long -unfixable = [] +ignore = ["E501"] # Black takes care of line-too-long + +[tool.ruff.isort] +known-first-party = ["kedro"] + +[tool.ruff.per-file-ignores] +"{tests,docs}/*" = ["PLR2004","PLR0913"] From 2676c8c755cd877c6e2febc687e0b0b00d3e834e Mon Sep 17 00:00:00 2001 From: Ankita Katiyar <110245118+ankatiyar@users.noreply.github.com> Date: Tue, 22 Aug 2023 14:46:10 +0100 Subject: [PATCH 38/70] Fix typos across the documentation (#2956) * Fix typos across docs Signed-off-by: Ankita Katiyar * Capitalisation stuff Signed-off-by: Ankita Katiyar --------- Signed-off-by: Ankita Katiyar Signed-off-by: Jason Hite --- docs/source/data/index.md | 2 +- docs/source/data/partitioned_and_incremental_datasets.md | 4 ++-- .../deployment/databricks/databricks_deployment_workflow.md | 6 +++--- .../databricks/databricks_notebooks_development_workflow.md | 4 ++-- docs/source/development/commands_reference.md | 2 +- docs/source/experiment_tracking/index.md | 2 +- docs/source/extend_kedro/architecture_overview.md | 2 +- 7 files changed, 11 insertions(+), 11 deletions(-) diff --git a/docs/source/data/index.md b/docs/source/data/index.md index b90a3d9961..a6196bcc13 100644 --- a/docs/source/data/index.md +++ b/docs/source/data/index.md @@ -22,7 +22,7 @@ The following page offers a range of examples of YAML specification for various data_catalog_yaml_examples ``` -Once you are familiar with the format of `catalog.yml`, you may find your catalog gets repetitive if you need to load multiple datasets with similar configuration. From Kedro 0.18.12 you can use dataset factories to generalise the configuration and reduce the number of similar catalog entries. This works by by matching datasets used in your project’s pipelines to dataset factory patterns and is explained in a new page about Kedro dataset factories: +Once you are familiar with the format of `catalog.yml`, you may find your catalog gets repetitive if you need to load multiple datasets with similar configuration. From Kedro 0.18.12 you can use dataset factories to generalise the configuration and reduce the number of similar catalog entries. This works by matching datasets used in your project’s pipelines to dataset factory patterns and is explained in a new page about Kedro dataset factories: ```{toctree} diff --git a/docs/source/data/partitioned_and_incremental_datasets.md b/docs/source/data/partitioned_and_incremental_datasets.md index fde9dfd90a..a57b56d2a4 100644 --- a/docs/source/data/partitioned_and_incremental_datasets.md +++ b/docs/source/data/partitioned_and_incremental_datasets.md @@ -75,12 +75,12 @@ my_partitioned_dataset: Here is an exhaustive list of the arguments supported by `PartitionedDataset`: | Argument | Required | Supported types | Description | -| ----------------- | ------------------------------ | ------------------------------------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| ----------------- | ------------------------------ | ------------------------------------------------ |-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | `path` | Yes | `str` | Path to the folder containing partitioned data. If path starts with the protocol (e.g., `s3://`) then the corresponding `fsspec` concrete filesystem implementation will be used. If protocol is not specified, local filesystem will be used | | `dataset` | Yes | `str`, `Type[AbstractDataset]`, `Dict[str, Any]` | Underlying dataset definition, for more details see the section below | | `credentials` | No | `Dict[str, Any]` | Protocol-specific options that will be passed to `fsspec.filesystemcall`, for more details see the section below | | `load_args` | No | `Dict[str, Any]` | Keyword arguments to be passed into `find()` method of the corresponding filesystem implementation | -| `filepath_arg` | No | `str` (defaults to `filepath`) | Argument name of the underlying dataset initializer that will contain a path to an individual partition | +| `filepath_arg` | No | `str` (defaults to `filepath`) | Argument name of the underlying dataset initialiser that will contain a path to an individual partition | | `filename_suffix` | No | `str` (defaults to an empty string) | If specified, partitions that don't end with this string will be ignored | ### Dataset definition diff --git a/docs/source/deployment/databricks/databricks_deployment_workflow.md b/docs/source/deployment/databricks/databricks_deployment_workflow.md index 245708e6bf..26c7a1634b 100644 --- a/docs/source/deployment/databricks/databricks_deployment_workflow.md +++ b/docs/source/deployment/databricks/databricks_deployment_workflow.md @@ -33,7 +33,7 @@ For those reasons, the packaging approach is unsuitable for development projects The sequence of steps described in this section is as follows: 1. [Note your Databricks username and host](#note-your-databricks-username-and-host) -2. [Install Kedro and the databricks CLI in a new virtual environment](#install-kedro-and-the-databricks-cli-in-a-new-virtual-environment) +2. [Install Kedro and the Databricks CLI in a new virtual environment](#install-kedro-and-the-databricks-cli-in-a-new-virtual-environment) 3. [Authenticate the Databricks CLI](#authenticate-the-databricks-cli) 4. [Create a new Kedro project](#create-a-new-kedro-project) 5. [Create an entry point for Databricks](#create-an-entry-point-for-databricks) @@ -49,10 +49,10 @@ Find your Databricks username in the top right of the workspace UI and the host ![Find Databricks host and username](../../meta/images/find_databricks_host_and_username.png) ```{note} -Your databricks host must include the protocol (`https://`). +Your Databricks host must include the protocol (`https://`). ``` -### Install Kedro and the databricks CLI in a new virtual environment +### Install Kedro and the Databricks CLI in a new virtual environment The following commands will create a new `conda` environment, activate it, and then install Kedro and the Databricks CLI. diff --git a/docs/source/deployment/databricks/databricks_notebooks_development_workflow.md b/docs/source/deployment/databricks/databricks_notebooks_development_workflow.md index 5867163ab9..ef2081a28a 100644 --- a/docs/source/deployment/databricks/databricks_notebooks_development_workflow.md +++ b/docs/source/deployment/databricks/databricks_notebooks_development_workflow.md @@ -4,7 +4,7 @@ This guide demonstrates a workflow for developing Kedro projects on Databricks u This method of developing a Kedro project for use on Databricks is ideal for developers who prefer developing their projects in notebooks rather than an in an IDE. It also avoids the overhead of setting up and syncing a local environment with Databricks. If you want to take advantage of the powerful features of an IDE to develop your project, consider following the [guide for developing a Kedro project for Databricks using your local environment](./databricks_ide_development_workflow.md). -In this guide, you will store your project's code in a repository on [GitHub](https://github.com/). Databricks integrates with many [Git providers](https://docs.databricks.com/repos/index.html#supported-git-providers), including GitLab and Azure Devops. The steps to create a Git repository and sync it with Databricks also generally apply to these Git providers, though the exact details may vary. +In this guide, you will store your project's code in a repository on [GitHub](https://github.com/). Databricks integrates with many [Git providers](https://docs.databricks.com/repos/index.html#supported-git-providers), including GitLab and Azure DevOps. The steps to create a Git repository and sync it with Databricks also generally apply to these Git providers, though the exact details may vary. ## What this page covers @@ -263,7 +263,7 @@ Now that your project has run successfully once, you can make changes using the The `databricks-iris` starter uses a default 80-20 ratio of training data to test data when training the classifier. You will edit this ratio to 70-30 and re-run your project to view the different result. -In the Databricks workspace, click on the `Repos` tab in the side bar and navigate to `/iris-databricks/conf/base/`. Open the the file `parameters.yml` by double-clicking it. This will take you to a built-in file editor. Edit the line `train_fraction: 0.8` to `train_fraction: 0.7`, your changes will automatically be saved. +In the Databricks workspace, click on the `Repos` tab in the side bar and navigate to `/iris-databricks/conf/base/`. Open the file `parameters.yml` by double-clicking it. This will take you to a built-in file editor. Edit the line `train_fraction: 0.8` to `train_fraction: 0.7`, your changes will automatically be saved. ![Databricks edit file](../../meta/images/databricks_edit_file.png) diff --git a/docs/source/development/commands_reference.md b/docs/source/development/commands_reference.md index adf3db84c3..ded8da9dcc 100644 --- a/docs/source/development/commands_reference.md +++ b/docs/source/development/commands_reference.md @@ -446,7 +446,7 @@ kedro micropkg package Further information is available in the [micro-packaging documentation](../nodes_and_pipelines/micro_packaging.md). ##### Pull a micro-package in your project -The following command pulls all the files related to a micro-package, e.g. a modular pipeline, from either [Pypi](https://pypi.org/) or a storage location of a [Python source distribution file](https://packaging.python.org/overview/#python-source-distributions). +The following command pulls all the files related to a micro-package, e.g. a modular pipeline, from either [PyPI](https://pypi.org/) or a storage location of a [Python source distribution file](https://packaging.python.org/overview/#python-source-distributions). ```bash kedro micropkg pull (or path to a sdist file) diff --git a/docs/source/experiment_tracking/index.md b/docs/source/experiment_tracking/index.md index 31bff89ee2..3004fe28e0 100644 --- a/docs/source/experiment_tracking/index.md +++ b/docs/source/experiment_tracking/index.md @@ -32,7 +32,7 @@ Kedro-Viz version 6.2 includes support for collaborative experiment tracking usi The choice of experiment tracking tool depends on your use case and choice of complementary tools, such as MLflow and Neptune: - **Kedro** - If you need experiment tracking, are looking for improved metrics visualisation and want a lightweight tool to work alongside existing functionality in Kedro. Kedro does not support a model registry. -- **MLflow** - You can combine MLFlow with Kedro by using [`kedro-mlflow`](https://kedro-mlflow.readthedocs.io/en/stable/) if you require experiment tracking, model registry and/or model serving capabilities or have access to Managed MLflow within the Databricks ecosystem. +- **MLflow** - You can combine MLflow with Kedro by using [`kedro-mlflow`](https://kedro-mlflow.readthedocs.io/en/stable/) if you require experiment tracking, model registry and/or model serving capabilities or have access to Managed MLflow within the Databricks ecosystem. - **Neptune** - If you require experiment tracking and model registry functionality, improved visualisation of metrics and support for collaborative data science, you may consider [`kedro-neptune`](https://docs.neptune.ai/integrations/kedro/) for your workflow. [We support a growing list of integrations](../extend_kedro/plugins.md). diff --git a/docs/source/extend_kedro/architecture_overview.md b/docs/source/extend_kedro/architecture_overview.md index 272fcef572..44d046fd02 100644 --- a/docs/source/extend_kedro/architecture_overview.md +++ b/docs/source/extend_kedro/architecture_overview.md @@ -3,7 +3,7 @@ There are different ways to leverage Kedro in your work, you can: - Commit to using all of Kedro (framework, project, starters and library); which is preferable to take advantage of the full value proposition of Kedro - - You can leverage parts of Kedro, like the DataCatalog (I/O), ConfigLoader, Pipelines and Runner, by using it as a Python libary; this best supports a workflow where you don't want to adopt the Kedro project template + - You can leverage parts of Kedro, like the DataCatalog (I/O), ConfigLoader, Pipelines and Runner, by using it as a Python library; this best supports a workflow where you don't want to adopt the Kedro project template - Or, you can develop extensions for Kedro e.g. custom starters, plugins, Hooks and more At a high level, Kedro consists of five main parts: From 53cc5d785ddad4bbbab0e4140fc2ecc9fd0f4d6f Mon Sep 17 00:00:00 2001 From: Ankita Katiyar <110245118+ankatiyar@users.noreply.github.com> Date: Tue, 22 Aug 2023 16:58:01 +0100 Subject: [PATCH 39/70] Setup Vale linter as a GHA workflow (#2953) * Add vale GHA Signed-off-by: Ankita Katiyar * Update condition Signed-off-by: Ankita Katiyar * Update .github/styles/Kedro/ignore.txt Co-authored-by: Jo Stichbury * add some styles Signed-off-by: Jo Stichbury * Remove styles for now Signed-off-by: Ankita Katiyar --------- Signed-off-by: Ankita Katiyar Signed-off-by: Jo Stichbury Co-authored-by: Jo Stichbury Signed-off-by: Jason Hite --- .github/styles/Kedro/Spellings.yml | 6 ++ .github/styles/Kedro/ignore-names.txt | 111 +++++++++++++++++++++ .github/styles/Kedro/ignore.txt | 31 ++++++ .github/workflows/docs-language-linter.yml | 16 +++ .github/workflows/docs-only-checks.yml | 2 +- .vale.ini | 7 ++ 6 files changed, 172 insertions(+), 1 deletion(-) create mode 100644 .github/styles/Kedro/Spellings.yml create mode 100644 .github/styles/Kedro/ignore-names.txt create mode 100644 .github/styles/Kedro/ignore.txt create mode 100644 .github/workflows/docs-language-linter.yml create mode 100644 .vale.ini diff --git a/.github/styles/Kedro/Spellings.yml b/.github/styles/Kedro/Spellings.yml new file mode 100644 index 0000000000..00a486ff0d --- /dev/null +++ b/.github/styles/Kedro/Spellings.yml @@ -0,0 +1,6 @@ +extends: spelling +message: "Did you really mean '%s'?" +level: warning +ignore: + - Kedro/ignore.txt + - Kedro/ignore-names.txt diff --git a/.github/styles/Kedro/ignore-names.txt b/.github/styles/Kedro/ignore-names.txt new file mode 100644 index 0000000000..6ab1616c02 --- /dev/null +++ b/.github/styles/Kedro/ignore-names.txt @@ -0,0 +1,111 @@ +Ahdra +Aris +Alves +Alam +Andrew +Andrii +Anton +Antony +Ankita +Batori +Braccialli +Brummitt +Cano +Carvalho +Cvetanka +Czakon +Chan +Comym +Couto +Daiha +Dada +Danov +Datta +DeBold +Deepyaman +Deriabin +Diaz +Dmitrii +Dmitry +Emamjome +Erins +Gabriel +Gundaniya +Guilherme +Hamza +Hoang +Holzer +Huong +Helmhold +Ignacio +Ivaniuk +Ivan +Jakub +Jannic +Jankowski +Jitendra +Jiri +Jo +Joel +Juan +Kadiri +Kaiser +Kaltsas +Kanchwala +Katiyar +Khan +Kirilenko +Kiyohito +Klein +Kunii +Kumar +Lais +Laura +Liam +Lim +Lorena +Machado +Mackay +Malte +Malloy +Marcin +Meisam +Merali +Merel +Milne +Nasef +Nechevska +Nero +Nguyen +Nikolaos +Nikolic +Nok +Okwa +Oza +Paricio +Patel +Peteris +Pilla +Rashida +Ravi +Richard +Schwarzmann +Sorokin +Stichbury +Susanna +Sajid +Stephanie +Takieddine +Theisen +Tynan +Tsaousis +Valtazanos +Vladimir +Waylon +Westenra +Wong +Wrigley +Yolan +Yetunde +Zablocki +Zain diff --git a/.github/styles/Kedro/ignore.txt b/.github/styles/Kedro/ignore.txt new file mode 100644 index 0000000000..6ede60d83b --- /dev/null +++ b/.github/styles/Kedro/ignore.txt @@ -0,0 +1,31 @@ +Kedro +Kedro's +Kubeflow +Databricks +Conda +conda +Cookiecutter +config +fsspec +Kaggle +namespace +namespaces +namespaced +regressors +repo +Repo +dbx +MLflow +csv +yaml +matplotlib +Matplotlib +IPython +APIs +networkx +Plotly +Pylint +SQLAlchemy +Astro +Xebia +pytest diff --git a/.github/workflows/docs-language-linter.yml b/.github/workflows/docs-language-linter.yml new file mode 100644 index 0000000000..02b60b7324 --- /dev/null +++ b/.github/workflows/docs-language-linter.yml @@ -0,0 +1,16 @@ +name: Language Linter for Kedro Docs +on: + pull_request: + paths: + - "docs/**" + - '**.md' + +jobs: + vale: + name: runner / vale + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + - uses: errata-ai/vale-action@reviewdog + with: + reporter: github-pr-review diff --git a/.github/workflows/docs-only-checks.yml b/.github/workflows/docs-only-checks.yml index b7940e85be..d13a76c589 100644 --- a/.github/workflows/docs-only-checks.yml +++ b/.github/workflows/docs-only-checks.yml @@ -17,7 +17,7 @@ on: - '**.md' jobs: - lint-tests: + lint: strategy: matrix: os: [ ubuntu-latest ] diff --git a/.vale.ini b/.vale.ini new file mode 100644 index 0000000000..7d486713e7 --- /dev/null +++ b/.vale.ini @@ -0,0 +1,7 @@ +StylesPath = .github/styles + +MinAlertLevel = suggestion + +[*.md] +BasedOnStyles = Vale, Kedro +Vale.Spelling = NO From 476a18368acf2620896ee73c228d989a82f7208d Mon Sep 17 00:00:00 2001 From: Jo Stichbury Date: Wed, 23 Aug 2023 12:50:24 +0100 Subject: [PATCH 40/70] Fix README to show graphics on PyPI (#2961) * Fix for PyPI Signed-off-by: Jo Stichbury * Few more tweaks Signed-off-by: Jo Stichbury * Few more tweaks Signed-off-by: Jo Stichbury * Few more tweaks Signed-off-by: Jo Stichbury * Few more tweaks Signed-off-by: Jo Stichbury * Update README.md * Update README.md --------- Signed-off-by: Jo Stichbury Signed-off-by: Jason Hite --- README.md | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index f329a8331f..cd3f8683ad 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,5 @@ -![Kedro Logo Banner - Light](.github/demo-dark.png#gh-dark-mode-only) -![Kedro Logo Banner - Dark](.github/demo-light.png#gh-light-mode-only) +![Kedro Logo Banner - Light](https://raw.githubusercontent.com/kedro-org/kedro/main/.github/demo-dark.png#gh-dark-mode-only) +![Kedro Logo Banner - Dark](https://raw.githubusercontent.com/kedro-org/kedro/main/.github/demo-light.png#gh-light-mode-only) [![Python version](https://img.shields.io/badge/python-3.7%20%7C%203.8%20%7C%203.9%20%7C%203.10%20%7C%203.11-blue.svg)](https://pypi.org/project/kedro/) [![PyPI version](https://badge.fury.io/py/kedro.svg)](https://pypi.org/project/kedro/) [![Conda version](https://img.shields.io/conda/vn/conda-forge/kedro.svg)](https://anaconda.org/conda-forge/kedro) @@ -37,9 +37,6 @@ Our [Get Started guide](https://docs.kedro.org/en/stable/get_started/install.htm ## What are the main features of Kedro? -![Kedro-Viz Pipeline Visualisation](https://github.com/kedro-org/kedro-viz/blob/main/.github/img/banner.png) -_A pipeline visualisation generated using [Kedro-Viz](https://github.com/kedro-org/kedro-viz)_ - | Feature | What is this? | | -------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | Project Template | A standard, modifiable and easy-to-use project template based on [Cookiecutter Data Science](https://github.com/drivendata/cookiecutter-data-science/). | @@ -54,7 +51,15 @@ The [Kedro documentation](https://docs.kedro.org/en/stable/) first explains [how You can then review the [spaceflights tutorial](https://docs.kedro.org/en/stable/tutorial/spaceflights_tutorial.html) to build a Kedro project for hands-on experience -For new and intermediate Kedro users, there's a comprehensive section on [how to visualise Kedro projects using Kedro-Viz](https://docs.kedro.org/en/stable/visualisation/index.html) and [how to work with Kedro and Jupyter notebooks](https://docs.kedro.org/en/stable/notebooks_and_ipython/index.html). We also recommend the [API reference documentation](/kedro) for additional information. +For new and intermediate Kedro users, there's a comprehensive section on [how to visualise Kedro projects using Kedro-Viz](https://docs.kedro.org/en/stable/visualisation/index.html). + + +

+ + A pipeline visualisation generated using Kedro-Viz +

+ +Additional documentation explains [how to work with Kedro and Jupyter notebooks](https://docs.kedro.org/en/stable/notebooks_and_ipython/index.html), and there are a set of advanced user guides for advanced for key Kedro features. We also recommend the [API reference documentation](/kedro) for further information. ## Why does Kedro exist? From 9426a1bbfa83907798880275b0323d4e7f25638c Mon Sep 17 00:00:00 2001 From: Jo Stichbury Date: Wed, 23 Aug 2023 17:10:27 +0100 Subject: [PATCH 41/70] Add some Vale styles (#2963) * add some styles Signed-off-by: Jo Stichbury * Tweaks to styles Signed-off-by: Jo Stichbury * more tweaks Signed-off-by: Jo Stichbury * Revise some styles Signed-off-by: Jo Stichbury * Revise severity of some styles Signed-off-by: Jo Stichbury * Update .github/styles/Kedro/words.yml Co-authored-by: Deepyaman Datta --------- Signed-off-by: Jo Stichbury Co-authored-by: Deepyaman Datta Signed-off-by: Jason Hite --- .github/styles/Kedro/abbreviations.yml | 11 ++ .github/styles/Kedro/gender.yml | 13 ++ .github/styles/Kedro/headings.yml | 56 ++++++ .github/styles/Kedro/inclusive.yml | 12 ++ .github/styles/Kedro/links.yml | 24 +++ .github/styles/Kedro/oxfordcomma.yml | 6 + .github/styles/Kedro/pronouns.yml | 11 ++ .github/styles/Kedro/quotes.yml | 10 ++ .github/styles/Kedro/sentencelength.yml | 11 ++ .github/styles/Kedro/toowordy.yml | 218 ++++++++++++++++++++++++ .github/styles/Kedro/ukspelling.yml | 25 +++ .github/styles/Kedro/weaselwords.yml | 207 ++++++++++++++++++++++ .github/styles/Kedro/words.yml | 53 ++++++ docs/source/data/data_catalog.md | 1 + 14 files changed, 658 insertions(+) create mode 100644 .github/styles/Kedro/abbreviations.yml create mode 100644 .github/styles/Kedro/gender.yml create mode 100644 .github/styles/Kedro/headings.yml create mode 100644 .github/styles/Kedro/inclusive.yml create mode 100644 .github/styles/Kedro/links.yml create mode 100644 .github/styles/Kedro/oxfordcomma.yml create mode 100644 .github/styles/Kedro/pronouns.yml create mode 100644 .github/styles/Kedro/quotes.yml create mode 100644 .github/styles/Kedro/sentencelength.yml create mode 100644 .github/styles/Kedro/toowordy.yml create mode 100644 .github/styles/Kedro/ukspelling.yml create mode 100644 .github/styles/Kedro/weaselwords.yml create mode 100644 .github/styles/Kedro/words.yml diff --git a/.github/styles/Kedro/abbreviations.yml b/.github/styles/Kedro/abbreviations.yml new file mode 100644 index 0000000000..345ecc31a4 --- /dev/null +++ b/.github/styles/Kedro/abbreviations.yml @@ -0,0 +1,11 @@ +extends: substitution +message: "Use '%s' instead of abbreviations like '%s'." +ignorecase: true +level: warning +nonword: true +action: + name: replace +swap: + '\b(?:eg|e\.g\.|eg\.)[\s,]': for example + '\b(?:ie|i\.e\.|ie\.)[\s,]': that is + '\b(?:etc)[\s\n,.]': and more diff --git a/.github/styles/Kedro/gender.yml b/.github/styles/Kedro/gender.yml new file mode 100644 index 0000000000..f06d0f9cc5 --- /dev/null +++ b/.github/styles/Kedro/gender.yml @@ -0,0 +1,13 @@ +extends: existence +message: "Use a gender-neutral pronoun instead of '%s'." +level: error +ignorecase: true +tokens: + - he/she + - s/he + - \(s\)he + - \bhe\b + - \bhim\b + - \bhis\b + - \bshe\b + - \bher\b diff --git a/.github/styles/Kedro/headings.yml b/.github/styles/Kedro/headings.yml new file mode 100644 index 0000000000..3c83043bd3 --- /dev/null +++ b/.github/styles/Kedro/headings.yml @@ -0,0 +1,56 @@ +extends: capitalization +message: "'%s' should use sentence-style capitalization." +level: warning +scope: heading +match: $sentence +exceptions: + - Kedro + - Kedro-Viz + - Airflow + - Amazon + - Amazon Web Services + - AWS Step Functions + - AWS Systems Manager + - Azure + - Azure App Service + - Azure App Service Plan + - Azure Blob Storage + - Azure Event Hub + - CI/CD + - DataCatalog + - Data Catalog + - Docker + - Docker Compose + - Docker Swarm + - Dockerfile + - GitHub + - GitHub Actions + - Google + - Google Analytics + - Google Cloud + - Google Cloud Functions + - GraphQL + - Hook + - Hooks + - IDs + - Jenkins + - JFrog + - JFrog Artifactory + - Jira + - Kafka + - Kubernetes + - Kubernetes Engine + - Kubernetes Pod + - Kubernetes Service + - Lambda + - Linux + - MySQL + - Python + - QuantumBlack + - QuantumBlack Labs + - Red Hat + - Redis + - Slack + - Ubuntu + - Unix + - URLs diff --git a/.github/styles/Kedro/inclusive.yml b/.github/styles/Kedro/inclusive.yml new file mode 100644 index 0000000000..21792cc49f --- /dev/null +++ b/.github/styles/Kedro/inclusive.yml @@ -0,0 +1,12 @@ +extends: substitution +message: "Use '%s' instead of '%s'." +ignorecase: true +level: error +action: + name: replace +swap: + # bad: good + 'black ?list': 'disallow list|exclude list' + 'master': primary + 'slave': secondary + 'white ?list': 'allow list|include list' diff --git a/.github/styles/Kedro/links.yml b/.github/styles/Kedro/links.yml new file mode 100644 index 0000000000..aa7597d554 --- /dev/null +++ b/.github/styles/Kedro/links.yml @@ -0,0 +1,24 @@ +extends: substitution +message: "Avoid vague text in links like '%s' unless you can pair it with more descriptive text." +link: 'https://github.com/kedro-org/kedro/wiki/Kedro-documentation-style-guide' +ignorecase: true +scope: raw +nonword: true +level: error + +swap: +# For the word 'here' in Markdown and HTML links + '\[here\]\(.*?\)': 'here' + '\s*here\s*': 'here' + +# For the word 'this' in Markdown and HTML links + '\[this\]\(.*?\)': 'this' + '\s*this\s*': 'this' + +# For the word 'page' in Markdown and HTML links + '\[page\]\(.*?\)': 'page' + '\s*page\s*': 'page' + +# For the phrase 'this page' in Markdown and HTML links + '\[this page\]\(.*?\)': 'this page' + '\s*this page\s*': 'this page' diff --git a/.github/styles/Kedro/oxfordcomma.yml b/.github/styles/Kedro/oxfordcomma.yml new file mode 100644 index 0000000000..464d6e6997 --- /dev/null +++ b/.github/styles/Kedro/oxfordcomma.yml @@ -0,0 +1,6 @@ +extends: existence +message: "Use the Oxford comma in '%s'." +scope: sentence +level: suggestion +tokens: + - '(?:[^,]+,){1,}\s\w+\s(?:and|or)' diff --git a/.github/styles/Kedro/pronouns.yml b/.github/styles/Kedro/pronouns.yml new file mode 100644 index 0000000000..cea1aca18e --- /dev/null +++ b/.github/styles/Kedro/pronouns.yml @@ -0,0 +1,11 @@ +extends: existence +message: "Avoid first-person singular pronouns such as '%s'." +level: warning +nonword: true +tokens: + - (?<=^|\s)I(?=\s) + - (?<=^|\s)I,(?=\s) + - \bI'm\b + - (?<=\s)[Mm]e\b + - (?<=\s)[Mm]y\b + - (?<=\s)[Mm]ine\b diff --git a/.github/styles/Kedro/quotes.yml b/.github/styles/Kedro/quotes.yml new file mode 100644 index 0000000000..7e4ed44be0 --- /dev/null +++ b/.github/styles/Kedro/quotes.yml @@ -0,0 +1,10 @@ +extends: existence +message: Use straight quotes instead of smart quotes. +level: warning +nonword: true +action: +tokens: + - “ + - ” + - ‘ + - ’ diff --git a/.github/styles/Kedro/sentencelength.yml b/.github/styles/Kedro/sentencelength.yml new file mode 100644 index 0000000000..6c135a84c5 --- /dev/null +++ b/.github/styles/Kedro/sentencelength.yml @@ -0,0 +1,11 @@ +extends: occurrence +message: "Try to keep your sentence length to 30 words or fewer." +level: suggestion +# Here, we're counting the number of words +# in a sentence. +# +# If there are more than 30, we'll flag it. +scope: sentence +ignorecase: false +max: 30 +token: (\w+) diff --git a/.github/styles/Kedro/toowordy.yml b/.github/styles/Kedro/toowordy.yml new file mode 100644 index 0000000000..262b5ca419 --- /dev/null +++ b/.github/styles/Kedro/toowordy.yml @@ -0,0 +1,218 @@ +# Write Good's "Too wordy" rule https://github.com/testthedocs/vale-styles/blob/master/write-good/TooWordy.yml +extends: existence +message: "'%s' is too wordy" +ignorecase: true +level: warning +tokens: + - a number of + - abundance + - accede to + - accelerate + - accentuate + - accompany + - accomplish + - accorded + - accrue + - acquiesce + - acquire + - adjacent to + - adjustment + - admissible + - advantageous + - adversely impact + - advise + - aforementioned + - aggregate + - aircraft + - all of + - all things considered + - alleviate + - allocate + - along the lines of + - already existing + - alternatively + - amazing + - ameliorate + - anticipate + - apparent + - appreciable + - as a matter of fact + - as a means of + - as far as I'm concerned + - as of yet + - as to + - as yet + - ascertain + - assistance + - at the present time + - at this time + - attain + - attributable to + - because of the fact that + - belated + - benefit from + - bestow + - by means of + - by virtue of the fact that + - by virtue of + - cease + - close proximity + - commence + - comply with + - concerning + - consequently + - consolidate + - constitutes + - demonstrate + - depart + - designate + - discontinue + - due to the fact that + - each and every + - economical + - eliminate + - elucidate + - employ + - endeavor + - enumerate + - equitable + - equivalent + - evaluate + - evidenced + - exclusively + - expedite + - expend + - expiration + - facilitate + - factual evidence + - feasible + - finalise + - first and foremost + - for all intents and purposes + - for the most part + - for the purpose of + - forfeit + - formulate + - have a tendency to + - honest truth + - however + - if and when + - impacted + - implement + - in a manner of speaking + - in a timely manner + - in a very real sense + - in accordance with + - in addition + - in all likelihood + - in an effort to + - in between + - in excess of + - in lieu of + - in light of the fact that + - in many cases + - in my opinion + - in order to + - in regard to + - in some instances + - in terms of + - in the case of + - in the event that + - in the final analysis + - in the nature of + - in the near future + - in the process of + - inception + - incumbent upon + - indicate + - indication + - initiate + - irregardless + - is applicable to + - is authorised to + - is responsible for + - it is essential + - it seems that + - it was + - magnitude + - maximum + - methodology + - minimise + - modify + - monitor + - multiple + - necessitate + - nevertheless + - not certain + - not many + - not often + - not unless + - not unlike + - notwithstanding + - null and void + - numerous + - objective + - obligate + - obtain + - on the contrary + - on the other hand + - one particular + - optimum + - overall + - owing to the fact that + - participate + - particulars + - pass away + - pertaining to + - point in time + - portion + - possess + - preclude + - previously + - prior to + - prioritise + - procure + - proficiency + - provided that + - purchase + - put simply + - readily apparent + - refer back + - regarding + - relocate + - remainder + - remuneration + - requirement + - reside + - residence + - retain + - satisfy + - shall + - should you wish + - similar to + - solicit + - span across + - strategise + - subsequent + - substantial + - successfully complete + - sufficient + - terminate + - the month of + - the point I am trying to make + - therefore + - time period + - took advantage of + - transmit + - transpire + - type of + - until such time as + - utilisation + - utilise + - validate + - various different + - what I mean to say is + - whether or not + - with respect to + - with the exception of + - witnessed diff --git a/.github/styles/Kedro/ukspelling.yml b/.github/styles/Kedro/ukspelling.yml new file mode 100644 index 0000000000..fbd59b50ec --- /dev/null +++ b/.github/styles/Kedro/ukspelling.yml @@ -0,0 +1,25 @@ +extends: existence +message: "In general, use UK English spelling instead of '%s'." +link: 'https://github.com/kedro-org/kedro/wiki/Kedro-documentation-style-guide' +ignorecase: true +level: warning +tokens: + - '(?:\w+)nized' + - '(?:\w+)ize' + - '(?:\w+)log' + - '(?:\w+)lor' + - '(?:\w+)lyze' +exceptions: + - backlog + - blog + - capsize + - catalog + - Catalog + - DataCatalog + - dialog + - log + - maize + - prize + - seize + - size + - tailor diff --git a/.github/styles/Kedro/weaselwords.yml b/.github/styles/Kedro/weaselwords.yml new file mode 100644 index 0000000000..d6c6f73e09 --- /dev/null +++ b/.github/styles/Kedro/weaselwords.yml @@ -0,0 +1,207 @@ +# Write Good's Weasel Words rule https://github.com/testthedocs/vale-styles/blob/master/write-good/Weasel.yml +extends: existence +message: "'%s' is a weasel word!" +ignorecase: true +level: warning +tokens: + - absolutely + - accidentally + - additionally + - allegedly + - alternatively + - angrily + - anxiously + - approximately + - awkwardly + - badly + - barely + - beautifully + - blindly + - boldly + - bravely + - brightly + - briskly + - bristly + - bubbly + - busily + - calmly + - carefully + - carelessly + - cautiously + - cheerfully + - clearly + - closely + - coldly + - completely + - consequently + - correctly + - courageously + - crinkly + - cruelly + - crumbly + - cuddly + - currently + - daily + - daringly + - deadly + - definitely + - deliberately + - doubtfully + - dumbly + - eagerly + - easily + - elegantly + - enormously + - enthusiastically + - equally + - especially + - eventually + - exactly + - exceedingly + - exclusively + - extremely + - fairly + - faithfully + - fatally + - fiercely + - finally + - fondly + - few + - foolishly + - fortunately + - frankly + - frantically + - generously + - gently + - giggly + - gladly + - gracefully + - greedily + - happily + - hardly + - hastily + - healthily + - heartily + - helpfully + - honestly + - hourly + - hungrily + - hurriedly + - immediately + - impatiently + - inadequately + - ingeniously + - innocently + - inquisitively + - interestingly + - irritably + - jiggly + - joyously + - justly + - kindly + - largely + - lately + - lazily + - likely + - literally + - lonely + - loosely + - loudly + - loudly + - luckily + - madly + - many + - mentally + - mildly + - monthly + - mortally + - mostly + - mysteriously + - neatly + - nervously + - nightly + - noisily + - normally + - obediently + - occasionally + - only + - openly + - painfully + - particularly + - patiently + - perfectly + - politely + - poorly + - powerfully + - presumably + - previously + - promptly + - punctually + - quarterly + - quickly + - quietly + - rapidly + - rarely + - really + - recently + - recklessly + - regularly + - remarkably + - relatively + - reluctantly + - repeatedly + - rightfully + - roughly + - rudely + - sadly + - safely + - selfishly + - sensibly + - seriously + - sharply + - shortly + - shyly + - significantly + - silently + - simply + - sleepily + - slowly + - smartly + - smelly + - smoothly + - softly + - solemnly + - sparkly + - speedily + - stealthily + - sternly + - stupidly + - substantially + - successfully + - suddenly + - surprisingly + - suspiciously + - swiftly + - tenderly + - tensely + - thoughtfully + - tightly + - timely + - truthfully + - unexpectedly + - unfortunately + - usually + - very + - victoriously + - violently + - vivaciously + - warmly + - waverly + - weakly + - wearily + - weekly + - wildly + - wisely + - worldly + - wrinkly + - yearly diff --git a/.github/styles/Kedro/words.yml b/.github/styles/Kedro/words.yml new file mode 100644 index 0000000000..5ed27dd319 --- /dev/null +++ b/.github/styles/Kedro/words.yml @@ -0,0 +1,53 @@ +extends: substitution +message: "Use '%s' instead of '%s'." +ignorecase: false +level: warning +action: + name: replace +swap: + # bad: good + 'acknowledgement': 'acknowledgment' + 'auto-complete': 'autocomplete' + 'a number of': 'few|several|many' + 'and/or': 'and|or|either or' + 'back end': 'backend' + 'bear in mind': 'keep in mind' + 'culprit': 'cause' + 'data set': 'dataset' + 'drill down|drilling down|drill into|drilling into': 'examine|investigate|analyze' + 'figure out': 'determine' + 'fine tune|fine-tune': 'customize|optimize|refine' + 'for the most part': 'generally|usually' + 'front end': 'frontend' + 'highly|very': '' + 'hit': 'click|select' + 'in order to': 'to' + 'keep in mind': 'consider' + 'left up to': 'determined by' + 'leverage': 'use' + 'multi-alert': 'multi alert' + 'Note that': '**Note**:' + 'obviously|obvious': '' + 'on the fly': 'real-time|real time' + 'once': 'after' + 'play a hand': 'influence' + 'please|just': '' + 'easily|easy': '' + 'quickly|quick': '' + 'screen board': 'screenboard' + 'simply|simple': '' + 'stand for': 'represents|means' + 'reenable': 're-enable' + 'run time': 'runtime' + 'refer to|visit': 'see|read|follow' + 'time board': 'timeboard' + 'time series': 'timeseries' + 'toplist': 'top list' + 'tradeoff': 'trade-off' + 'turnkey': 'ready to use' + 'under the hood': '' + 'utilize': 'use' + 'via': 'with|through' + 'visit': 'see|read' + 'webserver': 'web server' + 'web site': 'website' diff --git a/docs/source/data/data_catalog.md b/docs/source/data/data_catalog.md index 241e339635..b4a6c4d7da 100644 --- a/docs/source/data/data_catalog.md +++ b/docs/source/data/data_catalog.md @@ -1,5 +1,6 @@ # Introduction to the Data Catalog + In a Kedro project, the Data Catalog is a registry of all data sources available for use by the project. It is specified with a YAML catalog file that maps the names of node inputs and outputs as keys in the `DataCatalog` class. This page introduces the basic sections of `catalog.yml`, which is the file used to register data sources for a Kedro project. From 230751b9f902772a378a4ad4102f36c3bf6684ba Mon Sep 17 00:00:00 2001 From: Ankita Katiyar Date: Thu, 24 Aug 2023 15:08:29 +0100 Subject: [PATCH 42/70] Minor changes to test + release notes + docs Signed-off-by: Ankita Katiyar Signed-off-by: Jason Hite --- RELEASE.md | 1 + .../nodes_and_pipelines/modular_pipelines.md | 8 ++- tests/framework/cli/conftest.py | 58 ------------------- tests/framework/cli/pipeline/conftest.py | 50 ++++++++++++++++ 4 files changed, 58 insertions(+), 59 deletions(-) diff --git a/RELEASE.md b/RELEASE.md index bd338addd5..0d85ace914 100644 --- a/RELEASE.md +++ b/RELEASE.md @@ -15,6 +15,7 @@ * Added support for Python 3.11. This includes tackling challenges like dependency pinning and test adjustments to ensure a smooth experience. Detailed migration tips are provided below for further context. * Added `kedro catalog resolve` CLI command that resolves dataset factories in the catalog with any explicit entries in the project pipeline. * Added support for global variables to `OmegaConfigLoader`. +* Allowed using of custom cookiecutter templates for creating pipelines with `--template` flag for `kedro pipeline create` ## Bug fixes and other changes diff --git a/docs/source/nodes_and_pipelines/modular_pipelines.md b/docs/source/nodes_and_pipelines/modular_pipelines.md index 5064ae7b87..3df2a699dc 100644 --- a/docs/source/nodes_and_pipelines/modular_pipelines.md +++ b/docs/source/nodes_and_pipelines/modular_pipelines.md @@ -59,7 +59,6 @@ Running the `kedro pipeline create` command adds boilerplate folders and files f │ └── pipelines │ ├── __init__.py │ └── {{pipeline_name}} <-- This folder defines the modular pipeline - │ ├── README.md <-- Pipeline-specific documentation │ ├── __init__.py <-- So that Python treats this pipeline as a module │ ├── nodes.py <-- To declare your nodes │ └── pipeline.py <-- To structure the pipeline itself @@ -75,6 +74,13 @@ Running the `kedro pipeline create` command adds boilerplate folders and files f +If you want to generate a pipeline with a custom Cookiecutter template, you can save it in `/templates/pipeline`. +The `kedro pipeline create` command will then pick up the custom template as the default. You can also specify the path to your custom +Cookiecutter pipeline template with the `--template` flag like this: +```bash +kedro pipeline create --template +``` + If you want to do the reverse and remove a modular pipeline, you can use ``kedro pipeline delete `` to do so. ### Ensuring portability diff --git a/tests/framework/cli/conftest.py b/tests/framework/cli/conftest.py index 160b390cc1..b6bba00a23 100644 --- a/tests/framework/cli/conftest.py +++ b/tests/framework/cli/conftest.py @@ -79,64 +79,6 @@ def dummy_config(fake_root_dir, fake_metadata): return config_path -def write_file_contents(fname: Path, contents: str): - """Little helper to make setting up a test template dir easier. - - Automatically creates the parent dir of a file first if it doesn't exist to cut - down on extraneous LOC. SO BE WARNED you need to clean up after yourself. - """ - fname.parent.mkdir(parents=True, exist_ok=True) - - with fname.open("w") as f: - f.write(contents) - - -@fixture() -def fake_local_template_dir(fake_repo_path): - """Set up a local template directory. This won't be functional we're just testing the actual layout works. - - Note that this is not scoped to module because we don't want to have this folder present in most of the tests, - so we will tear it down every time. - """ - template_path = fake_repo_path / Path("templates") - pipeline_template_path = template_path / Path("pipeline") - cookiecutter_template_path = ( - pipeline_template_path / "{{ cookiecutter.pipeline_name }}" - ) - - cookiecutter_template_path.mkdir(parents=True) - - # Create the absolute bare minimum files - write_file_contents( - pipeline_template_path / "cookiecutter.json", - """ - {"pipeline_name": "default", "kedro_version": "{{ cookiecutter.kedro_version }}"} - """.strip(), - ) - - write_file_contents( - cookiecutter_template_path / "pipeline_{{ cookiecutter.pipeline_name }}.py", - r"print('hello world')", - ) - - write_file_contents(cookiecutter_template_path / "__init__.py", "") - - write_file_contents( - cookiecutter_template_path - / r"config/parameters/{{ cookiecutter.pipeline_name }}.yml", - "foo: bar", - ) - - write_file_contents( - cookiecutter_template_path / r"tests/test_{{ cookiecutter.pipeline_name }}.py", - "", - ) - - yield template_path.resolve() - - shutil.rmtree(template_path) - - @fixture(scope="module") def fake_metadata(fake_root_dir): metadata = ProjectMetadata( diff --git a/tests/framework/cli/pipeline/conftest.py b/tests/framework/cli/pipeline/conftest.py index f934ab6939..672fee3eb6 100644 --- a/tests/framework/cli/pipeline/conftest.py +++ b/tests/framework/cli/pipeline/conftest.py @@ -1,10 +1,24 @@ +import json import shutil +from pathlib import Path import pytest from kedro.framework.project import settings +def _write_json(filepath: Path, content: dict): + filepath.parent.mkdir(parents=True, exist_ok=True) + json_str = json.dumps(content, indent=4) + filepath.write_text(json_str) + + +def _write_dummy_file(filepath: Path, content: str = ""): + filepath.parent.mkdir(parents=True, exist_ok=True) + with filepath.open("w") as f: + f.write(content) + + @pytest.fixture(autouse=True) def cleanup_micropackages(fake_repo_path, fake_package_path): packages = {p.name for p in fake_package_path.iterdir() if p.is_dir()} @@ -82,3 +96,39 @@ def cleanup_pyproject_toml(fake_repo_path): yield pyproject_toml.write_text(existing_toml) + + +@pytest.fixture() +def fake_local_template_dir(fake_repo_path): + """Set up a local template directory. This won't be functional we're just testing the actual layout works. + + Note that this is not scoped to module because we don't want to have this folder present in most of the tests, + so we will tear it down every time. + """ + template_path = fake_repo_path / Path("templates") + pipeline_template_path = template_path / Path("pipeline") + cookiecutter_template_path = ( + pipeline_template_path / "{{ cookiecutter.pipeline_name }}" + ) + + cookiecutter_template_path.mkdir(parents=True) + + # Create the absolute bare minimum files + cookiecutter_json = { + "pipeline_name": "default", + } + _write_json(pipeline_template_path / "cookiecutter.json", cookiecutter_json) + _write_dummy_file( + cookiecutter_template_path / "pipeline_{{ cookiecutter.pipeline_name }}.py", + ) + _write_dummy_file(cookiecutter_template_path / "__init__.py", "") + _write_dummy_file( + cookiecutter_template_path + / r"config/parameters/{{ cookiecutter.pipeline_name }}.yml", + ) + _write_dummy_file( + cookiecutter_template_path / r"tests/test_{{ cookiecutter.pipeline_name }}.py", + ) + yield template_path.resolve() + + shutil.rmtree(template_path) From 4c79063198db473e227809a0bfcf5514f3cd4c21 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 24 Aug 2023 13:28:24 +0100 Subject: [PATCH 43/70] Update anyconfig requirement from ~=0.10.0 to >=0.10,<0.14 (#2876) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Updates the requirements on [anyconfig](https://github.com/ssato/python-anyconfig) to permit the latest version. - [Changelog](https://github.com/ssato/python-anyconfig/blob/next/NEWS) - [Commits](https://github.com/ssato/python-anyconfig/compare/RELEASE_0.10.0...RELEASE_0.13.0) Signed-off-by: Jason Hite --- updated-dependencies: - dependency-name: anyconfig dependency-type: direct:production ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Juan Luis Cano Rodríguez Signed-off-by: Jason Hite --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 0421e10093..603c0837e5 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -12,7 +12,7 @@ authors = [ description = "Kedro helps you build production-ready data and analytics pipelines" requires-python = ">=3.7" dependencies = [ - "anyconfig~=0.10.0", + "anyconfig>=0.10,<0.14", "attrs>=21.3", "build", "cachetools~=5.3", From 55bfbe7c40a8684158c66638c0c2545adbf629f9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Juan=20Luis=20Cano=20Rodr=C3=ADguez?= Date: Fri, 25 Aug 2023 10:50:36 +0200 Subject: [PATCH 44/70] Move default template to static `pyproject.toml`, take 2 (#2853) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Remove leftover reference to micropkg setup.py Signed-off-by: Juan Luis Cano Rodríguez * Replace outdated extras requirement with kedro-datasets Signed-off-by: Juan Luis Cano Rodríguez * Remove outdated mention to environment.yml Signed-off-by: Juan Luis Cano Rodríguez * Move flake8 configuration to dedicated file Signed-off-by: Juan Luis Cano Rodríguez * Replace setup.py with pyproject.toml in default project template Signed-off-by: Juan Luis Cano Rodríguez * Adapt link to moving file in docs Signed-off-by: Juan Luis Cano Rodríguez * Revert "Replace outdated extras requirement with kedro-datasets" This reverts commit dfa8a9e8377ca60dcc3026e93087c5538791a0b5. Signed-off-by: Juan Luis Cano Rodríguez * Add release notes for pyproject.toml change Signed-off-by: Juan Luis Cano Rodríguez --------- Signed-off-by: Juan Luis Cano Rodríguez Signed-off-by: Jason Hite --- RELEASE.md | 2 + docs/source/conf.py | 4 +- docs/source/deployment/airflow_astronomer.md | 2 +- docs/source/development/linting.md | 2 +- docs/source/get_started/kedro_concepts.md | 2 +- docs/source/kedro_project_setup/starters.md | 4 +- .../{{ cookiecutter.repo_name }}/.flake8 | 3 ++ .../{{ cookiecutter.repo_name }}/README.md | 2 +- .../{{ cookiecutter.repo_name }}/setup.cfg | 5 --- .../src/pyproject.toml | 31 +++++++++++++++ .../{{ cookiecutter.repo_name }}/src/setup.py | 39 ------------------- .../{setup.cfg => .flake8} | 0 .../src/pyproject.toml | 31 +++++++++++++++ .../{{ cookiecutter.repo_name }}/src/setup.py | 39 ------------------- .../micropkg/test_micropkg_requirements.py | 4 +- 15 files changed, 78 insertions(+), 92 deletions(-) create mode 100644 features/steps/test_starter/{{ cookiecutter.repo_name }}/.flake8 delete mode 100644 features/steps/test_starter/{{ cookiecutter.repo_name }}/setup.cfg create mode 100644 features/steps/test_starter/{{ cookiecutter.repo_name }}/src/pyproject.toml delete mode 100644 features/steps/test_starter/{{ cookiecutter.repo_name }}/src/setup.py rename kedro/templates/project/{{ cookiecutter.repo_name }}/{setup.cfg => .flake8} (100%) create mode 100644 kedro/templates/project/{{ cookiecutter.repo_name }}/src/pyproject.toml delete mode 100644 kedro/templates/project/{{ cookiecutter.repo_name }}/src/setup.py diff --git a/RELEASE.md b/RELEASE.md index 0d85ace914..31d84f1928 100644 --- a/RELEASE.md +++ b/RELEASE.md @@ -20,6 +20,8 @@ ## Bug fixes and other changes * Updated `kedro pipeline create` and `kedro catalog create` to use new `/conf` file structure. +* Converted `setup.py` in default template to `pyproject.toml` and moved flake8 configuration + to dedicated file `.flake8`. ## Documentation changes * Revised the `data` section to restructure beginner and advanced pages about the Data Catalog and datasets. diff --git a/docs/source/conf.py b/docs/source/conf.py index 4519143968..e80f9b2b29 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -227,7 +227,9 @@ "https://opensource.org/license/apache2-0-php/", "https://docs.github.com/en/rest/overview/other-authentication-methods#via-username-and-password", "https://docs.snowflake.com/en/developer-guide/snowpark/reference/python/api/snowflake.snowpark.DataFrameWriter.saveAsTable.html", - "https://www.educative.io/blog/advanced-yaml-syntax-cheatsheet#anchors" + "https://www.educative.io/blog/advanced-yaml-syntax-cheatsheet#anchors", + # temporarily, until the file actually exists in `main` + "https://github.com/kedro-org/kedro/blob/main/kedro/templates/project/%7B%7B%20cookiecutter.repo_name%20%7D%7D/.flake8", ] # retry before render a link broken (fix for "too many requests") diff --git a/docs/source/deployment/airflow_astronomer.md b/docs/source/deployment/airflow_astronomer.md index 307e9ab903..b21408156b 100644 --- a/docs/source/deployment/airflow_astronomer.md +++ b/docs/source/deployment/airflow_astronomer.md @@ -74,7 +74,7 @@ To follow this tutorial, ensure you have the following: ├── plugins ├── pyproject.toml ├── requirements.txt - ├── setup.cfg + ├── .flake8 └── src ``` diff --git a/docs/source/development/linting.md b/docs/source/development/linting.md index d795086b51..a8bdbc0c44 100644 --- a/docs/source/development/linting.md +++ b/docs/source/development/linting.md @@ -60,7 +60,7 @@ It is a good practice to [split your line when it is too long](https://beta.ruff #### Configure `flake8` -Store your `flake8` configuration in a file named `setup.cfg` within your project root. The Kedro starters use the [following configuration](https://github.com/kedro-org/kedro-starters/blob/main/pandas-iris/%7B%7B%20cookiecutter.repo_name%20%7D%7D/setup.cfg): +Store your `flake8` configuration in a file named `.flake8` within your project root. The Kedro default project template use the [following configuration](https://github.com/kedro-org/kedro/blob/main/kedro/templates/project/%7B%7B%20cookiecutter.repo_name%20%7D%7D/.flake8): ```text [flake8] diff --git a/docs/source/get_started/kedro_concepts.md b/docs/source/get_started/kedro_concepts.md index 4a6d771da0..67f9bf84a9 100644 --- a/docs/source/get_started/kedro_concepts.md +++ b/docs/source/get_started/kedro_concepts.md @@ -72,7 +72,7 @@ project-dir # Parent directory of the template ├── notebooks # Project-related Jupyter notebooks (can be used for experimental code before moving the code to src) ├── pyproject.toml # Identifies the project root and contains configuration information ├── README.md # Project README -├── setup.cfg # Configuration options for `pytest` when doing `kedro test` and for the `isort` utility when doing `kedro lint` +├── .flake8 # Configuration options for `flake8` (linting) └── src # Project source code ``` diff --git a/docs/source/kedro_project_setup/starters.md b/docs/source/kedro_project_setup/starters.md index 4ad60cb0c2..305fe1de00 100644 --- a/docs/source/kedro_project_setup/starters.md +++ b/docs/source/kedro_project_setup/starters.md @@ -155,7 +155,7 @@ Here is the layout of the project as a Cookiecutter template: ├── docs # Project documentation ├── notebooks # Project related Jupyter notebooks (can be used for experimental code before moving the code to src) ├── README.md # Project README -├── setup.cfg # Configuration options for tools e.g. `pytest` or `black` +├── .flake8 # Configuration options for `flake8` (linting) └── src # Project source code └── {{ cookiecutter.python_package }} ├── __init.py__ @@ -164,7 +164,7 @@ Here is the layout of the project as a Cookiecutter template: ├── __main__.py └── settings.py ├── requirements.txt - ├── setup.py + ├── pyproject.toml └── tests ``` diff --git a/features/steps/test_starter/{{ cookiecutter.repo_name }}/.flake8 b/features/steps/test_starter/{{ cookiecutter.repo_name }}/.flake8 new file mode 100644 index 0000000000..8dd399ab55 --- /dev/null +++ b/features/steps/test_starter/{{ cookiecutter.repo_name }}/.flake8 @@ -0,0 +1,3 @@ +[flake8] +max-line-length = 88 +extend-ignore = E203 diff --git a/features/steps/test_starter/{{ cookiecutter.repo_name }}/README.md b/features/steps/test_starter/{{ cookiecutter.repo_name }}/README.md index 8041d41dd9..fd206a315d 100644 --- a/features/steps/test_starter/{{ cookiecutter.repo_name }}/README.md +++ b/features/steps/test_starter/{{ cookiecutter.repo_name }}/README.md @@ -17,7 +17,7 @@ In order to get the best out of the template: ## How to install dependencies -Declare any dependencies in `src/requirements.txt` for `pip` installation and `src/environment.yml` for `conda` installation. +Declare any dependencies in `src/requirements.txt` for `pip` installation. To install them, run: diff --git a/features/steps/test_starter/{{ cookiecutter.repo_name }}/setup.cfg b/features/steps/test_starter/{{ cookiecutter.repo_name }}/setup.cfg deleted file mode 100644 index 3edf923e16..0000000000 --- a/features/steps/test_starter/{{ cookiecutter.repo_name }}/setup.cfg +++ /dev/null @@ -1,5 +0,0 @@ -; Flake8 will be moved in `pyproject.toml` once the library -; starts supporting such config source -[flake8] -max-line-length = 88 -extend-ignore = E203 diff --git a/features/steps/test_starter/{{ cookiecutter.repo_name }}/src/pyproject.toml b/features/steps/test_starter/{{ cookiecutter.repo_name }}/src/pyproject.toml new file mode 100644 index 0000000000..ea581e7028 --- /dev/null +++ b/features/steps/test_starter/{{ cookiecutter.repo_name }}/src/pyproject.toml @@ -0,0 +1,31 @@ +[build-system] +requires = ["setuptools"] +build-backend = "setuptools.build_meta" + +[project] +name = "{{ cookiecutter.python_package }}" +version = "0.1" +dynamic = ["dependencies"] + +[tool.setuptools.packages.find] +exclude = ["tests"] + +[project.scripts] +{{ cookiecutter.repo_name }} = "{{ cookiecutter.python_package }}.__main__:main" + +[project.optional-dependencies] +docs = [ + "docutils<0.18.0", + "sphinx~=3.4.3", + "sphinx_rtd_theme==0.5.1", + "nbsphinx==0.8.1", + "nbstripout~=0.4", + "sphinx-autodoc-typehints==1.11.1", + "sphinx_copybutton==0.3.1", + "ipykernel>=5.3, <7.0", + "Jinja2<3.1.0", + "myst-parser~=0.17.2", +] + +[tool.setuptools.dynamic] +dependencies = {file = "requirements.txt"} diff --git a/features/steps/test_starter/{{ cookiecutter.repo_name }}/src/setup.py b/features/steps/test_starter/{{ cookiecutter.repo_name }}/src/setup.py deleted file mode 100644 index af5b101519..0000000000 --- a/features/steps/test_starter/{{ cookiecutter.repo_name }}/src/setup.py +++ /dev/null @@ -1,39 +0,0 @@ -from setuptools import find_packages, setup - -entry_point = ( - "{{ cookiecutter.repo_name }} = {{ cookiecutter.python_package }}.__main__:main" -) - - -# get the dependencies and installs -with open("requirements.txt", encoding="utf-8") as f: - # Make sure we strip all comments and options (e.g "--extra-index-url") - # that arise from a modified pip.conf file that configure global options - # when running kedro build-reqs - requires = [] - for line in f: - req = line.split("#", 1)[0].strip() - if req and not req.startswith("-r"): - requires.append(req) - -setup( - name="{{ cookiecutter.python_package }}", - version="0.1", - packages=find_packages(exclude=["tests"]), - entry_points={"console_scripts": [entry_point]}, - install_requires=requires, - extras_require={ - "docs": [ - "docutils<0.18.0", - "sphinx~=3.4.3", - "sphinx_rtd_theme==0.5.1", - "nbsphinx==0.8.1", - "nbstripout~=0.4", - "sphinx-autodoc-typehints==1.11.1", - "sphinx_copybutton==0.3.1", - "ipykernel>=5.3, <7.0", - "Jinja2<3.1.0", - "myst-parser~=0.17.2", - ] - }, -) diff --git a/kedro/templates/project/{{ cookiecutter.repo_name }}/setup.cfg b/kedro/templates/project/{{ cookiecutter.repo_name }}/.flake8 similarity index 100% rename from kedro/templates/project/{{ cookiecutter.repo_name }}/setup.cfg rename to kedro/templates/project/{{ cookiecutter.repo_name }}/.flake8 diff --git a/kedro/templates/project/{{ cookiecutter.repo_name }}/src/pyproject.toml b/kedro/templates/project/{{ cookiecutter.repo_name }}/src/pyproject.toml new file mode 100644 index 0000000000..ea581e7028 --- /dev/null +++ b/kedro/templates/project/{{ cookiecutter.repo_name }}/src/pyproject.toml @@ -0,0 +1,31 @@ +[build-system] +requires = ["setuptools"] +build-backend = "setuptools.build_meta" + +[project] +name = "{{ cookiecutter.python_package }}" +version = "0.1" +dynamic = ["dependencies"] + +[tool.setuptools.packages.find] +exclude = ["tests"] + +[project.scripts] +{{ cookiecutter.repo_name }} = "{{ cookiecutter.python_package }}.__main__:main" + +[project.optional-dependencies] +docs = [ + "docutils<0.18.0", + "sphinx~=3.4.3", + "sphinx_rtd_theme==0.5.1", + "nbsphinx==0.8.1", + "nbstripout~=0.4", + "sphinx-autodoc-typehints==1.11.1", + "sphinx_copybutton==0.3.1", + "ipykernel>=5.3, <7.0", + "Jinja2<3.1.0", + "myst-parser~=0.17.2", +] + +[tool.setuptools.dynamic] +dependencies = {file = "requirements.txt"} diff --git a/kedro/templates/project/{{ cookiecutter.repo_name }}/src/setup.py b/kedro/templates/project/{{ cookiecutter.repo_name }}/src/setup.py deleted file mode 100644 index 8e62d661f8..0000000000 --- a/kedro/templates/project/{{ cookiecutter.repo_name }}/src/setup.py +++ /dev/null @@ -1,39 +0,0 @@ -from setuptools import find_packages, setup - -entry_point = ( - "{{ cookiecutter.repo_name }} = {{ cookiecutter.python_package }}.__main__:main" -) - - -# get the dependencies and installs -with open("requirements.txt", encoding="utf-8") as f: - # Make sure we strip all comments and options (e.g "--extra-index-url") - # that arise from a modified pip.conf file that configure global options - # when running kedro build-reqs - requires = [] - for line in f: - req = line.split("#", 1)[0].strip() - if req and not req.startswith("--"): - requires.append(req) - -setup( - name="{{ cookiecutter.python_package }}", - version="0.1", - packages=find_packages(exclude=["tests"]), - entry_points={"console_scripts": [entry_point]}, - install_requires=requires, - extras_require={ - "docs": [ - "docutils<0.18.0", - "sphinx~=3.4.3", - "sphinx_rtd_theme==0.5.1", - "nbsphinx==0.8.1", - "nbstripout~=0.4", - "sphinx-autodoc-typehints==1.11.1", - "sphinx_copybutton==0.3.1", - "ipykernel>=5.3, <7.0", - "Jinja2<3.1.0", - "myst-parser~=0.17.2", - ] - }, -) diff --git a/tests/framework/cli/micropkg/test_micropkg_requirements.py b/tests/framework/cli/micropkg/test_micropkg_requirements.py index b0070a1bee..4b424073b7 100644 --- a/tests/framework/cli/micropkg/test_micropkg_requirements.py +++ b/tests/framework/cli/micropkg/test_micropkg_requirements.py @@ -249,8 +249,8 @@ def test_empty_pipeline_requirements_txt( def test_complex_requirements( self, requirement, fake_project_cli, fake_metadata, fake_package_path ): - """Options that are valid in requirements.txt but cannot be packaged using - setup.py.""" + """Options that are valid in requirements.txt but cannot be packaged in + pyproject.toml.""" self.call_pipeline_create(fake_project_cli, fake_metadata) pipeline_requirements_txt = ( fake_package_path / "pipelines" / PIPELINE_NAME / "requirements.txt" From 500a10cbb1d14ca794acf5847b3b222e62cb4e4d Mon Sep 17 00:00:00 2001 From: Sajid Alam <90610031+SajidAlamQB@users.noreply.github.com> Date: Fri, 25 Aug 2023 14:37:02 +0100 Subject: [PATCH 45/70] Add deprecation warnings to top-level use of layer in catalog definition (#2964) * add deprecation Signed-off-by: SajidAlamQB <90610031+SajidAlamQB@users.noreply.github.com> * lint Signed-off-by: SajidAlamQB <90610031+SajidAlamQB@users.noreply.github.com> * Use FutureWarning Signed-off-by: SajidAlamQB <90610031+SajidAlamQB@users.noreply.github.com> * changes based on review Signed-off-by: SajidAlamQB <90610031+SajidAlamQB@users.noreply.github.com> * add link to docs Signed-off-by: SajidAlamQB <90610031+SajidAlamQB@users.noreply.github.com> * Update RELEASE.md Co-authored-by: Jo Stichbury --------- Signed-off-by: SajidAlamQB <90610031+SajidAlamQB@users.noreply.github.com> Co-authored-by: Jo Stichbury Signed-off-by: Jason Hite --- RELEASE.md | 2 ++ kedro/io/data_catalog.py | 11 +++++++++++ 2 files changed, 13 insertions(+) diff --git a/RELEASE.md b/RELEASE.md index 31d84f1928..ebaf570f82 100644 --- a/RELEASE.md +++ b/RELEASE.md @@ -44,6 +44,8 @@ | `AbstractDataset` | `AbstractDataSet` | `kedro.io.core` | | `AbstractVersionedDataset` | `AbstractVersionedDataSet` | `kedro.io.core` | +* Using the `layer` attribute at the top level is deprecated; it will be removed in Kedro version 0.19.0. Please move `layer` inside the `metadata` -> `kedro-viz` attributes. + # Release 0.18.12 ## Major features and improvements diff --git a/kedro/io/data_catalog.py b/kedro/io/data_catalog.py index 156be2130f..4cbe6c0142 100644 --- a/kedro/io/data_catalog.py +++ b/kedro/io/data_catalog.py @@ -293,6 +293,17 @@ class to be loaded is specified with the key ``type`` and their dataset_patterns[ds_name] = ds_config else: + # Check if 'layer' attribute is defined at the top level + if "layer" in ds_config: + import warnings + + warnings.warn( + "Defining the 'layer' attribute at the top level is deprecated " + "and will be removed in Kedro 0.19.0. Please move 'layer' inside the 'metadata' -> " + "'kedro-viz' attributes. See https://docs.kedro.org/en/latest/visualisation/kedro" + "-viz_visualisation.html#visualise-layers for more information.", + FutureWarning, + ) ds_layer = ds_config.pop("layer", None) if ds_layer is not None: layers[ds_layer].add(ds_name) From 5a310aef489d67de2c9b3682de4cabd3031fcf3f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jose=20Nu=C3=B1ez?= <37588143+jmnunezd@users.noreply.github.com> Date: Sat, 26 Aug 2023 11:01:08 -0400 Subject: [PATCH 46/70] Update on credentials.md (#2787) * Update on credentials.md Updating example code in credentials.md to make it usable when copy/pasting. Since as of now it will generate a TypeError Signed-off-by: Jose * adding a more explicit code example, now we directly show the user that project_path is a pathlib.Path object Signed-off-by: Jose * Update docs/source/configuration/credentials.md Co-authored-by: Jo Stichbury --------- Signed-off-by: Jose Co-authored-by: Jo Stichbury Co-authored-by: Nok Lam Chan Signed-off-by: Jason Hite --- docs/source/configuration/credentials.md | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/docs/source/configuration/credentials.md b/docs/source/configuration/credentials.md index 0d91da9cbc..8252c9d76f 100644 --- a/docs/source/configuration/credentials.md +++ b/docs/source/configuration/credentials.md @@ -12,10 +12,13 @@ Credentials configuration can be loaded the same way as any other project config The following examples all use the default `ConfigLoader` class. ```python +from pathlib import Path + from kedro.config import ConfigLoader from kedro.framework.project import settings -conf_path = str(project_path / settings.CONF_SOURCE) +# Substitute with the [root folder for your project](https://docs.kedro.org/en/stable/tutorial/spaceflights_tutorial.html#terminology) +conf_path = str(Path() / settings.CONF_SOURCE) conf_loader = ConfigLoader(conf_source=conf_path) credentials = conf_loader["credentials"] ``` @@ -25,10 +28,12 @@ This loads configuration files from `conf/base` and `conf/local` whose filenames Calling `conf_loader[key]` in the example above throws a `MissingConfigException` error if no configuration files match the given key. But if this is a valid workflow for your application, you can handle it as follows: ```python +from pathlib import Path + from kedro.config import ConfigLoader, MissingConfigException from kedro.framework.project import settings -conf_path = str(project_path / settings.CONF_SOURCE) +conf_path = str(Path() / settings.CONF_SOURCE) conf_loader = ConfigLoader(conf_source=conf_path) try: From 6e2f457d75f85eebd4421252dbd1f9afd3e8443f Mon Sep 17 00:00:00 2001 From: Dmitry Sorokin <40151847+DimedS@users.noreply.github.com> Date: Tue, 29 Aug 2023 10:09:02 +0100 Subject: [PATCH 47/70] Cap pluggy 1.3 release (#2981) Signed-off-by: Dmitry Sorokin Signed-off-by: Jason Hite --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 603c0837e5..4ae5ca4273 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -29,7 +29,7 @@ dependencies = [ "omegaconf~=2.3", "parse~=1.19.0", "pip-tools>=6.5,<8", - "pluggy~=1.0", + "pluggy>=1.0, <1.3", # TODO: Uncap when dropping Python 3.7 support, see https://github.com/kedro-org/kedro/issues/2979 "PyYAML>=4.2, <7.0", "rich>=12.0, <14.0", "rope>=0.21, <2.0", # subject to LGPLv3 license From ed5ed3677889bbc2c00065df9d35dfeec933eb4a Mon Sep 17 00:00:00 2001 From: Ankita Katiyar <110245118+ankatiyar@users.noreply.github.com> Date: Tue, 29 Aug 2023 16:48:01 +0100 Subject: [PATCH 48/70] Make vale linter only run when PR opened or reopened (#2982) Signed-off-by: Ankita Katiyar Signed-off-by: Jason Hite --- .github/workflows/docs-language-linter.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/docs-language-linter.yml b/.github/workflows/docs-language-linter.yml index 02b60b7324..a0f6bca06a 100644 --- a/.github/workflows/docs-language-linter.yml +++ b/.github/workflows/docs-language-linter.yml @@ -1,6 +1,7 @@ name: Language Linter for Kedro Docs on: pull_request: + types: [opened, reopened] paths: - "docs/**" - '**.md' From 53b152ae6b86ad94d352cf5c79efd5a27fb6ad1a Mon Sep 17 00:00:00 2001 From: Nok Lam Chan Date: Tue, 29 Aug 2023 20:45:50 +0100 Subject: [PATCH 49/70] Update merge-gatekeeper.yml (#2960) Signed-off-by: Jason Hite --- .github/workflows/merge-gatekeeper.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/merge-gatekeeper.yml b/.github/workflows/merge-gatekeeper.yml index ff716a75fd..41a8eb46f0 100644 --- a/.github/workflows/merge-gatekeeper.yml +++ b/.github/workflows/merge-gatekeeper.yml @@ -22,6 +22,6 @@ jobs: uses: upsidr/merge-gatekeeper@v1 with: token: ${{ secrets.GITHUB_TOKEN }} - timeout: 1800 + timeout: 3600 interval: 30 ignored: 'ci/circleci: win_e2e_tests-3.7,ci/circleci: win_pip_compile-3.9,ci/circleci: win_e2e_tests-3.9,ci/circleci: win_pip_compile-3.8,ci/circleci: lint-3.7,ci/circleci: win_pip_compile-3.7,ci/circleci: pip_compile-3.7,ci/circleci: e2e_tests-3.7,ci/circleci: win_unit_tests-3.7,ci/circleci: win_unit_tests-3.9,ci/circleci: e2e_tests-3.8,ci/circleci: win_unit_tests-3.10,ci/circleci: win_pip_compile-3.10,ci/circleci: win_unit_tests-3.8,ci/circleci: e2e_tests-3.9,ci/circleci: unit_tests-3.10,ci/circleci: unit_tests-3.8,ci/circleci: e2e_tests-3.10,ci/circleci: lint-3.8,ci/circleci: unit_tests-3.9,ci/circleci: unit_tests-3.7,ci/circleci: win_e2e_tests-3.10,ci/circleci: pip_compile-3.8,ci/circleci: pip_compile-3.10,ci/circleci: win_e2e_tests-3.8,ci/circleci: lint-3.9,ci/circleci: pip_compile-3.9,ci/circleci: lint-3.10,build_code,ci/circleci: check-updated-files,regular' From c9841e2f847c8e46f79e3a9d983f5467dc1b5842 Mon Sep 17 00:00:00 2001 From: "L. R. Couto" <57910428+lrcouto@users.noreply.github.com> Date: Tue, 29 Aug 2023 18:06:49 -0300 Subject: [PATCH 50/70] Configure starters to use OmegaConfigLoader (#2974) * Change starters to use OmegaConfigLoader Signed-off-by: lrcouto * Fix linter error Signed-off-by: lrcouto * Suppress import outside toplevel linting for starters template Signed-off-by: L. R. Couto * Fix linter error Signed-off-by: lrcouto * Add changes to release notes Signed-off-by: lrcouto --------- Signed-off-by: lrcouto Signed-off-by: L. R. Couto Signed-off-by: Jason Hite --- RELEASE.md | 2 +- .../src/{{ cookiecutter.python_package }}/settings.py | 5 +++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/RELEASE.md b/RELEASE.md index ebaf570f82..159bf9c420 100644 --- a/RELEASE.md +++ b/RELEASE.md @@ -15,7 +15,7 @@ * Added support for Python 3.11. This includes tackling challenges like dependency pinning and test adjustments to ensure a smooth experience. Detailed migration tips are provided below for further context. * Added `kedro catalog resolve` CLI command that resolves dataset factories in the catalog with any explicit entries in the project pipeline. * Added support for global variables to `OmegaConfigLoader`. -* Allowed using of custom cookiecutter templates for creating pipelines with `--template` flag for `kedro pipeline create` +* Change Kedro starters to use `OmegaConfigLoader`. ## Bug fixes and other changes diff --git a/kedro/templates/project/{{ cookiecutter.repo_name }}/src/{{ cookiecutter.python_package }}/settings.py b/kedro/templates/project/{{ cookiecutter.repo_name }}/src/{{ cookiecutter.python_package }}/settings.py index 86a92b1c80..882ffca8a4 100644 --- a/kedro/templates/project/{{ cookiecutter.repo_name }}/src/{{ cookiecutter.python_package }}/settings.py +++ b/kedro/templates/project/{{ cookiecutter.repo_name }}/src/{{ cookiecutter.python_package }}/settings.py @@ -22,8 +22,9 @@ # CONF_SOURCE = "conf" # Class that manages how configuration is loaded. -# from kedro.config import OmegaConfigLoader -# CONFIG_LOADER_CLASS = OmegaConfigLoader +from kedro.config import OmegaConfigLoader # noqa: import-outside-toplevel + +CONFIG_LOADER_CLASS = OmegaConfigLoader # Keyword arguments to pass to the `CONFIG_LOADER_CLASS` constructor. # CONFIG_LOADER_ARGS = { # "config_patterns": { From ca5108b842cc48f349178a4055527dc803bfd9e7 Mon Sep 17 00:00:00 2001 From: Nok Lam Chan Date: Wed, 30 Aug 2023 02:46:40 +0100 Subject: [PATCH 51/70] Stop OmegaConfigLoader from reading config from hidden directory like `ipynb_checkpoints` (#2977) * Check plugins implement valid hooks Signed-off-by: Nok Chan * Add release note Signed-off-by: Nok Chan * Staging work - add custom functions to check hidden folder and files. Tests still failing Signed-off-by: Nok Chan * Fix test - checkpoints should use the same environment Signed-off-by: Nok Chan * Revert "Check plugins implement valid hooks" This reverts commit f10bede0401b80e3385e93458995835a2db721c9. * Update RELEASE.md Co-authored-by: Ankita Katiyar <110245118+ankatiyar@users.noreply.github.com> * fix lint Signed-off-by: Nok --------- Signed-off-by: Nok Chan Signed-off-by: Nok Co-authored-by: Ankita Katiyar <110245118+ankatiyar@users.noreply.github.com> Signed-off-by: Jason Hite --- RELEASE.md | 1 + kedro/config/omegaconf_config.py | 19 +++++++++---- tests/config/test_omegaconf_config.py | 40 +++++++++++++++++++++++++++ 3 files changed, 55 insertions(+), 5 deletions(-) diff --git a/RELEASE.md b/RELEASE.md index 159bf9c420..8e91fada51 100644 --- a/RELEASE.md +++ b/RELEASE.md @@ -22,6 +22,7 @@ * Updated `kedro pipeline create` and `kedro catalog create` to use new `/conf` file structure. * Converted `setup.py` in default template to `pyproject.toml` and moved flake8 configuration to dedicated file `.flake8`. +* Updated `OmegaConfigLoader` to ignore config from hidden directories like `.ipynb_checkpoints`. ## Documentation changes * Revised the `data` section to restructure beginner and advanced pages about the Data Catalog and datasets. diff --git a/kedro/config/omegaconf_config.py b/kedro/config/omegaconf_config.py index 0d7b5d740c..5a48d3cf28 100644 --- a/kedro/config/omegaconf_config.py +++ b/kedro/config/omegaconf_config.py @@ -250,11 +250,12 @@ def load_and_merge_dir_config( # noqa: too-many-arguments f"or is not a valid directory: {conf_path}" ) - paths = [ - Path(each) - for pattern in patterns - for each in self._fs.glob(Path(f"{str(conf_path)}/{pattern}").as_posix()) - ] + paths = [] + for pattern in patterns: + for each in self._fs.glob(Path(f"{str(conf_path)}/{pattern}").as_posix()): + if not self._is_hidden(each): + paths.append(Path(each)) + deduplicated_paths = set(paths) config_files_filtered = [ path for path in deduplicated_paths if self._is_valid_config_path(path) @@ -392,3 +393,11 @@ def _resolve_environment_variables(config: dict[str, Any]) -> None: OmegaConf.clear_resolver("oc.env") else: OmegaConf.resolve(config) + + def _is_hidden(self, path: str): + """Check if path contains any hidden directory or is a hidden file""" + path = Path(path).resolve().as_posix() + parts = path.split(self._fs.sep) # filesystem specific separator + HIDDEN = "." + # Check if any component (folder or file) starts with a dot (.) + return any(part.startswith(HIDDEN) for part in parts) diff --git a/tests/config/test_omegaconf_config.py b/tests/config/test_omegaconf_config.py index 948aac7372..4713d0da14 100644 --- a/tests/config/test_omegaconf_config.py +++ b/tests/config/test_omegaconf_config.py @@ -797,3 +797,43 @@ def test_bad_globals_underscore(self, tmp_path): match=r"Keys starting with '_' are not supported for globals.", ): conf["parameters"]["param2"] + + @pytest.mark.parametrize( + "hidden_path", ["/User/.hidden/dummy.yml", "/User/dummy/.hidden.yml"] + ) + def test_is_hidden_config(self, tmp_path, hidden_path): + conf = OmegaConfigLoader(str(tmp_path)) + assert conf._is_hidden(hidden_path) + + @pytest.mark.parametrize( + "hidden_path", + [ + "/User/conf/base/catalog.yml", + "/User/conf/local/catalog/data_science.yml", + "/User/notebooks/../conf/base/catalog", + ], + ) + def test_not_hidden_config(self, tmp_path, hidden_path): + conf = OmegaConfigLoader(str(tmp_path)) + assert not conf._is_hidden(hidden_path) + + def test_ignore_ipynb_checkpoints(self, tmp_path, mocker): + conf = OmegaConfigLoader(str(tmp_path), default_run_env=_BASE_ENV) + base_path = tmp_path / _BASE_ENV / "parameters.yml" + checkpoints_path = ( + tmp_path / _BASE_ENV / ".ipynb_checkpoints" / "parameters.yml" + ) + + base_config = {"param1": "dummy"} + checkpoints_config = {"param1": "dummy"} + + _write_yaml(base_path, base_config) + _write_yaml(checkpoints_path, checkpoints_config) + + # read successfully + conf["parameters"] + + mocker.patch.object(conf, "_is_hidden", return_value=False) # + with pytest.raises(ValueError, match="Duplicate keys found in"): + # fail because of reading the hidden files and get duplicate keys + conf["parameters"] From 71c11189328ea2ee0d3167653ecf09ac2aa9ef5d Mon Sep 17 00:00:00 2001 From: Merel Theisen <49397448+merelcht@users.noreply.github.com> Date: Wed, 30 Aug 2023 12:27:31 +0100 Subject: [PATCH 52/70] Add migration steps for `TemplatedConfigLoader` to `OmegaConfigLoader` (#2904) Signed-off-by: Merel Theisen Signed-off-by: Jason Hite --- .../configuration/config_loader_migration.md | 174 ++++++++++++++++++ 1 file changed, 174 insertions(+) diff --git a/docs/source/configuration/config_loader_migration.md b/docs/source/configuration/config_loader_migration.md index f27b244afe..21c25f9093 100644 --- a/docs/source/configuration/config_loader_migration.md +++ b/docs/source/configuration/config_loader_migration.md @@ -60,3 +60,177 @@ In this example, `"catalog"` is the key to the default catalog patterns specifie For error and exception handling, most errors are the same. Those you need to be aware of that are different between the original `ConfigLoader` and `OmegaConfigLoader` are as follows: * `OmegaConfigLoader` throws a `MissingConfigException` when configuration paths don't exist, rather than the `ValueError` used in `ConfigLoader`. * In `OmegaConfigLoader`, if there is bad syntax in your configuration files, it will trigger a `ParserError` instead of a `BadConfigException` used in `ConfigLoader`. + +## [`TemplatedConfigLoader`](/kedro.config.TemplatedConfigLoader) to [`OmegaConfigLoader`](/kedro.config.OmegaConfigLoader) + +### 1. Install the required library +The [`OmegaConfigLoader`](advanced_configuration.md#omegaconfigloader) was introduced in Kedro `0.18.5` and is based on [OmegaConf](https://omegaconf.readthedocs.io/). Features that replace `TemplatedConfigLoader` functionality have been released in later versions, so we recommend users +to install at least Kedro version `0.18.13` to properly replace the `TemplatedConfigLoader` with `OmegaConfigLoader`. +You can install both this Kedro version and `omegaconf` using `pip`: + +```bash +pip install "kedro>=0.18.13, <0.19.0" +``` +This would be the minimum required Kedro version which includes `omegaconf` as a dependency and the necessary functionality to replace `TemplatedConfigLoader`. +Or you can run: +```bash +pip install -U kedro +``` + +This command installs the most recent version of Kedro which also includes `omegaconf` as a dependency. + +### 2. Use the `OmegaConfigLoader` +To use `OmegaConfigLoader` in your project, set the `CONFIG_LOADER_CLASS` constant in your [`src//settings.py`](../kedro_project_setup/settings.md): + +```diff ++ from kedro.config import OmegaConfigLoader # new import + ++ CONFIG_LOADER_CLASS = OmegaConfigLoader +``` + +### 3. Import statements +Replace the import statement for `TemplatedConfigLoader` with the one for `OmegaConfigLoader`: + +```diff +- from kedro.config import TemplatedConfigLoader ++ from kedro.config import OmegaConfigLoader +``` + +### 4. File format support +`OmegaConfigLoader` supports only `yaml` and `json` file formats. Make sure that all your configuration files are in one of these formats. If you were using other formats with `TemplatedConfigLoader`, convert them to `yaml` or `json`. + +### 5. Load configuration +The method to load the configuration using `OmegaConfigLoader` differs slightly from that used by `TemplatedConfigLoader`, which allowed users to access configuration through the `.get()` method and required patterns as argument. +When you migrate to use `OmegaConfigLoader` it requires you to fetch configuration through a configuration key that points to [configuration patterns specified in the loader class](configuration_basics.md#configuration-patterns) or [provided in the `CONFIG_LOADER_ARGS`](advanced_configuration.md#how-to-change-which-configuration-files-are-loaded) in `settings.py`. + +```diff +- conf_path = str(project_path / settings.CONF_SOURCE) +- conf_loader = TemplatedConfigLoader(conf_source=conf_path, env="local") +- catalog = conf_loader.get("catalog*") + ++ conf_path = str(project_path / settings.CONF_SOURCE) ++ config_loader = OmegaConfigLoader(conf_source=conf_path, env="local") ++ catalog = config_loader["catalog"] # note the key accessor syntax +``` + +In this example, the `"catalog"` key points to the default catalog patterns specified in the `OmegaConfigLoader` class. + +### 6. Templating of values +Templating of values is done through native [variable interpolation in `OmegaConfigLoader`](advanced_configuration.md#how-to-do-templating-with-the-omegaconfigloader). Where in `TemplatedConfigLoader` it was necessary to +provide the template values in a `globals` file or dictionary, in `OmegaConfigLoader` you can provide these values within the same file that has the placeholders or a file that has a name that follows [the same config pattern specified](configuration_basics.md#configuration-patterns). +The variable interpolation is scoped to a specific configuration type and environment. If you want to share templated values across configuration types and environments, [you will need to use globals](#7-globals). + +Suppose you are migrating a templated **catalog** file from using `TemplatedConfigLoader` to `OmegaConfigLoader` you would do the following: +1. Rename `conf/base/globals.yml` to match the patterns specified for catalog (`["catalog*", "catalog*/**", "**/catalog*"]`), for example `conf/base/catalog_globals.yml` +2. Add an underscore `_` to any catalog template values. This is needed because of how catalog entries are validated. + +```diff +- bucket_name: "my_s3_bucket" ++ _bucket_name: "my_s3_bucket" # kedro requires `_` to mark templatable keys +- key_prefix: "my/key/prefix/" ++ _key_prefix: "my/key/prefix/" + +- datasets: ++ _datasets: + csv: "pandas.CSVDataSet" + spark: "spark.SparkDataSet" + +``` + +3. Update `catalog.yml` with the underscores `_` at the beginning of the templated value names. +```diff +raw_boat_data: +- type: "${datasets.spark}" ++ type: "${_datasets.spark}" +- filepath: "s3a://${bucket_name}/${key_prefix}/raw/boats.csv" ++ filepath: "s3a://${_bucket_name}/${_key_prefix}/raw/boats.csv" + file_format: parquet + +raw_car_data: +- type: "${datasets.csv}" ++ type: "${_datasets.csv}" +- filepath: "s3://${bucket_name}/data/${key_prefix}/raw/cars.csv" ++ filepath: "s3://${_bucket_name}/data/${_key_prefix}/raw/cars.csv" +``` + +#### Providing default values for templates via `oc.select` +To provide a default for any template values you have to use [the omegaconf `oc.select` resolver](https://omegaconf.readthedocs.io/en/latest/custom_resolvers.html#oc-select). + +```diff +boats: + users: + - fred +- - "${write_only_user|ron}" ++ - "${oc.select:write_only_user,ron}" +``` + +### 7. Globals +If you want to share variables across configuration types, for example parameters and catalog, and environments you need to use [the custom globals resolver with the `OmegaConfigLoader`](advanced_configuration.md#how-to-use-global-variables-with-the-omegaconfigloader). +The `OmegaConfigLoader` requires global values to be provided in a `globals.yml` file. Note that using a `globals_dict` to provide globals is not supported with `OmegaConfigLoader`. The following section explains the differences between using globals with `TemplatedConfigLoader` and the `OmegaConfigLoader`. + +Let's assume your project contains a `conf/base/globals.yml` file with the following contents: + +```yaml +bucket_name: "my_s3_bucket" +key_prefix: "my/key/prefix/" + +datasets: + csv: "pandas.CSVDataSet" + spark: "spark.SparkDataSet" + +folders: + raw: "01_raw" + int: "02_intermediate" + pri: "03_primary" + fea: "04_feature" +``` + +You no longer need to set `CONFIG_LOADER_ARGS` variable in [`src//settings.py`](../kedro_project_setup/settings.md) to find this `globals.yml` file, because the +`OmegaConfigLoader` is configured to pick up files named `globals.yml` by default. + +```diff +- CONFIG_LOADER_ARGS = {"globals_pattern": "*globals.yml"} +``` + +The globals templating in your catalog configuration will need to be updated to use the globals resolver as follows: + +```diff +raw_boat_data: +- type: "${datasets.spark}" ++ type: "${globals:datasets.spark}" # nested paths into global dict are allowed +- filepath: "s3a://${bucket_name}/${key_prefix}/${folders.raw}/boats.csv" ++ filepath: "s3a://${globals:bucket_name}/${globals:key_prefix}/${globals:folders.raw}/boats.csv" + file_format: parquet + +raw_car_data: +- type: "${datasets.csv}" ++ type: "${globals:datasets.csv}" +- filepath: "s3://${bucket_name}/data/${key_prefix}/${folders.raw}/${filename|cars.csv}" # default to 'cars.csv' if the 'filename' key is not found in the global dict ++ filepath: "s3://${globals:bucket_name}/data/${globals:key_prefix}/${globals:folders.raw}/${globals:filename,'cars.csv'}" # default to 'cars.csv' if the 'filename' key is not found in the global dict +``` + +### 8. Deprecation of Jinja2 +`OmegaConfigLoader` does not support Jinja2 syntax in configuration. However, users can achieve similar functionality with the `OmegaConfigLoader` in combination with [dataset factories](../data/kedro_dataset_factories.md). +If you take the example from [the `TemplatedConfigLoader` with Jinja2 documentation](advanced_configuration.md#how-to-use-jinja2-syntax-in-configuration) you can rewrite your configuration as follows to work with `OmegaConfigLoader`: + +```diff +# catalog.yml +- {% for speed in ['fast', 'slow'] %} +- {{ speed }}-trains: ++ "{speed}-trains": + type: MemoryDataSet + +- {{ speed }}-cars: ++ "{speed}-cars": + type: pandas.CSVDataSet +- filepath: s3://${bucket_name}/{{ speed }}-cars.csv ++ filepath: s3://${bucket_name}/{speed}-cars.csv + save_args: + index: true + +- {% endfor %} +``` + +### 9. Exception handling +For error and exception handling, most errors are the same. Those you need to be aware of that are different between the original `TemplatedConfigLoader` and `OmegaConfigLoader` are as follows: +* For missing template values `OmegaConfigLoader` throws `omegaconf.errors.InterpolationKeyError`. From 9896691db18b516265e7f4774c15335aedbaa919 Mon Sep 17 00:00:00 2001 From: Ankita Katiyar <110245118+ankatiyar@users.noreply.github.com> Date: Wed, 30 Aug 2023 13:45:47 +0100 Subject: [PATCH 53/70] Introduce a sentinel value _NO_VALUE to improve Global resolvers to support defaults `0` or `None` (#2976) * Add None support to globals Signed-off-by: Ankita Katiyar * Add warning when default value is used Signed-off-by: Ankita Katiyar * Check keys Signed-off-by: Ankita Katiyar * Nok's suggestions Signed-off-by: Ankita Katiyar * Create the test to check the non-existing keys Signed-off-by: Nok * add more tests to catch case when global key is not a dict Signed-off-by: Nok * Fix the null test Signed-off-by: Nok * Introduce sentinel value _NO_VALUE Signed-off-by: Nok * rename test Signed-off-by: Nok * Improve error mesasge and raise InterpolationResolutionError when key does not exist and no default Signed-off-by: Nok * Fix non exist default test Signed-off-by: Nok * Fix test Signed-off-by: Nok * Use omegaconf to replace the custom resolving logic Signed-off-by: Nok * uncommented some tests Signed-off-by: Nok * Remove dead code Signed-off-by: Ankita Katiyar * Update error message Signed-off-by: Ankita Katiyar --------- Signed-off-by: Ankita Katiyar Signed-off-by: Nok Co-authored-by: Nok Signed-off-by: Jason Hite --- kedro/config/omegaconf_config.py | 31 +++++++-------- tests/config/test_omegaconf_config.py | 54 ++++++++++++++++++++++++--- 2 files changed, 62 insertions(+), 23 deletions(-) diff --git a/kedro/config/omegaconf_config.py b/kedro/config/omegaconf_config.py index 5a48d3cf28..29f7b65389 100644 --- a/kedro/config/omegaconf_config.py +++ b/kedro/config/omegaconf_config.py @@ -20,6 +20,8 @@ _config_logger = logging.getLogger(__name__) +_NO_VALUE = object() + class OmegaConfigLoader(AbstractConfigLoader): """Recursively scan directories (config paths) contained in ``conf_source`` for @@ -316,31 +318,26 @@ def _register_globals_resolver(self): """Register the globals resolver""" OmegaConf.register_new_resolver( "globals", - lambda variable, default_value=None: self._get_globals_value( - variable, default_value - ), + self._get_globals_value, replace=True, ) - def _get_globals_value(self, variable, default_value): + def _get_globals_value(self, variable, default_value=_NO_VALUE): """Return the globals values to the resolver""" if variable.startswith("_"): raise InterpolationResolutionError( "Keys starting with '_' are not supported for globals." ) - keys = variable.split(".") - value = self["globals"] - for k in keys: - value = value.get(k) - if not value: - if default_value: - _config_logger.debug( - f"Using the default value for the global variable {variable}." - ) - return default_value - msg = f"Globals key '{variable}' not found and no default value provided. " - raise InterpolationResolutionError(msg) - return value + global_omegaconf = OmegaConf.create(self["globals"]) + interpolated_value = OmegaConf.select( + global_omegaconf, variable, default=default_value + ) + if interpolated_value != _NO_VALUE: + return interpolated_value + else: + raise InterpolationResolutionError( + f"Globals key '{variable}' not found and no default value provided." + ) @staticmethod def _register_new_resolvers(resolvers: dict[str, Callable]): diff --git a/tests/config/test_omegaconf_config.py b/tests/config/test_omegaconf_config.py index 4713d0da14..824508b5d0 100644 --- a/tests/config/test_omegaconf_config.py +++ b/tests/config/test_omegaconf_config.py @@ -676,7 +676,7 @@ def test_custom_resolvers(self, tmp_path): def test_globals(self, tmp_path): globals_params = tmp_path / _BASE_ENV / "globals.yml" globals_config = { - "x": 34, + "x": 0, } _write_yaml(globals_params, globals_config) conf = OmegaConfigLoader(tmp_path, default_run_env="") @@ -704,7 +704,6 @@ def test_globals_resolution(self, tmp_path): _write_yaml(globals_params, globals_config) _write_yaml(base_catalog, catalog_config) conf = OmegaConfigLoader(tmp_path, default_run_env="") - assert OmegaConf.has_resolver("globals") # Globals are resolved correctly in parameter assert conf["parameters"]["my_param"] == globals_config["x"] # The default value is used if the key does not exist @@ -760,25 +759,68 @@ def test_globals_across_env(self, tmp_path): # Base global value is accessible to local params assert conf["parameters"]["param2"] == base_globals_config["x"] - def test_bad_globals(self, tmp_path): + def test_globals_default(self, tmp_path): + base_params = tmp_path / _BASE_ENV / "parameters.yml" + base_globals = tmp_path / _BASE_ENV / "globals.yml" + base_param_config = { + "int": "${globals:x.NOT_EXIST, 1}", + "str": "${globals: x.NOT_EXIST, '2'}", + "dummy": "${globals: x.DUMMY.DUMMY, '2'}", + } + base_globals_config = {"x": {"DUMMY": 3}} + _write_yaml(base_params, base_param_config) + _write_yaml(base_globals, base_globals_config) + conf = OmegaConfigLoader(tmp_path, default_run_env="") + # Default value is being used as int + assert conf["parameters"]["int"] == 1 + # Default value is being used as str + assert conf["parameters"]["str"] == "2" + # Test when x.DUMMY is not a dictionary it should still work + assert conf["parameters"]["dummy"] == "2" + + def test_globals_default_none(self, tmp_path): base_params = tmp_path / _BASE_ENV / "parameters.yml" base_globals = tmp_path / _BASE_ENV / "globals.yml" base_param_config = { - "param1": "${globals:x.y}", + "zero": "${globals: x.NOT_EXIST, 0}", + "null": "${globals: x.NOT_EXIST, null}", + "null2": "${globals: x.y}", } base_globals_config = { "x": { "z": 23, + "y": None, }, } _write_yaml(base_params, base_param_config) _write_yaml(base_globals, base_globals_config) conf = OmegaConfigLoader(tmp_path, default_run_env="") + # Default value can be 0 or null + assert conf["parameters"]["zero"] == 0 + assert conf["parameters"]["null"] is None + # Global value is null + assert conf["parameters"]["null2"] is None + + def test_globals_missing_default(self, tmp_path): + base_params = tmp_path / _BASE_ENV / "parameters.yml" + globals_params = tmp_path / _BASE_ENV / "globals.yml" + param_config = { + "NOT_OK": "${globals:nested.NOT_EXIST}", + } + globals_config = { + "nested": { + "y": 42, + }, + } + _write_yaml(base_params, param_config) + _write_yaml(globals_params, globals_config) + conf = OmegaConfigLoader(tmp_path, default_run_env="") + with pytest.raises( InterpolationResolutionError, - match=r"Globals key 'x.y' not found and no default value provided.", + match="Globals key 'nested.NOT_EXIST' not found and no default value provided.", ): - conf["parameters"]["param1"] + conf["parameters"]["NOT_OK"] def test_bad_globals_underscore(self, tmp_path): base_params = tmp_path / _BASE_ENV / "parameters.yml" From 8c6c371e0cee9ceb67b6a747a22a1d10925aba30 Mon Sep 17 00:00:00 2001 From: Ankita Katiyar <110245118+ankatiyar@users.noreply.github.com> Date: Thu, 31 Aug 2023 10:19:39 +0100 Subject: [PATCH 54/70] Release 0.18.13 (#2988) Signed-off-by: Jason Hite --- RELEASE.md | 32 +++++++++++++------ docs/source/development/commands_reference.md | 2 +- kedro/__init__.py | 2 +- setup.py | 2 +- 4 files changed, 25 insertions(+), 13 deletions(-) diff --git a/RELEASE.md b/RELEASE.md index 8e91fada51..8d5cb62415 100644 --- a/RELEASE.md +++ b/RELEASE.md @@ -7,28 +7,37 @@ ## Breaking changes to the API ## Migration guide from Kedro 0.18.* to 0.19.* +# Upcoming Release 0.18.14 -# Upcoming Release 0.18.13 +## Major features and improvements +## Bug fixes and other changes +## Documentation changes +## Breaking changes to the API +## Upcoming deprecations for Kedro 0.19.0 + +# Release 0.18.13 ## Major features and improvements -* Allowed registering of custom resolvers to `OmegaConfigLoader` through `CONFIG_LOADER_ARGS`. * Added support for Python 3.11. This includes tackling challenges like dependency pinning and test adjustments to ensure a smooth experience. Detailed migration tips are provided below for further context. +* Added new `OmegaConfigLoader` features: + * Allowed registering of custom resolvers to `OmegaConfigLoader` through `CONFIG_LOADER_ARGS`. + * Added support for global variables to `OmegaConfigLoader`. * Added `kedro catalog resolve` CLI command that resolves dataset factories in the catalog with any explicit entries in the project pipeline. -* Added support for global variables to `OmegaConfigLoader`. -* Change Kedro starters to use `OmegaConfigLoader`. - +* Implemented a flat `conf/` structure for modular pipelines, and accordingly, updated the `kedro pipeline create` and `kedro catalog create` command. +* Updated new Kedro project template and Kedro starters: + * Change Kedro starters and new Kedro projects to use `OmegaConfigLoader`. + * Converted `setup.py` in new Kedro project template and Kedro starters to `pyproject.toml` and moved flake8 configuration + to dedicated file `.flake8`. + * Updated the spaceflights starter to use the new flat `conf/` structure. ## Bug fixes and other changes -* Updated `kedro pipeline create` and `kedro catalog create` to use new `/conf` file structure. -* Converted `setup.py` in default template to `pyproject.toml` and moved flake8 configuration - to dedicated file `.flake8`. * Updated `OmegaConfigLoader` to ignore config from hidden directories like `.ipynb_checkpoints`. ## Documentation changes * Revised the `data` section to restructure beginner and advanced pages about the Data Catalog and datasets. * Moved contributor documentation to the [GitHub wiki](https://github.com/kedro-org/kedro/wiki/Contribute-to-Kedro). -* Update example of using generator functions in nodes. -* Added migration guide from the `ConfigLoader` to the `OmegaConfigLoader`. The `ConfigLoader` is deprecated and will be removed in the `0.19.0` release. +* Updated example of using generator functions in nodes. +* Added migration guide from the `ConfigLoader` and the `TemplatedConfigLoader` to the `OmegaConfigLoader`. The `ConfigLoader` and the `TemplatedConfigLoader` are deprecated and will be removed in the `0.19.0` release. ## Migration Tips for Python 3.11: * PyTables on Windows: Users on Windows with Python >=3.8 should note we've pinned `pytables` to `3.8.0` due to compatibility issues. @@ -47,6 +56,9 @@ * Using the `layer` attribute at the top level is deprecated; it will be removed in Kedro version 0.19.0. Please move `layer` inside the `metadata` -> `kedro-viz` attributes. +## Community contributions +Thanks to [Laíza Milena Scheid Parizotto](https://github.com/laizaparizotto) and [Jonathan Cohen](https://github.com/JonathanDCohen). + # Release 0.18.12 ## Major features and improvements diff --git a/docs/source/development/commands_reference.md b/docs/source/development/commands_reference.md index ded8da9dcc..5403f1b563 100644 --- a/docs/source/development/commands_reference.md +++ b/docs/source/development/commands_reference.md @@ -116,7 +116,7 @@ Returns output similar to the following, depending on the version of Kedro used | |/ / _ \/ _` | '__/ _ \ | < __/ (_| | | | (_) | |_|\_\___|\__,_|_| \___/ -v0.18.12 +v0.18.13 Kedro is a Python framework for creating reproducible, maintainable diff --git a/kedro/__init__.py b/kedro/__init__.py index 7a7db37ae2..c2f7185d6e 100644 --- a/kedro/__init__.py +++ b/kedro/__init__.py @@ -6,7 +6,7 @@ import sys import warnings -__version__ = "0.18.12" +__version__ = "0.18.13" class KedroPythonVersionWarning(UserWarning): diff --git a/setup.py b/setup.py index 8d94b9c965..139501feb1 100644 --- a/setup.py +++ b/setup.py @@ -97,7 +97,7 @@ def _collect_requirements(requires): "sphinxcontrib-mermaid~=0.7.1", "myst-parser~=1.0.0", "Jinja2<3.1.0", - "kedro-datasets[all]~=1.5.3", + "kedro-datasets[all]~=1.6.0", ], "geopandas": _collect_requirements(geopandas_require), "matplotlib": _collect_requirements(matplotlib_require), From cb9a0cde5fbe6fa316f0844f49e254f0eee81960 Mon Sep 17 00:00:00 2001 From: "L. R. Couto" <57910428+lrcouto@users.noreply.github.com> Date: Fri, 1 Sep 2023 11:02:55 -0300 Subject: [PATCH 55/70] Fix docstrings on kedro/extras/datasets (#2995) * Fix docstrings on kedro/extras/datasets Signed-off-by: lrcouto * Fix formatting error Signed-off-by: lrcouto * Explicitly define code block Signed-off-by: lrcouto * Fix empty line under code block Signed-off-by: lrcouto * Fix formatting error Signed-off-by: lrcouto * Fix broken link Signed-off-by: lrcouto * Bump kedro-datasets version Signed-off-by: Ankita Katiyar * Update links in partitioned and incremental datasets Signed-off-by: Ankita Katiyar * Update links in partitioned and incremental datasets Signed-off-by: Ankita Katiyar * Update links in partitioned dataset Signed-off-by: Ankita Katiyar * Update links in partitioned dataset Signed-off-by: Ankita Katiyar * Add polars.GenericDataSet to .rst Signed-off-by: Ankita Katiyar --------- Signed-off-by: lrcouto Signed-off-by: Ankita Katiyar Co-authored-by: Ankita Katiyar Signed-off-by: Jason Hite --- docs/source/kedro_datasets.rst | 1 + kedro/extras/datasets/api/api_dataset.py | 5 +++-- kedro/extras/datasets/dask/parquet_dataset.py | 5 +++-- kedro/extras/datasets/json/json_dataset.py | 4 ++-- .../datasets/matplotlib/matplotlib_writer.py | 4 ++-- kedro/extras/datasets/pandas/csv_dataset.py | 4 ++-- kedro/extras/datasets/pandas/excel_dataset.py | 8 ++++---- kedro/extras/datasets/pandas/feather_dataset.py | 4 ++-- kedro/extras/datasets/pandas/gbq_dataset.py | 4 ++-- kedro/extras/datasets/pandas/generic_dataset.py | 4 ++-- kedro/extras/datasets/pandas/hdf_dataset.py | 5 +++-- kedro/extras/datasets/pandas/json_dataset.py | 5 +++-- kedro/extras/datasets/pandas/parquet_dataset.py | 5 +++-- kedro/extras/datasets/pandas/sql_dataset.py | 11 +++++++---- kedro/extras/datasets/pandas/xml_dataset.py | 2 +- kedro/extras/datasets/pickle/pickle_dataset.py | 5 +++-- kedro/extras/datasets/pillow/image_dataset.py | 2 +- kedro/extras/datasets/plotly/json_dataset.py | 5 +++-- kedro/extras/datasets/plotly/plotly_dataset.py | 5 +++-- kedro/extras/datasets/redis/redis_dataset.py | 5 +++-- kedro/extras/datasets/spark/deltatable_dataset.py | 5 +++-- kedro/extras/datasets/spark/spark_dataset.py | 5 +++-- kedro/extras/datasets/spark/spark_hive_dataset.py | 5 +++-- kedro/extras/datasets/spark/spark_jdbc_dataset.py | 5 +++-- .../extras/datasets/svmlight/svmlight_dataset.py | 5 +++-- .../tensorflow/tensorflow_model_dataset.py | 5 +++-- kedro/extras/datasets/text/text_dataset.py | 5 +++-- kedro/extras/datasets/tracking/json_dataset.py | 5 +++-- kedro/extras/datasets/tracking/metrics_dataset.py | 5 +++-- kedro/extras/datasets/video/video_dataset.py | 5 +++-- kedro/extras/datasets/yaml/yaml_dataset.py | 5 +++-- kedro/io/partitioned_dataset.py | 15 ++++++++------- setup.py | 2 +- 33 files changed, 95 insertions(+), 70 deletions(-) diff --git a/docs/source/kedro_datasets.rst b/docs/source/kedro_datasets.rst index 18bff88086..d1e06429c3 100644 --- a/docs/source/kedro_datasets.rst +++ b/docs/source/kedro_datasets.rst @@ -41,6 +41,7 @@ kedro_datasets kedro_datasets.plotly.JSONDataSet kedro_datasets.plotly.PlotlyDataSet kedro_datasets.polars.CSVDataSet + kedro_datasets.polars.GenericDataSet kedro_datasets.redis.PickleDataSet kedro_datasets.snowflake.SnowparkTableDataSet kedro_datasets.spark.DeltaTableDataSet diff --git a/kedro/extras/datasets/api/api_dataset.py b/kedro/extras/datasets/api/api_dataset.py index f288c96814..0e79f9aad2 100644 --- a/kedro/extras/datasets/api/api_dataset.py +++ b/kedro/extras/datasets/api/api_dataset.py @@ -19,7 +19,8 @@ class APIDataSet(AbstractDataset[None, requests.Response]): Example usage for the `YAML API `_: + data_catalog_yaml_examples.html>`_: + .. code-block:: yaml @@ -36,7 +37,7 @@ class APIDataSet(AbstractDataset[None, requests.Response]): Example usage for the `Python API `_: + advanced_data_catalog_usage.html>`_: :: >>> from kedro.extras.datasets.api import APIDataSet diff --git a/kedro/extras/datasets/dask/parquet_dataset.py b/kedro/extras/datasets/dask/parquet_dataset.py index 23dc7a701b..21fcfe25b0 100644 --- a/kedro/extras/datasets/dask/parquet_dataset.py +++ b/kedro/extras/datasets/dask/parquet_dataset.py @@ -22,7 +22,7 @@ class ParquetDataSet(AbstractDataset[dd.DataFrame, dd.DataFrame]): Example usage for the `YAML API `_: + data_catalog_yaml_examples.html>`_: .. code-block:: yaml @@ -38,9 +38,10 @@ class ParquetDataSet(AbstractDataset[dd.DataFrame, dd.DataFrame]): Example usage for the `Python API `_: + advanced_data_catalog_usage.html>`_: :: + >>> from kedro.extras.datasets.dask import ParquetDataSet >>> import pandas as pd >>> import dask.dataframe as dd diff --git a/kedro/extras/datasets/json/json_dataset.py b/kedro/extras/datasets/json/json_dataset.py index 5e05dd46ed..f5907cc162 100644 --- a/kedro/extras/datasets/json/json_dataset.py +++ b/kedro/extras/datasets/json/json_dataset.py @@ -27,7 +27,7 @@ class JSONDataSet(AbstractVersionedDataset[Any, Any]): Example usage for the `YAML API `_: + data_catalog_yaml_examples.html>`_: .. code-block:: yaml @@ -40,7 +40,7 @@ class JSONDataSet(AbstractVersionedDataset[Any, Any]): Example usage for the `Python API `_: + advanced_data_catalog_usage.html>`_: :: >>> from kedro.extras.datasets.json import JSONDataSet diff --git a/kedro/extras/datasets/matplotlib/matplotlib_writer.py b/kedro/extras/datasets/matplotlib/matplotlib_writer.py index 204e4673c5..6c29b4d5ba 100644 --- a/kedro/extras/datasets/matplotlib/matplotlib_writer.py +++ b/kedro/extras/datasets/matplotlib/matplotlib_writer.py @@ -33,7 +33,7 @@ class MatplotlibWriter( Example usage for the `YAML API `_: + data_catalog_yaml_examples.html>`_: .. code-block:: yaml @@ -45,7 +45,7 @@ class MatplotlibWriter( Example usage for the `Python API `_: + advanced_data_catalog_usage.html>`_: :: >>> import matplotlib.pyplot as plt diff --git a/kedro/extras/datasets/pandas/csv_dataset.py b/kedro/extras/datasets/pandas/csv_dataset.py index 01b044969c..26816da5d4 100644 --- a/kedro/extras/datasets/pandas/csv_dataset.py +++ b/kedro/extras/datasets/pandas/csv_dataset.py @@ -32,7 +32,7 @@ class CSVDataSet(AbstractVersionedDataset[pd.DataFrame, pd.DataFrame]): Example usage for the `YAML API `_: + data_catalog_yaml_examples.html>`_: .. code-block:: yaml @@ -54,7 +54,7 @@ class CSVDataSet(AbstractVersionedDataset[pd.DataFrame, pd.DataFrame]): Example usage for the `Python API `_: + advanced_data_catalog_usage.html>`_: :: >>> from kedro.extras.datasets.pandas import CSVDataSet diff --git a/kedro/extras/datasets/pandas/excel_dataset.py b/kedro/extras/datasets/pandas/excel_dataset.py index 21139c7ca9..ebf5015b72 100644 --- a/kedro/extras/datasets/pandas/excel_dataset.py +++ b/kedro/extras/datasets/pandas/excel_dataset.py @@ -37,7 +37,7 @@ class ExcelDataSet( Example usage for the `YAML API `_: + data_catalog_yaml_examples.html>`_: .. code-block:: yaml @@ -58,7 +58,7 @@ class ExcelDataSet( Example usage for the `Python API `_: + advanced_data_catalog_usage.html>`_: :: >>> from kedro.extras.datasets.pandas import ExcelDataSet @@ -78,7 +78,7 @@ class ExcelDataSet( Example usage for the `YAML API `_ + data_catalog_yaml_examples.html>`_ for a multi-sheet Excel file: .. code-block:: yaml @@ -91,7 +91,7 @@ class ExcelDataSet( Example usage for the `Python API `_ + advanced_data_catalog_usage.html>`_ for a multi-sheet Excel file: :: diff --git a/kedro/extras/datasets/pandas/feather_dataset.py b/kedro/extras/datasets/pandas/feather_dataset.py index b43ecc1814..445cd9758a 100644 --- a/kedro/extras/datasets/pandas/feather_dataset.py +++ b/kedro/extras/datasets/pandas/feather_dataset.py @@ -34,7 +34,7 @@ class FeatherDataSet(AbstractVersionedDataset[pd.DataFrame, pd.DataFrame]): Example usage for the `YAML API `_: + data_catalog_yaml_examples.html>`_: .. code-block:: yaml @@ -52,7 +52,7 @@ class FeatherDataSet(AbstractVersionedDataset[pd.DataFrame, pd.DataFrame]): Example usage for the `Python API `_: + advanced_data_catalog_usage.html>`_: :: >>> from kedro.extras.datasets.pandas import FeatherDataSet diff --git a/kedro/extras/datasets/pandas/gbq_dataset.py b/kedro/extras/datasets/pandas/gbq_dataset.py index 16cea01213..5a7c460c7c 100644 --- a/kedro/extras/datasets/pandas/gbq_dataset.py +++ b/kedro/extras/datasets/pandas/gbq_dataset.py @@ -31,7 +31,7 @@ class GBQTableDataSet(AbstractDataset[None, pd.DataFrame]): Example usage for the `YAML API `_: + data_catalog_yaml_examples.html>`_: .. code-block:: yaml @@ -48,7 +48,7 @@ class GBQTableDataSet(AbstractDataset[None, pd.DataFrame]): Example usage for the `Python API `_: + advanced_data_catalog_usage.html>`_: :: >>> from kedro.extras.datasets.pandas import GBQTableDataSet diff --git a/kedro/extras/datasets/pandas/generic_dataset.py b/kedro/extras/datasets/pandas/generic_dataset.py index 7212310e8f..9d173d6524 100644 --- a/kedro/extras/datasets/pandas/generic_dataset.py +++ b/kedro/extras/datasets/pandas/generic_dataset.py @@ -41,7 +41,7 @@ class GenericDataSet(AbstractVersionedDataset[pd.DataFrame, pd.DataFrame]): Example usage for the `YAML API `_: + data_catalog_yaml_examples.html>`_: .. code-block:: yaml @@ -71,7 +71,7 @@ class GenericDataSet(AbstractVersionedDataset[pd.DataFrame, pd.DataFrame]): Example usage for the `Python API `_: + advanced_data_catalog_usage.html>`_: :: >>> from kedro.extras.datasets.pandas import GenericDataSet diff --git a/kedro/extras/datasets/pandas/hdf_dataset.py b/kedro/extras/datasets/pandas/hdf_dataset.py index 0d337af42d..aa02434776 100644 --- a/kedro/extras/datasets/pandas/hdf_dataset.py +++ b/kedro/extras/datasets/pandas/hdf_dataset.py @@ -30,7 +30,8 @@ class HDFDataSet(AbstractVersionedDataset[pd.DataFrame, pd.DataFrame]): Example usage for the `YAML API `_: + data_catalog_yaml_examples.html>`_: + .. code-block:: yaml @@ -42,7 +43,7 @@ class HDFDataSet(AbstractVersionedDataset[pd.DataFrame, pd.DataFrame]): Example usage for the `Python API `_: + advanced_data_catalog_usage.html>`_: :: >>> from kedro.extras.datasets.pandas import HDFDataSet diff --git a/kedro/extras/datasets/pandas/json_dataset.py b/kedro/extras/datasets/pandas/json_dataset.py index 8148d325c5..c2cf971bb9 100644 --- a/kedro/extras/datasets/pandas/json_dataset.py +++ b/kedro/extras/datasets/pandas/json_dataset.py @@ -32,7 +32,8 @@ class JSONDataSet(AbstractVersionedDataset[pd.DataFrame, pd.DataFrame]): Example usage for the `YAML API `_: + data_catalog_yaml_examples.html>`_: + .. code-block:: yaml @@ -49,7 +50,7 @@ class JSONDataSet(AbstractVersionedDataset[pd.DataFrame, pd.DataFrame]): Example usage for the `Python API `_: + advanced_data_catalog_usage.html>`_: :: >>> from kedro.extras.datasets.pandas import JSONDataSet diff --git a/kedro/extras/datasets/pandas/parquet_dataset.py b/kedro/extras/datasets/pandas/parquet_dataset.py index 4bdba28772..43c603f2ae 100644 --- a/kedro/extras/datasets/pandas/parquet_dataset.py +++ b/kedro/extras/datasets/pandas/parquet_dataset.py @@ -33,7 +33,8 @@ class ParquetDataSet(AbstractVersionedDataset[pd.DataFrame, pd.DataFrame]): Example usage for the `YAML API `_: + data_catalog_yaml_examples.html>`_: + .. code-block:: yaml @@ -61,7 +62,7 @@ class ParquetDataSet(AbstractVersionedDataset[pd.DataFrame, pd.DataFrame]): Example usage for the `Python API `_: + advanced_data_catalog_usage.html>`_: :: >>> from kedro.extras.datasets.pandas import ParquetDataSet diff --git a/kedro/extras/datasets/pandas/sql_dataset.py b/kedro/extras/datasets/pandas/sql_dataset.py index 373663ce84..03b3c43aee 100644 --- a/kedro/extras/datasets/pandas/sql_dataset.py +++ b/kedro/extras/datasets/pandas/sql_dataset.py @@ -109,7 +109,8 @@ class SQLTableDataSet(AbstractDataset[pd.DataFrame, pd.DataFrame]): Example usage for the `YAML API `_: + data_catalog_yaml_examples.html>`_: + .. code-block:: yaml @@ -132,7 +133,7 @@ class SQLTableDataSet(AbstractDataset[pd.DataFrame, pd.DataFrame]): Example usage for the `Python API `_: + advanced_data_catalog_usage.html>`_: :: >>> from kedro.extras.datasets.pandas import SQLTableDataSet @@ -280,7 +281,8 @@ class SQLQueryDataSet(AbstractDataset[None, pd.DataFrame]): Example usage for the `YAML API `_: + data_catalog_yaml_examples.html>`_: + .. code-block:: yaml @@ -311,9 +313,10 @@ class SQLQueryDataSet(AbstractDataset[None, pd.DataFrame]): Example usage for the `Python API `_: + advanced_data_catalog_usage.html>`_: :: + >>> from kedro.extras.datasets.pandas import SQLQueryDataSet >>> import pandas as pd >>> diff --git a/kedro/extras/datasets/pandas/xml_dataset.py b/kedro/extras/datasets/pandas/xml_dataset.py index ad91b4ad4b..30bd777252 100644 --- a/kedro/extras/datasets/pandas/xml_dataset.py +++ b/kedro/extras/datasets/pandas/xml_dataset.py @@ -32,7 +32,7 @@ class XMLDataSet(AbstractVersionedDataset[pd.DataFrame, pd.DataFrame]): Example usage for the `Python API `_: + advanced_data_catalog_usage.html>`_: :: >>> from kedro.extras.datasets.pandas import XMLDataSet diff --git a/kedro/extras/datasets/pickle/pickle_dataset.py b/kedro/extras/datasets/pickle/pickle_dataset.py index 19bda78f96..93bbbc2dbc 100644 --- a/kedro/extras/datasets/pickle/pickle_dataset.py +++ b/kedro/extras/datasets/pickle/pickle_dataset.py @@ -31,7 +31,8 @@ class PickleDataSet(AbstractVersionedDataset[Any, Any]): Example usage for the `YAML API `_: + data_catalog_yaml_examples.html>`_: + .. code-block:: yaml @@ -50,7 +51,7 @@ class PickleDataSet(AbstractVersionedDataset[Any, Any]): Example usage for the `Python API `_: + advanced_data_catalog_usage.html>`_: :: >>> from kedro.extras.datasets.pickle import PickleDataSet diff --git a/kedro/extras/datasets/pillow/image_dataset.py b/kedro/extras/datasets/pillow/image_dataset.py index 1244035df1..a403b74b27 100644 --- a/kedro/extras/datasets/pillow/image_dataset.py +++ b/kedro/extras/datasets/pillow/image_dataset.py @@ -27,7 +27,7 @@ class ImageDataSet(AbstractVersionedDataset[Image.Image, Image.Image]): Example usage for the `Python API `_: + advanced_data_catalog_usage.html>`_: :: >>> from kedro.extras.datasets.pillow import ImageDataSet diff --git a/kedro/extras/datasets/plotly/json_dataset.py b/kedro/extras/datasets/plotly/json_dataset.py index 5fa555d665..3c686ab896 100644 --- a/kedro/extras/datasets/plotly/json_dataset.py +++ b/kedro/extras/datasets/plotly/json_dataset.py @@ -29,7 +29,8 @@ class JSONDataSet( Example usage for the `YAML API `_: + data_catalog_yaml_examples.html>`_: + .. code-block:: yaml @@ -41,7 +42,7 @@ class JSONDataSet( Example usage for the `Python API `_: + advanced_data_catalog_usage.html>`_: :: >>> from kedro.extras.datasets.plotly import JSONDataSet diff --git a/kedro/extras/datasets/plotly/plotly_dataset.py b/kedro/extras/datasets/plotly/plotly_dataset.py index 68dc27b012..7cb6477b25 100644 --- a/kedro/extras/datasets/plotly/plotly_dataset.py +++ b/kedro/extras/datasets/plotly/plotly_dataset.py @@ -28,7 +28,8 @@ class PlotlyDataSet(JSONDataSet): Example usage for the `YAML API `_: + data_catalog_yaml_examples.html>`_: + .. code-block:: yaml @@ -48,7 +49,7 @@ class PlotlyDataSet(JSONDataSet): Example usage for the `Python API `_: + advanced_data_catalog_usage.html>`_: :: >>> from kedro.extras.datasets.plotly import PlotlyDataSet diff --git a/kedro/extras/datasets/redis/redis_dataset.py b/kedro/extras/datasets/redis/redis_dataset.py index bac3a15b65..d4d7b11f74 100644 --- a/kedro/extras/datasets/redis/redis_dataset.py +++ b/kedro/extras/datasets/redis/redis_dataset.py @@ -24,7 +24,8 @@ class PickleDataSet(AbstractDataset[Any, Any]): Example usage for the `YAML API `_: + data_catalog_yaml_examples.html>`_: + .. code-block:: yaml @@ -45,7 +46,7 @@ class PickleDataSet(AbstractDataset[Any, Any]): Example usage for the `Python API `_: + advanced_data_catalog_usage.html>`_: :: >>> from kedro.extras.datasets.redis import PickleDataSet diff --git a/kedro/extras/datasets/spark/deltatable_dataset.py b/kedro/extras/datasets/spark/deltatable_dataset.py index 0f6655ac8c..6df51fcdd7 100644 --- a/kedro/extras/datasets/spark/deltatable_dataset.py +++ b/kedro/extras/datasets/spark/deltatable_dataset.py @@ -24,7 +24,8 @@ class DeltaTableDataSet(AbstractDataset[None, DeltaTable]): Example usage for the `YAML API `_: + data_catalog_yaml_examples.html>`_: + .. code-block:: yaml @@ -39,7 +40,7 @@ class DeltaTableDataSet(AbstractDataset[None, DeltaTable]): Example usage for the `Python API `_: + advanced_data_catalog_usage.html>`_: :: >>> from pyspark.sql import SparkSession diff --git a/kedro/extras/datasets/spark/spark_dataset.py b/kedro/extras/datasets/spark/spark_dataset.py index 317e173d24..0547b3e804 100644 --- a/kedro/extras/datasets/spark/spark_dataset.py +++ b/kedro/extras/datasets/spark/spark_dataset.py @@ -167,7 +167,8 @@ class SparkDataSet(AbstractVersionedDataset[DataFrame, DataFrame]): Example usage for the `YAML API `_: + data_catalog_yaml_examples.html>`_: + .. code-block:: yaml @@ -201,7 +202,7 @@ class SparkDataSet(AbstractVersionedDataset[DataFrame, DataFrame]): Example usage for the `Python API `_: + advanced_data_catalog_usage.html>`_: :: >>> from pyspark.sql import SparkSession diff --git a/kedro/extras/datasets/spark/spark_hive_dataset.py b/kedro/extras/datasets/spark/spark_hive_dataset.py index 2abbd1f166..746f7ae6df 100644 --- a/kedro/extras/datasets/spark/spark_hive_dataset.py +++ b/kedro/extras/datasets/spark/spark_hive_dataset.py @@ -33,7 +33,8 @@ class SparkHiveDataSet(AbstractDataset[DataFrame, DataFrame]): Example usage for the `YAML API `_: + data_catalog_yaml_examples.html>`_: + .. code-block:: yaml @@ -45,7 +46,7 @@ class SparkHiveDataSet(AbstractDataset[DataFrame, DataFrame]): Example usage for the `Python API `_: + advanced_data_catalog_usage.html>`_: :: >>> from pyspark.sql import SparkSession diff --git a/kedro/extras/datasets/spark/spark_jdbc_dataset.py b/kedro/extras/datasets/spark/spark_jdbc_dataset.py index 3abeeb312a..bacb492cbd 100644 --- a/kedro/extras/datasets/spark/spark_jdbc_dataset.py +++ b/kedro/extras/datasets/spark/spark_jdbc_dataset.py @@ -23,7 +23,8 @@ class SparkJDBCDataSet(AbstractDataset[DataFrame, DataFrame]): Example usage for the `YAML API `_: + data_catalog_yaml_examples.html>`_: + .. code-block:: yaml @@ -41,7 +42,7 @@ class SparkJDBCDataSet(AbstractDataset[DataFrame, DataFrame]): Example usage for the `Python API `_: + advanced_data_catalog_usage.html>`_: :: >>> import pandas as pd diff --git a/kedro/extras/datasets/svmlight/svmlight_dataset.py b/kedro/extras/datasets/svmlight/svmlight_dataset.py index 697253ef2a..af4a1323ad 100644 --- a/kedro/extras/datasets/svmlight/svmlight_dataset.py +++ b/kedro/extras/datasets/svmlight/svmlight_dataset.py @@ -45,7 +45,8 @@ class SVMLightDataSet(AbstractVersionedDataset[_DI, _DO]): Example usage for the `YAML API `_: + data_catalog_yaml_examples.html>`_: + .. code-block:: yaml @@ -70,7 +71,7 @@ class SVMLightDataSet(AbstractVersionedDataset[_DI, _DO]): Example usage for the `Python API `_: + advanced_data_catalog_usage.html>`_: :: >>> from kedro.extras.datasets.svmlight import SVMLightDataSet diff --git a/kedro/extras/datasets/tensorflow/tensorflow_model_dataset.py b/kedro/extras/datasets/tensorflow/tensorflow_model_dataset.py index c0e916d01f..ce6043b18d 100644 --- a/kedro/extras/datasets/tensorflow/tensorflow_model_dataset.py +++ b/kedro/extras/datasets/tensorflow/tensorflow_model_dataset.py @@ -31,7 +31,8 @@ class TensorFlowModelDataset(AbstractVersionedDataset[tf.keras.Model, tf.keras.M Example usage for the `YAML API `_: + data_catalog_yaml_examples.html>`_: + .. code-block:: yaml @@ -47,7 +48,7 @@ class TensorFlowModelDataset(AbstractVersionedDataset[tf.keras.Model, tf.keras.M Example usage for the `Python API `_: + advanced_data_catalog_usage.html>`_: :: >>> from kedro.extras.datasets.tensorflow import TensorFlowModelDataset diff --git a/kedro/extras/datasets/text/text_dataset.py b/kedro/extras/datasets/text/text_dataset.py index 3c8a859445..253ee92826 100644 --- a/kedro/extras/datasets/text/text_dataset.py +++ b/kedro/extras/datasets/text/text_dataset.py @@ -26,7 +26,8 @@ class TextDataSet(AbstractVersionedDataset[str, str]): Example usage for the `YAML API `_: + data_catalog_yaml_examples.html>`_: + .. code-block:: yaml @@ -36,7 +37,7 @@ class TextDataSet(AbstractVersionedDataset[str, str]): Example usage for the `Python API `_: + advanced_data_catalog_usage.html>`_: :: >>> from kedro.extras.datasets.text import TextDataSet diff --git a/kedro/extras/datasets/tracking/json_dataset.py b/kedro/extras/datasets/tracking/json_dataset.py index a77e162719..a41491492b 100644 --- a/kedro/extras/datasets/tracking/json_dataset.py +++ b/kedro/extras/datasets/tracking/json_dataset.py @@ -20,7 +20,8 @@ class JSONDataSet(JDS): Example usage for the `YAML API `_: + data_catalog_yaml_examples.html>`_: + .. code-block:: yaml @@ -30,7 +31,7 @@ class JSONDataSet(JDS): Example usage for the `Python API `_: + advanced_data_catalog_usage.html>`_: :: >>> from kedro.extras.datasets.tracking import JSONDataSet diff --git a/kedro/extras/datasets/tracking/metrics_dataset.py b/kedro/extras/datasets/tracking/metrics_dataset.py index 3b615b6d64..b2a1949702 100644 --- a/kedro/extras/datasets/tracking/metrics_dataset.py +++ b/kedro/extras/datasets/tracking/metrics_dataset.py @@ -22,7 +22,8 @@ class MetricsDataSet(JSONDataSet): Example usage for the `YAML API `_: + data_catalog_yaml_examples.html>`_: + .. code-block:: yaml @@ -32,7 +33,7 @@ class MetricsDataSet(JSONDataSet): Example usage for the `Python API `_: + advanced_data_catalog_usage.html>`_: :: >>> from kedro.extras.datasets.tracking import MetricsDataSet diff --git a/kedro/extras/datasets/video/video_dataset.py b/kedro/extras/datasets/video/video_dataset.py index 4f5e793f29..08e93126ec 100644 --- a/kedro/extras/datasets/video/video_dataset.py +++ b/kedro/extras/datasets/video/video_dataset.py @@ -198,7 +198,8 @@ class VideoDataSet(AbstractDataset[AbstractVideo, AbstractVideo]): Example usage for the `YAML API `_: + data_catalog_yaml_examples.html>`_: + .. code-block:: yaml @@ -213,7 +214,7 @@ class VideoDataSet(AbstractDataset[AbstractVideo, AbstractVideo]): Example usage for the `Python API `_: + advanced_data_catalog_usage.html>`_: :: >>> from kedro.extras.datasets.video import VideoDataSet diff --git a/kedro/extras/datasets/yaml/yaml_dataset.py b/kedro/extras/datasets/yaml/yaml_dataset.py index 7ea2760cdf..a98e76314e 100644 --- a/kedro/extras/datasets/yaml/yaml_dataset.py +++ b/kedro/extras/datasets/yaml/yaml_dataset.py @@ -27,7 +27,8 @@ class YAMLDataSet(AbstractVersionedDataset[Dict, Dict]): Example usage for the `YAML API `_: + data_catalog_yaml_examples.html>`_: + .. code-block:: yaml @@ -37,7 +38,7 @@ class YAMLDataSet(AbstractVersionedDataset[Dict, Dict]): Example usage for the `Python API `_: + advanced_data_catalog_usage.html>`_: :: >>> from kedro.extras.datasets.yaml import YAMLDataSet diff --git a/kedro/io/partitioned_dataset.py b/kedro/io/partitioned_dataset.py index 66df5294a8..bccbd9e628 100644 --- a/kedro/io/partitioned_dataset.py +++ b/kedro/io/partitioned_dataset.py @@ -43,12 +43,13 @@ class PartitionedDataset(AbstractDataset): https://github.com/intake/filesystem_spec. It also supports advanced features like - `lazy saving `_. + `lazy saving `_. Example usage for the `YAML API `_: + data_catalog_yaml_examples.html>`_: + .. code-block:: yaml @@ -66,7 +67,7 @@ class PartitionedDataset(AbstractDataset): Example usage for the `Python API `_: + advanced_data_catalog_usage.html>`_: :: >>> import pandas as pd @@ -178,7 +179,7 @@ def __init__( # noqa: too-many-arguments and the dataset initializer. If the dataset config contains explicit credentials spec, then such spec will take precedence. All possible credentials management scenarios are documented here: - https://kedro.readthedocs.io/en/stable/data/kedro_io.html#partitioned-dataset-credentials + https://docs.kedro.org/en/stable/data/partitioned_and_incremental_datasets.html#partitioned-dataset-credentials load_args: Keyword arguments to be passed into ``find()`` method of the filesystem implementation. fs_args: Extra arguments to pass into underlying filesystem class constructor @@ -418,7 +419,7 @@ def __init__( # noqa: too-many-arguments with the corresponding dataset definition including ``filepath`` (unlike ``dataset`` argument). Checkpoint configuration is described here: - https://kedro.readthedocs.io/en/stable/data/kedro_io.html#checkpoint-configuration + https://docs.kedro.org/en/stable/data/partitioned_and_incremental_datasets.html#checkpoint-configuration Credentials for the checkpoint can be explicitly specified in this configuration. filepath_arg: Underlying dataset initializer argument that will @@ -433,7 +434,7 @@ def __init__( # noqa: too-many-arguments the dataset or the checkpoint configuration contains explicit credentials spec, then such spec will take precedence. All possible credentials management scenarios are documented here: - https://kedro.readthedocs.io/en/stable/data/kedro_io.html#partitioned-dataset-credentials + https://docs.kedro.org/en/stable/data/partitioned_and_incremental_datasets.html#checkpoint-configuration load_args: Keyword arguments to be passed into ``find()`` method of the filesystem implementation. fs_args: Extra arguments to pass into underlying filesystem class constructor diff --git a/setup.py b/setup.py index 139501feb1..afea8c9587 100644 --- a/setup.py +++ b/setup.py @@ -97,7 +97,7 @@ def _collect_requirements(requires): "sphinxcontrib-mermaid~=0.7.1", "myst-parser~=1.0.0", "Jinja2<3.1.0", - "kedro-datasets[all]~=1.6.0", + "kedro-datasets[all]~=1.7.0", ], "geopandas": _collect_requirements(geopandas_require), "matplotlib": _collect_requirements(matplotlib_require), From ee2f5a2b6349664e274a494825ad3f29d9e4dd3c Mon Sep 17 00:00:00 2001 From: Jo Stichbury Date: Fri, 1 Sep 2023 16:37:36 +0100 Subject: [PATCH 56/70] Minor docs changes on data section to create a PR and test Vale styles (#2966) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Minor changes to create a PR and test Vale styles Signed-off-by: Jo Stichbury * fix some vale warnings Signed-off-by: Jo Stichbury --------- Signed-off-by: Jo Stichbury Co-authored-by: Juan Luis Cano Rodríguez Signed-off-by: Jason Hite --- .github/styles/Kedro/quotes.yml | 10 ---------- docs/source/data/advanced_data_catalog_usage.md | 2 +- docs/source/data/data_catalog.md | 2 +- docs/source/data/data_catalog_yaml_examples.md | 2 +- docs/source/data/how_to_create_a_custom_dataset.md | 2 +- docs/source/data/index.md | 2 +- docs/source/data/kedro_dataset_factories.md | 2 +- .../data/partitioned_and_incremental_datasets.md | 2 +- 8 files changed, 7 insertions(+), 17 deletions(-) delete mode 100644 .github/styles/Kedro/quotes.yml diff --git a/.github/styles/Kedro/quotes.yml b/.github/styles/Kedro/quotes.yml deleted file mode 100644 index 7e4ed44be0..0000000000 --- a/.github/styles/Kedro/quotes.yml +++ /dev/null @@ -1,10 +0,0 @@ -extends: existence -message: Use straight quotes instead of smart quotes. -level: warning -nonword: true -action: -tokens: - - “ - - ” - - ‘ - - ’ diff --git a/docs/source/data/advanced_data_catalog_usage.md b/docs/source/data/advanced_data_catalog_usage.md index 1906500d35..78fb6e183a 100644 --- a/docs/source/data/advanced_data_catalog_usage.md +++ b/docs/source/data/advanced_data_catalog_usage.md @@ -6,7 +6,7 @@ You can define a Data Catalog in two ways. Most use cases can be through a YAML To use the `DataCatalog` API, construct a `DataCatalog` object programmatically in a file like `catalog.py`. -In the following, we are using several pre-built data loaders documented in the [API reference documentation](/kedro_datasets). +In the following code, we use several pre-built data loaders documented in the [API reference documentation](/kedro_datasets). ```python from kedro.io import DataCatalog diff --git a/docs/source/data/data_catalog.md b/docs/source/data/data_catalog.md index b4a6c4d7da..8c95ac8309 100644 --- a/docs/source/data/data_catalog.md +++ b/docs/source/data/data_catalog.md @@ -3,7 +3,7 @@ In a Kedro project, the Data Catalog is a registry of all data sources available for use by the project. It is specified with a YAML catalog file that maps the names of node inputs and outputs as keys in the `DataCatalog` class. -This page introduces the basic sections of `catalog.yml`, which is the file used to register data sources for a Kedro project. +This page introduces the basic sections of `catalog.yml`, which is the file Kedro uses to register data sources for a project. ## The basics of `catalog.yml` A separate page of [Data Catalog YAML examples](./data_catalog_yaml_examples.md) gives further examples of how to work with `catalog.yml`, but here we revisit the [basic `catalog.yml` introduced by the spaceflights tutorial](../tutorial/set_up_data.md). diff --git a/docs/source/data/data_catalog_yaml_examples.md b/docs/source/data/data_catalog_yaml_examples.md index f27981600d..4ee0a64a93 100644 --- a/docs/source/data/data_catalog_yaml_examples.md +++ b/docs/source/data/data_catalog_yaml_examples.md @@ -8,7 +8,7 @@ This page contains a set of examples to help you structure your YAML configurati ## Load data from a local binary file using `utf-8` encoding -The `open_args_load` and `open_args_save` parameters are passed to the filesystem's `open` method to configure how a dataset file (on a specific filesystem) is opened during a load or save operation, respectively. +The `open_args_load` and `open_args_save` parameters are passed to the filesystem `open` method to configure how a dataset file (on a specific filesystem) is opened during a load or save operation respectively. ```yaml test_dataset: diff --git a/docs/source/data/how_to_create_a_custom_dataset.md b/docs/source/data/how_to_create_a_custom_dataset.md index 46364031a0..8aedac6984 100644 --- a/docs/source/data/how_to_create_a_custom_dataset.md +++ b/docs/source/data/how_to_create_a_custom_dataset.md @@ -4,7 +4,7 @@ ## AbstractDataset -For contributors, if you would like to submit a new dataset, you must extend the [`AbstractDataset` interface](/kedro.io.AbstractDataset) or [`AbstractVersionedDataset` interface](/kedro.io.AbstractVersionedDataset) if you plan to support versioning. It requires subclasses to override the `_load` and `_save` and provides `load` and `save` methods that enrich the corresponding private methods with uniform error handling. It also requires subclasses to override `_describe`, which is used in logging the internal information about the instances of your custom `AbstractDataset` implementation. +If you are a contributor and would like to submit a new dataset, you must extend the [`AbstractDataset` interface](/kedro.io.AbstractDataset) or [`AbstractVersionedDataset` interface](/kedro.io.AbstractVersionedDataset) if you plan to support versioning. It requires subclasses to override the `_load` and `_save` and provides `load` and `save` methods that enrich the corresponding private methods with uniform error handling. It also requires subclasses to override `_describe`, which is used in logging the internal information about the instances of your custom `AbstractDataset` implementation. ## Scenario diff --git a/docs/source/data/index.md b/docs/source/data/index.md index a6196bcc13..6f95cf84f0 100644 --- a/docs/source/data/index.md +++ b/docs/source/data/index.md @@ -3,7 +3,7 @@ In a Kedro project, the Data Catalog is a registry of all data sources available for use by the project. The catalog is stored in a YAML file (`catalog.yml`) that maps the names of node inputs and outputs as keys in the `DataCatalog` class. -[Kedro provides different built-in datasets in the `kedro-datasets` package](/kedro_datasets) for numerous file types and file systems, so you don’t have to write any of the logic for reading/writing data. +[The `kedro-datasets` package offers built-in datasets](/kedro_datasets) for common file types and file systems. We first introduce the basic sections of `catalog.yml`, which is the file used to register data sources for a Kedro project. diff --git a/docs/source/data/kedro_dataset_factories.md b/docs/source/data/kedro_dataset_factories.md index 2a65b4359e..bb6714e21d 100644 --- a/docs/source/data/kedro_dataset_factories.md +++ b/docs/source/data/kedro_dataset_factories.md @@ -1,7 +1,7 @@ # Kedro dataset factories You can load multiple datasets with similar configuration using dataset factories, introduced in Kedro 0.18.12. -The syntax allows you to generalise the configuration and reduce the number of similar catalog entries by matching datasets used in your project's pipelines to dataset factory patterns. +The syntax allows you to generalise your configuration and reduce the number of similar catalog entries by matching datasets used in your project's pipelines to dataset factory patterns. ## How to generalise datasets with similar names and types diff --git a/docs/source/data/partitioned_and_incremental_datasets.md b/docs/source/data/partitioned_and_incremental_datasets.md index a57b56d2a4..f54d9b998b 100644 --- a/docs/source/data/partitioned_and_incremental_datasets.md +++ b/docs/source/data/partitioned_and_incremental_datasets.md @@ -2,7 +2,7 @@ ## Partitioned datasets -Distributed systems play an increasingly important role in ETL data pipelines. They significantly increase the processing throughput, enabling us to work with much larger volumes of input data. However, these benefits sometimes come at a cost. When dealing with the input data generated by such distributed systems, you might encounter a situation where your Kedro node needs to read the data from a directory full of uniform files of the same type (e.g. JSON, CSV, Parquet, etc.) rather than from a single file. Tools like `PySpark` and the corresponding [SparkDataSet](/kedro_datasets.spark.SparkDataSet) cater for such use cases, but the use of Spark is not always feasible. +Distributed systems play an increasingly important role in ETL data pipelines. They increase the processing throughput, enabling us to work with much larger volumes of input data. A situation may arise where your Kedro node needs to read the data from a directory full of uniform files of the same type like JSON or CSV. Tools like `PySpark` and the corresponding [SparkDataSet](/kedro_datasets.spark.SparkDataSet) cater for such use cases but may not always be possible. This is why Kedro provides a built-in [PartitionedDataset](/kedro.io.PartitionedDataset), with the following features: From be0c34a6d2c95521e2f11e6c30c492221cd54c17 Mon Sep 17 00:00:00 2001 From: rxm7706 <95496360+rxm7706@users.noreply.github.com> Date: Fri, 1 Sep 2023 12:24:27 -0500 Subject: [PATCH 57/70] PiP/pyproject.toml and Conda/Meta.yaml Sync (#2922) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Resynced Signed-off-by: rxm7706 <95496360+rxm7706@users.noreply.github.com> * restore lower bound for pluggy Signed-off-by: rxm7706 <95496360+rxm7706@users.noreply.github.com> --------- Signed-off-by: rxm7706 <95496360+rxm7706@users.noreply.github.com> Co-authored-by: Juan Luis Cano Rodríguez Signed-off-by: Jason Hite --- pyproject.toml | 44 ++++++++++++++++++++++---------------------- 1 file changed, 22 insertions(+), 22 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 4ae5ca4273..32beed6464 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -12,30 +12,30 @@ authors = [ description = "Kedro helps you build production-ready data and analytics pipelines" requires-python = ">=3.7" dependencies = [ - "anyconfig>=0.10,<0.14", + "anyconfig>=0.10.0", "attrs>=21.3", - "build", - "cachetools~=5.3", - "click<9.0", - "cookiecutter>=2.1.1, <3.0", - "dynaconf>=3.1.2, <4.0", - "fsspec>=2021.4, <2024.1", # Upper bound set arbitrarily, to be reassessed in early 2024 - "gitpython~=3.0", - "importlib-metadata>=3.6; python_version >= '3.8'", - "importlib_metadata>=3.6, <5.0; python_version < '3.8'", # The "selectable" entry points were introduced in `importlib_metadata` 3.6 and Python 3.10. Bandit on Python 3.7 relies on a library with `importlib_metadata` < 5.0 - "importlib_resources>=1.3", # The `files()` API was introduced in `importlib_resources` 1.3 and Python 3.9. - "jmespath>=0.9.5, <2.0", - "more_itertools>=9,<11", - "omegaconf~=2.3", - "parse~=1.19.0", - "pip-tools>=6.5,<8", - "pluggy>=1.0, <1.3", # TODO: Uncap when dropping Python 3.7 support, see https://github.com/kedro-org/kedro/issues/2979 - "PyYAML>=4.2, <7.0", - "rich>=12.0, <14.0", - "rope>=0.21, <2.0", # subject to LGPLv3 license + "build>=0.7.0,", + "cachetools>=4.1", + "click>=4.0", + "cookiecutter>=2.1.1,<3.0", + "dynaconf>=3.1.2,<4.0", + "fsspec>=2021.4", + "gitpython>=3.0", + "importlib_metadata>=3.6,<5.0; python_version < '3.8'", # The "selectable" entry points were introduced in `importlib_metadata` 3.6 and Python 3.10. Bandit on Python 3.7 relies on a library with `importlib_metadata` < 5.0 + "importlib-metadata>=3.6,<7.0; python_version >= '3.8'", + "importlib_resources>=1.3,<7.0", # The `files()` API was introduced in `importlib_resources` 1.3 and Python 3.9. + "jmespath>=0.9.5", + "more_itertools>=8.14.0", + "omegaconf>=2.1.1", + "parse>=1.19.0", + "pip-tools>=6.5", + "pluggy>=1.0,<1.3", # TODO: Uncap when dropping Python 3.7 support, see https://github.com/kedro-org/kedro/issues/2979 + "PyYAML>=4.2,<7.0", + "rich>=12.0,<14.0", + "rope>=0.21,<2.0", # subject to LGPLv3 license "setuptools>=65.5.1", - "toml~=0.10", - "toposort~=1.5", # Needs to be at least 1.5 to be able to raise CircularDependencyError + "toml>=0.10.0", + "toposort>=1.5", # Needs to be at least 1.5 to be able to raise CircularDependencyError ] keywords = [ "pipelines", From 7ebbc12ec8ec6652a32e4ceb467acea56b987eed Mon Sep 17 00:00:00 2001 From: rxm7706 <95496360+rxm7706@users.noreply.github.com> Date: Mon, 4 Sep 2023 03:21:27 -0500 Subject: [PATCH 58/70] fix typo for build (#3001) Signed-off-by: rxm7706 <95496360+rxm7706@users.noreply.github.com> Signed-off-by: Jason Hite --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 32beed6464..55711a1186 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -14,7 +14,7 @@ requires-python = ">=3.7" dependencies = [ "anyconfig>=0.10.0", "attrs>=21.3", - "build>=0.7.0,", + "build>=0.7.0", "cachetools>=4.1", "click>=4.0", "cookiecutter>=2.1.1,<3.0", From f9485f054c7066b7b111cb2bd336041fcd5bda75 Mon Sep 17 00:00:00 2001 From: Nok Lam Chan Date: Tue, 5 Sep 2023 14:23:56 +0100 Subject: [PATCH 59/70] Add hook example to access `metadata` (#2998) * Check plugins implement valid hooks Signed-off-by: Nok Chan * add Metadtahook Signed-off-by: Nok Chan * Fix docs according to comments Signed-off-by: Nok --------- Signed-off-by: Nok Chan Signed-off-by: Nok Co-authored-by: Jo Stichbury Signed-off-by: Jason Hite --- docs/source/hooks/common_use_cases.md | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/docs/source/hooks/common_use_cases.md b/docs/source/hooks/common_use_cases.md index 085bcf7136..c7070a6671 100644 --- a/docs/source/hooks/common_use_cases.md +++ b/docs/source/hooks/common_use_cases.md @@ -200,3 +200,17 @@ HOOKS = (AzureSecretsHook(),) ```{note} Note: `DefaultAzureCredential()` is Azure's recommended approach to authorise access to data in your storage accounts. For more information, consult the [documentation about how to authenticate to Azure and authorize access to blob data](https://learn.microsoft.com/en-us/azure/storage/blobs/storage-quickstart-blobs-python). ``` + +## Use a Hook to read `metadata` from `DataCatalog` +Use the `after_catalog_created` Hook to access `metadata` to extend Kedro. + +```python +class MetadataHook: + @hook_impl + def after_catalog_created( + self, + catalog: DataCatalog, + ): + for dataset_name, dataset in catalog.datasets.__dict__.items(): + print(f"{dataset_name} metadata: \n {str(dataset.metadata)}") +``` From 25785603b76963fd30d26a95c5a00db91e9ea22b Mon Sep 17 00:00:00 2001 From: Jason Hite Date: Wed, 6 Sep 2023 10:55:51 -0400 Subject: [PATCH 60/70] Bump release notes version Signed-off-by: Jason Hite --- RELEASE.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/RELEASE.md b/RELEASE.md index 8d5cb62415..b44d4cab97 100644 --- a/RELEASE.md +++ b/RELEASE.md @@ -15,6 +15,9 @@ ## Breaking changes to the API ## Upcoming deprecations for Kedro 0.19.0 +# Upcoming release 0.18.14 +* Allowed using of custom cookiecutter templates for creating pipelines with `--template` flag for `kedro pipeline create` or via `template/pipeline` folder. + # Release 0.18.13 ## Major features and improvements From 0d7793824ca2bb89663dc6e931b06afe0dd0015b Mon Sep 17 00:00:00 2001 From: Jason Hite Date: Wed, 6 Sep 2023 12:05:46 -0400 Subject: [PATCH 61/70] Expand docs for customized pipeline templates. Signed-off-by: Jason Hite --- .../nodes_and_pipelines/modular_pipelines.md | 25 ++++++++++++++++++- 1 file changed, 24 insertions(+), 1 deletion(-) diff --git a/docs/source/nodes_and_pipelines/modular_pipelines.md b/docs/source/nodes_and_pipelines/modular_pipelines.md index 3df2a699dc..ad04d74441 100644 --- a/docs/source/nodes_and_pipelines/modular_pipelines.md +++ b/docs/source/nodes_and_pipelines/modular_pipelines.md @@ -74,15 +74,38 @@ Running the `kedro pipeline create` command adds boilerplate folders and files f +### Custom templates + If you want to generate a pipeline with a custom Cookiecutter template, you can save it in `/templates/pipeline`. The `kedro pipeline create` command will then pick up the custom template as the default. You can also specify the path to your custom Cookiecutter pipeline template with the `--template` flag like this: ```bash kedro pipeline create --template ``` +A template folder passed on the command line via the `--template` argument will take precedence over any local templates +in `/templates/pipeline`, and you can use multiple different templates within a project using the `--template` argument. +Currently only one template in the project `templates` directory is supported; use the command line flag if you need multiple +templates. If you want to do the reverse and remove a modular pipeline, you can use ``kedro pipeline delete `` to do so. +#### Creating Custom Templates + +It is up to you to create functional Cookiecutter templates for custom modular pipelines. Please ensure you understand the +basic structure of a modular pipeline. The most important requirement is that your template should render to a valid, importable +Python module containing a `create_pipeline` function at the top level that returns a `Pipeline` object. You will also need appropriate +`config` and `tests` subdirectories that will be copied to the project `config` and `tests` directories when creating a pipeline. +Currently, the `config` and `tests` directories need to follow the exact same layout as in the default template and cannot +be customized, though the contents of the parameters and actual test file can be changed. File and folder names or structure +do not matter beyond that and can be customized according to your needs. You can use the +default template that Kedro uses as a starting point, found [here](../../../kedro/templates/pipeline). + +Pipeline templates are rendered using [Cookiecutter](https://cookiecutter.readthedocs.io/), and must also contain a `cookiecutter.json` +See the Kedro default template linked above for an example. **One very important note:** if you are embedding your custom pipeline template within a +Kedro starter template, you must tell cookiecutter not to render this template when creating a new project from the starter. To do this, +you must add [`_copy_without_render: ["templates"]`](https://cookiecutter.readthedocs.io/en/latest/advanced/copy_without_render.html) to the `cookiecutter.json` file *for the starter* (not the `cookiecutter.json` for +the pipeline template). + ### Ensuring portability Modular pipelines are shareable between Kedro codebases via [micro-packaging](micro_packaging.md), but you must follow a couple of rules to ensure portability: @@ -297,4 +320,4 @@ final_pipeline = alpha_pipeline + beta_pipeline -![namespaced_params](../meta/images/cook_params.png) +![namespaced_params](../meta/images/cook_params.png) \ No newline at end of file From c12a5ded69dc9ead2bb42ed523e414eb31d791ac Mon Sep 17 00:00:00 2001 From: Ankita Katiyar Date: Thu, 7 Sep 2023 14:08:17 +0100 Subject: [PATCH 62/70] Update release notes and docs Signed-off-by: Ankita Katiyar Signed-off-by: Jason Hite --- RELEASE.md | 7 ++++-- .../nodes_and_pipelines/modular_pipelines.md | 23 ++++++++++--------- 2 files changed, 17 insertions(+), 13 deletions(-) diff --git a/RELEASE.md b/RELEASE.md index b44d4cab97..155e5e44e3 100644 --- a/RELEASE.md +++ b/RELEASE.md @@ -10,13 +10,16 @@ # Upcoming Release 0.18.14 ## Major features and improvements +* Allowed using of custom cookiecutter templates for creating pipelines with `--template` flag for `kedro pipeline create` or via `template/pipeline` folder. + ## Bug fixes and other changes ## Documentation changes ## Breaking changes to the API ## Upcoming deprecations for Kedro 0.19.0 +## Community contributions +Many thanks to the following Kedroids for contributing PRs to this release: -# Upcoming release 0.18.14 -* Allowed using of custom cookiecutter templates for creating pipelines with `--template` flag for `kedro pipeline create` or via `template/pipeline` folder. +* [Jason Hite](https://github.com/jasonmhite) # Release 0.18.13 diff --git a/docs/source/nodes_and_pipelines/modular_pipelines.md b/docs/source/nodes_and_pipelines/modular_pipelines.md index ad04d74441..b9abb2ff79 100644 --- a/docs/source/nodes_and_pipelines/modular_pipelines.md +++ b/docs/source/nodes_and_pipelines/modular_pipelines.md @@ -74,6 +74,8 @@ Running the `kedro pipeline create` command adds boilerplate folders and files f +If you want to do the reverse and remove a modular pipeline, you can use ``kedro pipeline delete `` to do so. + ### Custom templates If you want to generate a pipeline with a custom Cookiecutter template, you can save it in `/templates/pipeline`. @@ -82,29 +84,28 @@ Cookiecutter pipeline template with the `--template` flag like this: ```bash kedro pipeline create --template ``` -A template folder passed on the command line via the `--template` argument will take precedence over any local templates +A template folder passed via the command line using the `--template` argument will take precedence over any local templates in `/templates/pipeline`, and you can use multiple different templates within a project using the `--template` argument. -Currently only one template in the project `templates` directory is supported; use the command line flag if you need multiple +Currently, only one template in the project `templates` directory is supported; use the command line flag if you need multiple templates. -If you want to do the reverse and remove a modular pipeline, you can use ``kedro pipeline delete `` to do so. - #### Creating Custom Templates It is up to you to create functional Cookiecutter templates for custom modular pipelines. Please ensure you understand the basic structure of a modular pipeline. The most important requirement is that your template should render to a valid, importable Python module containing a `create_pipeline` function at the top level that returns a `Pipeline` object. You will also need appropriate -`config` and `tests` subdirectories that will be copied to the project `config` and `tests` directories when creating a pipeline. +`config` and `tests` subdirectories that will be copied to the project `config` and `tests` directories when creating a pipeline. Currently, the `config` and `tests` directories need to follow the exact same layout as in the default template and cannot be customized, though the contents of the parameters and actual test file can be changed. File and folder names or structure -do not matter beyond that and can be customized according to your needs. You can use the -default template that Kedro uses as a starting point, found [here](../../../kedro/templates/pipeline). +do not matter beyond that and can be customized according to your needs. You can use [the +default template that Kedro](https://github.com/kedro-org/kedro/tree/main/kedro/templates/pipeline) uses as a starting point. Pipeline templates are rendered using [Cookiecutter](https://cookiecutter.readthedocs.io/), and must also contain a `cookiecutter.json` -See the Kedro default template linked above for an example. **One very important note:** if you are embedding your custom pipeline template within a +See the [`cookiecutter.json` file in the Kedro default template]((https://github.com/kedro-org/kedro/tree/main/kedro/templates/pipeline/cookiecutter.json)) for an example. +It is important to note that if you are embedding your custom pipeline template within a Kedro starter template, you must tell cookiecutter not to render this template when creating a new project from the starter. To do this, -you must add [`_copy_without_render: ["templates"]`](https://cookiecutter.readthedocs.io/en/latest/advanced/copy_without_render.html) to the `cookiecutter.json` file *for the starter* (not the `cookiecutter.json` for -the pipeline template). +you must add [`_copy_without_render: ["templates"]`](https://cookiecutter.readthedocs.io/en/latest/advanced/copy_without_render.html) to the `cookiecutter.json` file for the starter +and not the `cookiecutter.json` for the pipeline template. ### Ensuring portability @@ -320,4 +321,4 @@ final_pipeline = alpha_pipeline + beta_pipeline -![namespaced_params](../meta/images/cook_params.png) \ No newline at end of file +![namespaced_params](../meta/images/cook_params.png) From c710d38f7042a11c0a2a816fc92f1f39580b688e Mon Sep 17 00:00:00 2001 From: Ankita Katiyar Date: Thu, 7 Sep 2023 14:48:07 +0100 Subject: [PATCH 63/70] Update language linter to also run when PR converted ready for review Signed-off-by: Ankita Katiyar Signed-off-by: Jason Hite --- .github/workflows/docs-language-linter.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/docs-language-linter.yml b/.github/workflows/docs-language-linter.yml index a0f6bca06a..cf9490c4d1 100644 --- a/.github/workflows/docs-language-linter.yml +++ b/.github/workflows/docs-language-linter.yml @@ -1,7 +1,7 @@ name: Language Linter for Kedro Docs on: pull_request: - types: [opened, reopened] + types: [opened, reopened, ready_for_review] paths: - "docs/**" - '**.md' From 8b7cb9a737aeed7ba7b7b4599b5edef2de27993e Mon Sep 17 00:00:00 2001 From: Ankita Katiyar <110245118+ankatiyar@users.noreply.github.com> Date: Wed, 6 Sep 2023 15:59:54 +0100 Subject: [PATCH 64/70] Fix ci - Use `--resolver=backtracking` with `kedro build-reqs` in e2e tests (#3010) * Bump build version Signed-off-by: Ankita Katiyar * Remove telemetry from test default starter Signed-off-by: Ankita Katiyar * Add package_name back Signed-off-by: Ankita Katiyar * Pin build only for 3.7 Signed-off-by: Ankita Katiyar * Try upgrade pip Signed-off-by: Ankita Katiyar * Add constraint Signed-off-by: Ankita Katiyar * Update for windows Signed-off-by: Ankita Katiyar * Run build-reqs with backtracking resolver Signed-off-by: Ankita Katiyar * Run build-reqs with backtracking resolver Signed-off-by: Ankita Katiyar --------- Signed-off-by: Ankita Katiyar Signed-off-by: Jason Hite --- .github/workflows/e2e-tests.yml | 1 + features/build_reqs.feature | 4 ++-- features/package.feature | 2 +- 3 files changed, 4 insertions(+), 3 deletions(-) diff --git a/.github/workflows/e2e-tests.yml b/.github/workflows/e2e-tests.yml index 911498a102..b35ccad0e7 100644 --- a/.github/workflows/e2e-tests.yml +++ b/.github/workflows/e2e-tests.yml @@ -39,6 +39,7 @@ jobs: uses: microsoft/setup-msbuild@v1 - name: Install dependencies run: | + pip install --upgrade pip make install-test-requirements make install-pre-commit - name: pip freeze diff --git a/features/build_reqs.feature b/features/build_reqs.feature index 085cab2242..05bf551961 100644 --- a/features/build_reqs.feature +++ b/features/build_reqs.feature @@ -5,9 +5,9 @@ Feature: build-reqs target in new project Given I have prepared a config file And I have run a non-interactive kedro new with starter "default" And I have updated kedro requirements - And I have executed the kedro command "build-reqs" + And I have executed the kedro command "build-reqs --resolver=backtracking" When I add scrapy>=1.7.3 to the requirements - And I execute the kedro command "build-reqs" + And I execute the kedro command "build-reqs --resolver=backtracking" Then I should get a successful exit code And requirements should be generated And scrapy should be in the requirements diff --git a/features/package.feature b/features/package.feature index 21873e775c..663ea87c49 100644 --- a/features/package.feature +++ b/features/package.feature @@ -16,7 +16,7 @@ Feature: Package target in new project @fresh_venv Scenario: Install package after running kedro build-reqs Given I have updated kedro requirements - When I execute the kedro command "build-reqs" + When I execute the kedro command "build-reqs --resolver=backtracking" Then I should get a successful exit code When I execute the kedro command "package" Then I should get a successful exit code From 9620f805a8c6be2de322a359f8422733a1e242e8 Mon Sep 17 00:00:00 2001 From: Ankita Katiyar Date: Thu, 7 Sep 2023 16:38:05 +0100 Subject: [PATCH 65/70] Update docs Signed-off-by: Ankita Katiyar Signed-off-by: Jason Hite --- .../nodes_and_pipelines/modular_pipelines.md | 17 ++++++++--------- 1 file changed, 8 insertions(+), 9 deletions(-) diff --git a/docs/source/nodes_and_pipelines/modular_pipelines.md b/docs/source/nodes_and_pipelines/modular_pipelines.md index b9abb2ff79..e8e20ff0d7 100644 --- a/docs/source/nodes_and_pipelines/modular_pipelines.md +++ b/docs/source/nodes_and_pipelines/modular_pipelines.md @@ -79,21 +79,20 @@ If you want to do the reverse and remove a modular pipeline, you can use ``kedro ### Custom templates If you want to generate a pipeline with a custom Cookiecutter template, you can save it in `/templates/pipeline`. -The `kedro pipeline create` command will then pick up the custom template as the default. You can also specify the path to your custom +The `kedro pipeline create` command will pick up the custom template in your project as the default. You can also specify the path to your custom Cookiecutter pipeline template with the `--template` flag like this: ```bash kedro pipeline create --template ``` -A template folder passed via the command line using the `--template` argument will take precedence over any local templates -in `/templates/pipeline`, and you can use multiple different templates within a project using the `--template` argument. -Currently, only one template in the project `templates` directory is supported; use the command line flag if you need multiple -templates. +A template folder passed through the command line using the `--template` argument will take precedence over any local templates. +Kedro supports having a single pipeline template in your project. If you need to have many pipeline templates, consider saving them in a +separate folder and pointing to them with the `--template` flag. -#### Creating Custom Templates +#### Creating custom templates It is up to you to create functional Cookiecutter templates for custom modular pipelines. Please ensure you understand the -basic structure of a modular pipeline. The most important requirement is that your template should render to a valid, importable -Python module containing a `create_pipeline` function at the top level that returns a `Pipeline` object. You will also need appropriate +basic structure of a modular pipeline. Your template should render to a valid, importable Python module containing a +`create_pipeline` function at the top level that returns a `Pipeline` object. You will also need appropriate `config` and `tests` subdirectories that will be copied to the project `config` and `tests` directories when creating a pipeline. Currently, the `config` and `tests` directories need to follow the exact same layout as in the default template and cannot be customized, though the contents of the parameters and actual test file can be changed. File and folder names or structure @@ -101,7 +100,7 @@ do not matter beyond that and can be customized according to your needs. You can default template that Kedro](https://github.com/kedro-org/kedro/tree/main/kedro/templates/pipeline) uses as a starting point. Pipeline templates are rendered using [Cookiecutter](https://cookiecutter.readthedocs.io/), and must also contain a `cookiecutter.json` -See the [`cookiecutter.json` file in the Kedro default template]((https://github.com/kedro-org/kedro/tree/main/kedro/templates/pipeline/cookiecutter.json)) for an example. +See the [`cookiecutter.json` file in the Kedro default template](https://github.com/kedro-org/kedro/tree/main/kedro/templates/pipeline/cookiecutter.json) for an example. It is important to note that if you are embedding your custom pipeline template within a Kedro starter template, you must tell cookiecutter not to render this template when creating a new project from the starter. To do this, you must add [`_copy_without_render: ["templates"]`](https://cookiecutter.readthedocs.io/en/latest/advanced/copy_without_render.html) to the `cookiecutter.json` file for the starter From 88971f726597b806789bbe99b2c86d35c00b49cb Mon Sep 17 00:00:00 2001 From: Jo Stichbury Date: Fri, 8 Sep 2023 10:41:36 +0100 Subject: [PATCH 66/70] Update style to catch some more US spellings Signed-off-by: Jo Stichbury Signed-off-by: Jason Hite --- .github/styles/Kedro/ukspelling.yml | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/.github/styles/Kedro/ukspelling.yml b/.github/styles/Kedro/ukspelling.yml index fbd59b50ec..4767ee7225 100644 --- a/.github/styles/Kedro/ukspelling.yml +++ b/.github/styles/Kedro/ukspelling.yml @@ -4,7 +4,10 @@ link: 'https://github.com/kedro-org/kedro/wiki/Kedro-documentation-style-guide' ignorecase: true level: warning tokens: - - '(?:\w+)nized' + - '(?:\w+)zation' + - '(?:\w+)izing' + - '(?:\w+)izer' + - '(?:\w+)ized' - '(?:\w+)ize' - '(?:\w+)log' - '(?:\w+)lor' From 94c769f20ba026f7e0252adf41b284be60334967 Mon Sep 17 00:00:00 2001 From: Ankita Katiyar <110245118+ankatiyar@users.noreply.github.com> Date: Thu, 7 Sep 2023 17:52:53 +0100 Subject: [PATCH 67/70] Make dataset factory resolve nested dict properly (#2993) Signed-off-by: Jason Hite --- RELEASE.md | 2 ++ kedro/framework/cli/catalog.py | 18 +++++++++++++-- kedro/io/data_catalog.py | 41 +++++++++++++++++++++------------- tests/io/test_data_catalog.py | 38 ++++++++++++++++++++++++++++++- 4 files changed, 80 insertions(+), 19 deletions(-) diff --git a/RELEASE.md b/RELEASE.md index 155e5e44e3..bdcf127c37 100644 --- a/RELEASE.md +++ b/RELEASE.md @@ -13,6 +13,8 @@ * Allowed using of custom cookiecutter templates for creating pipelines with `--template` flag for `kedro pipeline create` or via `template/pipeline` folder. ## Bug fixes and other changes +* Updated dataset factories to resolve nested catalog config properly. + ## Documentation changes ## Breaking changes to the API ## Upcoming deprecations for Kedro 0.19.0 diff --git a/kedro/framework/cli/catalog.py b/kedro/framework/cli/catalog.py index b8849b5843..24816a9492 100644 --- a/kedro/framework/cli/catalog.py +++ b/kedro/framework/cli/catalog.py @@ -1,4 +1,5 @@ """A collection of CLI commands for working with Kedro catalog.""" +import copy from collections import defaultdict from itertools import chain @@ -84,7 +85,13 @@ def list_datasets(metadata: ProjectMetadata, pipeline, env): data_catalog._dataset_patterns, ds_name ) if matched_pattern: - ds_config = data_catalog._resolve_config(ds_name, matched_pattern) + ds_config_copy = copy.deepcopy( + data_catalog._dataset_patterns[matched_pattern] + ) + + ds_config = data_catalog._resolve_config( + ds_name, matched_pattern, ds_config_copy + ) factory_ds_by_type[ds_config["type"]].append(ds_name) default_ds = default_ds - set(chain.from_iterable(factory_ds_by_type.values())) @@ -244,7 +251,14 @@ def resolve_patterns(metadata: ProjectMetadata, env): data_catalog._dataset_patterns, ds_name ) if matched_pattern: - ds_config = data_catalog._resolve_config(ds_name, matched_pattern) + ds_config_copy = copy.deepcopy( + data_catalog._dataset_patterns[matched_pattern] + ) + + ds_config = data_catalog._resolve_config( + ds_name, matched_pattern, ds_config_copy + ) + ds_config["filepath"] = _trim_filepath( str(context.project_path) + "/", ds_config["filepath"] ) diff --git a/kedro/io/data_catalog.py b/kedro/io/data_catalog.py index 4cbe6c0142..031abb5b51 100644 --- a/kedro/io/data_catalog.py +++ b/kedro/io/data_catalog.py @@ -11,7 +11,7 @@ import logging import re from collections import defaultdict -from typing import Any, Dict, Iterable +from typing import Any, Dict from parse import parse @@ -388,7 +388,10 @@ def _get_dataset( if data_set_name not in self._data_sets and matched_pattern: # If the dataset is a patterned dataset, materialise it and add it to # the catalog - data_set_config = self._resolve_config(data_set_name, matched_pattern) + config_copy = copy.deepcopy(self._dataset_patterns[matched_pattern]) + data_set_config = self._resolve_config( + data_set_name, matched_pattern, config_copy + ) ds_layer = data_set_config.pop("layer", None) if ds_layer: self.layers = self.layers or {} @@ -436,27 +439,33 @@ def __contains__(self, data_set_name): return True return False + @classmethod def _resolve_config( - self, + cls, data_set_name: str, matched_pattern: str, + config: dict, ) -> dict[str, Any]: """Get resolved AbstractDataset from a factory config""" result = parse(matched_pattern, data_set_name) - config_copy = copy.deepcopy(self._dataset_patterns[matched_pattern]) # Resolve the factory config for the dataset - for key, value in config_copy.items(): - if isinstance(value, Iterable) and "}" in value: - # result.named: gives access to all dict items in the match result. - # format_map fills in dict values into a string with {...} placeholders - # of the same key name. - try: - config_copy[key] = str(value).format_map(result.named) - except KeyError as exc: - raise DatasetError( - f"Unable to resolve '{key}' for the pattern '{matched_pattern}'" - ) from exc - return config_copy + if isinstance(config, dict): + for key, value in config.items(): + config[key] = cls._resolve_config(data_set_name, matched_pattern, value) + elif isinstance(config, (list, tuple)): + config = [ + cls._resolve_config(data_set_name, matched_pattern, value) + for value in config + ] + elif isinstance(config, str) and "}" in config: + try: + config = str(config).format_map(result.named) + except KeyError as exc: + raise DatasetError( + f"Unable to resolve '{config}' from the pattern '{matched_pattern}'. Keys used in the configuration " + f"should be present in the dataset factory pattern." + ) from exc + return config def load(self, name: str, version: str = None) -> Any: """Loads a registered data set. diff --git a/tests/io/test_data_catalog.py b/tests/io/test_data_catalog.py index 9273fa5200..f4ac13974f 100644 --- a/tests/io/test_data_catalog.py +++ b/tests/io/test_data_catalog.py @@ -107,6 +107,29 @@ def config_with_dataset_factories(): } +@pytest.fixture +def config_with_dataset_factories_nested(): + return { + "catalog": { + "{brand}_cars": { + "type": "PartitionedDataset", + "path": "data/01_raw", + "dataset": "pandas.CSVDataSet", + "metadata": { + "my-plugin": { + "brand": "{brand}", + "list_config": [ + "NA", + "{brand}", + ], + "nested_list_dict": [{}, {"brand": "{brand}"}], + } + }, + }, + }, + } + + @pytest.fixture def config_with_dataset_factories_with_default(config_with_dataset_factories): config_with_dataset_factories["catalog"]["{default_dataset}"] = { @@ -840,7 +863,10 @@ def test_unmatched_key_error_when_parsing_config( ): """Check error raised when key mentioned in the config is not in pattern name""" catalog = DataCatalog.from_config(**config_with_dataset_factories_bad_pattern) - pattern = "Unable to resolve 'filepath' for the pattern '{type}@planes'" + pattern = ( + "Unable to resolve 'data/01_raw/{brand}_plane.pq' from the pattern '{type}@planes'. " + "Keys used in the configuration should be present in the dataset factory pattern." + ) with pytest.raises(DatasetError, match=re.escape(pattern)): catalog._get_dataset("jet@planes") @@ -896,3 +922,13 @@ def test_factory_config_versioned( microsecond=current_ts.microsecond // 1000 * 1000, tzinfo=None ) assert actual_timestamp == expected_timestamp + + def test_factory_nested_config(self, config_with_dataset_factories_nested): + catalog = DataCatalog.from_config(**config_with_dataset_factories_nested) + dataset = catalog._get_dataset("tesla_cars") + assert dataset.metadata["my-plugin"]["brand"] == "tesla" + assert dataset.metadata["my-plugin"]["list_config"] == ["NA", "tesla"] + assert dataset.metadata["my-plugin"]["nested_list_dict"] == [ + {}, + {"brand": "tesla"}, + ] From c6786be8a8b61774fc13b57abad0d10fdb654534 Mon Sep 17 00:00:00 2001 From: Jason Hite <868722+jasonmhite@users.noreply.github.com> Date: Fri, 8 Sep 2023 10:18:37 -0400 Subject: [PATCH 68/70] Apply suggestions from code review Co-authored-by: Jo Stichbury Signed-off-by: Jason Hite --- .../nodes_and_pipelines/modular_pipelines.md | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/docs/source/nodes_and_pipelines/modular_pipelines.md b/docs/source/nodes_and_pipelines/modular_pipelines.md index e8e20ff0d7..4cd1ba1e77 100644 --- a/docs/source/nodes_and_pipelines/modular_pipelines.md +++ b/docs/source/nodes_and_pipelines/modular_pipelines.md @@ -84,19 +84,19 @@ Cookiecutter pipeline template with the `--template` flag like this: ```bash kedro pipeline create --template ``` -A template folder passed through the command line using the `--template` argument will take precedence over any local templates. -Kedro supports having a single pipeline template in your project. If you need to have many pipeline templates, consider saving them in a +A template folder passed to `kedro pipeline create` using the `--template` argument will take precedence over any local templates. +Kedro supports having a single pipeline template in your project. If you need to have multiple pipeline templates, consider saving them in a separate folder and pointing to them with the `--template` flag. #### Creating custom templates -It is up to you to create functional Cookiecutter templates for custom modular pipelines. Please ensure you understand the +It is your responsibility to create functional Cookiecutter templates for custom modular pipelines. Please ensure you understand the basic structure of a modular pipeline. Your template should render to a valid, importable Python module containing a `create_pipeline` function at the top level that returns a `Pipeline` object. You will also need appropriate -`config` and `tests` subdirectories that will be copied to the project `config` and `tests` directories when creating a pipeline. -Currently, the `config` and `tests` directories need to follow the exact same layout as in the default template and cannot -be customized, though the contents of the parameters and actual test file can be changed. File and folder names or structure -do not matter beyond that and can be customized according to your needs. You can use [the +`config` and `tests` subdirectories that will be copied to the project `config` and `tests` directories when the pipeline is created. +The `config` and `tests` directories need to follow the same layout as in the default template and cannot +be customised, although the contents of the parameters and actual test file can be changed. File and folder names or structure +do not matter beyond that and can be customised according to your needs. You can use [the default template that Kedro](https://github.com/kedro-org/kedro/tree/main/kedro/templates/pipeline) uses as a starting point. Pipeline templates are rendered using [Cookiecutter](https://cookiecutter.readthedocs.io/), and must also contain a `cookiecutter.json` From 3742bedba382956b3b4a9e74021584ae0e83e3b6 Mon Sep 17 00:00:00 2001 From: Jason Hite <868722+jasonmhite@users.noreply.github.com> Date: Fri, 8 Sep 2023 10:20:08 -0400 Subject: [PATCH 69/70] Resolve suggestions from code review Signed-off-by: Jason Hite --- docs/source/nodes_and_pipelines/modular_pipelines.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/source/nodes_and_pipelines/modular_pipelines.md b/docs/source/nodes_and_pipelines/modular_pipelines.md index 4cd1ba1e77..09c0d5b54e 100644 --- a/docs/source/nodes_and_pipelines/modular_pipelines.md +++ b/docs/source/nodes_and_pipelines/modular_pipelines.md @@ -102,7 +102,7 @@ default template that Kedro](https://github.com/kedro-org/kedro/tree/main/kedro/ Pipeline templates are rendered using [Cookiecutter](https://cookiecutter.readthedocs.io/), and must also contain a `cookiecutter.json` See the [`cookiecutter.json` file in the Kedro default template](https://github.com/kedro-org/kedro/tree/main/kedro/templates/pipeline/cookiecutter.json) for an example. It is important to note that if you are embedding your custom pipeline template within a -Kedro starter template, you must tell cookiecutter not to render this template when creating a new project from the starter. To do this, +Kedro starter template, you must tell Cookiecutter not to render this template when creating a new project from the starter. To do this, you must add [`_copy_without_render: ["templates"]`](https://cookiecutter.readthedocs.io/en/latest/advanced/copy_without_render.html) to the `cookiecutter.json` file for the starter and not the `cookiecutter.json` for the pipeline template. From 30e838b7ae2b326180be26ef64196117f31c8a8d Mon Sep 17 00:00:00 2001 From: Ankita Katiyar <110245118+ankatiyar@users.noreply.github.com> Date: Tue, 12 Sep 2023 10:30:01 +0100 Subject: [PATCH 70/70] Revert changes ukspelling.yml Signed-off-by: Ankita Katiyar --- .github/styles/Kedro/ukspelling.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/.github/styles/Kedro/ukspelling.yml b/.github/styles/Kedro/ukspelling.yml index 14a9212375..fbd59b50ec 100644 --- a/.github/styles/Kedro/ukspelling.yml +++ b/.github/styles/Kedro/ukspelling.yml @@ -4,7 +4,6 @@ link: 'https://github.com/kedro-org/kedro/wiki/Kedro-documentation-style-guide' ignorecase: true level: warning tokens: - - '(?:\w+)nized' - '(?:\w+)ize' - '(?:\w+)log'