Script 'mail_helper' called by obssrc
Hello community,

here is the log from the commit of package python-aioftp for openSUSE:Factory 
checked in at 2025-06-03 19:10:44
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Comparing /work/SRC/openSUSE:Factory/python-aioftp (Old)
 and      /work/SRC/openSUSE:Factory/.python-aioftp.new.16005 (New)
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

Package is "python-aioftp"

Tue Jun  3 19:10:44 2025 rev:12 rq:1282358 version:0.25.1

Changes:
--------
--- /work/SRC/openSUSE:Factory/python-aioftp/python-aioftp.changes      
2025-01-22 16:39:11.241023231 +0100
+++ /work/SRC/openSUSE:Factory/.python-aioftp.new.16005/python-aioftp.changes   
2025-06-03 19:10:46.041988060 +0200
@@ -1,0 +2,9 @@
+Tue Jun  3 06:47:47 UTC 2025 - John Paul Adrian Glaubitz 
<adrian.glaub...@suse.com>
+
+- Update to 0.25.1
+  * do not start explicit tls if implicit mode used (fixes #184) (#185)
+- from version 0.25.0
+  * client: add partial client support for explicit tls (#183)
+- Update BuildRequires from pyproject.toml
+
+-------------------------------------------------------------------

Old:
----
  aioftp-0.24.1.tar.gz

New:
----
  aioftp-0.25.1.tar.gz

++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

Other differences:
------------------
++++++ python-aioftp.spec ++++++
--- /var/tmp/diff_new_pack.eB2K5R/_old  2025-06-03 19:10:46.626012319 +0200
+++ /var/tmp/diff_new_pack.eB2K5R/_new  2025-06-03 19:10:46.630012485 +0200
@@ -18,7 +18,7 @@
 
 %{?sle15_python_module_pythons}
 Name:           python-aioftp
-Version:        0.24.1
+Version:        0.25.1
 Release:        0
 Summary:        FTP client/server for asyncio
 License:        Apache-2.0
@@ -34,6 +34,7 @@
 BuildRequires:  %{python_module async_timeout >= 4.0.0}
 BuildRequires:  %{python_module pytest-asyncio}
 BuildRequires:  %{python_module pytest-cov}
+BuildRequires:  %{python_module pytest-mock}
 BuildRequires:  %{python_module pytest}
 BuildRequires:  %{python_module siosocks >= 0.2.0}
 BuildRequires:  %{python_module trustme}

++++++ aioftp-0.24.1.tar.gz -> aioftp-0.25.1.tar.gz ++++++
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/aioftp-0.24.1/PKG-INFO new/aioftp-0.25.1/PKG-INFO
--- old/aioftp-0.24.1/PKG-INFO  2024-12-13 13:22:30.911728900 +0100
+++ new/aioftp-0.25.1/PKG-INFO  2025-04-11 20:53:50.560357600 +0200
@@ -1,9 +1,9 @@
-Metadata-Version: 2.1
+Metadata-Version: 2.4
 Name: aioftp
-Version: 0.24.1
+Version: 0.25.1
 Summary: ftp client/server for asyncio
 Author: yieyu, rsichnyi, jw4js, oleksandr-kuzmenko, ndhansen, modelmat, greut, 
PonyPC, jacobtomlinson, bachya, CrafterKolyan, jkr78
-Author-email: pohmelie <multisosnoo...@gmail.com>, asvetlov 
<andrew.svet...@gmail.com>, decaz <deca...@gmail.com>, janneronkko 
<janne.ron...@iki.fi>, thirtyseven <t...@shlashdot.org>, ported-pw 
<cont...@ported.pw>, Olegt0rr <t...@mail.ru>, michalc <mic...@charemza.name>, 
ch3pjw <p...@concertdaw.co.uk>, puddly <pudd...@gmail.com>, AMDmi3 
<amd...@amdmi3.ru>, webknjaz <webknjaz+github/prof...@redhat.com>, rcfox 
<r...@rcfox.ca>
+Author-email: pohmelie <multisosnoo...@gmail.com>, asvetlov 
<andrew.svet...@gmail.com>, decaz <deca...@gmail.com>, janneronkko 
<janne.ron...@iki.fi>, thirtyseven <t...@shlashdot.org>, ported-pw 
<cont...@ported.pw>, Olegt0rr <t...@mail.ru>, michalc <mic...@charemza.name>, 
ch3pjw <p...@concertdaw.co.uk>, puddly <pudd...@gmail.com>, AMDmi3 
<amd...@amdmi3.ru>, webknjaz <webknjaz+github/prof...@redhat.com>, rcfox 
<r...@rcfox.ca>, bellini666 <thi...@bellini.dev>
 License:                                  Apache License
                                    Version 2.0, January 2004
                                 http://www.apache.org/licenses/
@@ -220,6 +220,7 @@
 Requires-Dist: async_timeout>=4.0.0; extra == "dev"
 Requires-Dist: pytest-asyncio; extra == "dev"
 Requires-Dist: pytest-cov; extra == "dev"
+Requires-Dist: pytest-mock; extra == "dev"
 Requires-Dist: pytest; extra == "dev"
 Requires-Dist: siosocks; extra == "dev"
 Requires-Dist: trustme; extra == "dev"
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/aioftp-0.24.1/history.rst 
new/aioftp-0.25.1/history.rst
--- old/aioftp-0.24.1/history.rst       2024-12-13 13:22:27.000000000 +0100
+++ new/aioftp-0.25.1/history.rst       2025-04-11 20:53:44.000000000 +0200
@@ -1,130 +1,131 @@
-x.x.x (xx-xx-xxxx)
+x.x.x (xxxx-xx-xx)
 
-0.24.1 (13-12-2024)
+0.25.1 (2025-04-11)
+-------------------
+- client: do not start explicit tls if implicit mode used (#184)
+
+0.25.0 (2025-04-08)
+-------------------
+- client: add partial client support for explicit tls (#183)
+Thanks to `bellini666 <https://github.com/bellini666>`_
+
+0.24.1 (2024-12-13)
+-------------------
 - server: use single line pasv response (fix #142)
 
-0.24.0 (11-12-2024)
+0.24.0 (2024-12-11)
 -------------------
 - remove documentation dependencies from pyproject.toml (moved to 
docs/requirements.txt)
 - include symlink destination in path info for unix legacy mode (#169)
 - update documentation links (#180)
 Thanks to `webknjaz <https://github.com/webknjaz>`_, `rcfox 
<https://github.com/rcfox>`_
 
-0.23.1 (14-10-2024)
+0.23.1 (2024-10-14)
 -------------------
 - update ci
 
-0.23.0 (14-10-2024)
+0.23.0 (2024-10-14)
 -------------------
 - server: fix pathlib `relative_to` issue (#179)
 - minimal python version upgraded to 3.9
 
-0.22.3 (05-01-2024)
+0.22.3 (2024-01-05)
 -------------------
 - minimal python version downgraded to 3.8
 
-0.22.2 (29-12-2023)
+0.22.2 (2023-12-29)
 -------------------
 - ci: separate build and publish jobs
 
-0.22.1 (29-12-2023)
+0.22.1 (2023-12-29)
 -------------------
 - docs: update/fix readthedocs configuration
 - ci: fix workflow file extension from `yaml` to `yml`
 
-0.22.0 (29-12-2023)
+0.22.0 (2023-12-29)
 -------------------
 - client.list: fix infinite symlink loop for `.` and `..` on FTP servers with 
UNIX-like filesystem for `client.list(path, recursive=True)`
 - project file structure: refactor to use `pyproject.toml`
 - minimal python version bumped to 3.11
 - ci: update publish/deploy job (#171)
 
-0.21.4 (13-10-2022)
+0.21.4 (2022-10-13)
 -------------------
 - tests: use `pytest_asyncio` `strict` mode and proper decorations (#155)
 - setup/tests: set low bound for version of `async-timeout` (#159)
 
-0.21.3 (15-07-2022)
+0.21.3 (2022-07-15)
 -------------------
 - server/`LIST`: prevent broken links are listed, but can't be used with `stat`
 - server: make `User.get_permissions` async
 
-0.21.2 (22-04-2022)
+0.21.2 (2022-04-22)
 -------------------
 - tests: remove exception representation check
 
-0.21.1 (20-04-2022)
+0.21.1 (2022-04-20)
 -------------------
 - tests: replace more specific `ConnectionRefusedError` with `OSError` for 
compatibility with FreeBSD (#152)
 Thanks to `AMDmi3 https://github.com/AMDmi3`_
 
-0.21.0 (18-03-2022)
-------------------
+0.21.0 (2022-03-18)
+-------------------
 - server: support PASV response with custom address (#150)
 Thanks to `janneronkko https://github.com/janneronkko`_
 
-0.20.1 (15-02-2022)
-------------------
+0.20.1 (2022-02-15)
+-------------------
 - server: fix real directory resolve for windows (#147)
 Thanks to `ported-pw https://github.com/ported-pw`_
 
-0.20.0 (27-12-2021)
-------------------
-
+0.20.0 (2021-12-27)
+-------------------
 - add client argument to set priority of custom list parser 
(`parse_list_line_custom_first`) (#145)
 - do not ignore failed parsing of list response (#144)
 Thanks to `spolloni https://github.com/spolloni`_
 
-0.19.0 (08-10-2021)
-------------------
-
+0.19.0 (2021-10-08)
+-------------------
 - add client connection timeout (#140)
 - remove explicit coroutine passing to `asyncio.wait` (#134)
 Thanks to `decaz <https://github.com/decaz>`_
 
-0.18.1 (03-10-2020)
-------------------
-
+0.18.1 (2020-10-03)
+-------------------
 - sync tests with new `siosocks` (#127)
 - some docs fixes
 - log level changes
 
-0.18.0 (03-09-2020)
-------------------
-
+0.18.0 (2020-09-03)
+-------------------
 - server: fix `MLSX` time format (#125)
 - server: resolve server address from connection (#125)
 Thanks to `PonyPC <https://github.com/PonyPC>`_
 
-0.17.2 (21-08-2020)
-------------------
-
+0.17.2 (2020-08-21)
+-------------------
 - server: fix broken `python -m aioftp` after 3.7 migration
 
-0.17.1 (14-08-2020)
-------------------
-
+0.17.1 (2020-08-14)
+-------------------
 - common/stream: add `readexactly` proxy method
 
-0.17.0 (11-08-2020)
-------------------
-
+0.17.0 (2020-08-11)
+-------------------
 - tests: fix test_unlink_on_dir on POSIX compatible systems (#118)
 - docs: fix extra parentheses (#122)
 - client: replace `ClientSession` with `Client.context`
 Thanks to `AMDmi3 <https://github.com/AMDmi3>`_, `Olegt0rr 
<https://github.com/Olegt0rr>`_
 
-0.16.1 (09-07-2020)
-------------------
-
+0.16.1 (2020-07-09)
+-------------------
 - client: strip date before parsing (#113)
 - client: logger no longer prints out plaintext password (#114)
 - client: add custom passive commands to client (#116)
 Thanks to `ndhansen <https://github.com/ndhansen>`_
 
-0.16.0 (11-03-2020)
-------------------
-
+0.16.0 (2020-03-11)
+-------------------
 - server: remove obsolete `pass` to `pass_` command renaming
 Thanks to `Puddly <https://github.com/puddly>`_
 
@@ -132,58 +133,49 @@
 - all: add base exception class
 Thanks to `decaz <https://github.com/decaz>`_
 
-0.15.0 (07-01-2020)
+0.15.0 (2020-01-07)
 -------------------
-
 - server: use explicit mapping of available commands for security reasons
 Thanks to `Puddly` for report
 
-0.14.0 (30-12-2019)
+0.14.0 (2019-12-30)
 -------------------
-
 - client: add socks proxy support via `siosocks 
<https://github.com/pohmelie/siosocks>`_ (#94)
 - client: add custom `list` parser (#95)
 Thanks to `purpleskyfall <https://github.com/purpleskyfall>`_, `VyachAp 
<https://github.com/VyachAp>`_
 
-0.13.0 (24-03-2019)
+0.13.0 (2019-03-24)
 -------------------
-
 - client: add windows list parser (#82)
 - client/server: fix implicit ssl mode (#89)
 - tests: move to pytest
 - all: small fixes
 Thanks to `jw4js <https://github.com/jw4js>`_, `PonyPC 
<https://github.com/PonyPC>`_
 
-0.12.0 (15-10-2018)
+0.12.0 (2018-10-15)
 -------------------
-
 - all: add implicit ftps mode support (#81)
 Thanks to `alxpy <https://github.com/alxpy>`_, `webknjaz 
<https://github.com/webknjaz>`_
 
-0.11.1 (30-08-2018)
+0.11.1 (2018-08-30)
 -------------------
-
 - server: fix memory pathio is not shared between connections
 - client: add argument to `list` to allow manually specifying raw command (#78)
 Thanks to `thirtyseven <https://github.com/thirtyseven>`_
 
-
-0.11.0 (04-07-2018)
+0.11.0 (2018-07-04)
 -------------------
-
 - client: fix parsing `ls` modify time (#60)
 - all: add python3.7 support (`__aiter__` must be regular function since now) 
(#76, #77)
 Thanks to `saulcruz <https://github.com/saulcruz>`_, `NickG123 
<https://github.com/NickG123>`_, `rsichny <https://github.com/rsichny>`_, 
`Modelmat <https://github.com/Modelmat>`_, `webknjaz 
<https://github.com/webknjaz>`_
 
-0.10.1 (01-03-2018)
+0.10.1 (2018-03-01)
 -------------------
-
 - client: more flexible `EPSV` response parsing
 Thanks to `p4l1ly <https://github.com/p4l1ly>`_
 
-0.10.0 (03-02-2018)
+0.10.0 (2018-02-03)
 -------------------
-
 - server: fix ipv6 peername unpack
 - server: `connection` object is accessible from path-io layer since now
 - main: add command line argument to set version of IP protocol
@@ -193,29 +185,25 @@
 - client: change `PASV` to `EPSV` with fallback to `PASV`
 Thanks to `jacobtomlinson <https://github.com/jacobtomlinson>`_, `mbkr1992 
<https://github.com/mbkr1992>`_
 
-0.9.0 (04-01-2018)
+0.9.0 (2018-01-04)
 ------------------
-
 - server: fix server address in passive mode
 - server: do not reraise dispatcher exceptions
 - server: remove `wait_closed`, `close` is coroutine since now
 Thanks to `yieyu <https://github.com/yieyu>`_, `jkr78 
<https://github.com/jkr78>`_
 
-0.8.1 (08-10-2017)
+0.8.1 (2017-10-08)
 ------------------
-
 - client: ignore LIST lines, which can't be parsed
 Thanks to `bachya <https://github.com/bachya>`_
 
-0.8.0 (06-08-2017)
+0.8.0 (2017-08-06)
 ------------------
-
 - client/server: add explicit encoding
 Thanks to `anan-lee <https://github.com/anan-lee>`_
 
-0.7.0 (17-04-2017)
+0.7.0 (2017-04-17)
 ------------------
-
 - client: add base `LIST` parsing
 - client: add `client.list` fallback on `MLSD` «not implemented» status code 
to `LIST`
 - client: add `client.stat` fallback on `MLST` «not implemented» status code 
to `LIST`
@@ -224,51 +212,44 @@
 - server: fix `PASV` sequencies before data transfer (latest `PASV` win)
 Thanks to `jw4js <https://github.com/jw4js>`_, `rsichny 
<https://github.com/rsichny>`_
 
-0.6.3 (02-03-2017)
+0.6.3 (2017-03-02)
 ------------------
-
 - `stream.read` will read whole data by default (as 
`asyncio.StreamReader.read`)
 Thanks to `sametmax <https://github.com/sametmax>`_
 
-0.6.2 (27-02-2017)
+0.6.2 (2017-02-27)
 ------------------
-
 - replace `docopt` with `argparse`
 - add `syst` server command
 - improve client `list` documentation
 Thanks to `thelostt <https://github.com/thelostt>`_, `yieyu 
<https://github.com/yieyu>`_
 
-0.6.1 (16-04-2016)
+0.6.1 (2016-04-16)
 ------------------
-
 - fix documentation main page client example
 
-0.6.0 (16-04-2016)
+0.6.0 (2016-04-16)
 ------------------
-
 - fix `modifed time` field for `list` command result
 - add `ClientSession` context
 - add `REST` command to server and client
 Thanks to `rsichny <https://github.com/rsichny>`_
 
-0.5.0 (12-02-2016)
+0.5.0 (2016-02-12)
 ------------------
-
 - change development status to production/stable
 - add configuration to restrict port range for passive server
 - build LIST string with stat.filemode
 Thanks to `rsichny <https://github.com/rsichny>`_
 
-0.4.1 (21-12-2015)
+0.4.1 (2015-12-21)
 ------------------
-
 - improved performance on non-throttled streams
 - default path io layer for client and server is PathIO since now
 - added benchmark result
 
-0.4.0 (17-12-2015)
+0.4.0 (2015-12-17)
 ------------------
-
 - `async for` for pathio list function
 - async context manager for streams and pathio files io
 - python 3.5 only
@@ -276,14 +257,12 @@
 - all path errors are now reraised as PathIOError
 - server does not drop connection on path io errors since now, but return 
"451" code
 
-0.3.1 (09-11-2015)
+0.3.1 (2015-11-09)
 ------------------
-
 - fixed setup.py long-description
 
-0.3.0 (09-11-2015)
+0.3.0 (2015-11-09)
 ------------------
-
 - added handling of OSError in dispatcher
 - fixed client/server close not opened file in finally
 - handling PASS after login
@@ -300,9 +279,8 @@
 - all socket streams are ThrottleStreamIO since now
 Thanks to `rsichny <https://github.com/rsichny>`_, `tier2003 
<https://github.com/tier2003>`_
 
-0.2.0 (22-09-2015)
+0.2.0 (2015-09-22)
 ------------------
-
 - client throttle
 - new server dispatcher (can wait for connections)
 - maximum connections per user/server
@@ -313,54 +291,45 @@
 - "sh" module removed from test requirements
 Thanks to `rsichny <https://github.com/rsichny>`_, `jettify 
<https://github.com/jettify>`_
 
-0.1.7 (03-09-2015)
+0.1.7 (2015-09-03)
 ------------------
-
 - bugfix on windows (can't make passive connection to 0.0.0.0:port)
 - default host is "127.0.0.1" since now
 - silently ignoring ipv6 sockets in server binding list
 
-0.1.6 (03-09-2015)
+0.1.6 (2015-09-03)
 ------------------
-
 - bugfix on windows (ipv6 address come first in list of binded sockets)
 
-0.1.5 (01-09-2015)
+0.1.5 (2015-09-01)
 ------------------
-
 - bugfix server on windows (PurePosixPath for virtual path)
 
-0.1.4 (31-08-2015)
+0.1.4 (2015-08-31)
 ------------------
-
 - close data connection after client disconnects
 Thanks to `rsichny <https://github.com/rsichny>`_
 
-0.1.3 (28-08-2015)
+0.1.3 (2015-08-28)
 ------------------
-
 - pep8 "Method definitions inside a class are surrounded by a single blank 
line"
 - MemoryPathIO.Stats should include st_mode
 Thanks to `rsichny <https://github.com/rsichny>`_
 
-0.1.2 (11-06-2015)
+0.1.2 (2015-06-11)
 ------------------
-
 - aioftp now executes like script ("python -m aioftp")
 
-0.1.1 (10-06-2015)
+0.1.1 (2015-06-10)
 ------------------
-
 - typos in server strings
 - docstrings for path abstraction layer
 
-0.1.0 (05-06-2015)
+0.1.0 (2015-06-05)
 ------------------
-
 - server functionality
 - path abstraction layer
 
-0.0.1 (24-04-2015)
+0.0.1 (2015-04-24)
 ------------------
-
 - first release (client only)
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/aioftp-0.24.1/pyproject.toml 
new/aioftp-0.25.1/pyproject.toml
--- old/aioftp-0.24.1/pyproject.toml    2024-12-13 13:22:27.000000000 +0100
+++ new/aioftp-0.25.1/pyproject.toml    2025-04-11 20:53:44.000000000 +0200
@@ -1,6 +1,6 @@
 [project]
 name = "aioftp"
-version = "0.24.1"
+version = "0.25.1"
 description = "ftp client/server for asyncio"
 readme = "README.rst"
 requires-python = ">= 3.9"
@@ -31,6 +31,7 @@
     {name = "AMDmi3", email="amd...@amdmi3.ru"},
     {name = "webknjaz", email="webknjaz+github/prof...@redhat.com"},
     {name = "rcfox", email="r...@rcfox.ca"},
+    {name = "bellini666", email="thi...@bellini.dev"},
 ]
 classifiers = [
     "Programming Language :: Python",
@@ -52,6 +53,7 @@
     "async_timeout >= 4.0.0",
     "pytest-asyncio",
     "pytest-cov",
+    "pytest-mock",
     "pytest",
     "siosocks",
     "trustme",
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/aioftp-0.24.1/src/aioftp/client.py 
new/aioftp-0.25.1/src/aioftp/client.py
--- old/aioftp-0.24.1/src/aioftp/client.py      2024-12-13 13:22:27.000000000 
+0100
+++ new/aioftp-0.25.1/src/aioftp/client.py      2025-04-11 20:53:44.000000000 
+0200
@@ -6,6 +6,7 @@
 import logging
 import pathlib
 import re
+import ssl
 from functools import partial
 
 from . import errors, pathio
@@ -19,6 +20,7 @@
     HALF_OF_YEAR_IN_SECONDS,
     TWO_YEARS_IN_SECONDS,
     AsyncListerMixin,
+    SSLSessionBoundContext,
     StreamThrottle,
     ThrottleStreamIO,
     async_enterable,
@@ -138,6 +140,8 @@
         self.parse_list_line_custom_first = parse_list_line_custom_first
         self._passive_commands = passive_commands
         self._open_connection = partial(open_connection, ssl=self.ssl, 
**siosocks_asyncio_kwargs)
+        self._upgraded_to_tls = False
+        self._logged_in = False
 
     async def connect(self, host, port=DEFAULT_PORT):
         self.server_host = host
@@ -636,6 +640,46 @@
         code, info = await self.command(None, "220", "120")
         return info
 
+    async def _send_tls_protection_commands(self) -> None:
+        """
+        :py:func:`asyncio.coroutine`
+
+        Sends the PBSZ and PROT commands as required for TLS connection.
+        """
+        await self.command("PBSZ 0", "200")
+        await self.command("PROT P", "200")
+
+    async def upgrade_to_tls(self, sslcontext=None):
+        """
+        :py:func:`asyncio.coroutine`
+
+        Attempts to upgrade the connection to TLS (explicit TLS).
+        Downgrading via the CCC or REIN commands is not supported.  Both the 
command
+        and data channels will be encrypted after using this command.  You may
+        call this command at any point during the connection, but not every 
FTP server
+        will allow a connection upgrade after logging in.
+
+        :param sslcontext: custom ssl context
+        :type sslcontext: :py:class:`ssl.SSLContext`
+        """
+        if self.ssl:
+            raise RuntimeError("SSL context is already set in implicit mode, 
can't use explicit mode")
+
+        if self._upgraded_to_tls:
+            return
+
+        await self.command("AUTH TLS", "234")
+
+        if sslcontext:
+            self.ssl = sslcontext
+        elif not isinstance(self.ssl, ssl.SSLContext):
+            self.ssl = ssl.create_default_context()
+
+        await self.stream.start_tls(sslcontext=self.ssl, 
server_hostname=self.server_host)
+
+        if self._logged_in:
+            await self._send_tls_protection_commands()
+
     async def login(
         self,
         user=DEFAULT_USER,
@@ -674,6 +718,11 @@
                 censor_after=censor_after,
             )
 
+        self._logged_in = True
+
+        if self._upgraded_to_tls:
+            await self._send_tls_protection_commands()
+
     async def get_current_directory(self):
         """
         :py:func:`asyncio.coroutine`
@@ -779,7 +828,7 @@
                 cls.parse_line = self.parse_mlsx_line
                 if raw_command not in [None, "MLSD", "LIST"]:
                     raise ValueError(
-                        "raw_command must be one of MLSD or " f"LIST, but got 
{raw_command}",
+                        f"raw_command must be one of MLSD or LIST, but got 
{raw_command}",
                     )
                 if raw_command in [None, "MLSD"]:
                     try:
@@ -1190,6 +1239,18 @@
             throttles={"_": self.throttle},
             timeout=self.socket_timeout,
         )
+
+        ssl_object = self.stream.writer.transport.get_extra_info("ssl_object")
+        if ssl_object and not self.ssl:
+            await writer.start_tls(
+                sslcontext=SSLSessionBoundContext(
+                    ssl.PROTOCOL_TLS_CLIENT,
+                    context=ssl_object.context,
+                    session=ssl_object.session,
+                ),
+                server_hostname=self.server_host,
+            )
+
         return stream
 
     async def abort(self, *, wait=True):
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/aioftp-0.24.1/src/aioftp/common.py 
new/aioftp-0.25.1/src/aioftp/common.py
--- old/aioftp-0.24.1/src/aioftp/common.py      2024-12-13 13:22:27.000000000 
+0100
+++ new/aioftp-0.25.1/src/aioftp/common.py      2025-04-11 20:53:44.000000000 
+0200
@@ -3,6 +3,8 @@
 import collections
 import functools
 import locale
+import socket
+import ssl
 import threading
 from contextlib import contextmanager
 
@@ -24,9 +26,9 @@
     "DEFAULT_PASSWORD",
     "DEFAULT_ACCOUNT",
     "setlocale",
+    "SSLSessionBoundContext",
 )
 
-
 END_OF_LINE = "\r\n"
 DEFAULT_BLOCK_SIZE = 8192
 
@@ -319,6 +321,16 @@
         """
         self.writer.close()
 
+    async def start_tls(self, sslcontext, server_hostname):
+        """
+        Upgrades the connection to TLS
+        """
+        await self.writer.start_tls(
+            sslcontext=sslcontext,
+            server_hostname=server_hostname,
+            ssl_handshake_timeout=self.write_timeout,
+        )
+
 
 class Throttle:
     """
@@ -394,7 +406,7 @@
         return Throttle(limit=self._limit, reset_rate=self.reset_rate)
 
     def __repr__(self):
-        return f"{self.__class__.__name__}(limit={self._limit!r}, " 
f"reset_rate={self.reset_rate!r})"
+        return f"{self.__class__.__name__}(limit={self._limit!r}, 
reset_rate={self.reset_rate!r})"
 
 
 class StreamThrottle(collections.namedtuple("StreamThrottle", "read write")):
@@ -589,3 +601,67 @@
             yield locale.setlocale(locale.LC_ALL, name)
         finally:
             locale.setlocale(locale.LC_ALL, old_locale)
+
+
+# class from https://github.com/python/cpython/issues/79152 (with some changes)
+class SSLSessionBoundContext(ssl.SSLContext):
+    """ssl.SSLContext bound to an existing SSL session.
+
+    Actually asyncio doesn't support TLS session resumption, the 
loop.create_connection() API
+    does not take any TLS session related argument. There is ongoing work to 
add support for this
+    at https://github.com/python/cpython/issues/79152.
+
+    The loop.create_connection() API takes a SSL context argument though, the 
SSLSessionBoundContext
+    is used to wrap a SSL context and inject a SSL session on calls to
+        - SSLSessionBoundContext.wrap_socket()
+        - SSLSessionBoundContext.wrap_bio()
+
+    This wrapper is compatible with any TLS application which calls only the 
methods above when
+    making new TLS connections. This class is NOT a subclass of 
ssl.SSLContext, so it will be
+    rejected by applications which ensure the SSL context is an instance of 
ssl.SSLContext. Not being
+    a subclass of ssl.SSLContext makes this wrapper lightweight.
+    """
+
+    __slots__ = ("context", "session")
+
+    def __init__(self, protocol: int, context: ssl.SSLContext, session: 
ssl.SSLSession):
+        self.context = context
+        self.session = session
+
+    def wrap_socket(
+        self,
+        sock: socket.socket,
+        server_side: bool = False,
+        do_handshake_on_connect: bool = True,
+        suppress_ragged_eofs: bool = True,
+        server_hostname: bool = None,
+        session: ssl.SSLSession = None,
+    ) -> ssl.SSLSocket:
+        if session is not None:
+            raise ValueError("expected session to be None")
+        return self.context.wrap_socket(
+            sock=sock,
+            server_hostname=server_hostname,
+            server_side=server_side,
+            do_handshake_on_connect=do_handshake_on_connect,
+            suppress_ragged_eofs=suppress_ragged_eofs,
+            session=self.session,
+        )
+
+    def wrap_bio(
+        self,
+        incoming: ssl.MemoryBIO,
+        outgoing: ssl.MemoryBIO,
+        server_side: bool = False,
+        server_hostname: bool = None,
+        session: ssl.SSLSession = None,
+    ) -> ssl.SSLObject:
+        if session is not None:
+            raise ValueError("expected session to be None")
+        return self.context.wrap_bio(
+            incoming=incoming,
+            outgoing=outgoing,
+            server_hostname=server_hostname,
+            server_side=server_side,
+            session=self.session,
+        )
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/aioftp-0.24.1/src/aioftp/errors.py 
new/aioftp-0.25.1/src/aioftp/errors.py
--- old/aioftp-0.24.1/src/aioftp/errors.py      2024-12-13 13:22:27.000000000 
+0100
+++ new/aioftp-0.25.1/src/aioftp/errors.py      2025-04-11 20:53:44.000000000 
+0200
@@ -43,7 +43,7 @@
 
     def __init__(self, expected_codes, received_codes, info):
         super().__init__(
-            f"Waiting for {expected_codes} but got " f"{received_codes} 
{info!r}",
+            f"Waiting for {expected_codes} but got {received_codes} {info!r}",
         )
         self.expected_codes = common.wrap_with_container(expected_codes)
         self.received_codes = common.wrap_with_container(received_codes)
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/aioftp-0.24.1/src/aioftp/pathio.py 
new/aioftp-0.25.1/src/aioftp/pathio.py
--- old/aioftp-0.24.1/src/aioftp/pathio.py      2024-12-13 13:22:27.000000000 
+0100
+++ new/aioftp-0.25.1/src/aioftp/pathio.py      2025-04-11 20:53:44.000000000 
+0200
@@ -117,7 +117,7 @@
     async def wrapper(self, file, *args, **kwargs):
         if isinstance(file, AsyncPathIOContext):
             raise ValueError(
-                "Native path io file methods can not be used " "with wrapped 
file object",
+                "Native path io file methods can not be used with wrapped file 
object",
             )
         return await coro(self, file, *args, **kwargs)
 
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/aioftp-0.24.1/src/aioftp/server.py 
new/aioftp-0.25.1/src/aioftp/server.py
--- old/aioftp-0.24.1/src/aioftp/server.py      2024-12-13 13:22:27.000000000 
+0100
+++ new/aioftp-0.25.1/src/aioftp/server.py      2025-04-11 20:53:44.000000000 
+0200
@@ -72,7 +72,7 @@
             return False
 
     def __repr__(self):
-        return f"{self.__class__.__name__}({self.path!r}, " 
f"readable={self.readable!r}, writable={self.writable!r})"
+        return f"{self.__class__.__name__}({self.path!r}, 
readable={self.readable!r}, writable={self.writable!r})"
 
 
 class User:
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/aioftp-0.24.1/src/aioftp.egg-info/PKG-INFO 
new/aioftp-0.25.1/src/aioftp.egg-info/PKG-INFO
--- old/aioftp-0.24.1/src/aioftp.egg-info/PKG-INFO      2024-12-13 
13:22:30.000000000 +0100
+++ new/aioftp-0.25.1/src/aioftp.egg-info/PKG-INFO      2025-04-11 
20:53:50.000000000 +0200
@@ -1,9 +1,9 @@
-Metadata-Version: 2.1
+Metadata-Version: 2.4
 Name: aioftp
-Version: 0.24.1
+Version: 0.25.1
 Summary: ftp client/server for asyncio
 Author: yieyu, rsichnyi, jw4js, oleksandr-kuzmenko, ndhansen, modelmat, greut, 
PonyPC, jacobtomlinson, bachya, CrafterKolyan, jkr78
-Author-email: pohmelie <multisosnoo...@gmail.com>, asvetlov 
<andrew.svet...@gmail.com>, decaz <deca...@gmail.com>, janneronkko 
<janne.ron...@iki.fi>, thirtyseven <t...@shlashdot.org>, ported-pw 
<cont...@ported.pw>, Olegt0rr <t...@mail.ru>, michalc <mic...@charemza.name>, 
ch3pjw <p...@concertdaw.co.uk>, puddly <pudd...@gmail.com>, AMDmi3 
<amd...@amdmi3.ru>, webknjaz <webknjaz+github/prof...@redhat.com>, rcfox 
<r...@rcfox.ca>
+Author-email: pohmelie <multisosnoo...@gmail.com>, asvetlov 
<andrew.svet...@gmail.com>, decaz <deca...@gmail.com>, janneronkko 
<janne.ron...@iki.fi>, thirtyseven <t...@shlashdot.org>, ported-pw 
<cont...@ported.pw>, Olegt0rr <t...@mail.ru>, michalc <mic...@charemza.name>, 
ch3pjw <p...@concertdaw.co.uk>, puddly <pudd...@gmail.com>, AMDmi3 
<amd...@amdmi3.ru>, webknjaz <webknjaz+github/prof...@redhat.com>, rcfox 
<r...@rcfox.ca>, bellini666 <thi...@bellini.dev>
 License:                                  Apache License
                                    Version 2.0, January 2004
                                 http://www.apache.org/licenses/
@@ -220,6 +220,7 @@
 Requires-Dist: async_timeout>=4.0.0; extra == "dev"
 Requires-Dist: pytest-asyncio; extra == "dev"
 Requires-Dist: pytest-cov; extra == "dev"
+Requires-Dist: pytest-mock; extra == "dev"
 Requires-Dist: pytest; extra == "dev"
 Requires-Dist: siosocks; extra == "dev"
 Requires-Dist: trustme; extra == "dev"
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/aioftp-0.24.1/src/aioftp.egg-info/SOURCES.txt 
new/aioftp-0.25.1/src/aioftp.egg-info/SOURCES.txt
--- old/aioftp-0.24.1/src/aioftp.egg-info/SOURCES.txt   2024-12-13 
13:22:30.000000000 +0100
+++ new/aioftp-0.25.1/src/aioftp.egg-info/SOURCES.txt   2025-04-11 
20:53:50.000000000 +0200
@@ -33,4 +33,5 @@
 tests/test_restart.py
 tests/test_simple_functions.py
 tests/test_throttle.py
+tests/test_tls.py
 tests/test_user.py
\ No newline at end of file
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/aioftp-0.24.1/src/aioftp.egg-info/requires.txt 
new/aioftp-0.25.1/src/aioftp.egg-info/requires.txt
--- old/aioftp-0.24.1/src/aioftp.egg-info/requires.txt  2024-12-13 
13:22:30.000000000 +0100
+++ new/aioftp-0.25.1/src/aioftp.egg-info/requires.txt  2025-04-11 
20:53:50.000000000 +0200
@@ -3,6 +3,7 @@
 async_timeout>=4.0.0
 pytest-asyncio
 pytest-cov
+pytest-mock
 pytest
 siosocks
 trustme
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/aioftp-0.24.1/tests/conftest.py 
new/aioftp-0.25.1/tests/conftest.py
--- old/aioftp-0.24.1/tests/conftest.py 2024-12-13 13:22:27.000000000 +0100
+++ new/aioftp-0.25.1/tests/conftest.py 2025-04-11 20:53:44.000000000 +0200
@@ -192,7 +192,7 @@
         ok = math.isclose(self.delay, delay, rel_tol=rel_tol, abs_tol=abs_tol)
         if not ok:
             print(
-                f"latest sleep: {self.delay}; expected delay: " f"{delay}; 
rel: {rel_tol}",
+                f"latest sleep: {self.delay}; expected delay: {delay}; rel: 
{rel_tol}",
             )
         return ok
 
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/aioftp-0.24.1/tests/test_tls.py 
new/aioftp-0.25.1/tests/test_tls.py
--- old/aioftp-0.24.1/tests/test_tls.py 1970-01-01 01:00:00.000000000 +0100
+++ new/aioftp-0.25.1/tests/test_tls.py 2025-04-11 20:53:44.000000000 +0200
@@ -0,0 +1,113 @@
+import sys
+
+import pytest
+
+if sys.version_info < (3, 11):
+    pytest.skip(reason="required python 3.11+", allow_module_level=True)
+
+
+async def _auth_response(connection, rest):
+    connection.response("234", ":P")
+    return True
+
+
+async def _ok_response(connection, rest):
+    connection.response("200", ":P")
+    return True
+
+
+@pytest.mark.asyncio
+async def test_upgrade_to_tls(mocker, pair_factory):
+    ssl_context = object()
+    create_default_context = 
mocker.patch("aioftp.client.ssl.create_default_context", 
return_value=ssl_context)
+
+    async with pair_factory(logged=False, do_quit=False) as pair:
+        pair.server.commands_mapping["auth"] = _auth_response
+
+        start_tls = mocker.patch.object(pair.client.stream, "start_tls")
+        command_spy = mocker.spy(pair.client, "command")
+
+        await pair.client.upgrade_to_tls()
+
+    create_default_context.assert_called_once_with()
+    start_tls.assert_called_once_with(sslcontext=ssl_context, 
server_hostname=pair.client.server_host)
+    command_spy.assert_called_once_with("AUTH TLS", "234")
+
+
+@pytest.mark.asyncio
+async def test_upgrade_to_tls_custom_ssl_context(mocker, pair_factory):
+    ssl_context = object()
+    create_default_context = 
mocker.patch("aioftp.client.ssl.create_default_context")
+
+    async with pair_factory(logged=False, do_quit=False) as pair:
+        pair.server.commands_mapping["auth"] = _auth_response
+
+        start_tls = mocker.patch.object(pair.client.stream, "start_tls")
+        command_spy = mocker.spy(pair.client, "command")
+
+        await pair.client.upgrade_to_tls(sslcontext=ssl_context)
+
+    create_default_context.assert_not_called()
+    start_tls.assert_called_once_with(sslcontext=ssl_context, 
server_hostname=pair.client.server_host)
+    command_spy.assert_called_once_with("AUTH TLS", "234")
+
+
+@pytest.mark.asyncio
+async def test_upgrade_to_tls_does_nothing_when_already_updated(mocker, 
pair_factory):
+    mocker.patch("aioftp.client.ssl.create_default_context")
+
+    async with pair_factory(logged=False, do_quit=False) as pair:
+        pair.client._upgraded_to_tls = True
+
+        start_tls = mocker.patch.object(pair.client.stream, "start_tls")
+        command_spy = mocker.spy(pair.client, "command")
+
+        await pair.client.upgrade_to_tls()
+
+    start_tls.assert_not_called()
+    command_spy.assert_not_called()
+
+
+@pytest.mark.asyncio
+async def test_upgrade_to_tls_when_logged_in(mocker, pair_factory):
+    mocker.patch("aioftp.client.ssl.create_default_context")
+
+    async with pair_factory(logged=False, do_quit=False) as pair:
+        pair.server.commands_mapping["auth"] = _auth_response
+        pair.server.commands_mapping["pbsz"] = _ok_response
+        pair.server.commands_mapping["prot"] = _ok_response
+        pair.client._logged_in = True
+
+        mocker.patch.object(pair.client.stream, "start_tls")
+        command_spy = mocker.spy(pair.client, "command")
+
+        await pair.client.upgrade_to_tls()
+
+    command_spy.assert_has_calls(
+        [
+            mocker.call("AUTH TLS", "234"),
+            mocker.call("PBSZ 0", "200"),
+            mocker.call("PROT P", "200"),
+        ],
+    )
+
+
+@pytest.mark.asyncio
+async def test_login_should_send_tls_protection_when_upgraded(mocker, 
pair_factory):
+    mocker.patch("aioftp.client.ssl.create_default_context")
+
+    async with pair_factory(logged=False, do_quit=False) as pair:
+        pair.server.commands_mapping["pbsz"] = _ok_response
+        pair.server.commands_mapping["prot"] = _ok_response
+        pair.client._upgraded_to_tls = True
+
+        command_spy = mocker.spy(pair.client, "command")
+
+        await pair.client.login("foo", "bar")
+
+    command_spy.assert_has_calls(
+        [
+            mocker.call("PBSZ 0", "200"),
+            mocker.call("PROT P", "200"),
+        ],
+    )

Reply via email to