diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 10fd19e..5e8b594 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -1 +1 @@ -* @JimmyPettersson85 @xernobyl @yaziine +* @JimmyPettersson85 @xernobyl diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index d057329..2545a09 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -16,16 +16,12 @@ jobs: strategy: max-parallel: 1 matrix: - python: ['3.7', '3.8', '3.9', '3.10', '3.11'] + python: ['3.8', '3.9', '3.10', '3.11', '3.12'] steps: - uses: actions/checkout@v3 with: fetch-depth: 0 # gives the commit linter access to previous commits - - name: Commit message linter - if: ${{ matrix.python == '3.7' }} - uses: wagoid/commitlint-github-action@v4 - - uses: actions/setup-python@v3 with: python-version: ${{ matrix.python }} @@ -34,7 +30,7 @@ jobs: run: pip install -q ".[test, ci]" - name: Lint with ${{ matrix.python }} - if: ${{ matrix.python == '3.7' }} + if: ${{ matrix.python == '3.8' }} run: make lint - name: Install, test and code coverage with ${{ matrix.python }} diff --git a/CHANGELOG.md b/CHANGELOG.md index f66fbe3..d158ba3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,8 @@ All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines. +### [5.3.1](https://github.com/GetStream/stream-python/compare/v5.2.1...v5.3.1) (2023-10-25) + ### [5.2.1](https://github.com/GetStream/stream-python/compare/v5.2.0...v5.2.1) (2023-02-27) ## [5.2.0](https://github.com/GetStream/stream-python/compare/v5.1.1...v5.2.0) (2023-02-16) diff --git a/SECURITY.md b/SECURITY.md new file mode 100644 index 0000000..4094801 --- /dev/null +++ b/SECURITY.md @@ -0,0 +1,16 @@ +# Reporting a Vulnerability +At Stream we are committed to the security of our Software. We appreciate your efforts in disclosing vulnerabilities responsibly and we will make every effort to acknowledge your contributions. + +Report security vulnerabilities at the following email address: +``` +[security@getstream.io](mailto:security@getstream.io) +``` +Alternatively it is also possible to open a new issue in the affected repository, tagging it with the `security` tag. + +A team member will acknowledge the vulnerability and will follow-up with more detailed information. A representative of the security team will be in touch if more information is needed. + +# Information to include in a report +While we appreciate any information that you are willing to provide, please make sure to include the following: +* Which repository is affected +* Which branch, if relevant +* Be as descriptive as possible, the team will replicate the vulnerability before working on a fix. diff --git a/setup.py b/setup.py index 487ba99..49b41fb 100644 --- a/setup.py +++ b/setup.py @@ -5,10 +5,10 @@ from stream import __version__, __maintainer__, __email__, __license__ install_requires = [ - "requests>=2.28.0,<3", - "pyjwt>=2.6.0,<3", - "pytz>=2022.7.1", - "aiohttp>=3.8.4", + "requests>=2.31.0,<3", + "pyjwt>=2.8.0,<3", + "pytz>=2023.3.post1", + "aiohttp>=3.9.0b0", ] tests_require = ["pytest", "pytest-cov", "python-dateutil", "pytest-asyncio"] ci_require = ["black", "flake8", "pytest-cov"] diff --git a/stream/__init__.py b/stream/__init__.py index 1129601..d769388 100644 --- a/stream/__init__.py +++ b/stream/__init__.py @@ -5,7 +5,7 @@ __copyright__ = "Copyright 2022, Stream.io, Inc" __credits__ = ["Thierry Schellenbach, mellowmorning.com, @tschellenbach"] __license__ = "BSD-3-Clause" -__version__ = "5.2.1" +__version__ = "5.3.1" __maintainer__ = "Thierry Schellenbach" __email__ = "support@getstream.io" __status__ = "Production" @@ -31,6 +31,9 @@ def connect( """ from stream.client import AsyncStreamClient, StreamClient + if location is None: + location = os.environ.get("STREAM_REGION") + stream_url = os.environ.get("STREAM_URL") # support for the heroku STREAM_URL syntax if stream_url and not api_key: diff --git a/stream/reactions/base.py b/stream/reactions/base.py index 31078d0..31e2842 100644 --- a/stream/reactions/base.py +++ b/stream/reactions/base.py @@ -23,7 +23,11 @@ def update(self, reaction_id, data=None, target_feeds=None): pass @abstractmethod - def delete(self, reaction_id): + def delete(self, reaction_id, soft=False): + pass + + @abstractmethod + def restore(self, reaction_id): pass @abstractmethod diff --git a/stream/reactions/reaction.py b/stream/reactions/reaction.py index 0466b21..f65403c 100644 --- a/stream/reactions/reaction.py +++ b/stream/reactions/reaction.py @@ -42,9 +42,18 @@ def update(self, reaction_id, data=None, target_feeds=None): data=payload, ) - def delete(self, reaction_id): + def delete(self, reaction_id, soft=False): url = f"{self.API_ENDPOINT}{reaction_id}" return self.client.delete( + url, + service_name=self.SERVICE_NAME, + signature=self.token, + params={"soft": soft}, + ) + + def restore(self, reaction_id): + url = f"{self.API_ENDPOINT}{reaction_id}/restore" + return self.client.put( url, service_name=self.SERVICE_NAME, signature=self.token ) @@ -123,9 +132,18 @@ async def update(self, reaction_id, data=None, target_feeds=None): data=payload, ) - async def delete(self, reaction_id): + async def delete(self, reaction_id, soft=False): url = f"{self.API_ENDPOINT}{reaction_id}" return await self.client.delete( + url, + service_name=self.SERVICE_NAME, + signature=self.token, + params={"soft": soft}, + ) + + async def restore(self, reaction_id): + url = f"{self.API_ENDPOINT}{reaction_id}/restore" + return await self.client.put( url, service_name=self.SERVICE_NAME, signature=self.token ) diff --git a/stream/tests/test_async_client.py b/stream/tests/test_async_client.py index bb1bc27..d4b0c0f 100644 --- a/stream/tests/test_async_client.py +++ b/stream/tests/test_async_client.py @@ -8,7 +8,7 @@ from dateutil.tz import tzlocal import stream -from stream.exceptions import ApiKeyException, InputException +from stream.exceptions import ApiKeyException, InputException, DoesNotExistException def assert_first_activity_id_equal(activities, correct_activity_id): @@ -1049,6 +1049,44 @@ async def test_reaction_delete(async_client): await async_client.reactions.delete(response["id"]) +@pytest.mark.asyncio +async def test_reaction_hard_delete(async_client): + response = await async_client.reactions.add( + "like", "54a60c1e-4ee3-494b-a1e3-50c06acb5ed4", "mike" + ) + await async_client.reactions.delete(response["id"], soft=False) + + +@pytest.mark.asyncio +async def test_reaction_soft_delete(async_client): + response = await async_client.reactions.add( + "like", "54a60c1e-4ee3-494b-a1e3-50c06acb5ed4", "mike" + ) + await async_client.reactions.delete(response["id"], soft=True) + + +@pytest.mark.asyncio +async def test_reaction_soft_delete_and_restore(async_client): + response = await async_client.reactions.add( + "like", "54a60c1e-4ee3-494b-a1e3-50c06acb5ed4", "mike" + ) + await async_client.reactions.delete(response["id"], soft=True) + r1 = await async_client.reactions.get(response["id"]) + assert r1.get("deleted_at", None) is not None + await async_client.reactions.restore(response["id"]) + r1 = await async_client.reactions.get(response["id"]) + assert "deleted_at" not in r1 + + +@pytest.mark.asyncio +async def test_reaction_invalid_restore(async_client): + response = await async_client.reactions.add( + "like", "54a60c1e-4ee3-494b-a1e3-50c06acb5ed4", "mike" + ) + with pytest.raises(DoesNotExistException): + await async_client.reactions.restore(response["id"]) + + @pytest.mark.asyncio async def test_reaction_add_child(async_client): response = await async_client.reactions.add( diff --git a/stream/tests/test_client.py b/stream/tests/test_client.py index aced337..3ccb8cc 100644 --- a/stream/tests/test_client.py +++ b/stream/tests/test_client.py @@ -17,7 +17,7 @@ import stream from stream import serializer -from stream.exceptions import ApiKeyException, InputException +from stream.exceptions import ApiKeyException, InputException, DoesNotExistException from stream.feed import Feed @@ -150,14 +150,14 @@ def test_api_url(self): ) def test_collections_url_default(self): - c = stream.connect("key", "secret") + c = stream.connect("key", "secret", location="") feed_url = c.get_full_url(relative_url="meta/", service_name="api") if not self.local_tests: self.assertEqual(feed_url, "https://api.stream-io-api.com/api/v1.0/meta/") def test_personalization_url_default(self): - c = stream.connect("key", "secret") + c = stream.connect("key", "secret", location="") feed_url = c.get_full_url( relative_url="recommended", service_name="personalization" ) @@ -169,7 +169,7 @@ def test_personalization_url_default(self): ) def test_api_url_default(self): - c = stream.connect("key", "secret") + c = stream.connect("key", "secret", location="") feed_url = c.get_full_url(service_name="api", relative_url="feed/") if not self.local_tests: @@ -1439,6 +1439,37 @@ def test_reaction_delete(self): ) self.c.reactions.delete(response["id"]) + def test_reaction_hard_delete(self): + response = self.c.reactions.add( + "like", "54a60c1e-4ee3-494b-a1e3-50c06acb5ed4", "mike" + ) + self.c.reactions.delete(response["id"], soft=False) + + def test_reaction_soft_delete(self): + response = self.c.reactions.add( + "like", "54a60c1e-4ee3-494b-a1e3-50c06acb5ed4", "mike" + ) + self.c.reactions.delete(response["id"], soft=True) + + def test_reaction_soft_delete_and_restore(self): + response = self.c.reactions.add( + "like", "54a60c1e-4ee3-494b-a1e3-50c06acb5ed4", "mike" + ) + self.c.reactions.delete(response["id"], soft=True) + r1 = self.c.reactions.get(response["id"]) + self.assertIsNot(r1["deleted_at"], None) + self.c.reactions.restore(response["id"]) + r1 = self.c.reactions.get(response["id"]) + self.assertTrue("deleted_at" not in r1) + + def test_reaction_invalid_restore(self): + response = self.c.reactions.add( + "like", "54a60c1e-4ee3-494b-a1e3-50c06acb5ed4", "mike" + ) + self.assertRaises( + DoesNotExistException, lambda: self.c.reactions.restore(response["id"]) + ) + def test_reaction_add_child(self): response = self.c.reactions.add( "like", "54a60c1e-4ee3-494b-a1e3-50c06acb5ed4", "mike"