diff --git a/elt-common/pyproject.toml b/elt-common/pyproject.toml index 7ceca461..f03dc394 100644 --- a/elt-common/pyproject.toml +++ b/elt-common/pyproject.toml @@ -17,6 +17,7 @@ dependencies = [ "pydantic>=2.13.4", "dbt-core>=1.12.0", "dbt-trino>=1.10.3", + "python-dotenv>=1.2.2", ] diff --git a/elt-common/src/elt_common/ingest.py b/elt-common/src/elt_common/ingest.py index 99c021e7..6d29c8a5 100644 --- a/elt-common/src/elt_common/ingest.py +++ b/elt-common/src/elt_common/ingest.py @@ -10,6 +10,7 @@ import pyarrow as pa import pyarrow.compute as pc +from dotenv import load_dotenv, find_dotenv from pyiceberg.exceptions import NoSuchTableError from elt_common.extract import ( @@ -29,6 +30,8 @@ def run_ingest(job: ELTIngestManifest) -> dict[str, int]: """Import the extract function, call it, and write results to Iceberg.""" + load_dotenv(find_dotenv(usecwd=True), verbose=True) + # Create the object that will do the extraction. # Environment variables for the object's configuration must have been set # before reaching here. diff --git a/elt-common/uv.lock b/elt-common/uv.lock index d90c868f..4cde522e 100644 --- a/elt-common/uv.lock +++ b/elt-common/uv.lock @@ -652,6 +652,7 @@ dependencies = [ { name = "pydantic" }, { name = "pydantic-settings" }, { name = "pyiceberg", extra = ["pyiceberg-core"] }, + { name = "python-dotenv" }, { name = "s3fs" }, ] @@ -698,6 +699,7 @@ requires-dist = [ { name = "pydantic", specifier = ">=2.13.4" }, { name = "pydantic-settings", specifier = ">=2.14.2" }, { name = "pyiceberg", extras = ["pyiceberg-core"], specifier = ">=0.11.1" }, + { name = "python-dotenv", specifier = ">=1.2.2" }, { name = "s3fs", specifier = "<2026.2.0" }, { name = "sqlalchemy", marker = "extra == 'iceberg-maintenance'", specifier = ">=2.0.51" }, { name = "tenacity", marker = "extra == 'm365'", specifier = ">=9.1.2" }, @@ -848,7 +850,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/02/2f/28592176381b9ab2cafa12829ba7b472d177f3acc35d8fbcf3673d966fff/greenlet-3.3.0-cp313-cp313-macosx_11_0_universal2.whl", hash = "sha256:a1e41a81c7e2825822f4e068c48cb2196002362619e2d70b148f20a831c00739", size = 275140, upload-time = "2025-12-04T14:23:01.282Z" }, { url = "https://files.pythonhosted.org/packages/2c/80/fbe937bf81e9fca98c981fe499e59a3f45df2a04da0baa5c2be0dca0d329/greenlet-3.3.0-cp313-cp313-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9f515a47d02da4d30caaa85b69474cec77b7929b2e936ff7fb853d42f4bf8808", size = 599219, upload-time = "2025-12-04T14:50:08.309Z" }, { url = "https://files.pythonhosted.org/packages/c2/ff/7c985128f0514271b8268476af89aee6866df5eec04ac17dcfbc676213df/greenlet-3.3.0-cp313-cp313-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:7d2d9fd66bfadf230b385fdc90426fcd6eb64db54b40c495b72ac0feb5766c54", size = 610211, upload-time = "2025-12-04T14:57:43.968Z" }, - { url = "https://files.pythonhosted.org/packages/79/07/c47a82d881319ec18a4510bb30463ed6891f2ad2c1901ed5ec23d3de351f/greenlet-3.3.0-cp313-cp313-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:30a6e28487a790417d036088b3bcb3f3ac7d8babaa7d0139edbaddebf3af9492", size = 624311, upload-time = "2025-12-04T15:07:14.697Z" }, { url = "https://files.pythonhosted.org/packages/fd/8e/424b8c6e78bd9837d14ff7df01a9829fc883ba2ab4ea787d4f848435f23f/greenlet-3.3.0-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:087ea5e004437321508a8d6f20efc4cfec5e3c30118e1417ea96ed1d93950527", size = 612833, upload-time = "2025-12-04T14:26:03.669Z" }, { url = "https://files.pythonhosted.org/packages/b5/ba/56699ff9b7c76ca12f1cdc27a886d0f81f2189c3455ff9f65246780f713d/greenlet-3.3.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ab97cf74045343f6c60a39913fa59710e4bd26a536ce7ab2397adf8b27e67c39", size = 1567256, upload-time = "2025-12-04T15:04:25.276Z" }, { url = "https://files.pythonhosted.org/packages/1e/37/f31136132967982d698c71a281a8901daf1a8fbab935dce7c0cf15f942cc/greenlet-3.3.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:5375d2e23184629112ca1ea89a53389dddbffcf417dad40125713d88eb5f96e8", size = 1636483, upload-time = "2025-12-04T14:27:30.804Z" }, @@ -856,7 +857,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/d7/7c/f0a6d0ede2c7bf092d00bc83ad5bafb7e6ec9b4aab2fbdfa6f134dc73327/greenlet-3.3.0-cp314-cp314-macosx_11_0_universal2.whl", hash = "sha256:60c2ef0f578afb3c8d92ea07ad327f9a062547137afe91f38408f08aacab667f", size = 275671, upload-time = "2025-12-04T14:23:05.267Z" }, { url = "https://files.pythonhosted.org/packages/44/06/dac639ae1a50f5969d82d2e3dd9767d30d6dbdbab0e1a54010c8fe90263c/greenlet-3.3.0-cp314-cp314-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0a5d554d0712ba1de0a6c94c640f7aeba3f85b3a6e1f2899c11c2c0428da9365", size = 646360, upload-time = "2025-12-04T14:50:10.026Z" }, { url = "https://files.pythonhosted.org/packages/e0/94/0fb76fe6c5369fba9bf98529ada6f4c3a1adf19e406a47332245ef0eb357/greenlet-3.3.0-cp314-cp314-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:3a898b1e9c5f7307ebbde4102908e6cbfcb9ea16284a3abe15cab996bee8b9b3", size = 658160, upload-time = "2025-12-04T14:57:45.41Z" }, - { url = "https://files.pythonhosted.org/packages/93/79/d2c70cae6e823fac36c3bbc9077962105052b7ef81db2f01ec3b9bf17e2b/greenlet-3.3.0-cp314-cp314-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:dcd2bdbd444ff340e8d6bdf54d2f206ccddbb3ccfdcd3c25bf4afaa7b8f0cf45", size = 671388, upload-time = "2025-12-04T15:07:15.789Z" }, { url = "https://files.pythonhosted.org/packages/b8/14/bab308fc2c1b5228c3224ec2bf928ce2e4d21d8046c161e44a2012b5203e/greenlet-3.3.0-cp314-cp314-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5773edda4dc00e173820722711d043799d3adb4f01731f40619e07ea2750b955", size = 660166, upload-time = "2025-12-04T14:26:05.099Z" }, { url = "https://files.pythonhosted.org/packages/4b/d2/91465d39164eaa0085177f61983d80ffe746c5a1860f009811d498e7259c/greenlet-3.3.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:ac0549373982b36d5fd5d30beb8a7a33ee541ff98d2b502714a09f1169f31b55", size = 1615193, upload-time = "2025-12-04T15:04:27.041Z" }, { url = "https://files.pythonhosted.org/packages/42/1b/83d110a37044b92423084d52d5d5a3b3a73cafb51b547e6d7366ff62eff1/greenlet-3.3.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:d198d2d977460358c3b3a4dc844f875d1adb33817f0613f663a656f463764ccc", size = 1683653, upload-time = "2025-12-04T14:27:32.366Z" }, @@ -864,7 +864,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/a0/66/bd6317bc5932accf351fc19f177ffba53712a202f9df10587da8df257c7e/greenlet-3.3.0-cp314-cp314t-macosx_11_0_universal2.whl", hash = "sha256:d6ed6f85fae6cdfdb9ce04c9bf7a08d666cfcfb914e7d006f44f840b46741931", size = 282638, upload-time = "2025-12-04T14:25:20.941Z" }, { url = "https://files.pythonhosted.org/packages/30/cf/cc81cb030b40e738d6e69502ccbd0dd1bced0588e958f9e757945de24404/greenlet-3.3.0-cp314-cp314t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d9125050fcf24554e69c4cacb086b87b3b55dc395a8b3ebe6487b045b2614388", size = 651145, upload-time = "2025-12-04T14:50:11.039Z" }, { url = "https://files.pythonhosted.org/packages/9c/ea/1020037b5ecfe95ca7df8d8549959baceb8186031da83d5ecceff8b08cd2/greenlet-3.3.0-cp314-cp314t-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:87e63ccfa13c0a0f6234ed0add552af24cc67dd886731f2261e46e241608bee3", size = 654236, upload-time = "2025-12-04T14:57:47.007Z" }, - { url = "https://files.pythonhosted.org/packages/69/cc/1e4bae2e45ca2fa55299f4e85854606a78ecc37fead20d69322f96000504/greenlet-3.3.0-cp314-cp314t-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:2662433acbca297c9153a4023fe2161c8dcfdcc91f10433171cf7e7d94ba2221", size = 662506, upload-time = "2025-12-04T15:07:16.906Z" }, { url = "https://files.pythonhosted.org/packages/57/b9/f8025d71a6085c441a7eaff0fd928bbb275a6633773667023d19179fe815/greenlet-3.3.0-cp314-cp314t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:3c6e9b9c1527a78520357de498b0e709fb9e2f49c3a513afd5a249007261911b", size = 653783, upload-time = "2025-12-04T14:26:06.225Z" }, { url = "https://files.pythonhosted.org/packages/f6/c7/876a8c7a7485d5d6b5c6821201d542ef28be645aa024cfe1145b35c120c1/greenlet-3.3.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:286d093f95ec98fdd92fcb955003b8a3d054b4e2cab3e2707a5039e7b50520fd", size = 1614857, upload-time = "2025-12-04T15:04:28.484Z" }, { url = "https://files.pythonhosted.org/packages/4f/dc/041be1dff9f23dac5f48a43323cd0789cb798342011c19a248d9c9335536/greenlet-3.3.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:6c10513330af5b8ae16f023e8ddbfb486ab355d04467c4679c5cfe4659975dd9", size = 1676034, upload-time = "2025-12-04T14:27:33.531Z" }, diff --git a/elt-pipelines/README.md b/elt-pipelines/README.md index ddccbf5e..5f5fdd48 100644 --- a/elt-pipelines/README.md +++ b/elt-pipelines/README.md @@ -39,10 +39,12 @@ Pipelines are run using the `elt` CLI tool. As an example, with `elt-pipelines` connection, so configuration follows [their approach](https://py.iceberg.apache.org/configuration/). See the [getting started guide](../docs-devel/getting-started.md#configure-iceberg-connection) for the local configuration values -- Any pipelines that include a `config_cls` require/have optional configuration values to be set +- Any pipelines that include a `config_cls` require/have optional configuration values - These use [`pydantic_settings`](https://pydantic.dev/docs/validation/latest/concepts/pydantic_settings/) - Environment variables are used to set the values. The name of the variable(s) must be prefixed with `__`, where `` is the name of the pipeline + - Values for variables can also be specified in a `.env` file in the working directory. If a value is present both + in the `.env` file and as an environment variable, the environment variable is preferred ## Writing a pipeline diff --git a/elt-pipelines/facility_ops/ingest/computing/jira/jira.py b/elt-pipelines/facility_ops/ingest/computing/jira/jira.py index 5005bf2e..cf3ccf8c 100644 --- a/elt-pipelines/facility_ops/ingest/computing/jira/jira.py +++ b/elt-pipelines/facility_ops/ingest/computing/jira/jira.py @@ -64,7 +64,7 @@ def extract_resource_properties(self) -> Iterator[tuple[str, ResourceProperties] ), ) yield ( - "issue_status_changelog", + "issue_status_changelogs", ResourceProperties( extractor=self.extract_issue_status_changelogs, write_properties=ResourceWriteProperties(write_mode="replace"), diff --git a/elt-pipelines/facility_ops/transform/models/intermediate/computing/int_times_in_status.sql b/elt-pipelines/facility_ops/transform/models/intermediate/computing/int_times_in_status.sql new file mode 100644 index 00000000..925b9ba2 --- /dev/null +++ b/elt-pipelines/facility_ops/transform/models/intermediate/computing/int_times_in_status.sql @@ -0,0 +1,43 @@ +-- Get the differences between the status. No need for final status, so this query is perfectly suitable +with status_to_from as ( + select + issue_key, + from_status as status, + lag(changed_at) over (partition by issue_key order by changed_at) as status_from, + changed_at as status_to + from {{ ref('stg_jira_issue_status_changelogs') }} as changelogs +), + +-- Populate null values of status from with the issue creation date. Join required +nn_status_to_from as ( + select + status_to_from.issue_key, + status_to_from.status, + COALESCE(status_to_from.status_from, issues.created) as status_from, + status_to_from.status_to + from status_to_from + inner join facility_ops_landing.computing_jira.isis_jira_issues as issues + on status_to_from.issue_key = issues.issue_key +), + +-- Subtract to and from date +status_durations as ( + select + issue_key, + status, + date_diff('second', status_from, status_to) as status_duration + from nn_status_to_from +), + +-- Aggregate similar statuses and add their durations +times_in_status as ( + select + issue_key, + status, + sum(status_duration) as time_in_status + from status_durations + group by issue_key, + status +) + +select * from times_in_status diff --git a/elt-pipelines/facility_ops/transform/models/marts/computing/time_in_status_computing_infrastructure.sql b/elt-pipelines/facility_ops/transform/models/marts/computing/time_in_status_computing_infrastructure.sql new file mode 100644 index 00000000..316d5d57 --- /dev/null +++ b/elt-pipelines/facility_ops/transform/models/marts/computing/time_in_status_computing_infrastructure.sql @@ -0,0 +1,84 @@ +{{ + config( + on_table_exists = 'drop' +) +}} + +with times_in_status as ( + select * from {{ ref('int_times_in_status') }} where issue_key like 'CI-%' +), + +time_in_status_data_driven_facility as ( + select + issue_key, + MAX( + case + when status = 'analyzing' then time_in_status + else null + end + ) as time_in_analysing_secs, + MAX( + case + when status = 'backlog' then time_in_status + else null + end + ) as time_in_backlog_secs, + MAX( + case + when status = 'done' then time_in_status + else null + end + ) as time_in_done_secs, + MAX( + case + when status = 'funnel' then time_in_status + else null + end + ) as time_in_funnel_secs, + MAX( + case + when status = 'implementing' then time_in_status + else null + end + ) as time_in_implementing_secs, + MAX( + case + when status = 'implementing (mvp)' then time_in_status + else null + end + ) as time_in_implementing_mvp_secs, + MAX( + case + when status = 'implementing (persevere)' then time_in_status + else null + end + ) as time_in_implementing_persevere_secs, + MAX( + case + when status = 'in progress' then time_in_status + else null + end + ) as time_in_in_progress_secs, + MAX( + case + when status = 'portfolio backlog' then time_in_status + else null + end + ) as time_in_portfolio_backlog_secs, + MAX( + case + when status = 'ready' then time_in_status + else null + end + ) as time_in_ready_secs, + MAX( + case + when status = 'reviewing' then time_in_status + else null + end + ) as time_in_reviewing_secs + from times_in_status + group by issue_key + +) +select * from time_in_status_data_driven_facility diff --git a/elt-pipelines/facility_ops/transform/models/marts/computing/time_in_status_computing_infrastructure.yml b/elt-pipelines/facility_ops/transform/models/marts/computing/time_in_status_computing_infrastructure.yml new file mode 100644 index 00000000..23044db8 --- /dev/null +++ b/elt-pipelines/facility_ops/transform/models/marts/computing/time_in_status_computing_infrastructure.yml @@ -0,0 +1,19 @@ +models: + - name: time_in_status_computing_infrastructure + description: > + Get length of time a Computing Infrastructure issue spends in each status for user software issues. + columns: + - name: issue_key + data_tests: + - not_null + - name: time_in_analysing_secs + - name: time_in_backlog_secs + - name: time_in_done_secs + - name: time_in_funnel_secs + - name: time_in_implementing_secs + - name: time_in_implementing_mvp_secs + - name: time_in_implementing_persevere_secs + - name: time_in_in_progress_secs + - name: time_in_portfolio_backlog_secs + - name: time_in_ready_secs + - name: time_in_reviewing_secs diff --git a/elt-pipelines/facility_ops/transform/models/marts/computing/time_in_status_data_driven_facility.sql b/elt-pipelines/facility_ops/transform/models/marts/computing/time_in_status_data_driven_facility.sql new file mode 100644 index 00000000..9544ea8a --- /dev/null +++ b/elt-pipelines/facility_ops/transform/models/marts/computing/time_in_status_data_driven_facility.sql @@ -0,0 +1,90 @@ +{{ + config( + on_table_exists = 'drop' +) +}} + +with times_in_status as ( + select * from {{ ref('int_times_in_status') }} where issue_key like 'DD-%' +), + +time_in_status_data_driven_facility as ( + select + issue_key, + MAX( + case + when status = 'analyzing' then time_in_status + else null + end + ) as time_in_analysing_secs, + MAX( + case + when status = 'backlog' then time_in_status + else null + end + ) as time_in_backlog_secs, + MAX( + case + when status = 'done' then time_in_status + else null + end + ) as time_in_done_secs, + MAX( + case + when status = 'funnel' then time_in_status + else null + end + ) as time_in_funnel_secs, + MAX( + case + when status = 'implementing' then time_in_status + else null + end + ) as time_in_implementing_secs, + MAX( + case + when status = 'implementing (mvp)' then time_in_status + else null + end + ) as time_in_implementing_mvp_secs, + MAX( + case + when status = 'implementing (persevere)' then time_in_status + else null + end + ) as time_in_implementing_persevere_secs, + MAX( + case + when status = 'in progress' then time_in_status + else null + end + ) as time_in_in_progress_secs, + MAX( + case + when status = 'portfolio backlog' then time_in_status + else null + end + ) as time_in_portfolio_backlog_secs, + MAX( + case + when status = 'ready' then time_in_status + else null + end + ) as time_in_ready_secs, + MAX( + case + when status = 'reviewing' then time_in_status + else null + end + ) as time_in_reviewing_secs, + MAX( + case + when status = 'selected for development' then time_in_status + else null + end + ) as time_in_selected_for_development_secs + from times_in_status + group by issue_key + +) +select * from time_in_status_data_driven_facility diff --git a/elt-pipelines/facility_ops/transform/models/marts/computing/time_in_status_data_driven_facility.yml b/elt-pipelines/facility_ops/transform/models/marts/computing/time_in_status_data_driven_facility.yml new file mode 100644 index 00000000..4b1ed2d9 --- /dev/null +++ b/elt-pipelines/facility_ops/transform/models/marts/computing/time_in_status_data_driven_facility.yml @@ -0,0 +1,20 @@ +models: + - name: time_in_status_data_driven_facility + description: > + Get length of time a Data-Driven Facility issue spends in each status for user software issues. + columns: + - name: issue_key + data_tests: + - not_null + - name: time_in_analysing_secs + - name: time_in_backlog_secs + - name: time_in_done_secs + - name: time_in_funnel_secs + - name: time_in_implementing_secs + - name: time_in_implementing_mvp_secs + - name: time_in_implementing_persevere_secs + - name: time_in_in_progress_secs + - name: time_in_portfolio_backlog_secs + - name: time_in_ready_secs + - name: time_in_reviewing_secs + - name: time_in_selected_for_development_secs diff --git a/elt-pipelines/facility_ops/transform/models/marts/computing/time_in_status_scientific_software.sql b/elt-pipelines/facility_ops/transform/models/marts/computing/time_in_status_scientific_software.sql new file mode 100644 index 00000000..5c0845f2 --- /dev/null +++ b/elt-pipelines/facility_ops/transform/models/marts/computing/time_in_status_scientific_software.sql @@ -0,0 +1,84 @@ +{{ + config( + on_table_exists = 'drop' +) +}} + +with times_in_status as ( + select * from {{ ref('int_times_in_status') }} where issue_key like 'SS-%' +), + +time_in_status_scientific_software as ( + select + issue_key, + MAX( + case + when status = 'analyzing' then time_in_status + else null + end + ) as time_in_analysing_secs, + MAX( + case + when status = 'backlog' then time_in_status + else null + end + ) as time_in_backlog_secs, + MAX( + case + when status = 'done' then time_in_status + else null + end + ) as time_in_done_secs, + MAX( + case + when status = 'funnel' then time_in_status + else null + end + ) as time_in_funnel_secs, + MAX( + case + when status = 'implementing' then time_in_status + else null + end + ) as time_in_implementing_secs, + MAX( + case + when status = 'implementing (mvp)' then time_in_status + else null + end + ) as time_in_implementing_mvp_secs, + MAX( + case + when status = 'implementing (persevere)' then time_in_status + else null + end + ) as time_in_implementing_persevere_secs, + MAX( + case + when status = 'in progress' then time_in_status + else null + end + ) as time_in_in_progress_secs, + MAX( + case + when status = 'portfolio backlog' then time_in_status + else null + end + ) as time_in_portfolio_backlog_secs, + MAX( + case + when status = 'ready' then time_in_status + else null + end + ) as time_in_ready_secs, + MAX( + case + when status = 'reviewing' then time_in_status + else null + end + ) as time_in_reviewing_secs + from times_in_status + group by issue_key + +) +select * from time_in_status_scientific_software diff --git a/elt-pipelines/facility_ops/transform/models/marts/computing/time_in_status_scientific_software.yml b/elt-pipelines/facility_ops/transform/models/marts/computing/time_in_status_scientific_software.yml new file mode 100644 index 00000000..d9fb7ff1 --- /dev/null +++ b/elt-pipelines/facility_ops/transform/models/marts/computing/time_in_status_scientific_software.yml @@ -0,0 +1,19 @@ +models: + - name: time_in_status_scientific_software + description: > + Get length of time a Scientific Software issue spends in each status + columns: + - name: issue_key + data_tests: + - not_null + - name: time_in_analysing_secs + - name: time_in_backlog_secs + - name: time_in_done_secs + - name: time_in_funnel_secs + - name: time_in_implementing_secs + - name: time_in_implementing_mvp_secs + - name: time_in_implementing_persevere_secs + - name: time_in_in_progress_secs + - name: time_in_portfolio_backlog_secs + - name: time_in_ready_secs + - name: time_in_reviewing_secs diff --git a/elt-pipelines/facility_ops/transform/models/marts/computing/time_in_status_user_software.sql b/elt-pipelines/facility_ops/transform/models/marts/computing/time_in_status_user_software.sql new file mode 100644 index 00000000..08067e37 --- /dev/null +++ b/elt-pipelines/facility_ops/transform/models/marts/computing/time_in_status_user_software.sql @@ -0,0 +1,102 @@ +{{ + config( + on_table_exists = 'drop' +) +}} + +with times_in_status as ( + select * from {{ ref('int_times_in_status') }} where issue_key like 'US-%' +), + +time_in_status_user_software as ( + select + issue_key, + MAX( + case + when status = 'analyzing' then time_in_status + else null + end + ) as time_in_analysing_secs, + MAX( + case + when status = 'backlog' then time_in_status + else null + end + ) as time_in_backlog_secs, + MAX( + case + when status = 'blocked' then time_in_status + else null + end + ) as time_in_blocked_secs, + MAX( + case + when status = 'closed' then time_in_status + else null + end + ) as time_in_closed_secs, + MAX( + case + when status = 'done' then time_in_status + else null + end + ) as time_in_done_secs, + MAX( + case + when status = 'funnel' then time_in_status + else null + end + ) as time_in_funnel_secs, + MAX( + case + when status = 'implementing' then time_in_status + else null + end + ) as time_in_implementing_secs, + MAX( + case + when status = 'implementing (mvp)' then time_in_status + else null + end + ) as time_in_implementing_mvp_secs, + MAX( + case + when status = 'implementing (persevere)' then time_in_status + else null + end + ) as time_in_implementing_persevere_secs, + MAX( + case + when status = 'in progress' then time_in_status + else null + end + ) as time_in_in_progress_secs, + MAX( + case + when status = 'portfolio backlog' then time_in_status + else null + end + ) as time_in_portfolio_backlog_secs, + MAX( + case + when status = 'ready' then time_in_status + else null + end + ) as time_in_ready_secs, + MAX( + case + when status = 'reviewing' then time_in_status + else null + end + ) as time_in_reviewing_secs, + MAX( + case + when status = 'selected for development' then time_in_status + else null + end + ) as time_in_selected_for_development_secs + from times_in_status + group by issue_key + +) +select * from time_in_status_user_software diff --git a/elt-pipelines/facility_ops/transform/models/marts/computing/time_in_status_user_software.yml b/elt-pipelines/facility_ops/transform/models/marts/computing/time_in_status_user_software.yml new file mode 100644 index 00000000..91ac4e6d --- /dev/null +++ b/elt-pipelines/facility_ops/transform/models/marts/computing/time_in_status_user_software.yml @@ -0,0 +1,22 @@ +models: + - name: time_in_status_user_software + description: > + Get length of time a User Software issue spends in each status for user software issues. + columns: + - name: issue_key + data_tests: + - not_null + - name: time_in_analysing_secs + - name: time_in_backlog_secs + - name: time_in_blocked_secs + - name: time_in_closed_secs + - name: time_in_done_secs + - name: time_in_funnel_secs + - name: time_in_implementing_secs + - name: time_in_implementing_mvp_secs + - name: time_in_implementing_persevere_secs + - name: time_in_in_progress_secs + - name: time_in_portfolio_backlog_secs + - name: time_in_ready_secs + - name: time_in_reviewing_secs + - name: time_in_selected_for_development_secs diff --git a/elt-pipelines/facility_ops/transform/models/staging/accelerator/_accelerator__sources.yml b/elt-pipelines/facility_ops/transform/models/staging/accelerator/_accelerator__sources.yml new file mode 100644 index 00000000..28b76ead --- /dev/null +++ b/elt-pipelines/facility_ops/transform/models/staging/accelerator/_accelerator__sources.yml @@ -0,0 +1,12 @@ +version: 2 + +sources: + - name: accelerator_opralogweb + database: facility_ops_landing + tables: + - name: AdditionalColumns + - name: ChapterEntry + - name: Entries + - name: LogBookChapter + - name: Logbooks + - name: MoreEntryColumns diff --git a/elt-pipelines/facility_ops/transform/models/staging/accelerator/base/base_opralogweb__additional_columns.sql b/elt-pipelines/facility_ops/transform/models/staging/accelerator/base/base_opralogweb__additional_columns.sql new file mode 100644 index 00000000..b80c4a9a --- /dev/null +++ b/elt-pipelines/facility_ops/transform/models/staging/accelerator/base/base_opralogweb__additional_columns.sql @@ -0,0 +1,22 @@ +with + +source as ( + + select * from {{ source('accelerator_opralogweb', 'AdditionalColumns') }} + +), + +renamed as ( + + select + + AdditionalColumnId as additional_column_id, + trim(ColTitle) as column_title + +from + + source + +) + +select * from renamed diff --git a/elt-pipelines/facility_ops/transform/models/staging/accelerator/base/base_opralogweb__additional_columns.yml b/elt-pipelines/facility_ops/transform/models/staging/accelerator/base/base_opralogweb__additional_columns.yml new file mode 100644 index 00000000..3463a69f --- /dev/null +++ b/elt-pipelines/facility_ops/transform/models/staging/accelerator/base/base_opralogweb__additional_columns.yml @@ -0,0 +1,7 @@ +models: + - name: base_opralogweb__additional_columns + description: Opralogweb additional_columns table with basic cleaning + columns: + - name: additional_column_id + tests: + - not_null diff --git a/elt-pipelines/facility_ops/transform/models/staging/accelerator/base/base_opralogweb__chapter_entry.sql b/elt-pipelines/facility_ops/transform/models/staging/accelerator/base/base_opralogweb__chapter_entry.sql new file mode 100644 index 00000000..15c8f39e --- /dev/null +++ b/elt-pipelines/facility_ops/transform/models/staging/accelerator/base/base_opralogweb__chapter_entry.sql @@ -0,0 +1,24 @@ +with + +source as ( + + select * from {{ source('accelerator_opralogweb', 'ChapterEntry') }} + +), + +renamed as ( + +select + + EntryId as entry_id, + PrincipalLogbook as principal_logbook, + LogbookChapterNo as logbook_chapter_no, + LogbookId as logbook_id + +from + + source + +) + +select * from renamed diff --git a/elt-pipelines/facility_ops/transform/models/staging/accelerator/base/base_opralogweb__chapter_entry.yml b/elt-pipelines/facility_ops/transform/models/staging/accelerator/base/base_opralogweb__chapter_entry.yml new file mode 100644 index 00000000..c57b7ff0 --- /dev/null +++ b/elt-pipelines/facility_ops/transform/models/staging/accelerator/base/base_opralogweb__chapter_entry.yml @@ -0,0 +1,7 @@ +models: + - name: base_opralogweb__chapter_entry + description: Opralogweb chapter_entry table with basic cleaning + columns: + - name: entry_id + tests: + - not_null diff --git a/elt-pipelines/facility_ops/transform/models/staging/accelerator/base/base_opralogweb__entries.sql b/elt-pipelines/facility_ops/transform/models/staging/accelerator/base/base_opralogweb__entries.sql new file mode 100644 index 00000000..ecfd6624 --- /dev/null +++ b/elt-pipelines/facility_ops/transform/models/staging/accelerator/base/base_opralogweb__entries.sql @@ -0,0 +1,26 @@ +with + +source as ( + + select * from {{ source('accelerator_opralogweb', 'Entries') }} + +), + +renamed as ( + + select + + EntryId as entry_id, + with_timezone(EntryTimestamp, 'UTC') as fault_occurred_at, + cast({{ adapter.quote('EntryTimestamp') }} as date) as fault_date, + trim(AdditionalComment) as fault_description, + case + when LogicallyDeleted = 'Y' then true + else false + end as logically_deleted + + from source + +) + +select * from renamed diff --git a/elt-pipelines/facility_ops/transform/models/staging/accelerator/base/base_opralogweb__entries.yml b/elt-pipelines/facility_ops/transform/models/staging/accelerator/base/base_opralogweb__entries.yml new file mode 100644 index 00000000..ef22cea4 --- /dev/null +++ b/elt-pipelines/facility_ops/transform/models/staging/accelerator/base/base_opralogweb__entries.yml @@ -0,0 +1,8 @@ +models: + - name: base_opralogweb__entries + description: > + Opralogweb "entries" table with basic cleaning. + columns: + - name: entry_id + tests: + - not_null diff --git a/elt-pipelines/facility_ops/transform/models/staging/accelerator/base/base_opralogweb__logbook_chapter.sql b/elt-pipelines/facility_ops/transform/models/staging/accelerator/base/base_opralogweb__logbook_chapter.sql new file mode 100644 index 00000000..d615464c --- /dev/null +++ b/elt-pipelines/facility_ops/transform/models/staging/accelerator/base/base_opralogweb__logbook_chapter.sql @@ -0,0 +1,21 @@ +with + +source as ( + + select * from {{ source('accelerator_opralogweb', 'LogBookChapter') }} + +), + +renamed as ( + + select + + LogBookChapterNo as logbook_chapter_no + + from + + source + +) + +select * from renamed diff --git a/elt-pipelines/facility_ops/transform/models/staging/accelerator/base/base_opralogweb__logbook_chapter.yml b/elt-pipelines/facility_ops/transform/models/staging/accelerator/base/base_opralogweb__logbook_chapter.yml new file mode 100644 index 00000000..6401cc8c --- /dev/null +++ b/elt-pipelines/facility_ops/transform/models/staging/accelerator/base/base_opralogweb__logbook_chapter.yml @@ -0,0 +1,7 @@ +models: + - name: base_opralogweb__logbook_chapter + description: Opralogweb logbook_chapter table with basic cleaning + columns: + - name: logbook_chapter_no + tests: + - not_null diff --git a/elt-pipelines/facility_ops/transform/models/staging/accelerator/base/base_opralogweb__logbooks.sql b/elt-pipelines/facility_ops/transform/models/staging/accelerator/base/base_opralogweb__logbooks.sql new file mode 100644 index 00000000..fada42d6 --- /dev/null +++ b/elt-pipelines/facility_ops/transform/models/staging/accelerator/base/base_opralogweb__logbooks.sql @@ -0,0 +1,21 @@ +with + +source as ( + + select * from {{ source('accelerator_opralogweb', 'Logbooks') }} + +), + +renamed as ( + + select + + LogbookId as logbook_id, + LogbookName as logbook_name + + from + + source +) + +select * from renamed diff --git a/elt-pipelines/facility_ops/transform/models/staging/accelerator/base/base_opralogweb__logbooks.yml b/elt-pipelines/facility_ops/transform/models/staging/accelerator/base/base_opralogweb__logbooks.yml new file mode 100644 index 00000000..21346c87 --- /dev/null +++ b/elt-pipelines/facility_ops/transform/models/staging/accelerator/base/base_opralogweb__logbooks.yml @@ -0,0 +1,7 @@ +models: + - name: base_opralogweb__logbooks + description: Opralogweb logbooks table with basic cleaning + columns: + - name: logbook_id + tests: + - not_null diff --git a/elt-pipelines/facility_ops/transform/models/staging/accelerator/base/base_opralogweb__more_entry_columns.sql b/elt-pipelines/facility_ops/transform/models/staging/accelerator/base/base_opralogweb__more_entry_columns.sql new file mode 100644 index 00000000..8c8d28a2 --- /dev/null +++ b/elt-pipelines/facility_ops/transform/models/staging/accelerator/base/base_opralogweb__more_entry_columns.sql @@ -0,0 +1,22 @@ +with + +source as ( + select * from {{ source('accelerator_opralogweb', 'MoreEntryColumns') }} +), + +renamed as ( + + select + + EntryId as entry_id, + {{ normalize_whitespace('ColData') }} as string_data, + NumberValue as number_data, + AdditionalColumnId as additional_column_id + + from + + source + +) + +select * from renamed diff --git a/elt-pipelines/facility_ops/transform/models/staging/accelerator/base/base_opralogweb__more_entry_columns.yml b/elt-pipelines/facility_ops/transform/models/staging/accelerator/base/base_opralogweb__more_entry_columns.yml new file mode 100644 index 00000000..8794b332 --- /dev/null +++ b/elt-pipelines/facility_ops/transform/models/staging/accelerator/base/base_opralogweb__more_entry_columns.yml @@ -0,0 +1,7 @@ +models: + - name: base_opralogweb__more_entry_columns + description: Opralogweb more_entry_columns table with basic cleaning + columns: + - name: entry_id + tests: + - not_null diff --git a/elt-pipelines/facility_ops/transform/models/staging/accelerator/stg_opralogweb__mcr_equipment_downtime.sql b/elt-pipelines/facility_ops/transform/models/staging/accelerator/stg_opralogweb__mcr_equipment_downtime.sql new file mode 100644 index 00000000..e0b780ba --- /dev/null +++ b/elt-pipelines/facility_ops/transform/models/staging/accelerator/stg_opralogweb__mcr_equipment_downtime.sql @@ -0,0 +1,97 @@ +{%- set OPRALOG_EPOCH = dbt.string_literal("2017-04-25") -%} +{%- set MCR_LOGBOOK = dbt.string_literal("MCR Running Log") -%} + +with + +staging_entries as ( select * from {{ ref('base_opralogweb__entries') }} ), + +staging_chapter_entry as ( select * from {{ ref('base_opralogweb__chapter_entry') }} ), + +staging_logbook_chapter as ( select * from {{ ref('base_opralogweb__logbook_chapter') }} ), + +staging_logbooks as ( select * from {{ ref('base_opralogweb__logbooks') }} ), + +staging_more_entry_columns as ( select * from {{ ref('base_opralogweb__more_entry_columns') }} ), + +staging_additional_columns as ( select * from {{ ref('base_opralogweb__additional_columns') }} ), + +denormalized as ( + select + + staging_entries.entry_id, + staging_entries.fault_occurred_at, + staging_entries.fault_date, + staging_additional_columns.column_title, + staging_more_entry_columns.string_data, + staging_more_entry_columns.number_data, + staging_entries.fault_description + + from + staging_entries + join staging_chapter_entry on staging_chapter_entry.entry_id = staging_entries.entry_id + join staging_logbook_chapter on staging_logbook_chapter.logbook_chapter_no = staging_chapter_entry.logbook_chapter_no + join staging_logbooks on staging_logbooks.logbook_id = staging_chapter_entry.logbook_id + left outer join staging_more_entry_columns on staging_more_entry_columns.entry_id = staging_entries.entry_id + left outer join staging_additional_columns on staging_additional_columns.additional_column_id = staging_more_entry_columns.additional_column_id + + where + + staging_entries.logically_deleted = false + and staging_entries.fault_date >= from_iso8601_date({{ OPRALOG_EPOCH }}) + and staging_logbooks.logbook_name = {{ MCR_LOGBOOK }} + and staging_chapter_entry.logbook_id = staging_chapter_entry.principal_logbook + and staging_additional_columns.column_title in ('Equipment', 'Group', 'Lost Time', 'Group Leader comments') + and ( + staging_more_entry_columns.string_data is not null + or staging_more_entry_columns.number_data is not null + ) +), + +mcr_equipment_downtime as ( + select + * + from + ( + select + + min( + case + column_title + when 'Equipment' then string_data + end + ) as equipment, + min( + case + column_title + when 'Lost Time' then number_data + end + ) as downtime_mins, + fault_date, + fault_occurred_at, + min( + case + column_title + when 'Group' then string_data + end + ) as {{ adapter.quote('group') }}, + fault_description, + min( + case + column_title + when 'Group Leader comments' then string_data + end + ) as managers_comments + from + denormalized + group by + fault_occurred_at, + fault_date, + fault_description + ) + where + equipment is not null + and downtime_mins is not null + and {{ adapter.quote('group') }} is not null +) + +select * from mcr_equipment_downtime diff --git a/elt-pipelines/facility_ops/transform/models/staging/accelerator/stg_opralogweb__mcr_equipment_downtime.yml b/elt-pipelines/facility_ops/transform/models/staging/accelerator/stg_opralogweb__mcr_equipment_downtime.yml new file mode 100644 index 00000000..ff022ffc --- /dev/null +++ b/elt-pipelines/facility_ops/transform/models/staging/accelerator/stg_opralogweb__mcr_equipment_downtime.yml @@ -0,0 +1,41 @@ +models: + - name: stg_opralogweb_mcr_equipment_downtime + description: > + Equipment downtime records for the MCR assembled from the relational + database of the web-based Opralog application. Entries before cycle 17/01 + are discarded as Opralog was in testing mode before this. + +unit_tests: + - name: test_stg_opralogweb__mcr_equipment_downtime + description: "Check null & before epoch entries are discarded" + model: stg_opralogweb__mcr_equipment_downtime + given: + - input: ref('base_opralogweb__entries') + format: sql + fixture: base_opralogweb__entries + - input: ref('base_opralogweb__chapter_entry') + format: sql + fixture: base_opralogweb__chapter_entry + - input: ref('base_opralogweb__logbook_chapter') + format: sql + fixture: base_opralogweb__logbook_chapter + - input: ref('base_opralogweb__logbooks') + format: sql + fixture: base_opralogweb__logbooks + - input: ref('base_opralogweb__more_entry_columns') + format: sql + fixture: base_opralogweb__more_entry_columns + - input: ref('base_opralogweb__additional_columns') + format: sql + fixture: base_opralogweb__additional_columns + expect: + rows: + - { + equipment: "Equipment 101", + downtime_mins: 5.3, + fault_date: "2024-01-01", + fault_occurred_at: "2024-01-01 00:00:01 UTC", + group: "Group 101", + fault_description: "Comment 101", + managers_comments: null, + } diff --git a/elt-pipelines/facility_ops/transform/models/staging/computing/_computing__sources.yml b/elt-pipelines/facility_ops/transform/models/staging/computing/_computing__sources.yml index a8e912f2..3ec40230 100644 --- a/elt-pipelines/facility_ops/transform/models/staging/computing/_computing__sources.yml +++ b/elt-pipelines/facility_ops/transform/models/staging/computing/_computing__sources.yml @@ -5,3 +5,4 @@ sources: database: facility_ops_landing tables: - name: isis_jira_issues + - name: issue_status_changelogs diff --git a/elt-pipelines/facility_ops/transform/models/staging/computing/stg_jira_issue_status_changelogs.sql b/elt-pipelines/facility_ops/transform/models/staging/computing/stg_jira_issue_status_changelogs.sql new file mode 100644 index 00000000..5a4891d2 --- /dev/null +++ b/elt-pipelines/facility_ops/transform/models/staging/computing/stg_jira_issue_status_changelogs.sql @@ -0,0 +1,14 @@ +with source as ( +select * from {{source('computing_jira', 'issue_status_changelogs')}} +), + +cleaned as ( +select + issue_key as issue_key, + TRIM(LOWER(from_status)) as from_status, + TRIM(LOWER(to_status)) as to_status, + changed_at as changed_at +from source +) + +select * from cleaned diff --git a/elt-pipelines/facility_ops/transform/models/staging/computing/stg_jira_issue_status_changelogs.yml b/elt-pipelines/facility_ops/transform/models/staging/computing/stg_jira_issue_status_changelogs.yml new file mode 100644 index 00000000..d6230403 --- /dev/null +++ b/elt-pipelines/facility_ops/transform/models/staging/computing/stg_jira_issue_status_changelogs.yml @@ -0,0 +1,17 @@ +models: + - name: stg_jira_issue_status_changelogs + description: > + Get length of time an issue spends in each status + columns: + - name: issue_key + data_tests: + - not_null + - name: from_status + data_tests: + - not_null + - name: to_status + data_tests: + - not_null + - name: changed_at + data_tests: + - not_null diff --git a/elt-pipelines/pyproject.toml b/elt-pipelines/pyproject.toml index 5bdc8562..c8475923 100644 --- a/elt-pipelines/pyproject.toml +++ b/elt-pipelines/pyproject.toml @@ -9,6 +9,9 @@ dependencies = [ "pydantic-settings>=2.14.2", ] +[tool.setuptools] +packages = ["facility_ops", "fase"] + [project.optional-dependencies] proposal = [ "sqlalchemy[postgresql-psycopgbinary]>=2.0.0", diff --git a/elt-pipelines/uv.lock b/elt-pipelines/uv.lock index 3e604567..82b942bf 100644 --- a/elt-pipelines/uv.lock +++ b/elt-pipelines/uv.lock @@ -698,6 +698,7 @@ dependencies = [ { name = "pydantic" }, { name = "pydantic-settings" }, { name = "pyiceberg", extra = ["pyiceberg-core"] }, + { name = "python-dotenv" }, { name = "s3fs" }, ] @@ -720,6 +721,7 @@ requires-dist = [ { name = "pydantic", specifier = ">=2.13.4" }, { name = "pydantic-settings", specifier = ">=2.14.2" }, { name = "pyiceberg", extras = ["pyiceberg-core"], specifier = ">=0.11.1" }, + { name = "python-dotenv", specifier = ">=1.2.2" }, { name = "s3fs", specifier = "<2026.2.0" }, { name = "sqlalchemy", marker = "extra == 'iceberg-maintenance'", specifier = ">=2.0.51" }, { name = "tenacity", marker = "extra == 'm365'", specifier = ">=9.1.2" }, @@ -939,9 +941,7 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/fb/3d/8cef5f724ec0d4add2af8961d504535ec60c3cca9e464f6d03bdba29d85b/greenlet-3.5.5-cp313-cp313-macosx_11_0_universal2.whl", hash = "sha256:b79fd2a5bc099b5e744f34c4c9a58954a5f4cb7529fb4b6e8446057d61b6edaa", size = 294730, upload-time = "2026-08-10T13:27:51.206Z" }, { url = "https://files.pythonhosted.org/packages/88/4b/8e7aa3f514273aecff30a16ab1bac09ff54cfc7e6860fdd8058c37ff2499/greenlet-3.5.5-cp313-cp313-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:634cf15a233a949136879dd388e25d3296e16f3f1e217d2456797b8579ebc6ed", size = 614536, upload-time = "2026-08-10T14:14:36.589Z" }, { url = "https://files.pythonhosted.org/packages/85/48/4e95e9dd5a8a397dc6a6345dd7f1935113d0fca4f85e89d3976da9cd988d/greenlet-3.5.5-cp313-cp313-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:499adea519f748407fc6806d20eedabac2884fd73b9f38d81236e190ba20dfef", size = 626924, upload-time = "2026-08-10T14:27:27.048Z" }, - { url = "https://files.pythonhosted.org/packages/0e/84/eaa476d6bf3816828d0d70e80dcc36bf30a058233bd889e707e693f6e860/greenlet-3.5.5-cp313-cp313-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:f7278591501941bb2456af102bb9cd59aab48c6cfd6e2dd68fa1290bb0c49a42", size = 632726, upload-time = "2026-08-10T14:30:09.874Z" }, { url = "https://files.pythonhosted.org/packages/89/5d/398a1c71fa7a277deeb376c999979de6786f08fc2d5747a0b9d6e11738dd/greenlet-3.5.5-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2eabb980975cba5b93a95f6f69287d05fc05ac955bfd6a320a7c083eeb52c0b0", size = 623906, upload-time = "2026-08-10T13:40:50.501Z" }, - { url = "https://files.pythonhosted.org/packages/d0/f2/0cc2849ede68579291e9c59b3ab6ec1958f98681cca5b14d8fc75bf674a4/greenlet-3.5.5-cp313-cp313-manylinux_2_39_riscv64.whl", hash = "sha256:4dfc7c4470354e7b09184d1a3a985761053a2fd694ddb5b5c80242afc2c8c90b", size = 434966, upload-time = "2026-08-10T14:30:03.729Z" }, { url = "https://files.pythonhosted.org/packages/04/1b/745450fc5ea9e0cb17d840d248f284db3363de736d362c7d2d883e3eadba/greenlet-3.5.5-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:03115c2e0a371999bf8ae616aa8d653f96641d4705c457aebaa187276e9f7537", size = 1581430, upload-time = "2026-08-10T14:15:06.853Z" }, { url = "https://files.pythonhosted.org/packages/d4/29/d51b296e3191bb15d3d81ec375af1909e4466c0f395d744ed475801798a9/greenlet-3.5.5-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4441153ffba21b90d3ca89fe3d31f5c093ae6c0bf0cfdfc98f54cde22f95b62e", size = 1645684, upload-time = "2026-08-10T13:40:32.133Z" }, { url = "https://files.pythonhosted.org/packages/12/63/369f1a1625e64e9e31df3963c6044056e3fdfa3fa3fdba3c54ffefa6e987/greenlet-3.5.5-cp313-cp313-win_amd64.whl", hash = "sha256:95c5b1f4b3a193f8a0c2de4bfdcb48d119f7f1063941f1de1f2168051b3e52dd", size = 324075, upload-time = "2026-08-10T13:26:58.974Z" }, @@ -949,9 +949,7 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/7f/8c/080e881fa2be95ff1ddbd6994b2bab3b1a78df3b3fcab39306011764fcc7/greenlet-3.5.5-cp314-cp314-macosx_11_0_universal2.whl", hash = "sha256:d4a389a852e392a6366058651a20fa5ba40d979865aa81bea2ccbdc44805070d", size = 295309, upload-time = "2026-08-10T13:26:03.032Z" }, { url = "https://files.pythonhosted.org/packages/25/cc/0ac614e6586c0e42d4cc281a5819150f4f43685744a4c5ff77139286409d/greenlet-3.5.5-cp314-cp314-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:70b157cd319873e8b544ddc2de158f55bbd0a9b0218c8ce9332039801518e328", size = 661185, upload-time = "2026-08-10T14:14:37.867Z" }, { url = "https://files.pythonhosted.org/packages/5e/b9/6808725354be8ad305dfe5172377664fc9642d4fc043be246b3314cf4482/greenlet-3.5.5-cp314-cp314-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:8bdfd1424abcf26832961e766570cae79efdb9599d709088c9cb6ef82b194926", size = 673419, upload-time = "2026-08-10T14:27:28.652Z" }, - { url = "https://files.pythonhosted.org/packages/eb/52/f005d579acde46c3d1cc3cab1c9f3d5708c8a3006a4120e8cf5da801afe9/greenlet-3.5.5-cp314-cp314-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d98ef6f92e67c6dbf299dbfd8facc1b0d2d9cedf91e325e73b3d0373fe4309d8", size = 677863, upload-time = "2026-08-10T14:30:11.663Z" }, { url = "https://files.pythonhosted.org/packages/42/2e/40c509967da7f254680826a2fa0dd22138ec79946c70b97542d74cde8b43/greenlet-3.5.5-cp314-cp314-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:182de51c6b572a705f2fafaab2e783bcf7d2760940229dfe73086cbae037af3e", size = 670822, upload-time = "2026-08-10T13:40:51.833Z" }, - { url = "https://files.pythonhosted.org/packages/c4/8a/a75f8a2bdcef3c358a3147cdc9db3aa83755f0a038f766ab0bedb66f512c/greenlet-3.5.5-cp314-cp314-manylinux_2_39_riscv64.whl", hash = "sha256:159df1942d88e8f784cbb38d6f18bdb365cd11319cfbb3e89623de2b97892d53", size = 480554, upload-time = "2026-08-10T14:30:05.171Z" }, { url = "https://files.pythonhosted.org/packages/2d/22/c3c2eee4a8fe191d6d1d183086c56133d646024e3d70bfd414829f64560b/greenlet-3.5.5-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:8fec3f165dfe332e490c3247c0f6c23b0bfc45f06496ad7f00ddb00e3d35e4dc", size = 1628469, upload-time = "2026-08-10T14:15:08.11Z" }, { url = "https://files.pythonhosted.org/packages/f7/87/25babd09b94cb1f03e71db815fde463f0262e40cfbd953d58a8d77311351/greenlet-3.5.5-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:c6ce25fee6cabc8bf22cb8b52e642cbb821be5b9aec8094d07ff03378141b8e9", size = 1691952, upload-time = "2026-08-10T13:40:33.502Z" }, { url = "https://files.pythonhosted.org/packages/2e/3d/5cc9701117ea4dc0eb7bf1f4f9b7888a6e2e5277ddfae095805ace50f2b6/greenlet-3.5.5-cp314-cp314-win_amd64.whl", hash = "sha256:7dffc5c859fe6059974df1e37d7923d654a83e2ae18fdd616994270e001115e1", size = 327458, upload-time = "2026-08-10T13:27:02.868Z" }, @@ -959,18 +957,14 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/24/e0/50cd600b469e5734c72709b6b1838b6bc63f307b573c772c3132d6ecfe92/greenlet-3.5.5-cp314-cp314t-macosx_11_0_universal2.whl", hash = "sha256:0e5a7de979d764aea1f5b6e95cf92b5b37741b9823702041f34b126e7f690277", size = 305471, upload-time = "2026-08-10T13:26:20.568Z" }, { url = "https://files.pythonhosted.org/packages/75/a3/77acd66dfc6387b5219b2080806c0cabb73c10eb1bb44b413c40a62015ba/greenlet-3.5.5-cp314-cp314t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:fef01bd457f11fc158b130ca0027a3c365693280e8e231b65bdaf57999f39f5b", size = 672470, upload-time = "2026-08-10T14:14:39.058Z" }, { url = "https://files.pythonhosted.org/packages/b9/71/0d178142dca3ec19f46fb2212ae73d30ad53b9d548dc64804086033a7089/greenlet-3.5.5-cp314-cp314t-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:5173a72310725a74afc82c164f0e52cb8ad0de62f2bb623f24f6c0cc07d80272", size = 679973, upload-time = "2026-08-10T14:27:30.072Z" }, - { url = "https://files.pythonhosted.org/packages/aa/ac/0d7887aa4bbfc9eba075cc428244dfc96f623478454d5ec81180d0d6bd5a/greenlet-3.5.5-cp314-cp314t-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5e9ec2e7c98e895fcea0c5cc57b2606cf86ece6d0a56578f3eb225e2af4f0387", size = 681587, upload-time = "2026-08-10T14:30:13.519Z" }, { url = "https://files.pythonhosted.org/packages/6e/31/46eb8567302eaf787abf88d09df014e14ae3baf460af1b8b0efdbd3efcd5/greenlet-3.5.5-cp314-cp314t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:44f08341873200ba8a60a8bc14ace3d91f1754f7fa7bc66157714a8cd420a476", size = 676634, upload-time = "2026-08-10T13:40:53.004Z" }, - { url = "https://files.pythonhosted.org/packages/4f/18/8d58ba1c429b0383e3219a3d0e0bba241d0444d8ed05b73349953c7d7c7b/greenlet-3.5.5-cp314-cp314t-manylinux_2_39_riscv64.whl", hash = "sha256:102817506f6090b5176c746a82603341a549b40e5c3d5b72a4c672228a918c41", size = 510175, upload-time = "2026-08-10T14:30:07.047Z" }, { url = "https://files.pythonhosted.org/packages/a3/e9/b88bbf5b29970cb84172dc2c32aa3e5e579ceb94c808e81c826454138850/greenlet-3.5.5-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:d246c0db9a2513cd45f019ba178ea4d4d4705bd210ee465e2c15d76a1ab13874", size = 1637320, upload-time = "2026-08-10T14:15:09.317Z" }, { url = "https://files.pythonhosted.org/packages/6d/8c/7631ed29cc6f0392f11830076e172ce4885e70b0bc2c1bce1731176d4b4e/greenlet-3.5.5-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:72507285b5caa1d17904a3f7c322ca780823a54170a0e04ec3f37bcc60d4db71", size = 1697412, upload-time = "2026-08-10T13:40:34.924Z" }, { url = "https://files.pythonhosted.org/packages/da/0f/f7dd935f9c4cb1be49098770587f54d8a78518e55c89bce86c4fb4109057/greenlet-3.5.5-cp314-cp314t-win_amd64.whl", hash = "sha256:7805655781fb8f28a55d05fe57ed61f5f10f1892fb587673e3bb5264f28041f0", size = 331514, upload-time = "2026-08-10T13:29:20.611Z" }, { url = "https://files.pythonhosted.org/packages/b7/e5/681b01f8fbc1b55232822f99e8f8afeb78a55a7c76a7bf9dbdc7ccb03a6d/greenlet-3.5.5-cp315-cp315-macosx_11_0_universal2.whl", hash = "sha256:c0db80fcd5b8aece93f66c64f78a786bbb6b96c5fe63ef5a5a4581ecf8bab206", size = 295975, upload-time = "2026-08-10T13:28:45.985Z" }, { url = "https://files.pythonhosted.org/packages/11/f2/69b488cd9e7267bf4b0fe8cdebf25d8d6df680d21bdf41150d23e23d6652/greenlet-3.5.5-cp315-cp315-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6b241c32f912ada659808d68e308c568baf577eebf757d15471472de0c18cfad", size = 666823, upload-time = "2026-08-10T14:14:40.222Z" }, { url = "https://files.pythonhosted.org/packages/84/d4/d5bc2fdebbdda0c94555925ba79948b8395d75a7f6a36cc85dce5bab9f11/greenlet-3.5.5-cp315-cp315-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ef6a08349401d8eaf3cb12688ac8557de95788556b8631ef17555a4a173022c0", size = 677613, upload-time = "2026-08-10T14:27:31.543Z" }, - { url = "https://files.pythonhosted.org/packages/65/53/4e13642efc4d7ad6554ecb2242a5be42666b2e1a067323e88dfc0124a04b/greenlet-3.5.5-cp315-cp315-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:37faa97daccb6d9f4c2141ce3118d023c3c5506864a7d8bdf726f665018c1f76", size = 681436, upload-time = "2026-08-10T14:30:14.839Z" }, { url = "https://files.pythonhosted.org/packages/bd/93/542d8a3a90f3b35c6ad8bf7e56a03010287f2cafa289a5b7985b5207db39/greenlet-3.5.5-cp315-cp315-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f2e3d061b8e13aec2f0441689b3c71b244a20e5d274a52cb0f7e31bd1d139552", size = 675930, upload-time = "2026-08-10T13:40:54.205Z" }, - { url = "https://files.pythonhosted.org/packages/cd/32/188447c9a468d6977d2989397226b0c6b65ab6f4cf943f931643328512fc/greenlet-3.5.5-cp315-cp315-manylinux_2_39_riscv64.whl", hash = "sha256:b18007dc2473a7942fd157366b55f01da6fed7ce85318591005b419e0a439474", size = 487404, upload-time = "2026-08-10T14:30:08.903Z" }, { url = "https://files.pythonhosted.org/packages/52/b5/89c9f2e8460d71101037d47a1feed11928615a5edd42370be290e0657eeb/greenlet-3.5.5-cp315-cp315-musllinux_1_2_aarch64.whl", hash = "sha256:9ab5f5b93655e77fe0d6c2dfd22b5eac751bb1f876d8ec21761b7c1fb9266007", size = 1633878, upload-time = "2026-08-10T14:15:10.693Z" }, { url = "https://files.pythonhosted.org/packages/b8/60/297de93f3b02ac78a5e04d32bb8bbe3080f4a73d8ed95016561463b70618/greenlet-3.5.5-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:f0e5a21bd4452a88cf032fc43c4a5b307ab1380eacb63b5988f9c0317885e773", size = 1696597, upload-time = "2026-08-10T13:40:36.252Z" }, { url = "https://files.pythonhosted.org/packages/18/25/54c6eaff4f337fb670215e89eb2d00d9499487b658e709d4b477be4a342e/greenlet-3.5.5-cp315-cp315-win_amd64.whl", hash = "sha256:469dbb0a78625642f4a626cfd0c6e8bccc0385b5e49189b6308bbe849ec88a8e", size = 327700, upload-time = "2026-08-10T13:28:06.752Z" }, @@ -978,9 +972,7 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/10/e2/3144c0a116067ac1e30457b0139a94d60d1d36a86e015de68e9ac87cb3bc/greenlet-3.5.5-cp315-cp315t-macosx_11_0_universal2.whl", hash = "sha256:68184dfcf50ccaa8e864770fe0633a7e27250ea9329f8192ef47ee9ecfd78e1c", size = 306387, upload-time = "2026-08-10T13:27:00.897Z" }, { url = "https://files.pythonhosted.org/packages/5c/a1/cb4223a7e9b9f43b8807e8eb212358bfe2dfaa174a9ea2889eb1714dcba2/greenlet-3.5.5-cp315-cp315t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9ec0dc0e59dc9c61af5c47348365ccbbd7addfafe0a93b00336ff3da2907bdc6", size = 676472, upload-time = "2026-08-10T14:14:41.417Z" }, { url = "https://files.pythonhosted.org/packages/9e/cd/a154b4498e5d8f12ada291cfb3b8d596eadde2177f5bf09a9be699d2a446/greenlet-3.5.5-cp315-cp315t-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:e604f58e35833fc46ef20302bcb314dddbfd3fcf33a4f936216d51dd678d63ae", size = 684238, upload-time = "2026-08-10T14:27:32.946Z" }, - { url = "https://files.pythonhosted.org/packages/ce/f4/e450a68a152f819491d8c7df6a8254e761d87e6a78759268961f8c5bd4dd/greenlet-3.5.5-cp315-cp315t-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:2888a3a38bc5ee5bb6c438372197152e815837e4fab7ed7a1f86ef18ffd58ad1", size = 686022, upload-time = "2026-08-10T14:30:15.96Z" }, { url = "https://files.pythonhosted.org/packages/bf/bb/b0031d260c2968a3c87deebc51d80c64e499377f993aafe06ee3b7488cc2/greenlet-3.5.5-cp315-cp315t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:40239b5384f96da3963585cc6d7eaa9b56f8ae67e8d92cc82dd9e202fc847de3", size = 681246, upload-time = "2026-08-10T13:40:55.402Z" }, - { url = "https://files.pythonhosted.org/packages/18/23/17e63d6bf3b9c9b9dbea981b7f643a71f79603bdfb4f1c3a9cf353e22aed/greenlet-3.5.5-cp315-cp315t-manylinux_2_39_riscv64.whl", hash = "sha256:1e8d9391fe77f15649589a907cef972dbbd6352ef7ff7dc0492f658c0c26495f", size = 516951, upload-time = "2026-08-10T14:30:10.907Z" }, { url = "https://files.pythonhosted.org/packages/9a/07/da554b71ab88e649da146e1065d86a48a5c5d92e50ab74ef41b504aa7f56/greenlet-3.5.5-cp315-cp315t-musllinux_1_2_aarch64.whl", hash = "sha256:a1eaccf5c3a1d3e46dead602c72e6836731e8e245c9de6a27764567b6b62d4c0", size = 1642735, upload-time = "2026-08-10T14:15:11.92Z" }, { url = "https://files.pythonhosted.org/packages/78/76/26a3782a051677668af9d92beaa47cd87ba9dd5072f762961144a03dd4c6/greenlet-3.5.5-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:19e4e026fe20691f333b8eb1a3bc9625eceba8c3f9d62ec5a6f8581afbc6b5a5", size = 1700925, upload-time = "2026-08-10T13:40:37.656Z" }, { url = "https://files.pythonhosted.org/packages/28/d9/fe7baf4190c2ae71f267efb9de21b3172bb35bc0ed1ef53dd6027d658e33/greenlet-3.5.5-cp315-cp315t-win_amd64.whl", hash = "sha256:712aee154f648bde84634654bb38bb78c69ac640c37a45c9effed800735049d8", size = 331829, upload-time = "2026-08-10T13:26:48.851Z" },