Repository: getmoto/moto
Pull request #5822 (2023-01-06 14:41:46)

support $Latest and $Default when describing launch templates


Patch:
diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml
index 7b3ceff72..e0efb2b23 100644
--- a/.github/workflows/build.yml
+++ b/.github/workflows/build.yml
@@ -25,7 +25,7 @@ jobs:
         uses: actions/cache@v3
         with:
           path: ${{ steps.pip-cache-dir.outputs.dir }}
-          key: pip-${{ matrix.python-version }}-${{ hashFiles('**/setup.py') }}-4
+          key: pip-${{ matrix.python-version }}-${{ hashFiles('**/setup.cfg') }}-4
       - name: Update pip
         if: ${{ steps.pip-cache.outputs.cache-hit != 'true' }}
         run: |
@@ -57,7 +57,7 @@ jobs:
       uses: actions/cache@v3
       with:
         path: ${{ steps.pip-cache.outputs.dir }}
-        key: pip-${{ matrix.python-version }}-${{ hashFiles('**/setup.py') }}-4
+        key: pip-${{ matrix.python-version }}-${{ hashFiles('**/setup.cfg') }}
     # Update PIP
     - name: Update pip
       run: |
@@ -95,7 +95,7 @@ jobs:
       uses: actions/cache@v3
       with:
         path: ${{ steps.pip-cache.outputs.dir }}
-        key: pip-${{ matrix.python-version }}-${{ hashFiles('**/setup.py') }}-4
+        key: pip-${{ matrix.python-version }}-${{ hashFiles('**/setup.cfg') }}
     - name: Update pip
       run: |
         python -m pip install --upgrade pip
@@ -108,7 +108,7 @@ jobs:
       run: |
         make test-only
     - name: "Upload coverage to Codecov"
-      if: ${{ github.repository == 'spulec/moto'}}
+      if: ${{ github.repository == 'getmoto/moto'}}
       uses: codecov/codecov-action@v3
       with:
         fail_ci_if_error: false
@@ -133,7 +133,8 @@ jobs:
         python-version: ${{ matrix.python-version }}
     - name: Start MotoServer
       run: |
-        python setup.py sdist
+        pip install build
+        python -m build
         docker run --rm -t --name motoserver -e TEST_SERVER_MODE=true -e AWS_SECRET_ACCESS_KEY=server_secret -e AWS_ACCESS_KEY_ID=server_key -v `pwd`:/moto -p 5000:5000 -v /var/run/docker.sock:/var/run/docker.sock python:3.7-buster /moto/scripts/ci_moto_server.sh &
         python scripts/ci_wait_for_server.py
     - name: Get pip cache dir
@@ -144,7 +145,7 @@ jobs:
       uses: actions/cache@v3
       with:
         path: ${{ steps.pip-cache.outputs.dir }}
-        key: pip-${{ matrix.python-version }}-${{ hashFiles('**/setup.py') }}-4
+        key: pip-${{ matrix.python-version }}-${{ hashFiles('**/setup.cfg') }}
     - name: Update pip
       run: |
         python -m pip install --upgrade pip
@@ -157,7 +158,7 @@ jobs:
       run: |
         make test-only
     - name: "Upload coverage to Codecov"
-      if: ${{ github.repository == 'spulec/moto'}}
+      if: ${{ github.repository == 'getmoto/moto'}}
       uses: codecov/codecov-action@v3
       with:
         fail_ci_if_error: false
@@ -182,7 +183,7 @@ jobs:
     name: Deploy
     runs-on: ubuntu-latest
     needs: [test, testserver ]
-    if: ${{ github.event_name == 'push' && github.ref == 'refs/heads/master' && github.repository == 'spulec/moto' }}
+    if: ${{ github.event_name == 'push' && github.ref == 'refs/heads/master' && github.repository == 'getmoto/moto' }}
     strategy:
       matrix:
         python-version: [3.8]
@@ -196,9 +197,9 @@ jobs:
           python-version: ${{ matrix.python-version }}
       - name: Update & Build
         run: |
-          pip install wheel packaging
+          pip install build
           python update_version_from_git.py
-          python setup.py sdist bdist_wheel
+          python -m build
       - name: Publish to PyPI
         uses: pypa/gh-action-pypi-publish@release/v1
         with:
@@ -269,7 +270,7 @@ jobs:
           fi
       - name: Leave PR comment with Moto version
         uses: peter-evans/create-or-update-comment@v1
-        if: env.pr_found == 'true' && env.pr_repo == 'spulec/moto'
+        if: env.pr_found == 'true' && env.pr_repo == 'getmoto/moto'
         with:
           issue-number: ${{ env.pr_nr }}
           body: |
diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
index 99b9c7f98..be94d056e 100644
--- a/.github/workflows/release.yml
+++ b/.github/workflows/release.yml
@@ -27,7 +27,7 @@ jobs:
     - name: Install dependencies
       run: |
         python -m pip install --upgrade pip
-        pip install wheel setuptools packaging twine --upgrade
+        pip install wheel setuptools packaging twine build --upgrade
     - name: Verify Tag does not exist
       run: |
         ! git rev-parse ${{ env.VERSION }} || { echo "Ensure that no tag exists for ${{ env.VERSION }}" ; exit 1; }
@@ -37,7 +37,7 @@ jobs:
     - name: Set version number
       run: python update_version_from_git.py ${{ env.VERSION }}
     - name: Build
-      run: python setup.py sdist bdist_wheel
+      run: python -m build
     - name: Publish to PyPI
       uses: pypa/gh-action-pypi-publish@release/v1
       with:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index a486af918..a5edeeecf 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -2,6 +2,15 @@ Moto Changelog
 ==============
 
 
+4.1.0
+-----
+Docker Digest for 4.1.0: _sha256:1c38613f7273054650d08f3cb3ce118753296d08ffbfee8c96400c89a529c9ca_
+
+    General:
+        * Dropped support for Py 3.6
+        * Moto now uses setup.cfg to hold our packaging configuration, instead of setup.py, making Moto compatible with more recent Pip-versions
+
+
 4.0.13
 -----
 Docker Digest for 4.0.13: _sha256:703a9d464c11e1f4cacff66acdc9b46f9fa8fb0b969ca9f1e79fa4eb41678565_
@@ -1437,7 +1446,7 @@ Docker Digest for 3.0.4: _sha256:320e1d2ab89729d5580dbe08d8c2153a28db4c28023c577
 -----
     General:
         * The mock_dynamodb and mock_rds-decorators have been deprecated, and will be repurposed in a later release.
-          Please see https://github.com/spulec/moto/issues/4526 for more information.
+          Please see https://github.com/getmoto/moto/issues/4526 for more information.
 
     New Methods:
         * API Gateway:
@@ -2339,7 +2348,7 @@ Docker Digest for 3.0.4: _sha256:320e1d2ab89729d5580dbe08d8c2153a28db4c28023c577
 2.0.0
 ----
 Full list of PRs merged in this release:
-https://github.com/spulec/moto/pulls?q=is%3Apr+is%3Aclosed+merged%3A2020-09-07..2021-02-23
+https://github.com/getmoto/moto/pulls?q=is%3Apr+is%3Aclosed+merged%3A2020-09-07..2021-02-23
 
     General Changes:
         * When installing, it is now required to specify the service you want to use:
@@ -2530,7 +2539,7 @@ https://github.com/spulec/moto/pulls?q=is%3Apr+is%3Aclosed+merged%3A2020-09-07..
 1.3.16
 -----
 Full list of PRs merged in this release:
-https://github.com/spulec/moto/pulls?q=is%3Apr+is%3Aclosed+merged%3A2019-11-14..2020-09-07
+https://github.com/getmoto/moto/pulls?q=is%3Apr+is%3Aclosed+merged%3A2019-11-14..2020-09-07
 
 
     General Changes:
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index 3cee7c392..02001259e 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -8,7 +8,7 @@
 
 # Contributing code
 
-Moto has a [Code of Conduct](https://github.com/spulec/moto/blob/master/CODE_OF_CONDUCT.md), you can expect to be treated with respect at all times when interacting with this project.
+Moto has a [Code of Conduct](https://github.com/getmoto/moto/blob/master/CODE_OF_CONDUCT.md), you can expect to be treated with respect at all times when interacting with this project.
 
 # Development Guide
 Please see our documentation for information on how to contribute:
@@ -33,7 +33,7 @@ Use `make format` to automatically format your code, if it does not conform to `
 ## Releasing a new version of Moto
 
 * Ensure the CHANGELOG document mentions the new release, and lists all significant changes.
-* Go to the dedicated [Release Action](https://github.com/spulec/moto/actions/workflows/release.yml) in our CI
+* Go to the dedicated [Release Action](https://github.com/getmoto/moto/actions/workflows/release.yml) in our CI
 * Click 'Run workflow' on the top right
 * Provide the version you want to release
 * That's it - everything else is automated.
diff --git a/MANIFEST.in b/MANIFEST.in
index c4c45ceb7..691fde1b9 100644
--- a/MANIFEST.in
+++ b/MANIFEST.in
@@ -1,5 +1,5 @@
 include README.md LICENSE AUTHORS.md
-include requirements.txt requirements-dev.txt tox.ini
+include requirements.txt requirements-dev.txt
 include moto/config/resources/aws_managed_rules.json
 include moto/ec2/_models/*.py
 include moto/ec2/resources/instance_types.json
diff --git a/Makefile b/Makefile
index c4a2b06eb..24158fc3a 100644
--- a/Makefile
+++ b/Makefile
@@ -15,7 +15,7 @@ else
 endif
 
 init:
-	@python setup.py develop
+	@pip install -e .
 	@pip install -r requirements-dev.txt
 
 lint:
diff --git a/README.md b/README.md
index c39c29835..c74861826 100644
--- a/README.md
+++ b/README.md
@@ -2,8 +2,8 @@
 
 [![Join the chat at https://gitter.im/awsmoto/Lobby](https://badges.gitter.im/awsmoto/Lobby.svg)](https://gitter.im/awsmoto/Lobby?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge)
 
-[![Build Status](https://github.com/spulec/moto/workflows/TestNDeploy/badge.svg)](https://github.com/spulec/moto/actions)
-[![Coverage Status](https://codecov.io/gh/spulec/moto/branch/master/graph/badge.svg)](https://codecov.io/gh/spulec/moto)
+[![Build Status](https://github.com/getmoto/moto/workflows/TestNDeploy/badge.svg)](https://github.com/getmoto/moto/actions)
+[![Coverage Status](https://codecov.io/gh/getmoto/moto/branch/master/graph/badge.svg)](https://codecov.io/gh/getmoto/moto)
 [![Docs](https://readthedocs.org/projects/pip/badge/?version=stable)](http://docs.getmoto.org)
 [![PyPI](https://img.shields.io/pypi/v/moto.svg)](https://pypi.org/project/moto/)
 [![PyPI - Python Version](https://img.shields.io/pypi/pyversions/moto.svg)](#)
@@ -61,7 +61,7 @@ def test_my_model_save():
 
 With the decorator wrapping the test, all the calls to s3 are automatically mocked out. The mock keeps the state of the buckets and keys.
 
-For a full list of which services and features are covered, please see our [implementation coverage](https://github.com/spulec/moto/blob/master/IMPLEMENTATION_COVERAGE.md).
+For a full list of which services and features are covered, please see our [implementation coverage](https://github.com/getmoto/moto/blob/master/IMPLEMENTATION_COVERAGE.md).
 
 
 ### Documentation
diff --git a/docs/docs/aws_config.rst b/docs/docs/aws_config.rst
index 59e0a6541..49d7fa9ff 100644
--- a/docs/docs/aws_config.rst
+++ b/docs/docs/aws_config.rst
@@ -109,7 +109,7 @@ The aggregation and non-aggregation querying can and should just use the same ov
 1. Non-aggregated listing will specify the region-name of the resource backend `backend_region`
 1. Aggregated listing will need to be able to list resource types across ALL backends and filter optionally by passing in `resource_region`.
 
-An example of a working implementation of this is `S3 <https://github.com/spulec/moto/blob/master/moto/s3/config.py>`_.
+An example of a working implementation of this is `S3 <https://github.com/getmoto/moto/blob/master/moto/s3/config.py>`_.
 
 Pagination should generally be able to pull out the resource across any region so should be sharded by `region-item-name` -- not done for S3
 because S3 has a globally unique name space.
@@ -131,4 +131,4 @@ return for it.
 When implementing resource config fetching, you will need to return at a minimum `None` if the resource is not found, or a `dict` that looks
 like what AWS Config would return.
 
-It's recommended to read the comment for the `ConfigQueryModel` 's `get_config_resource` function in `the base class <https://github.com/spulec/moto/blob/master/moto/core/models.py>`_.
\ No newline at end of file
+It's recommended to read the comment for the `ConfigQueryModel` 's `get_config_resource` function in `the base class <https://github.com/getmoto/moto/blob/master/moto/core/models.py>`_.
\ No newline at end of file
diff --git a/docs/docs/contributing/checklist.rst b/docs/docs/contributing/checklist.rst
index 32f708d27..6e720a24d 100644
--- a/docs/docs/contributing/checklist.rst
+++ b/docs/docs/contributing/checklist.rst
@@ -23,4 +23,4 @@ Create the PR anyway! Others can use your work and build on it to finish the fea
 .. note::  You can indicate a PR that's not quite ready with the `needs-help`-label.
 
 Are you not sure on what you want to implement, or want some advice on how to approach things?
-Feel free to open a new issue on Github: https://github.com/spulec/moto/issues
+Feel free to open a new issue on Github: https://github.com/getmoto/moto/issues
diff --git a/docs/docs/getting_started.rst b/docs/docs/getting_started.rst
index 569fa5f21..4ceb64d8a 100644
--- a/docs/docs/getting_started.rst
+++ b/docs/docs/getting_started.rst
@@ -19,9 +19,9 @@ If you don't care about the number of dependencies, or if you want to mock many
 
 If you want to install ``moto`` from source::
 
-    git clone git://github.com/spulec/moto.git
+    git clone git://github.com/getmoto/moto.git
     cd moto
-    python setup.py install
+    pip install '.[all]'
 
 
 Moto usage
diff --git a/docs/docs/iam.rst b/docs/docs/iam.rst
index b2e65158e..a88fcd2fd 100644
--- a/docs/docs/iam.rst
+++ b/docs/docs/iam.rst
@@ -4,7 +4,7 @@
 IAM-like Access Control
 =======================
 
-Moto also has the ability to authenticate and authorize actions, just like it's done by IAM in AWS. This functionality can be enabled by either setting the `INITIAL_NO_AUTH_ACTION_COUNT` environment variable or using the `set_initial_no_auth_action_count` decorator. Note that the current implementation is very basic, see `the source code <https://github.com/spulec/moto/blob/master/moto/iam/access_control.py>`_ for more information.
+Moto also has the ability to authenticate and authorize actions, just like it's done by IAM in AWS. This functionality can be enabled by either setting the `INITIAL_NO_AUTH_ACTION_COUNT` environment variable or using the `set_initial_no_auth_action_count` decorator. Note that the current implementation is very basic, see `the source code <https://github.com/getmoto/moto/blob/master/moto/iam/access_control.py>`_ for more information.
 
 `INITIAL_NO_AUTH_ACTION_COUNT`
 ------------------------------
@@ -44,4 +44,4 @@ This is a decorator that works similarly to the environment variable, but the se
         assert len(instances) == 0
 
 
-See `the related test suite <https://github.com/spulec/moto/blob/master/tests/test_core/test_auth.py>`_ for more examples.
+See `the related test suite <https://github.com/getmoto/moto/blob/master/tests/test_core/test_auth.py>`_ for more examples.
diff --git a/docs/docs/server_mode.rst b/docs/docs/server_mode.rst
index 9ded4f48a..abe1a2613 100644
--- a/docs/docs/server_mode.rst
+++ b/docs/docs/server_mode.rst
@@ -232,8 +232,8 @@ For the above services, this is required because the hostname is in the form of
 As a result, you need to add that entry to your host file for your tests to function properly.
 
 
-.. _Java: https://github.com/spulec/moto/blob/master/other_langs/sqsSample.java
-.. _Ruby: https://github.com/spulec/moto/blob/master/other_langs/test.rb
-.. _Javascript: https://github.com/spulec/moto/blob/master/other_langs/test.js
+.. _Java: https://github.com/getmoto/moto/blob/master/other_langs/sqsSample.java
+.. _Ruby: https://github.com/getmoto/moto/blob/master/other_langs/test.rb
+.. _Javascript: https://github.com/getmoto/moto/blob/master/other_langs/test.js
 .. _Homebrew: https://brew.sh
 .. _Terraform Docs: https://registry.terraform.io/providers/hashicorp/aws/latest/docs/guides/custom-service-endpoints
diff --git a/docs/docs/services/sagemaker.rst b/docs/docs/services/sagemaker.rst
index 4533cca92..a9e90a411 100644
--- a/docs/docs/services/sagemaker.rst
+++ b/docs/docs/services/sagemaker.rst
@@ -170,7 +170,7 @@ sagemaker
 - [ ] describe_monitoring_schedule
 - [ ] describe_notebook_instance
 - [X] describe_notebook_instance_lifecycle_config
-- [ ] describe_pipeline
+- [X] describe_pipeline
 - [ ] describe_pipeline_definition_for_execution
 - [ ] describe_pipeline_execution
 - [X] describe_processing_job
diff --git a/docs/index.rst b/docs/index.rst
index 959f1ec5f..0ba6134a9 100644
--- a/docs/index.rst
+++ b/docs/index.rst
@@ -22,8 +22,8 @@ Additional Resources
 * `Moto Issue Tracker`_
 
 .. _AWS infrastructure: http://aws.amazon.com/
-.. _Moto Issue Tracker: https://github.com/spulec/moto/issues
-.. _Moto Source Repository: https://github.com/spulec/moto
+.. _Moto Issue Tracker: https://github.com/getmoto/moto/issues
+.. _Moto Source Repository: https://github.com/getmoto/moto
 
 .. toctree::
    :maxdepth: 2
diff --git a/moto/__init__.py b/moto/__init__.py
index 0be8546e5..b45c3d2ca 100644
--- a/moto/__init__.py
+++ b/moto/__init__.py
@@ -191,7 +191,7 @@ mock_all = MockAll
 # logging.getLogger('boto').setLevel(logging.CRITICAL)
 
 __title__ = "moto"
-__version__ = "4.1.0.dev"
+__version__ = "4.1.1.dev"
 
 
 try:
diff --git a/moto/core/custom_responses_mock.py b/moto/core/custom_responses_mock.py
index 272ec64a5..3daed7229 100644
--- a/moto/core/custom_responses_mock.py
+++ b/moto/core/custom_responses_mock.py
@@ -103,7 +103,7 @@ def not_implemented_callback(
 
 
 # Modify behaviour of the matcher to only/always return the first match
-# Default behaviour is to return subsequent matches for subsequent requests, which leads to https://github.com/spulec/moto/issues/2567
+# Default behaviour is to return subsequent matches for subsequent requests, which leads to https://github.com/getmoto/moto/issues/2567
 #  - First request matches on the appropriate S3 URL
 #  - Same request, executed again, will be matched on the subsequent match, which happens to be the catch-all, not-yet-implemented, callback
 # Fix: Always return the first match
diff --git a/moto/dynamodb/models/__init__.py b/moto/dynamodb/models/__init__.py
index bc915da4c..854727ee7 100644
--- a/moto/dynamodb/models/__init__.py
+++ b/moto/dynamodb/models/__init__.py
@@ -54,7 +54,7 @@ def dynamo_json_dump(dynamo_object):
     return json.dumps(dynamo_object, cls=DynamoJsonEncoder)
 
 
-# https://github.com/spulec/moto/issues/1874
+# https://github.com/getmoto/moto/issues/1874
 # Ensure that the total size of an item does not exceed 400kb
 class LimitedSizeDict(dict):
     def __init__(self, *args, **kwargs):
diff --git a/moto/ec2/exceptions.py b/moto/ec2/exceptions.py
index bac72ceff..3f1f2f5b8 100644
--- a/moto/ec2/exceptions.py
+++ b/moto/ec2/exceptions.py
@@ -520,7 +520,7 @@ class MotoNotImplementedError(NotImplementedError):
         super().__init__(
             f"{blurb} has not been implemented in Moto yet."
             " Feel free to open an issue at"
-            " https://github.com/spulec/moto/issues"
+            " https://github.com/getmoto/moto/issues"
         )
 
 
diff --git a/moto/ec2/responses/general.py b/moto/ec2/responses/general.py
index d452a0989..018784a77 100644
--- a/moto/ec2/responses/general.py
+++ b/moto/ec2/responses/general.py
@@ -6,7 +6,7 @@ class General(BaseResponse):
         instance_id = self._get_param("InstanceId")
         if not instance_id:
             # For compatibility with boto.
-            # See: https://github.com/spulec/moto/pull/1152#issuecomment-332487599
+            # See: https://github.com/getmoto/moto/pull/1152#issuecomment-332487599
             instance_id = self._get_multi_param("InstanceId")[0]
 
         instance = self.ec2_backend.get_instance(instance_id)
diff --git a/moto/ec2/responses/instances.py b/moto/ec2/responses/instances.py
index 5a8a12272..fa587a111 100644
--- a/moto/ec2/responses/instances.py
+++ b/moto/ec2/responses/instances.py
@@ -229,7 +229,7 @@ class InstanceResponse(EC2BaseResponse):
         msg = (
             "This specific call to ModifyInstanceAttribute has not been"
             " implemented in Moto yet. Feel free to open an issue at"
-            " https://github.com/spulec/moto/issues"
+            " https://github.com/getmoto/moto/issues"
         )
         raise NotImplementedError(msg)
 
diff --git a/moto/ec2/responses/launch_templates.py b/moto/ec2/responses/launch_templates.py
index d9797f77e..595f37f2f 100644
--- a/moto/ec2/responses/launch_templates.py
+++ b/moto/ec2/responses/launch_templates.py
@@ -226,7 +226,11 @@ class LaunchTemplates(EC2BaseResponse):
             ret_versions = []
             if versions:
                 for v in versions:
-                    ret_versions.append(template.get_version(int(v)))
+                    if str(v).lower() == "$latest" or "$default":
+                        tv = template.get_version(v)
+                    else:
+                        tv = template.get_version(int(v))
+                    ret_versions.append(tv)
             elif min_version:
                 if max_version:
                     vMax = max_version
diff --git a/moto/ec2/utils.py b/moto/ec2/utils.py
index 50e0bee4c..610a3c248 100644
--- a/moto/ec2/utils.py
+++ b/moto/ec2/utils.py
@@ -427,7 +427,7 @@ def passes_filter_dict(instance, filter_dict):
                 return False
         else:
             raise NotImplementedError(
-                "Filter dicts have not been implemented in Moto for '%s' yet. Feel free to open an issue at https://github.com/spulec/moto/issues"
+                "Filter dicts have not been implemented in Moto for '%s' yet. Feel free to open an issue at https://github.com/getmoto/moto/issues"
                 % filter_name
             )
     return True
@@ -473,7 +473,7 @@ def passes_igw_filter_dict(igw, filter_dict):
                 return False
         else:
             raise NotImplementedError(
-                "Internet Gateway filter dicts have not been implemented in Moto for '%s' yet. Feel free to open an issue at https://github.com/spulec/moto/issues",
+                "Internet Gateway filter dicts have not been implemented in Moto for '%s' yet. Feel free to open an issue at https://github.com/getmoto/moto/issues",
                 filter_name,
             )
     return True
diff --git a/moto/ecs/models.py b/moto/ecs/models.py
index c94887ad4..7374ce403 100644
--- a/moto/ecs/models.py
+++ b/moto/ecs/models.py
@@ -1143,6 +1143,7 @@ class EC2ContainerServiceBackend(BaseBackend):
         container_instances,
         overrides,
         started_by,
+        tags=None,
     ):
         cluster = self._get_cluster(cluster_str)
 
@@ -1169,6 +1170,7 @@ class EC2ContainerServiceBackend(BaseBackend):
                 backend=self,
                 overrides=overrides or {},
                 started_by=started_by or "",
+                tags=tags,
             )
             tasks.append(task)
             self.update_container_instance_resources(
@@ -1177,7 +1179,10 @@ class EC2ContainerServiceBackend(BaseBackend):
             self.tasks[cluster.name][task.task_arn] = task
         return tasks
 
-    def describe_tasks(self, cluster_str, tasks):
+    def describe_tasks(self, cluster_str, tasks, include=None):
+        """
+        Only include=TAGS is currently supported.
+        """
         self._get_cluster(cluster_str)
 
         if not tasks:
@@ -1192,6 +1197,11 @@ class EC2ContainerServiceBackend(BaseBackend):
                     or any(task_id in task for task in tasks)
                 ):
                     response.append(task)
+        if "TAGS" in (include or []):
+            return response
+
+        for task in response:
+            task.tags = []
         return response
 
     def list_tasks(
diff --git a/moto/ecs/responses.py b/moto/ecs/responses.py
index 40c1f86e3..5a4c38988 100644
--- a/moto/ecs/responses.py
+++ b/moto/ecs/responses.py
@@ -163,7 +163,8 @@ class EC2ContainerServiceResponse(BaseResponse):
     def describe_tasks(self):
         cluster = self._get_param("cluster", "default")
         tasks = self._get_param("tasks")
-        data = self.ecs_backend.describe_tasks(cluster, tasks)
+        include = self._get_param("include")
+        data = self.ecs_backend.describe_tasks(cluster, tasks, include)
         return json.dumps(
             {"tasks": [task.response_object for task in data], "failures": []}
         )
@@ -174,8 +175,14 @@ class EC2ContainerServiceResponse(BaseResponse):
         task_definition_str = self._get_param("taskDefinition")
         container_instances = self._get_param("containerInstances")
         started_by = self._get_param("startedBy")
+        tags = self._get_param("tags")
         tasks = self.ecs_backend.start_task(
-            cluster_str, task_definition_str, container_instances, overrides, started_by
+            cluster_str,
+            task_definition_str,
+            container_instances,
+            overrides,
+            started_by,
+            tags,
         )
         return json.dumps(
             {"tasks": [task.response_object for task in tasks], "failures": []}
diff --git a/moto/moto_server/templates/dashboard.html b/moto/moto_server/templates/dashboard.html
index 33859101a..a967af0ab 100644
--- a/moto/moto_server/templates/dashboard.html
+++ b/moto/moto_server/templates/dashboard.html
@@ -176,9 +176,9 @@
             <h4 class="modal-title" id="myModalLabel">About Moto</h4>
           </div>
           <div class="modal-body">
-            <p>Moto was created by <a href="https://twitter.com/spulec">Steve Pulec</a> and <a href="https://github.com/spulec/moto/blob/master/AUTHORS.md">many other contributors</a>.</p>
+            <p>Moto was created by <a href="https://twitter.com/spulec">Steve Pulec</a> and <a href="https://github.com/getmoto/moto/blob/master/AUTHORS.md">many other contributors</a>.</p>
 
-            <p>Please open any issues <a href="https://github.com/spulec/moto/issues">here</a>.</p>
+            <p>Please open any issues <a href="https://github.com/getmoto/moto/issues">here</a>.</p>
           </div>
         </div>
       </div>
diff --git a/moto/redshiftdata/models.py b/moto/redshiftdata/models.py
index 0ebddde74..8020c882b 100644
--- a/moto/redshiftdata/models.py
+++ b/moto/redshiftdata/models.py
@@ -200,7 +200,7 @@ def _validate_uuid(uuid):
 
 # For unknown reasons I cannot use the service name "redshift-data" as I should
 # It seems boto3 is unable to get the list of available regions for "redshift-data"
-# See code here https://github.com/spulec/moto/blob/master/moto/core/utils.py#L407
+# See code here https://github.com/getmoto/moto/blob/master/moto/core/utils.py#L407
 # sess.get_available_regions("redshift-data") returns an empty list
 # Then I use the service redshift since they share the same regions
 # See https://docs.aws.amazon.com/general/latest/gr/redshift-service.html
diff --git a/moto/sagemaker/models.py b/moto/sagemaker/models.py
index 1fbf54bc5..515091d46 100644
--- a/moto/sagemaker/models.py
+++ b/moto/sagemaker/models.py
@@ -1,6 +1,7 @@
 import json
 import os
 from datetime import datetime
+
 from moto.core import BaseBackend, BackendDict, BaseModel, CloudFormationModel
 from moto.sagemaker import validators
 from moto.utilities.paginator import paginate
@@ -10,6 +11,7 @@ from .exceptions import (
     AWSValidationException,
     ResourceNotFound,
 )
+from .utils import load_pipeline_definition_from_s3, arn_formatter
 
 
 PAGINATION_MODEL = {
@@ -44,10 +46,6 @@ PAGINATION_MODEL = {
 }
 
 
-def arn_formatter(_type, _id, account_id, region_name):
-    return f"arn:aws:sagemaker:{region_name}:{account_id}:{_type}/{_id}"
-
-
 class BaseObject(BaseModel):
     def camelCase(self, key):
         words = []
@@ -86,24 +84,42 @@ class FakePipeline(BaseObject):
         account_id,
         region_name,
         parallelism_configuration,
-        pipeline_definition_s3_location,
     ):
         self.pipeline_name = pipeline_name
         self.pipeline_arn = arn_formatter(
             "pipeline", pipeline_name, account_id, region_name
         )
-        self.pipeline_display_name = pipeline_display_name
+        self.pipeline_display_name = pipeline_display_name or pipeline_name
         self.pipeline_definition = pipeline_definition
         self.pipeline_description = pipeline_description
         self.role_arn = role_arn
         self.tags = tags or []
         self.parallelism_configuration = parallelism_configuration
-        self.pipeline_definition_s3_location = pipeline_definition_s3_location
 
         now_string = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
         self.creation_time = now_string
         self.last_modified_time = now_string
-        self.last_execution_time = now_string
+        self.last_execution_time = None
+
+        self.pipeline_status = "Active"
+        fake_user_profile_name = "fake-user-profile-name"
+        fake_domain_id = "fake-domain-id"
+        fake_user_profile_arn = arn_formatter(
+            "user-profile",
+            f"{fake_domain_id}/{fake_user_profile_name}",
+            account_id,
+            region_name,
+        )
+        self.created_by = {
+            "UserProfileArn": fake_user_profile_arn,
+            "UserProfileName": fake_user_profile_name,
+            "DomainId": fake_domain_id,
+        }
+        self.last_modified_by = {
+            "UserProfileArn": fake_user_profile_arn,
+            "UserProfileName": fake_user_profile_name,
+            "DomainId": fake_domain_id,
+        }
 
 
 class FakeProcessingJob(BaseObject):
@@ -1758,6 +1774,28 @@ class SageMakerModelBackend(BaseBackend):
         tags,
         parallelism_configuration,
     ):
+        if not any([pipeline_definition, pipeline_definition_s3_location]):
+            raise ValidationError(
+                "An error occurred (ValidationException) when calling the CreatePipeline operation: Either "
+                "Pipeline Definition or Pipeline Definition S3 location should be provided"
+            )
+        if all([pipeline_definition, pipeline_definition_s3_location]):
+            raise ValidationError(
+                "An error occurred (ValidationException) when calling the CreatePipeline operation: "
+                "Both Pipeline Definition and Pipeline Definition S3 Location shouldn't be present"
+            )
+
+        if pipeline_name in self.pipelines:
+            raise ValidationError(
+                f"An error occurred (ValidationException) when calling the CreatePipeline operation: Pipeline names "
+                f"must be unique within an AWS account and region. Pipeline with name ({pipeline_name}) already exists."
+            )
+
+        if pipeline_definition_s3_location:
+            pipeline_definition = load_pipeline_definition_from_s3(
+                pipeline_definition_s3_location, self.account_id
+            )
+
         pipeline = FakePipeline(
             pipeline_name,
             pipeline_display_name,
@@ -1767,7 +1805,6 @@ class SageMakerModelBackend(BaseBackend):
             tags,
             self.account_id,
             self.region_name,
-            pipeline_definition_s3_location,
             parallelism_configuration,
         )
 
@@ -1799,28 +1836,59 @@ class SageMakerModelBackend(BaseBackend):
                 message=f"Could not find pipeline with name {pipeline_name}."
             )
 
-        provided_kwargs = set(kwargs.keys())
-        allowed_kwargs = {
-            "pipeline_display_name",
-            "pipeline_definition",
-            "pipeline_definition_s3_location",
-            "pipeline_description",
-            "role_arn",
-            "parallelism_configuration",
-        }
-        invalid_kwargs = provided_kwargs - allowed_kwargs
-
-        if invalid_kwargs:
-            raise TypeError(
-                f"update_pipeline got unexpected keyword arguments '{invalid_kwargs}'"
+        if all(
+            [
+                kwargs.get("pipeline_definition"),
+                kwargs.get("pipeline_definition_s3_location"),
+            ]
+        ):
+            raise ValidationError(
+                "An error occurred (ValidationException) when calling the UpdatePipeline operation: "
+                "Both Pipeline Definition and Pipeline Definition S3 Location shouldn't be present"
             )
 
         for attr_key, attr_value in kwargs.items():
             if attr_value:
+                if attr_key == "pipeline_definition_s3_location":
+                    self.pipelines[
+                        pipeline_name
+                    ].pipeline_definition = load_pipeline_definition_from_s3(
+                        attr_value, self.account_id
+                    )
+                    continue
                 setattr(self.pipelines[pipeline_name], attr_key, attr_value)
 
         return pipeline_arn
 
+    def describe_pipeline(
+        self,
+        pipeline_name,
+    ):
+        try:
+            pipeline = self.pipelines[pipeline_name]
+        except KeyError:
+            raise ValidationError(
+                message=f"Could not find pipeline with name {pipeline_name}."
+            )
+
+        response = {
+            "PipelineArn": pipeline.pipeline_arn,
+            "PipelineName": pipeline.pipeline_name,
+            "PipelineDisplayName": pipeline.pipeline_display_name,
+            "PipelineDescription": pipeline.pipeline_description,
+            "PipelineDefinition": pipeline.pipeline_definition,
+            "RoleArn": pipeline.role_arn,
+            "PipelineStatus": pipeline.pipeline_status,
+            "CreationTime": pipeline.creation_time,
+            "LastModifiedTime": pipeline.last_modified_time,
+            "LastRunTime": pipeline.last_execution_time,
+            "CreatedBy": pipeline.created_by,
+            "LastModifiedBy": pipeline.last_modified_by,
+            "ParallelismConfiguration": pipeline.parallelism_configuration,
+        }
+
+        return response
+
     def list_pipelines(
         self,
         pipeline_name_prefix,
diff --git a/moto/sagemaker/responses.py b/moto/sagemaker/responses.py
index 34b3154aa..3c33ce99a 100644
--- a/moto/sagemaker/responses.py
+++ b/moto/sagemaker/responses.py
@@ -465,6 +465,13 @@ class SageMakerResponse(BaseResponse):
         response = self.sagemaker_backend.list_associations(self.request_params)
         return 200, {}, json.dumps(response)
 
+    @amzn_request_id
+    def describe_pipeline(self):
+        response = self.sagemaker_backend.describe_pipeline(
+            self._get_param("PipelineName")
+        )
+        return 200, {}, json.dumps(response)
+
     @amzn_request_id
     def create_pipeline(self):
         pipeline = self.sagemaker_backend.create_pipeline(
diff --git a/moto/sagemaker/utils.py b/moto/sagemaker/utils.py
new file mode 100644
index 000000000..1a83177ff
--- /dev/null
+++ b/moto/sagemaker/utils.py
@@ -0,0 +1,15 @@
+from moto.s3.models import s3_backends
+import json
+
+
+def load_pipeline_definition_from_s3(pipeline_definition_s3_location, account_id):
+    s3_backend = s3_backends[account_id]["global"]
+    result = s3_backend.get_object(
+        bucket_name=pipeline_definition_s3_location["Bucket"],
+        key_name=pipeline_definition_s3_location["ObjectKey"],
+    )
+    return json.loads(result.value)
+
+
+def arn_formatter(_type, _id, account_id, region_name):
+    return f"arn:aws:sagemaker:{region_name}:{account_id}:{_type}/{_id}"
diff --git a/moto/sns/models.py b/moto/sns/models.py
index 42cecea05..bdb5fe8f3 100644
--- a/moto/sns/models.py
+++ b/moto/sns/models.py
@@ -642,7 +642,7 @@ class SNSBackend(BaseBackend):
         group_id=None,
     ):
         if subject is not None and len(subject) > 100:
-            # Note that the AWS docs around length are wrong: https://github.com/spulec/moto/issues/1503
+            # Note that the AWS docs around length are wrong: https://github.com/getmoto/moto/issues/1503
             raise ValueError("Subject must be less than 100 characters")
 
         if phone_number:
diff --git a/pyproject.toml b/pyproject.toml
new file mode 100644
index 000000000..d984fdce7
--- /dev/null
+++ b/pyproject.toml
@@ -0,0 +1,3 @@
+[build-system]
+requires = ["setuptools >= 40.6.0", "wheel"]
+build-backend = "setuptools.build_meta"
\ No newline at end of file
diff --git a/requirements-dev.txt b/requirements-dev.txt
index 852da3205..e1cdf2549 100644
--- a/requirements-dev.txt
+++ b/requirements-dev.txt
@@ -10,4 +10,5 @@ inflection
 lxml
 mypy
 packaging
+build
 prompt_toolkit
diff --git a/scripts/bump_version b/scripts/bump_version
index d1af3a84b..4a3fdd842 100755
--- a/scripts/bump_version
+++ b/scripts/bump_version
@@ -21,7 +21,7 @@ main() {
   # Commit an updated IMPLEMENTATION_COVERAGE.md
   make implementation_coverage || true
   # Open a PR
-  open https://github.com/spulec/moto/compare/master...version-${version}
+  open https://github.com/getmoto/moto/compare/master...version-${version}
 }
 
 main $@
diff --git a/scripts/ec2_get_instance_type_offerings.py b/scripts/ec2_get_instance_type_offerings.py
index f34d57fc1..26af2828d 100755
--- a/scripts/ec2_get_instance_type_offerings.py
+++ b/scripts/ec2_get_instance_type_offerings.py
@@ -60,7 +60,7 @@ def main():
                 # Ensure we use the correct US-west availability zones
                 # There are only two - for some accounts they are called us-west-1b and us-west-1c
                 # As our EC2-module assumes us-west-1a and us-west-1b, we may have to rename the zones coming from AWS
-                # https://github.com/spulec/moto/issues/5494
+                # https://github.com/getmoto/moto/issues/5494
                 if region == "us-west-1" and location_type == "availability-zone":
                     zones = set([i["Location"] for i in instances])
                     if zones == {"us-west-1b", "us-west-1c"}:
diff --git a/setup.cfg b/setup.cfg
index 6416857cb..5c2b55b82 100644
--- a/setup.cfg
+++ b/setup.cfg
@@ -1,3 +1,214 @@
+[metadata]
+name = moto
+version = 4.1.0.dev
+long_description = file:README.md
+long_description_content_type = text/markdown
+author = Steve Pulec
+author_email = "spulec@gmail.com"
+url = https://github.com/getmoto/moto
+license = Apache License 2.0
+test_suite = tests
+classifiers =
+    Programming Language :: Python :: 3
+    Programming Language :: Python :: 3.7
+    Programming Language :: Python :: 3.8
+    Programming Language :: Python :: 3.9
+    Programming Language :: Python :: 3.10
+    Programming Language :: Python :: 3.11
+    License :: OSI Approved :: Apache Software License
+    Topic :: Software Development :: Testing
+keywords = aws ec2 s3 boto3 mock
+project_urls =
+    Documentation = http://docs.getmoto.org/en/latest/
+    Issue tracker = https://github.com/getmoto/moto/issues
+    Changelog = https://github.com/getmoto/moto/blob/master/CHANGELOG.md
+
+[options]
+python_requires = >=3.7
+install_requires =
+    boto3>=1.9.201
+    botocore>=1.12.201
+    cryptography>=3.3.1
+    requests>=2.5
+    xmltodict
+    werkzeug>=0.5,!=2.2.0,!=2.2.1
+    python-dateutil<3.0.0,>=2.1
+    responses>=0.13.0
+    Jinja2>=2.10.1
+    importlib_metadata ; python_version < '3.8'
+package_dir =
+    moto = moto
+include_package_data = True
+
+[options.extras_require]
+all =
+    python-jose[cryptography]>=3.1.0,<4.0.0
+    ecdsa!=0.15
+    docker>=2.5.1
+    graphql-core
+    PyYAML>=5.1
+    cfn-lint>=0.40.0
+    sshpubkeys>=3.1.0
+    openapi-spec-validator>=0.2.8
+    pyparsing>=3.0.7
+    jsondiff>=1.1.2
+    aws-xray-sdk!=0.96,>=0.93
+    setuptools
+server =
+    python-jose[cryptography]>=3.1.0,<4.0.0
+    ecdsa!=0.15
+    docker>=2.5.1
+    graphql-core
+    PyYAML>=5.1
+    cfn-lint>=0.40.0
+    sshpubkeys>=3.1.0
+    openapi-spec-validator>=0.2.8
+    pyparsing>=3.0.7
+    jsondiff>=1.1.2
+    aws-xray-sdk!=0.96,>=0.93
+    setuptools
+    flask!=2.2.0,!=2.2.1
+    flask-cors
+acm =
+acmpca =
+amp =
+apigateway =
+    PyYAML>=5.1
+    python-jose[cryptography]>=3.1.0,<4.0.0
+    ecdsa!=0.15
+    openapi-spec-validator>=0.2.8
+apigatewayv2 = PyYAML>=5.1
+applicationautoscaling =
+appsync = graphql-core
+athena =
+autoscaling =
+awslambda = docker>=2.5.1
+batch = docker>=2.5.1
+batch_simple =
+budgets =
+ce =
+cloudformation =
+    python-jose[cryptography]>=3.1.0,<4.0.0
+    ecdsa!=0.15
+    docker>=2.5.1
+    graphql-core
+    PyYAML>=5.1
+    cfn-lint>=0.40.0
+    sshpubkeys>=3.1.0
+    openapi-spec-validator>=0.2.8
+    pyparsing>=3.0.7
+    jsondiff>=1.1.2
+    aws-xray-sdk!=0.96,>=0.93
+    setuptools
+cloudfront =
+cloudtrail =
+cloudwatch =
+codebuild =
+codecommit =
+codepipeline =
+cognitoidentity =
+cognitoidp =
+    python-jose[cryptography]>=3.1.0,<4.0.0
+    ecdsa!=0.15
+comprehend =
+config =
+databrew =
+datapipeline =
+datasync =
+dax =
+dms =
+ds = sshpubkeys>=3.1.0
+dynamodb = docker>=2.5.1
+dynamodbstreams = docker>=2.5.1
+ebs = sshpubkeys>=3.1.0
+ec2 = sshpubkeys>=3.1.0
+ec2instanceconnect =
+ecr =
+ecs =
+efs = sshpubkeys>=3.1.0
+eks =
+elasticache =
+elasticbeanstalk =
+elastictranscoder =
+elb =
+elbv2 =
+emr =
+emrcontainers =
+emrserverless =
+es =
+events =
+firehose =
+forecast =
+glacier =
+glue = pyparsing>=3.0.7
+greengrass =
+guardduty =
+iam =
+iot =
+iotdata = jsondiff>=1.1.2
+kinesis =
+kinesisvideo =
+kinesisvideoarchivedmedia =
+kms =
+logs =
+managedblockchain =
+mediaconnect =
+medialive =
+mediapackage =
+mediastore =
+mediastoredata =
+meteringmarketplace =
+mq =
+opsworks =
+organizations =
+personalize =
+pinpoint =
+polly =
+quicksight =
+ram =
+rds =
+redshift =
+redshiftdata =
+rekognition =
+resourcegroups =
+resourcegroupstaggingapi =
+route53 =
+route53resolver = sshpubkeys>=3.1.0
+s3 = PyYAML>=5.1
+s3control =
+sagemaker =
+sdb =
+secretsmanager =
+servicediscovery =
+servicequotas =
+ses =
+signer =
+sns =
+sqs =
+ssm =
+    PyYAML>=5.1
+ssoadmin =
+stepfunctions =
+sts =
+support =
+swf =
+textract =
+timestreamwrite =
+transcribe =
+wafv2 =
+# XRay module uses pkg_resources, but doesn't have an explicit dependency listed.  This is fixed in 2.9.0:
+# https://github.com/aws/aws-xray-sdk-python/issues/305
+xray =
+    aws-xray-sdk!=0.96,>=0.93
+    setuptools
+
+[options.entry_points]
+console_scripts =
+    moto_server = moto.server:main
+
+[bdist_wheel]
+universal=1
+
 [tool:pytest]
 markers =
     network: marks tests which require network connection
diff --git a/setup.py b/setup.py
deleted file mode 100755
index 76bff66be..000000000
--- a/setup.py
+++ /dev/null
@@ -1,160 +0,0 @@
-#!/usr/bin/env python
-from io import open
-import os
-import re
-from setuptools import setup, find_packages
-import moto.__init__ as service_list
-
-# Borrowed from pip at https://github.com/pypa/pip/blob/62c27dee45625e1b63d1e023b0656310f276e050/setup.py#L11-L15
-here = os.path.abspath(os.path.dirname(__file__))
-
-
-def read(*parts):
-    # intentionally *not* adding an encoding option to open, See:
-    #   https://github.com/pypa/virtualenv/issues/201#issuecomment-3145690
-    with open(os.path.join(here, *parts), "r") as fp:
-        return fp.read()
-
-
-def get_version():
-    version_file = read("moto", "__init__.py")
-    version_match = re.search(
-        r'^__version__ = [\'"]([^\'"]*)[\'"]', version_file, re.MULTILINE
-    )
-    if version_match:
-        return version_match.group(1)
-    raise RuntimeError("Unable to find version string.")
-
-
-install_requires = [
-    "boto3>=1.9.201",
-    "botocore>=1.12.201",
-    "cryptography>=3.3.1",
-    "requests>=2.5",
-    "xmltodict",
-    "werkzeug>=0.5,!=2.2.0,!=2.2.1",
-    "python-dateutil<3.0.0,>=2.1",
-    "responses>=0.13.0",
-    "MarkupSafe!=2.0.0a1",  # This is a Jinja2 dependency, 2.0.0a1 currently seems broken
-    "Jinja2>=2.10.1",
-    "importlib_metadata ; python_version < '3.8'",
-]
-
-_dep_PyYAML = "PyYAML>=5.1"
-_dep_python_jose = "python-jose[cryptography]>=3.1.0,<4.0.0"
-_dep_python_jose_ecdsa_pin = (
-    "ecdsa!=0.15"  # https://github.com/spulec/moto/pull/3263#discussion_r477404984
-)
-_dep_docker = "docker>=2.5.1"
-_dep_graphql = "graphql-core"
-_dep_jsondiff = "jsondiff>=1.1.2"
-_dep_aws_xray_sdk = "aws-xray-sdk!=0.96,>=0.93"
-_dep_idna = "idna<4,>=2.5"
-_dep_cfn_lint = "cfn-lint>=0.40.0"
-_dep_sshpubkeys = "sshpubkeys>=3.1.0"
-_dep_openapi = "openapi-spec-validator>=0.2.8"
-_dep_pyparsing = "pyparsing>=3.0.7"
-_setuptools = "setuptools"
-
-all_extra_deps = [
-    _dep_PyYAML,
-    _dep_python_jose,
-    _dep_python_jose_ecdsa_pin,
-    _dep_docker,
-    _dep_graphql,
-    _dep_jsondiff,
-    _dep_aws_xray_sdk,
-    _dep_idna,
-    _dep_cfn_lint,
-    _dep_sshpubkeys,
-    _dep_pyparsing,
-    _dep_openapi,
-    _setuptools,
-]
-all_server_deps = all_extra_deps + ["flask!=2.2.0,!=2.2.1", "flask-cors"]
-
-extras_per_service = {}
-for service_name in [
-    service[5:]
-    for service in dir(service_list)
-    if service.startswith("mock_") and not service == "mock_all"
-]:
-    extras_per_service[service_name] = []
-extras_per_service.update(
-    {
-        "apigateway": [_dep_PyYAML, _dep_python_jose, _dep_python_jose_ecdsa_pin, _dep_openapi],
-        "apigatewayv2": [_dep_PyYAML],
-        "appsync": [_dep_graphql],
-        "awslambda": [_dep_docker],
-        "batch": [_dep_docker],
-        "cognitoidp": [_dep_python_jose, _dep_python_jose_ecdsa_pin],
-        "ec2": [_dep_sshpubkeys],
-        "glue": [_dep_pyparsing],
-        "iotdata": [_dep_jsondiff],
-        "s3": [_dep_PyYAML],
-        "ses": [],
-        "sns": [],
-        "sqs": [],
-        "ssm": [_dep_PyYAML],
-        # XRay module uses pkg_resources, but doesn't have an explicit
-        # dependency listed.  This should be fixed in the next version:
-        # https://github.com/aws/aws-xray-sdk-python/issues/305
-        "xray": [_dep_aws_xray_sdk, _setuptools],
-    }
-)
-
-# When a Table has a Stream, we'll always need to import AWSLambda to search for a corresponding function to send the table data to
-extras_per_service["dynamodb"] = extras_per_service["awslambda"]
-extras_per_service["dynamodb2"] = extras_per_service["dynamodb"]
-extras_per_service["dynamodbstreams"] = extras_per_service["awslambda"]
-# EBS depends on EC2 to create snapshots
-extras_per_service["ebs"] = extras_per_service["ec2"]
-# EFS depends on EC2 to find subnets etc
-extras_per_service["efs"] = extras_per_service["ec2"]
-# DirectoryService needs EC2 to verify VPCs and subnets.
-extras_per_service["ds"] = extras_per_service["ec2"]
-extras_per_service["route53resolver"] = extras_per_service["ec2"]
-# CloudFormation imports everything, so install everything
-extras_per_service["cloudformation"] = all_extra_deps
-extras_require = {
-    "all": all_extra_deps,
-    "server": all_server_deps,
-}
-
-extras_require.update(extras_per_service)
-
-
-setup(
-    name="moto",
-    version=get_version(),
-    description="A library that allows your python tests to easily"
-    " mock out the boto library",
-    long_description=read("README.md"),
-    long_description_content_type="text/markdown",
-    author="Steve Pulec",
-    author_email="spulec@gmail.com",
-    url="https://github.com/spulec/moto",
-    entry_points={"console_scripts": ["moto_server = moto.server:main"]},
-    packages=find_packages(exclude=("tests", "tests.*")),
-    python_requires=">=3.7",
-    install_requires=install_requires,
-    extras_require=extras_require,
-    include_package_data=True,
-    license="Apache License 2.0",
-    test_suite="tests",
-    classifiers=[
-        "Programming Language :: Python :: 3",
-        "Programming Language :: Python :: 3.7",
-        "Programming Language :: Python :: 3.8",
-        "Programming Language :: Python :: 3.9",
-        "Programming Language :: Python :: 3.10",
-        "Programming Language :: Python :: 3.11",
-        "License :: OSI Approved :: Apache Software License",
-        "Topic :: Software Development :: Testing",
-    ],
-    project_urls={
-        "Documentation": "http://docs.getmoto.org/en/latest/",
-        "Issue tracker": "https://github.com/spulec/moto/issues",
-        "Changelog": "https://github.com/spulec/moto/blob/master/CHANGELOG.md"
-    },
-)
diff --git a/update_version_from_git.py b/update_version_from_git.py
index e08ad75f8..94e9a498d 100644
--- a/update_version_from_git.py
+++ b/update_version_from_git.py
@@ -29,9 +29,8 @@ import sys
 from packaging.version import Version
 
 
-def migrate_source_attribute(attr, to_this, target_file, regex):
+def migrate_source_attribute(attr, to_this, target_file):
     """Updates __magic__ attributes in the source file"""
-    change_this = re.compile(regex, re.S)
     new_file = []
     found = False
 
@@ -41,7 +40,7 @@ def migrate_source_attribute(attr, to_this, target_file, regex):
     for line in lines:
         if line.startswith(attr):
             found = True
-            line = re.sub(change_this, to_this, line)
+            line = to_this
         new_file.append(line)
 
     if found:
@@ -49,14 +48,19 @@ def migrate_source_attribute(attr, to_this, target_file, regex):
             fp.writelines(new_file)
 
 
-def migrate_version(target_file, new_version):
-    """Updates __version__ in the source file"""
-    regex = r"['\"](.*)['\"]"
+def migrate_version(new_version):
+    initpy = os.path.abspath("moto/__init__.py")
+    setupcfg = os.path.abspath("setup.cfg")
+    """Updates __version__ in the init file"""
     migrate_source_attribute(
         "__version__",
-        '"{new_version}"'.format(new_version=new_version),
-        target_file,
-        regex,
+        to_this=f'__version__ = "{new_version}"\n',
+        target_file=initpy,
+    )
+    migrate_source_attribute(
+        "version =",
+        to_this=f'version = {new_version}\n',
+        target_file=setupcfg,
     )
 
 
@@ -130,7 +134,6 @@ def release_version_correct():
     """
     if is_master_branch():
         # update for a pre release version.
-        initpy = os.path.abspath("moto/__init__.py")
 
         new_version = prerelease_version()
         print(
@@ -141,7 +144,7 @@ def release_version_correct():
         assert (
             len(new_version.split(".")) >= 4
         ), "moto/__init__.py version should be like 0.0.2.dev"
-        migrate_version(initpy, new_version)
+        migrate_version(new_version)
     else:
         assert False, "No non-master deployments yet"
 
@@ -155,8 +158,8 @@ if __name__ == "__main__":
             new_version = arg
         if new_version == "patch":
             new_version = increase_patch_version(get_version())
-        initpy = os.path.abspath("moto/__init__.py")
-        migrate_version(initpy, new_version)
+
+        migrate_version(new_version)
     else:
         print(
             "Invalid usage. Supply 0 or 1 arguments. "


Test patch:
diff --git a/.github/workflows/dependency_test.yml b/.github/workflows/dependency_test.yml
index ff64bd4f4..155b05e94 100644
--- a/.github/workflows/dependency_test.yml
+++ b/.github/workflows/dependency_test.yml
@@ -6,12 +6,23 @@ on:
     - cron: '0 0 * * *'  # every day at midnight
 
 jobs:
+  prepare_list:
+    runs-on: ubuntu-latest
+    steps:
+    - uses: actions/checkout@v3
+    - id: set-matrix
+      run: echo "matrix=[$(ls -A1 moto | grep -v '.py' | awk '{ printf "%s\047%s\047", (NR==1?"":", "), $0 } END{ print "" }')]" >> $GITHUB_OUTPUT
+    - run: echo "matrix=[$(ls -A1 moto | grep -v '.py' | awk '{ printf "%s\047%s\047", (NR==1?"":", "), $0 } END{ print "" }')]"
+    outputs:
+      matrix: ${{ steps.set-matrix.outputs.matrix }}
   runtest:
     name: Run Dependency Test
     runs-on: ubuntu-latest
+    needs: prepare_list
     strategy:
       matrix:
         python-version: [ 3.8 ]
+        service: ${{ fromJson(needs.prepare_list.outputs.matrix) }}
 
     steps:
     - name: Checkout repo
@@ -25,11 +36,4 @@ jobs:
         AWS_ACCESS_KEY_ID: key
         AWS_SECRET_ACCESS_KEY: secret
       run: |
-        scripts/dependency_test.sh
-    - name: Archive logs
-      if: always()
-      uses: actions/upload-artifact@v3
-      with:
-        name: buildfolder-${{ matrix.python-version }}
-        path: |
-          test_results_*.log
+        scripts/dependency_test.sh ${{ matrix.service }}
diff --git a/.github/workflows/dockertests.yml b/.github/workflows/dockertests.yml
index 6beebc8ca..bf9564b3b 100644
--- a/.github/workflows/dockertests.yml
+++ b/.github/workflows/dockertests.yml
@@ -53,7 +53,8 @@ jobs:
         python-version: ${{ matrix.python-version }}
     - name: Start MotoServer on an unusual port
       run: |
-        python setup.py sdist
+        pip install --upgrade build
+        python -m build
         docker run --rm -t --name motoserver -e TEST_SERVER_MODE=true -e AWS_SECRET_ACCESS_KEY=server_secret -e MOTO_PORT=4555 -e AWS_ACCESS_KEY_ID=server_key -v `pwd`:/moto -p 4555:4555 -v /var/run/docker.sock:/var/run/docker.sock python:3.7-buster /moto/scripts/ci_moto_server.sh &
         MOTO_PORT=4555 python scripts/ci_wait_for_server.py
     - name: Get pip cache dir
@@ -111,7 +112,8 @@ jobs:
         python-version: ${{ matrix.python-version }}
     - name: Start MotoServer on a custom Docker network bridge
       run: |
-        python setup.py sdist
+        pip install --upgrade build
+        python -m build
         docker network create -d bridge my-custom-network
         docker run --rm -t -e TEST_SERVER_MODE=true -e MOTO_DOCKER_NETWORK_NAME=my-custom-network -e AWS_SECRET_ACCESS_KEY=server_secret -e AWS_ACCESS_KEY_ID=server_key -v `pwd`:/moto -p 5000:5000 --network my-custom-network -v /var/run/docker.sock:/var/run/docker.sock python:3.7-buster /moto/scripts/ci_moto_server.sh &
         python scripts/ci_wait_for_server.py
@@ -168,7 +170,8 @@ jobs:
         python-version: ${{ matrix.python-version }}
     - name: Start MotoServer on an unusual port
       run: |
-        python setup.py sdist
+        pip install --upgrade build
+        python -m build
         docker run --rm -t -e MOTO_DOCKER_NETWORK_MODE=host -e TEST_SERVER_MODE=true -e AWS_SECRET_ACCESS_KEY=server_secret -e MOTO_PORT=4555 -e AWS_ACCESS_KEY_ID=server_key -v `pwd`:/moto -p 4555:4555 -v /var/run/docker.sock:/var/run/docker.sock python:3.7-buster /moto/scripts/ci_moto_server.sh &
         MOTO_PORT=4555 python scripts/ci_wait_for_server.py
     - name: Get pip cache dir
diff --git a/.github/workflows/test_outdated_versions.yml b/.github/workflows/test_outdated_versions.yml
index 6a49974fe..47a38c786 100644
--- a/.github/workflows/test_outdated_versions.yml
+++ b/.github/workflows/test_outdated_versions.yml
@@ -45,7 +45,7 @@ jobs:
 
     - name: Start MotoServer
       run: |
-        python setup.py sdist
+        python -m build
         docker run --rm -t --name motoserver -e TEST_SERVER_MODE=true -e AWS_SECRET_ACCESS_KEY=server_secret -e AWS_ACCESS_KEY_ID=server_key -v `pwd`:/moto -p 5000:5000 -v /var/run/docker.sock:/var/run/docker.sock python:3.7-buster /moto/scripts/ci_moto_server.sh &
         python scripts/ci_wait_for_server.py
     - name: Test ServerMode/Coverage
diff --git a/.github/workflows/test_terraform.yml b/.github/workflows/test_terraform.yml
index ce4bde102..edd4f6860 100644
--- a/.github/workflows/test_terraform.yml
+++ b/.github/workflows/test_terraform.yml
@@ -31,8 +31,8 @@ jobs:
         python-version: "3.8"
     - name: Start MotoServer
       run: |
-        pip install PyYAML
-        python setup.py sdist
+        pip install PyYAML build
+        python -m build
         docker run --rm -t --name motoserver -e TEST_SERVER_MODE=true -e MOTO_PORT=4566 -e AWS_SECRET_ACCESS_KEY=server_secret -e AWS_ACCESS_KEY_ID=server_key -v `pwd`:/moto -p 4566:4566 -v /var/run/docker.sock:/var/run/docker.sock python:3.7-buster /moto/scripts/ci_moto_server.sh &
         MOTO_PORT=4566 python scripts/ci_wait_for_server.py
     - name: Download Cache
diff --git a/requirements-tests.txt b/requirements-tests.txt
index b24d60486..2bcfe7cf2 100644
--- a/requirements-tests.txt
+++ b/requirements-tests.txt
@@ -2,6 +2,6 @@ coverage
 pytest
 pytest-cov
 pytest-xdist
-sure
+surer
 freezegun
 pylint
diff --git a/scripts/dependency_test.sh b/scripts/dependency_test.sh
index b481cbdbd..8113dc781 100755
--- a/scripts/dependency_test.sh
+++ b/scripts/dependency_test.sh
@@ -33,7 +33,7 @@ valid_service() {
   # Verify whether this is a valid service
   # We'll ignore metadata folders, and folders that test generic Moto behaviour
   # We'll also ignore CloudFormation, as it will always depend on other services
-  local ignore_moto_folders="core instance_metadata __pycache__ templates cloudformation moto_api resourcegroupstaggingapi packages utilities s3bucket_path"
+  local ignore_moto_folders="core instance_metadata __pycache__ templates cloudformation moto_api moto_server resourcegroupstaggingapi packages utilities s3bucket_path"
   if echo $ignore_moto_folders | grep -q "$1"; then
     return 1
   else
@@ -46,53 +46,43 @@ test_service() {
   path_to_test_file=$2
   venv_path="test_venv_${service}"
   overwrite "Running tests for ${service}.."
-  python -m venv ${venv_path} > /dev/null
-  source ${venv_path}/bin/activate > /dev/null
+  python -m venv ${venv_path}
+  source ${venv_path}/bin/activate
+  pip install --upgrade pip setuptools
   # Can't just install requirements-file, as it points to all dependencies
-  pip install -r requirements-tests.txt > /dev/null
-  pip install .[$service] > /dev/null 2>&1
-  pip install boto > /dev/null 2>&1
+  pip install -r requirements-tests.txt
+  pip install .[$service]
+  pip install boto
   if [[ $service != "xray" ]]; then
-    pip uninstall setuptools pkg_resources -y > /dev/null 2>&1
+    pip uninstall setuptools pkg_resources -y
   fi
   # Restart venv - ensure these deps are loaded
   deactivate
-  source ${venv_path}/bin/activate > /dev/null
+  source ${venv_path}/bin/activate
   # Run tests for this service
-  test_result_filename="test_results_${service}.log"
-  touch $test_result_filename
-  pytest -sv --ignore-glob="**/test_server.py" --ignore-glob="**/test_*_cloudformation.py" --ignore-glob="**/test_*_integration.py" $path_to_test_file >$test_result_filename 2>&1
+  pytest -sv --ignore-glob="**/test_server.py" --ignore-glob="**/test_*_cloudformation.py" --ignore-glob="**/test_*_integration.py" $path_to_test_file
   RESULT=$?
+  deactivate
+  rm -rf ${venv_path}
   if [[ $RESULT != 0 ]]; then
     echo -e "Tests for ${service} have failed!\n"
-  else
-    rm $test_result_filename
+    exit -1
   fi
-  deactivate
-  rm -rf ${venv_path}
 }
 
-echo "Running Dependency tests..."
-ITER=0
-for file in moto/*
-do
-    if [[ -d $file ]]; then
-      service=${file:5}
-      path_to_test_file="tests/test_${service}"
-      if valid_service $service && [[ -d $path_to_test_file ]]; then
-        test_service $service $path_to_test_file &
-      elif valid_service $service; then
-        echo -e "No tests for ${service} can be found on ${path_to_test_file}!\n"
-      fi
-      if (( $ITER % 4 == 0 )); then
-        # Ensure we're only processing 4 services at the time
-        wait
-      fi
-    fi
-    ITER=$(expr $ITER + 1)
-done
-wait
-
-nr_of_failed_services=$(ls -1q test_results*.log 2>/dev/null | wc -l)
-echo "Nr of failed services: ${nr_of_failed_services}"
-exit $((nr_of_failed_services))
+service=$1
+if [[ $# -eq 0 ]] ; then
+    echo 'Please add the name of the service you want to test as the first argument:'
+    echo '    scripts/dependency_test.sh acm'
+    exit 0
+fi
+echo "Running Dependency tests for {$1}..."
+path_to_test_file="tests/test_${service}"
+if valid_service $service && [[ -d $path_to_test_file ]]; then
+  test_service $service $path_to_test_file
+  if [[ $? != 0 ]]; then
+    exit -1
+  fi
+elif valid_service $service; then
+  echo -e "No tests for ${service} can be found on ${path_to_test_file}!\n"
+fi
diff --git a/tests/test_batch/test_batch_jobs.py b/tests/test_batch/test_batch_jobs.py
index 6a659019e..48f7a4eb9 100644
--- a/tests/test_batch/test_batch_jobs.py
+++ b/tests/test_batch/test_batch_jobs.py
@@ -108,7 +108,7 @@ def test_submit_job():
     job_id = resp["jobId"]
 
     # Test that describe_jobs() returns 'createdAt'
-    # github.com/spulec/moto/issues/4364
+    # github.com/getmoto/moto/issues/4364
     resp = batch_client.describe_jobs(jobs=[job_id])
     created_at = resp["jobs"][0]["createdAt"]
     created_at.should.be.greater_than(start_time_milliseconds)
@@ -127,7 +127,7 @@ def test_submit_job():
     [event["message"] for event in resp["events"]].should.equal(["hello"])
 
     # Test that describe_jobs() returns timestamps in milliseconds
-    # github.com/spulec/moto/issues/4364
+    # github.com/getmoto/moto/issues/4364
     job = batch_client.describe_jobs(jobs=[job_id])["jobs"][0]
     created_at = job["createdAt"]
     started_at = job["startedAt"]
diff --git a/tests/test_cloudwatch/test_cloudwatch_boto3.py b/tests/test_cloudwatch/test_cloudwatch_boto3.py
index 1c07c5200..2fdc4b39a 100644
--- a/tests/test_cloudwatch/test_cloudwatch_boto3.py
+++ b/tests/test_cloudwatch/test_cloudwatch_boto3.py
@@ -194,7 +194,7 @@ def test_put_metric_data_with_statistics():
             dict(
                 MetricName="statmetric",
                 Timestamp=utc_now,
-                # no Value to test  https://github.com/spulec/moto/issues/1615
+                # no Value to test  https://github.com/getmoto/moto/issues/1615
                 StatisticValues=dict(
                     SampleCount=3.0, Sum=123.0, Maximum=100.0, Minimum=12.0
                 ),
diff --git a/tests/test_cognitoidp/test_cognitoidp.py b/tests/test_cognitoidp/test_cognitoidp.py
index 26785eb90..c8425128c 100644
--- a/tests/test_cognitoidp/test_cognitoidp.py
+++ b/tests/test_cognitoidp/test_cognitoidp.py
@@ -2881,7 +2881,7 @@ def test_change_password():
 
 @mock_cognitoidp
 def test_change_password__using_custom_user_agent_header():
-    # https://github.com/spulec/moto/issues/3098
+    # https://github.com/getmoto/moto/issues/3098
     # As the admin_initiate_auth-method is unauthenticated, we use the user-agent header to pass in the region
     # This test verifies this works, even if we pass in our own user-agent header
     from botocore.config import Config
@@ -4327,7 +4327,7 @@ if not settings.TEST_SERVER_MODE:
 
     @mock_cognitoidp
     def test_idtoken_contains_kid_header():
-        # https://github.com/spulec/moto/issues/3078
+        # https://github.com/getmoto/moto/issues/3078
         # Setup
         cognito = boto3.client("cognito-idp", "us-west-2")
         user_pool_id = cognito.create_user_pool(PoolName=str(uuid.uuid4()))["UserPool"][
diff --git a/tests/test_core/test_auth.py b/tests/test_core/test_auth.py
index baf8c7faf..0bdcf0010 100644
--- a/tests/test_core/test_auth.py
+++ b/tests/test_core/test_auth.py
@@ -291,7 +291,7 @@ def test_access_denied_with_not_allowing_policy():
 @set_initial_no_auth_action_count(3)
 @mock_ec2
 def test_access_denied_for_run_instances():
-    # https://github.com/spulec/moto/issues/2774
+    # https://github.com/getmoto/moto/issues/2774
     # The run-instances method was broken between botocore versions 1.15.8 and 1.15.12
     # This was due to the inclusion of '"idempotencyToken":true' in the response, somehow altering the signature and breaking the authentication
     # Keeping this test in place in case botocore decides to break again
diff --git a/tests/test_core/test_request_mocking.py b/tests/test_core/test_request_mocking.py
index 17855d75a..7add26bbc 100644
--- a/tests/test_core/test_request_mocking.py
+++ b/tests/test_core/test_request_mocking.py
@@ -29,7 +29,7 @@ if not settings.TEST_SERVER_MODE:
 @mock_s3
 def test_decorator_ordering() -> None:
     """
-    https://github.com/spulec/moto/issues/3790#issuecomment-803979809
+    https://github.com/getmoto/moto/issues/3790#issuecomment-803979809
     """
     bucket_name = "banana-slugs"
     key = "trash-file"
diff --git a/tests/test_core/test_url_base_regex.py b/tests/test_core/test_url_base_regex.py
index 54d9d9070..491512401 100644
--- a/tests/test_core/test_url_base_regex.py
+++ b/tests/test_core/test_url_base_regex.py
@@ -13,7 +13,7 @@ service_names = [
 
 class TestMockBucketStartingWithServiceName:
     """
-    https://github.com/spulec/moto/issues/4099
+    https://github.com/getmoto/moto/issues/4099
     """
 
     @pytest.mark.parametrize("service_name,decorator", service_names)
diff --git a/tests/test_dynamodb/test_dynamodb.py b/tests/test_dynamodb/test_dynamodb.py
index 244502cdb..8f3459ef9 100644
--- a/tests/test_dynamodb/test_dynamodb.py
+++ b/tests/test_dynamodb/test_dynamodb.py
@@ -1920,7 +1920,7 @@ def test_update_continuous_backups_errors():
     ex.response["Error"]["Message"].should.equal("Table not found: not-existing-table")
 
 
-# https://github.com/spulec/moto/issues/1043
+# https://github.com/getmoto/moto/issues/1043
 @mock_dynamodb
 def test_query_missing_expr_names():
     client = boto3.client("dynamodb", region_name="us-east-1")
@@ -1964,7 +1964,7 @@ def test_query_missing_expr_names():
     resp["Items"][0]["client"]["S"].should.equal("test2")
 
 
-# https://github.com/spulec/moto/issues/2328
+# https://github.com/getmoto/moto/issues/2328
 @mock_dynamodb
 def test_update_item_with_list():
     dynamodb = boto3.resource("dynamodb", region_name="us-east-1")
@@ -1986,7 +1986,7 @@ def test_update_item_with_list():
     resp["Item"].should.equal({"key": "the-key", "list": [1, 2]})
 
 
-# https://github.com/spulec/moto/issues/2328
+# https://github.com/getmoto/moto/issues/2328
 @mock_dynamodb
 def test_update_item_with_no_action_passed_with_list():
     dynamodb = boto3.resource("dynamodb", region_name="us-east-1")
@@ -2010,7 +2010,7 @@ def test_update_item_with_no_action_passed_with_list():
     resp["Item"].should.equal({"key": "the-key", "list": [1, 2]})
 
 
-# https://github.com/spulec/moto/issues/1342
+# https://github.com/getmoto/moto/issues/1342
 @mock_dynamodb
 def test_update_item_on_map():
     dynamodb = boto3.resource("dynamodb", region_name="us-east-1")
@@ -2089,7 +2089,7 @@ def test_update_item_on_map():
         )
 
 
-# https://github.com/spulec/moto/issues/1358
+# https://github.com/getmoto/moto/issues/1358
 @mock_dynamodb
 def test_update_if_not_exists():
     dynamodb = boto3.resource("dynamodb", region_name="us-east-1")
@@ -2133,7 +2133,7 @@ def test_update_if_not_exists():
     assert resp["Items"][0]["created_at"] == 123
 
 
-# https://github.com/spulec/moto/issues/1937
+# https://github.com/getmoto/moto/issues/1937
 @mock_dynamodb
 def test_update_return_attributes():
     dynamodb = boto3.client("dynamodb", region_name="us-east-1")
@@ -2175,7 +2175,7 @@ def test_update_return_attributes():
     err["Message"].should.equal("Return values set to invalid value")
 
 
-# https://github.com/spulec/moto/issues/3448
+# https://github.com/getmoto/moto/issues/3448
 @mock_dynamodb
 def test_update_return_updated_new_attributes_when_same():
     dynamo_client = boto3.resource("dynamodb", region_name="us-east-1")
@@ -2872,7 +2872,7 @@ def test_sorted_query_with_numerical_sort_key():
     ), "result items are not sorted by numerical value"
 
 
-# https://github.com/spulec/moto/issues/1874
+# https://github.com/getmoto/moto/issues/1874
 @mock_dynamodb
 def test_item_size_is_under_400KB():
     dynamodb = boto3.resource("dynamodb", region_name="us-east-1")
@@ -4206,7 +4206,7 @@ def create_simple_table_and_return_client():
     return dynamodb
 
 
-# https://github.com/spulec/moto/issues/2806
+# https://github.com/getmoto/moto/issues/2806
 # https://docs.aws.amazon.com/amazondynamodb/latest/APIReference/API_UpdateItem.html
 #       #DDB-UpdateItem-request-UpdateExpression
 @mock_dynamodb
@@ -4357,7 +4357,7 @@ def test_summing_up_2_strings_raises_exception():
         )
 
 
-# https://github.com/spulec/moto/issues/2806
+# https://github.com/getmoto/moto/issues/2806
 @mock_dynamodb
 def test_update_item_with_attribute_in_right_hand_side():
     """
@@ -5412,7 +5412,7 @@ def test_describe_endpoints(region):
 @mock_dynamodb
 def test_update_non_existing_item_raises_error_and_does_not_contain_item_afterwards():
     """
-    https://github.com/spulec/moto/issues/3729
+    https://github.com/getmoto/moto/issues/3729
     Exception is raised, but item was persisted anyway
     Happened because we would create a placeholder, before validating/executing the UpdateExpression
     :return:
@@ -5484,7 +5484,7 @@ def test_batch_write_item():
 
 @mock_dynamodb
 def test_gsi_lastevaluatedkey():
-    # github.com/spulec/moto/issues/3968
+    # github.com/getmoto/moto/issues/3968
     conn = boto3.resource("dynamodb", region_name="us-west-2")
     name = "test-table"
     table = conn.Table(name)
@@ -5544,7 +5544,7 @@ def test_gsi_lastevaluatedkey():
 
 @mock_dynamodb
 def test_filter_expression_execution_order():
-    # As mentioned here: https://github.com/spulec/moto/issues/3909
+    # As mentioned here: https://github.com/getmoto/moto/issues/3909
     # and documented here: https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/Query.html#Query.FilterExpression
     # the filter expression should be evaluated after the query.
     # The same applies to scan operations:
diff --git a/tests/test_dynamodb/test_dynamodb_update_expressions.py b/tests/test_dynamodb/test_dynamodb_update_expressions.py
index f2b36d779..82cd0c42d 100644
--- a/tests/test_dynamodb/test_dynamodb_update_expressions.py
+++ b/tests/test_dynamodb/test_dynamodb_update_expressions.py
@@ -5,7 +5,7 @@ from moto import mock_dynamodb
 
 @mock_dynamodb
 def test_update_different_map_elements_in_single_request():
-    # https://github.com/spulec/moto/issues/5552
+    # https://github.com/getmoto/moto/issues/5552
     dynamodb = boto3.resource("dynamodb", region_name="us-east-1")
     dynamodb.create_table(
         TableName="example_table",
diff --git a/tests/test_ec2/test_amis.py b/tests/test_ec2/test_amis.py
index 6a6410431..b8a46be3f 100644
--- a/tests/test_ec2/test_amis.py
+++ b/tests/test_ec2/test_amis.py
@@ -833,7 +833,7 @@ def test_ami_attribute_user_and_group_permissions():
 
 @mock_ec2
 def test_filter_description():
-    # https://github.com/spulec/moto/issues/4460
+    # https://github.com/getmoto/moto/issues/4460
     client = boto3.client("ec2", region_name="us-west-2")
 
     # Search for partial description
diff --git a/tests/test_ec2/test_elastic_block_store.py b/tests/test_ec2/test_elastic_block_store.py
index 5d872be59..fb1c24245 100644
--- a/tests/test_ec2/test_elastic_block_store.py
+++ b/tests/test_ec2/test_elastic_block_store.py
@@ -715,7 +715,7 @@ def test_modify_attribute_blockDeviceMapping():
     Reproduces the missing feature explained at [0], where we want to mock a
     call to modify an instance attribute of type: blockDeviceMapping.
 
-    [0] https://github.com/spulec/moto/issues/160
+    [0] https://github.com/getmoto/moto/issues/160
     """
     client = boto3.client("ec2", region_name="us-east-1")
     ec2 = boto3.resource("ec2", region_name="us-east-1")
diff --git a/tests/test_ec2/test_launch_templates.py b/tests/test_ec2/test_launch_templates.py
index 1133ee229..738c0e96d 100644
--- a/tests/test_ec2/test_launch_templates.py
+++ b/tests/test_ec2/test_launch_templates.py
@@ -85,6 +85,22 @@ def test_describe_launch_template_versions():
     templ = resp["LaunchTemplateVersions"][0]["LaunchTemplateData"]
     templ.should.equal(template_data)
 
+    # test using $Latest version
+    resp = cli.describe_launch_template_versions(
+        LaunchTemplateName=template_name, Versions=["$Latest"]
+    )
+
+    templ = resp["LaunchTemplateVersions"][0]["LaunchTemplateData"]
+    templ.should.equal(template_data)
+
+    # test using $Default version
+    resp = cli.describe_launch_template_versions(
+        LaunchTemplateName=template_name, Versions=["$Default"]
+    )
+
+    templ = resp["LaunchTemplateVersions"][0]["LaunchTemplateData"]
+    templ.should.equal(template_data)
+
 
 @mock_ec2
 def test_describe_launch_template_versions_by_name_when_absent():
diff --git a/tests/test_ec2/test_regions.py b/tests/test_ec2/test_regions.py
index d26e9658f..9109f5fa3 100644
--- a/tests/test_ec2/test_regions.py
+++ b/tests/test_ec2/test_regions.py
@@ -145,7 +145,7 @@ def test_describe_regions_dryrun():
 def test_describe_zones_and_get_instance_types(region_name):
     """
     Verify that instance types exist in all exposed Availability Zones
-    https://github.com/spulec/moto/issues/5494
+    https://github.com/getmoto/moto/issues/5494
     """
     client = boto3.client("ec2", region_name=region_name)
     zones = client.describe_availability_zones()["AvailabilityZones"]
diff --git a/tests/test_ec2/test_subnets.py b/tests/test_ec2/test_subnets.py
index 09a41f697..7eb43978d 100644
--- a/tests/test_ec2/test_subnets.py
+++ b/tests/test_ec2/test_subnets.py
@@ -680,7 +680,7 @@ def validate_subnet_details_after_creating_eni(
 
 @mock_ec2
 def test_run_instances_should_attach_to_default_subnet():
-    # https://github.com/spulec/moto/issues/2877
+    # https://github.com/getmoto/moto/issues/2877
     ec2 = boto3.resource("ec2", region_name="sa-east-1")
     client = boto3.client("ec2", region_name="sa-east-1")
     sec_group_name = str(uuid4())[0:6]
diff --git a/tests/test_ecs/test_ecs_boto3.py b/tests/test_ecs/test_ecs_boto3.py
index dc7127112..161212b01 100644
--- a/tests/test_ecs/test_ecs_boto3.py
+++ b/tests/test_ecs/test_ecs_boto3.py
@@ -1889,52 +1889,65 @@ def test_run_task_exceptions():
 @mock_ecs
 def test_start_task():
     client = boto3.client("ecs", region_name="us-east-1")
-    ec2 = boto3.resource("ec2", region_name="us-east-1")
-
     test_cluster_name = "test_ecs_cluster"
+    setup_ecs_cluster_with_ec2_instance(client, test_cluster_name)
 
-    _ = client.create_cluster(clusterName=test_cluster_name)
-
-    test_instance = ec2.create_instances(
-        ImageId=EXAMPLE_AMI_ID, MinCount=1, MaxCount=1
-    )[0]
+    container_instances = client.list_container_instances(cluster=test_cluster_name)
+    container_instance_id = container_instances["containerInstanceArns"][0].split("/")[
+        -1
+    ]
 
-    instance_id_document = json.dumps(
-        ec2_utils.generate_instance_identity_document(test_instance)
+    response = client.start_task(
+        cluster="test_ecs_cluster",
+        taskDefinition="test_ecs_task",
+        overrides={},
+        containerInstances=[container_instance_id],
+        startedBy="moto",
     )
 
-    client.register_container_instance(
-        cluster=test_cluster_name, instanceIdentityDocument=instance_id_document
+    len(response["tasks"]).should.equal(1)
+    response["tasks"][0]["taskArn"].should.contain(
+        f"arn:aws:ecs:us-east-1:{ACCOUNT_ID}:task/"
+    )
+    response["tasks"][0]["clusterArn"].should.equal(
+        f"arn:aws:ecs:us-east-1:{ACCOUNT_ID}:cluster/test_ecs_cluster"
+    )
+    response["tasks"][0]["taskDefinitionArn"].should.equal(
+        f"arn:aws:ecs:us-east-1:{ACCOUNT_ID}:task-definition/test_ecs_task:1"
+    )
+    response["tasks"][0]["containerInstanceArn"].should.equal(
+        f"arn:aws:ecs:us-east-1:{ACCOUNT_ID}:container-instance/test_ecs_cluster/{container_instance_id}"
     )
+    response["tasks"][0]["tags"].should.equal(
+        [],
+    )
+    response["tasks"][0]["overrides"].should.equal({})
+    response["tasks"][0]["lastStatus"].should.equal("RUNNING")
+    response["tasks"][0]["desiredStatus"].should.equal("RUNNING")
+    response["tasks"][0]["startedBy"].should.equal("moto")
+    response["tasks"][0]["stoppedReason"].should.equal("")
+
+
+@mock_ec2
+@mock_ecs
+def test_start_task_with_tags():
+    client = boto3.client("ecs", region_name="us-east-1")
+    test_cluster_name = "test_ecs_cluster"
+    setup_ecs_cluster_with_ec2_instance(client, test_cluster_name)
 
     container_instances = client.list_container_instances(cluster=test_cluster_name)
     container_instance_id = container_instances["containerInstanceArns"][0].split("/")[
         -1
     ]
 
-    _ = client.register_task_definition(
-        family="test_ecs_task",
-        containerDefinitions=[
-            {
-                "name": "hello_world",
-                "image": "docker/hello-world:latest",
-                "cpu": 1024,
-                "memory": 400,
-                "essential": True,
-                "environment": [
-                    {"name": "AWS_ACCESS_KEY_ID", "value": "SOME_ACCESS_KEY"}
-                ],
-                "logConfiguration": {"logDriver": "json-file"},
-            }
-        ],
-    )
-
+    task_tags = [{"key": "Name", "value": "test_ecs_start_task"}]
     response = client.start_task(
         cluster="test_ecs_cluster",
         taskDefinition="test_ecs_task",
         overrides={},
         containerInstances=[container_instance_id],
         startedBy="moto",
+        tags=task_tags,
     )
 
     len(response["tasks"]).should.equal(1)
@@ -1950,6 +1963,9 @@ def test_start_task():
     response["tasks"][0]["containerInstanceArn"].should.equal(
         f"arn:aws:ecs:us-east-1:{ACCOUNT_ID}:container-instance/test_ecs_cluster/{container_instance_id}"
     )
+    response["tasks"][0]["tags"].should.equal(
+        task_tags,
+    )
     response["tasks"][0]["overrides"].should.equal({})
     response["tasks"][0]["lastStatus"].should.equal("RUNNING")
     response["tasks"][0]["desiredStatus"].should.equal("RUNNING")
@@ -2056,40 +2072,75 @@ def test_list_tasks_exceptions():
 @mock_ecs
 def test_describe_tasks():
     client = boto3.client("ecs", region_name="us-east-1")
-    ec2 = boto3.resource("ec2", region_name="us-east-1")
-
     test_cluster_name = "test_ecs_cluster"
+    setup_ecs_cluster_with_ec2_instance(client, test_cluster_name)
 
-    _ = client.create_cluster(clusterName=test_cluster_name)
+    tasks_arns = [
+        task["taskArn"]
+        for task in client.run_task(
+            cluster="test_ecs_cluster",
+            overrides={},
+            taskDefinition="test_ecs_task",
+            count=2,
+            startedBy="moto",
+        )["tasks"]
+    ]
+    response = client.describe_tasks(cluster="test_ecs_cluster", tasks=tasks_arns)
 
-    test_instance = ec2.create_instances(
-        ImageId=EXAMPLE_AMI_ID, MinCount=1, MaxCount=1
-    )[0]
+    len(response["tasks"]).should.equal(2)
+    set(
+        [response["tasks"][0]["taskArn"], response["tasks"][1]["taskArn"]]
+    ).should.equal(set(tasks_arns))
 
-    instance_id_document = json.dumps(
-        ec2_utils.generate_instance_identity_document(test_instance)
+    # Test we can pass task ids instead of ARNs
+    response = client.describe_tasks(
+        cluster="test_ecs_cluster", tasks=[tasks_arns[0].split("/")[-1]]
     )
+    len(response["tasks"]).should.equal(1)
 
-    client.register_container_instance(
-        cluster=test_cluster_name, instanceIdentityDocument=instance_id_document
+
+@mock_ec2
+@mock_ecs
+def test_describe_tasks_empty_tags():
+    client = boto3.client("ecs", region_name="us-east-1")
+    test_cluster_name = "test_ecs_cluster"
+    setup_ecs_cluster_with_ec2_instance(client, test_cluster_name)
+
+    tasks_arns = [
+        task["taskArn"]
+        for task in client.run_task(
+            cluster="test_ecs_cluster",
+            overrides={},
+            taskDefinition="test_ecs_task",
+            count=2,
+            startedBy="moto",
+        )["tasks"]
+    ]
+    response = client.describe_tasks(
+        cluster="test_ecs_cluster", tasks=tasks_arns, include=["TAGS"]
     )
 
-    _ = client.register_task_definition(
-        family="test_ecs_task",
-        containerDefinitions=[
-            {
-                "name": "hello_world",
-                "image": "docker/hello-world:latest",
-                "cpu": 1024,
-                "memory": 400,
-                "essential": True,
-                "environment": [
-                    {"name": "AWS_ACCESS_KEY_ID", "value": "SOME_ACCESS_KEY"}
-                ],
-                "logConfiguration": {"logDriver": "json-file"},
-            }
-        ],
+    len(response["tasks"]).should.equal(2)
+    set(
+        [response["tasks"][0]["taskArn"], response["tasks"][1]["taskArn"]]
+    ).should.equal(set(tasks_arns))
+    response["tasks"][0]["tags"].should.equal([])
+
+    # Test we can pass task ids instead of ARNs
+    response = client.describe_tasks(
+        cluster="test_ecs_cluster", tasks=[tasks_arns[0].split("/")[-1]]
     )
+    len(response["tasks"]).should.equal(1)
+
+
+@mock_ec2
+@mock_ecs
+def test_describe_tasks_include_tags():
+    client = boto3.client("ecs", region_name="us-east-1")
+    test_cluster_name = "test_ecs_cluster"
+    setup_ecs_cluster_with_ec2_instance(client, test_cluster_name)
+
+    task_tags = [{"key": "Name", "value": "test_ecs_task"}]
     tasks_arns = [
         task["taskArn"]
         for task in client.run_task(
@@ -2098,14 +2149,18 @@ def test_describe_tasks():
             taskDefinition="test_ecs_task",
             count=2,
             startedBy="moto",
+            tags=task_tags,
         )["tasks"]
     ]
-    response = client.describe_tasks(cluster="test_ecs_cluster", tasks=tasks_arns)
+    response = client.describe_tasks(
+        cluster="test_ecs_cluster", tasks=tasks_arns, include=["TAGS"]
+    )
 
     len(response["tasks"]).should.equal(2)
     set(
         [response["tasks"][0]["taskArn"], response["tasks"][1]["taskArn"]]
     ).should.equal(set(tasks_arns))
+    response["tasks"][0]["tags"].should.equal(task_tags)
 
     # Test we can pass task ids instead of ARNs
     response = client.describe_tasks(
@@ -3721,3 +3776,35 @@ def setup_ecs(client, ec2):
     )
 
     return subnet, sg
+
+
+def setup_ecs_cluster_with_ec2_instance(client, test_cluster_name):
+    ec2 = boto3.resource("ec2", region_name="us-east-1")
+
+    _ = client.create_cluster(clusterName=test_cluster_name)
+    test_instance = ec2.create_instances(
+        ImageId=EXAMPLE_AMI_ID, MinCount=1, MaxCount=1
+    )[0]
+    instance_id_document = json.dumps(
+        ec2_utils.generate_instance_identity_document(test_instance)
+    )
+    client.register_container_instance(
+        cluster=test_cluster_name, instanceIdentityDocument=instance_id_document
+    )
+
+    _ = client.register_task_definition(
+        family="test_ecs_task",
+        containerDefinitions=[
+            {
+                "name": "hello_world",
+                "image": "docker/hello-world:latest",
+                "cpu": 1024,
+                "memory": 400,
+                "essential": True,
+                "environment": [
+                    {"name": "AWS_ACCESS_KEY_ID", "value": "SOME_ACCESS_KEY"}
+                ],
+                "logConfiguration": {"logDriver": "json-file"},
+            }
+        ],
+    )
diff --git a/tests/test_elbv2/test_elbv2.py b/tests/test_elbv2/test_elbv2.py
index ff16b35b9..fcded943b 100644
--- a/tests/test_elbv2/test_elbv2.py
+++ b/tests/test_elbv2/test_elbv2.py
@@ -304,7 +304,7 @@ def test_create_listeners_without_port():
 @mock_ec2
 @mock_elbv2
 def test_create_rule_forward_config_as_second_arg():
-    # https://github.com/spulec/moto/issues/4123
+    # https://github.com/getmoto/moto/issues/4123
     # Necessary because there was some convoluted way of parsing arguments
     # Actions with type=forward had to be the first action specified
     response, vpc, _, _, _, elbv2 = create_load_balancer()
diff --git a/tests/test_s3/test_s3.py b/tests/test_s3/test_s3.py
index 3ebee03fd..dac1d2d4a 100644
--- a/tests/test_s3/test_s3.py
+++ b/tests/test_s3/test_s3.py
@@ -1102,7 +1102,7 @@ def test_website_redirect_location():
     resp = s3.get_object(Bucket="mybucket", Key="steve")
     resp.get("WebsiteRedirectLocation").should.equal(None)
 
-    url = "https://github.com/spulec/moto"
+    url = "https://github.com/getmoto/moto"
     s3.put_object(
         Bucket="mybucket", Key="steve", Body=b"is awesome", WebsiteRedirectLocation=url
     )
@@ -3445,7 +3445,7 @@ def test_head_object_should_return_default_content_type():
 
 @mock_s3
 def test_request_partial_content_should_contain_all_metadata():
-    # github.com/spulec/moto/issues/4203
+    # github.com/getmoto/moto/issues/4203
     bucket = "bucket"
     object_key = "key"
     body = "some text"
diff --git a/tests/test_s3/test_s3_classdecorator.py b/tests/test_s3/test_s3_classdecorator.py
index 5b3021697..ecb10299d 100644
--- a/tests/test_s3/test_s3_classdecorator.py
+++ b/tests/test_s3/test_s3_classdecorator.py
@@ -7,7 +7,7 @@ from moto import mock_s3
 @mock_s3
 class ClassDecoratorTest(unittest.TestCase):
     """
-    https://github.com/spulec/moto/issues/3535
+    https://github.com/getmoto/moto/issues/3535
     An update to the mock-package introduced a failure during teardown.
     This test is in place to catch any similar failures with our mocking approach
     """
diff --git a/tests/test_s3/test_s3_file_handles.py b/tests/test_s3/test_s3_file_handles.py
index 984cbbdac..868717dfd 100644
--- a/tests/test_s3/test_s3_file_handles.py
+++ b/tests/test_s3/test_s3_file_handles.py
@@ -190,7 +190,7 @@ class TestS3FileHandleClosures(TestCase):
 
 
 def test_verify_key_can_be_copied_after_disposing():
-    # https://github.com/spulec/moto/issues/5588
+    # https://github.com/getmoto/moto/issues/5588
     # Multithreaded bug where:
     #  - User: calls list_object_versions
     #  - Moto creates a list of all keys
diff --git a/tests/test_s3/test_server.py b/tests/test_s3/test_server.py
index 222ced7b9..25531ecbb 100644
--- a/tests/test_s3/test_server.py
+++ b/tests/test_s3/test_server.py
@@ -202,7 +202,7 @@ def test_s3_server_post_cors():
 
 def test_s3_server_post_cors_exposed_header():
     """Test that we can override default CORS headers with custom bucket rules"""
-    # github.com/spulec/moto/issues/4220
+    # github.com/getmoto/moto/issues/4220
 
     cors_config_payload = """<CORSConfiguration xmlns="http://s3.amazonaws.com/doc/2006-03-01/">
   <CORSRule>
diff --git a/tests/test_sagemaker/test_sagemaker_pipeline.py b/tests/test_sagemaker/test_sagemaker_pipeline.py
index ea562646e..db76a42e5 100644
--- a/tests/test_sagemaker/test_sagemaker_pipeline.py
+++ b/tests/test_sagemaker/test_sagemaker_pipeline.py
@@ -1,18 +1,37 @@
-from moto import mock_sagemaker
+from contextlib import contextmanager
+from moto import mock_sagemaker, settings
 from time import sleep
 from datetime import datetime
 import boto3
 import botocore
+import json
 import pytest
+from unittest import SkipTest
 
+from moto.s3 import mock_s3
 from moto.core import DEFAULT_ACCOUNT_ID as ACCOUNT_ID
+from moto.sagemaker.utils import arn_formatter, load_pipeline_definition_from_s3
 
 FAKE_ROLE_ARN = f"arn:aws:iam::{ACCOUNT_ID}:role/FakeRole"
-TEST_REGION_NAME = "us-east-1"
+TEST_REGION_NAME = "us-west-1"
 
 
-def arn_formatter(_type, _id, account_id, region_name):
-    return f"arn:aws:sagemaker:{region_name}:{account_id}:{_type}/{_id}"
+@contextmanager
+def setup_s3_pipeline_definition(bucket_name, object_key, pipeline_definition):
+    client = boto3.client("s3")
+    client.create_bucket(
+        Bucket=bucket_name,
+        CreateBucketConfiguration={"LocationConstraint": TEST_REGION_NAME},
+    )
+    client.put_object(
+        Body=json.dumps(pipeline_definition),
+        Bucket=bucket_name,
+        Key=object_key,
+    )
+    yield
+
+    client.delete_object(Bucket=bucket_name, Key=object_key)
+    client.delete_bucket(Bucket=bucket_name)
 
 
 @pytest.fixture(name="sagemaker_client")
@@ -21,22 +40,46 @@ def fixture_sagemaker_client():
         yield boto3.client("sagemaker", region_name=TEST_REGION_NAME)
 
 
-def create_sagemaker_pipelines(sagemaker_client, pipeline_names, wait_seconds=0.0):
+def create_sagemaker_pipelines(sagemaker_client, pipelines, wait_seconds=0.0):
     responses = []
-    for pipeline_name in pipeline_names:
-        responses += sagemaker_client.create_pipeline(
-            PipelineName=pipeline_name,
-            RoleArn=FAKE_ROLE_ARN,
-        )
+    for pipeline in pipelines:
+        responses += sagemaker_client.create_pipeline(**pipeline)
         sleep(wait_seconds)
     return responses
 
 
+def test_load_pipeline_definition_from_s3():
+    if settings.TEST_SERVER_MODE:
+        raise SkipTest(
+            "Skipping test in server mode due to lack of access to s3_backend."
+        )
+
+    bucket_name = "some-bucket-1"
+    object_key = "some/object/key.json"
+    pipeline_definition = {"key": "value"}
+
+    with mock_s3():
+        with setup_s3_pipeline_definition(
+            bucket_name,
+            object_key,
+            pipeline_definition,
+        ):
+            observed_pipeline_definition = load_pipeline_definition_from_s3(
+                pipeline_definition_s3_location={
+                    "Bucket": bucket_name,
+                    "ObjectKey": object_key,
+                },
+                account_id=ACCOUNT_ID,
+            )
+    observed_pipeline_definition.should.equal(pipeline_definition)
+
+
 def test_create_pipeline(sagemaker_client):
     fake_pipeline_name = "MyPipelineName"
     response = sagemaker_client.create_pipeline(
         PipelineName=fake_pipeline_name,
         RoleArn=FAKE_ROLE_ARN,
+        PipelineDefinition=" ",
     )
     assert isinstance(response, dict)
     response["PipelineArn"].should.equal(
@@ -44,6 +87,48 @@ def test_create_pipeline(sagemaker_client):
     )
 
 
+@pytest.mark.parametrize(
+    "create_pipeline_kwargs",
+    [
+        {"PipelineName": "MyPipelineName", "RoleArn": FAKE_ROLE_ARN},
+        {"RoleArn": FAKE_ROLE_ARN, "PipelineDefinition": " "},
+        {"PipelineName": "MyPipelineName", "PipelineDefinition": " "},
+        {
+            "PipelineName": "MyPipelineName",
+            "RoleArn": FAKE_ROLE_ARN,
+            "PipelineDefinition": " ",
+            "PipelineDefinitionS3Location": {"key": "value"},
+        },
+    ],
+)
+def test_create_pipeline_invalid_required_kwargs(
+    sagemaker_client, create_pipeline_kwargs
+):
+    with pytest.raises(
+        (
+            botocore.exceptions.ParamValidationError,
+            botocore.exceptions.ClientError,
+        )
+    ):
+        _ = sagemaker_client.create_pipeline(
+            **create_pipeline_kwargs,
+        )
+
+
+def test_create_pipeline_duplicate_pipeline_name(sagemaker_client):
+    with pytest.raises(botocore.exceptions.ClientError):
+        _ = sagemaker_client.create_pipeline(
+            PipelineName="APipelineName",
+            RoleArn=FAKE_ROLE_ARN,
+            PipelineDefinition=" ",
+        )
+        _ = sagemaker_client.create_pipeline(
+            PipelineName="APipelineName",
+            RoleArn=FAKE_ROLE_ARN,
+            PipelineDefinition=" ",
+        )
+
+
 def test_list_pipelines_none(sagemaker_client):
     response = sagemaker_client.list_pipelines()
     assert isinstance(response, dict)
@@ -52,7 +137,15 @@ def test_list_pipelines_none(sagemaker_client):
 
 def test_list_pipelines_single(sagemaker_client):
     fake_pipeline_names = ["APipelineName"]
-    _ = create_sagemaker_pipelines(sagemaker_client, fake_pipeline_names)
+    pipelines = [
+        {
+            "PipelineName": fake_pipeline_names[0],
+            "RoleArn": FAKE_ROLE_ARN,
+            "PipelineDefinition": " ",
+        },
+    ]
+    _ = create_sagemaker_pipelines(sagemaker_client, pipelines)
+
     response = sagemaker_client.list_pipelines()
     response["PipelineSummaries"].should.have.length_of(1)
     response["PipelineSummaries"][0]["PipelineArn"].should.equal(
@@ -62,7 +155,16 @@ def test_list_pipelines_single(sagemaker_client):
 
 def test_list_pipelines_multiple(sagemaker_client):
     fake_pipeline_names = ["APipelineName", "BPipelineName"]
-    _ = create_sagemaker_pipelines(sagemaker_client, fake_pipeline_names)
+    pipelines = [
+        {
+            "PipelineName": fake_pipeline_name,
+            "RoleArn": FAKE_ROLE_ARN,
+            "PipelineDefinition": " ",
+        }
+        for fake_pipeline_name in fake_pipeline_names
+    ]
+    _ = create_sagemaker_pipelines(sagemaker_client, pipelines)
+
     response = sagemaker_client.list_pipelines(
         SortBy="Name",
         SortOrder="Ascending",
@@ -72,7 +174,16 @@ def test_list_pipelines_multiple(sagemaker_client):
 
 def test_list_pipelines_sort_name_ascending(sagemaker_client):
     fake_pipeline_names = ["APipelineName", "BPipelineName", "CPipelineName"]
-    _ = create_sagemaker_pipelines(sagemaker_client, fake_pipeline_names)
+    pipelines = [
+        {
+            "PipelineName": fake_pipeline_name,
+            "RoleArn": FAKE_ROLE_ARN,
+            "PipelineDefinition": " ",
+        }
+        for fake_pipeline_name in fake_pipeline_names
+    ]
+    _ = create_sagemaker_pipelines(sagemaker_client, pipelines)
+
     response = sagemaker_client.list_pipelines(
         SortBy="Name",
         SortOrder="Ascending",
@@ -90,7 +201,16 @@ def test_list_pipelines_sort_name_ascending(sagemaker_client):
 
 def test_list_pipelines_sort_creation_time_descending(sagemaker_client):
     fake_pipeline_names = ["APipelineName", "BPipelineName", "CPipelineName"]
-    _ = create_sagemaker_pipelines(sagemaker_client, fake_pipeline_names, 1)
+    pipelines = [
+        {
+            "PipelineName": fake_pipeline_name,
+            "RoleArn": FAKE_ROLE_ARN,
+            "PipelineDefinition": " ",
+        }
+        for fake_pipeline_name in fake_pipeline_names
+    ]
+    _ = create_sagemaker_pipelines(sagemaker_client, pipelines, 1.0)
+
     response = sagemaker_client.list_pipelines(
         SortBy="CreationTime",
         SortOrder="Descending",
@@ -108,14 +228,30 @@ def test_list_pipelines_sort_creation_time_descending(sagemaker_client):
 
 def test_list_pipelines_max_results(sagemaker_client):
     fake_pipeline_names = ["APipelineName", "BPipelineName", "CPipelineName"]
-    _ = create_sagemaker_pipelines(sagemaker_client, fake_pipeline_names, 0.0)
+    pipelines = [
+        {
+            "PipelineName": fake_pipeline_name,
+            "RoleArn": FAKE_ROLE_ARN,
+            "PipelineDefinition": " ",
+        }
+        for fake_pipeline_name in fake_pipeline_names
+    ]
+    _ = create_sagemaker_pipelines(sagemaker_client, pipelines)
+
     response = sagemaker_client.list_pipelines(MaxResults=2)
     response["PipelineSummaries"].should.have.length_of(2)
 
 
 def test_list_pipelines_next_token(sagemaker_client):
     fake_pipeline_names = ["APipelineName"]
-    _ = create_sagemaker_pipelines(sagemaker_client, fake_pipeline_names, 0.0)
+    pipelines = [
+        {
+            "PipelineName": fake_pipeline_names[0],
+            "RoleArn": FAKE_ROLE_ARN,
+            "PipelineDefinition": " ",
+        },
+    ]
+    _ = create_sagemaker_pipelines(sagemaker_client, pipelines)
 
     response = sagemaker_client.list_pipelines(NextToken="0")
     response["PipelineSummaries"].should.have.length_of(1)
@@ -123,7 +259,16 @@ def test_list_pipelines_next_token(sagemaker_client):
 
 def test_list_pipelines_pipeline_name_prefix(sagemaker_client):
     fake_pipeline_names = ["APipelineName", "BPipelineName", "CPipelineName"]
-    _ = create_sagemaker_pipelines(sagemaker_client, fake_pipeline_names, 0.0)
+    pipelines = [
+        {
+            "PipelineName": fake_pipeline_name,
+            "RoleArn": FAKE_ROLE_ARN,
+            "PipelineDefinition": " ",
+        }
+        for fake_pipeline_name in fake_pipeline_names
+    ]
+    _ = create_sagemaker_pipelines(sagemaker_client, pipelines)
+
     response = sagemaker_client.list_pipelines(PipelineNamePrefix="APipe")
     response["PipelineSummaries"].should.have.length_of(1)
     response["PipelineSummaries"][0]["PipelineName"].should.equal("APipelineName")
@@ -134,7 +279,15 @@ def test_list_pipelines_pipeline_name_prefix(sagemaker_client):
 
 def test_list_pipelines_created_after(sagemaker_client):
     fake_pipeline_names = ["APipelineName", "BPipelineName", "CPipelineName"]
-    _ = create_sagemaker_pipelines(sagemaker_client, fake_pipeline_names, 0.0)
+    pipelines = [
+        {
+            "PipelineName": fake_pipeline_name,
+            "RoleArn": FAKE_ROLE_ARN,
+            "PipelineDefinition": " ",
+        }
+        for fake_pipeline_name in fake_pipeline_names
+    ]
+    _ = create_sagemaker_pipelines(sagemaker_client, pipelines)
 
     created_after_str = "2099-12-31 23:59:59"
     response = sagemaker_client.list_pipelines(CreatedAfter=created_after_str)
@@ -151,7 +304,15 @@ def test_list_pipelines_created_after(sagemaker_client):
 
 def test_list_pipelines_created_before(sagemaker_client):
     fake_pipeline_names = ["APipelineName", "BPipelineName", "CPipelineName"]
-    _ = create_sagemaker_pipelines(sagemaker_client, fake_pipeline_names, 0.0)
+    pipelines = [
+        {
+            "PipelineName": fake_pipeline_name,
+            "RoleArn": FAKE_ROLE_ARN,
+            "PipelineDefinition": " ",
+        }
+        for fake_pipeline_name in fake_pipeline_names
+    ]
+    _ = create_sagemaker_pipelines(sagemaker_client, pipelines)
 
     created_before_str = "2000-12-31 23:59:59"
     response = sagemaker_client.list_pipelines(CreatedBefore=created_before_str)
@@ -182,7 +343,15 @@ def test_list_pipelines_invalid_values(sagemaker_client, list_pipelines_kwargs):
 
 def test_delete_pipeline_exists(sagemaker_client):
     fake_pipeline_names = ["APipelineName", "BPipelineName", "CPipelineName"]
-    _ = create_sagemaker_pipelines(sagemaker_client, fake_pipeline_names, 0.0)
+    pipelines = [
+        {
+            "PipelineName": fake_pipeline_name,
+            "RoleArn": FAKE_ROLE_ARN,
+            "PipelineDefinition": " ",
+        }
+        for fake_pipeline_name in fake_pipeline_names
+    ]
+    _ = create_sagemaker_pipelines(sagemaker_client, pipelines)
     pipeline_name_delete, pipeline_names_remain = (
         fake_pipeline_names[0],
         fake_pipeline_names[1:],
@@ -198,7 +367,7 @@ def test_delete_pipeline_exists(sagemaker_client):
     pipeline_names_exist = [
         pipeline["PipelineName"] for pipeline in response["PipelineSummaries"]
     ]
-    assert pipeline_names_remain == pipeline_names_exist
+    assert set(pipeline_names_remain) == set(pipeline_names_exist)
 
 
 def test_delete_pipeline_not_exists(sagemaker_client):
@@ -206,14 +375,36 @@ def test_delete_pipeline_not_exists(sagemaker_client):
         _ = sagemaker_client.delete_pipeline(PipelineName="some-pipeline-name")
 
 
-def test_update_pipeline(sagemaker_client):
+def test_update_pipeline_not_exists(sagemaker_client):
     with pytest.raises(botocore.exceptions.ClientError):
         _ = sagemaker_client.update_pipeline(PipelineName="some-pipeline-name")
 
 
+def test_update_pipeline_invalid_kwargs(sagemaker_client):
+    pipeline_name = "APipelineName"
+    pipeline = {
+        "PipelineName": pipeline_name,
+        "RoleArn": FAKE_ROLE_ARN,
+        "PipelineDefinition": " ",
+    }
+    _ = create_sagemaker_pipelines(sagemaker_client, [pipeline])
+
+    with pytest.raises(botocore.exceptions.ParamValidationError):
+        sagemaker_client.update_pipeline(
+            PipelineName=pipeline_name,
+            **{"InvalidKwarg": "some-value"},
+        )
+
+
 def test_update_pipeline_no_update(sagemaker_client):
     pipeline_name = "APipelineName"
-    _ = create_sagemaker_pipelines(sagemaker_client, [pipeline_name])
+    pipeline = {
+        "PipelineName": pipeline_name,
+        "RoleArn": FAKE_ROLE_ARN,
+        "PipelineDefinition": " ",
+    }
+    _ = create_sagemaker_pipelines(sagemaker_client, [pipeline])
+
     response = sagemaker_client.update_pipeline(PipelineName=pipeline_name)
     response["PipelineArn"].should.equal(
         arn_formatter("pipeline", pipeline_name, ACCOUNT_ID, TEST_REGION_NAME)
@@ -226,9 +417,14 @@ def test_update_pipeline_add_attribute(sagemaker_client):
     pipeline_name = "APipelineName"
     pipeline_display_name_update = "APipelineDisplayName"
 
-    _ = create_sagemaker_pipelines(sagemaker_client, [pipeline_name])
+    pipeline = {
+        "PipelineName": pipeline_name,
+        "RoleArn": FAKE_ROLE_ARN,
+        "PipelineDefinition": " ",
+    }
+    _ = create_sagemaker_pipelines(sagemaker_client, [pipeline])
     response = sagemaker_client.list_pipelines()
-    assert "PipelineDisplayName" not in response["PipelineSummaries"][0]
+    response["PipelineSummaries"][0]["PipelineDisplayName"].should.equal(pipeline_name)
 
     _ = sagemaker_client.update_pipeline(
         PipelineName=pipeline_name,
@@ -238,14 +434,19 @@ def test_update_pipeline_add_attribute(sagemaker_client):
     response["PipelineSummaries"][0]["PipelineDisplayName"].should.equal(
         pipeline_display_name_update
     )
-    response["PipelineSummaries"][0].should.have.length_of(7)
+    response["PipelineSummaries"][0].should.have.length_of(6)
 
 
 def test_update_pipeline_update_change_attribute(sagemaker_client):
     pipeline_name = "APipelineName"
     role_arn_update = f"{FAKE_ROLE_ARN}Test"
+    pipeline = {
+        "PipelineName": pipeline_name,
+        "RoleArn": FAKE_ROLE_ARN,
+        "PipelineDefinition": " ",
+    }
+    _ = create_sagemaker_pipelines(sagemaker_client, [pipeline])
 
-    _ = create_sagemaker_pipelines(sagemaker_client, [pipeline_name])
     _ = sagemaker_client.update_pipeline(
         PipelineName=pipeline_name,
         RoleArn=role_arn_update,
@@ -253,3 +454,36 @@ def test_update_pipeline_update_change_attribute(sagemaker_client):
     response = sagemaker_client.list_pipelines()
     response["PipelineSummaries"][0]["RoleArn"].should.equal(role_arn_update)
     response["PipelineSummaries"][0].should.have.length_of(6)
+
+
+def test_describe_pipeline_not_exists(sagemaker_client):
+    with pytest.raises(botocore.exceptions.ClientError):
+        _ = sagemaker_client.describe_pipeline(PipelineName="some-pipeline-name")
+
+
+@pytest.mark.parametrize(
+    "pipeline,expected_response_length",
+    [
+        (
+            {
+                "PipelineName": "APipelineName",
+                "RoleArn": FAKE_ROLE_ARN,
+                "PipelineDefinition": " ",
+            },
+            11,
+        ),
+        (
+            {
+                "PipelineName": "BPipelineName",
+                "RoleArn": FAKE_ROLE_ARN,
+                "PipelineDefinition": " ",
+                "PipelineDescription": "some pipeline description",
+            },
+            12,
+        ),
+    ],
+)
+def test_describe_pipeline_exists(sagemaker_client, pipeline, expected_response_length):
+    _ = create_sagemaker_pipelines(sagemaker_client, [pipeline])
+    response = sagemaker_client.describe_pipeline(PipelineName=pipeline["PipelineName"])
+    response.should.have.length_of(expected_response_length)
diff --git a/tests/test_sqs/test_server.py b/tests/test_sqs/test_server.py
index fd8b8a77e..9a737c093 100644
--- a/tests/test_sqs/test_server.py
+++ b/tests/test_sqs/test_server.py
@@ -20,7 +20,7 @@ def test_sqs_list_identities():
 
     # Make sure that we can receive messages from queues whose name contains dots (".")
     # The AWS API mandates that the names of FIFO queues use the suffix ".fifo"
-    # See: https://github.com/spulec/moto/issues/866
+    # See: https://github.com/getmoto/moto/issues/866
 
     for queue_name in ("testqueue", "otherqueue.fifo"):