fix: 포트 충돌 회피 — note_bridge 8098, intent_service 8099

Jellyfin(8096), OrbStack(8097) 포트 충돌으로 변경.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Hyungi Ahn
2026-03-19 13:53:55 +09:00
parent dc08d29509
commit c2257d3a86
2709 changed files with 619549 additions and 10 deletions

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,539 @@
from __future__ import annotations
import io
import json as _json
import sys
import typing
import warnings
from contextlib import asynccontextmanager
from socket import timeout as SocketTimeout
from .._collections import HTTPHeaderDict
from .._typing import _TYPE_BODY
from ..backend._async import AsyncLowLevelResponse
from ..exceptions import (
BaseSSLError,
HTTPError,
IncompleteRead,
ProtocolError,
ReadTimeoutError,
ResponseNotReady,
SSLError,
MustRedialError,
)
from ..response import ContentDecoder, HTTPResponse
from ..util.response import is_fp_closed, BytesQueueBuffer
from ..util.retry import Retry
from .connection import AsyncHTTPConnection
if typing.TYPE_CHECKING:
from email.message import Message
from .._async.connectionpool import AsyncHTTPConnectionPool
from ..contrib.webextensions._async import AsyncExtensionFromHTTP
from ..util._async.traffic_police import AsyncTrafficPolice
class AsyncHTTPResponse(HTTPResponse):
def __init__(
self,
body: _TYPE_BODY = "",
headers: typing.Mapping[str, str] | typing.Mapping[bytes, bytes] | None = None,
status: int = 0,
version: int = 0,
reason: str | None = None,
preload_content: bool = True,
decode_content: bool = True,
original_response: AsyncLowLevelResponse | None = None,
pool: AsyncHTTPConnectionPool | None = None,
connection: AsyncHTTPConnection | None = None,
msg: Message | None = None,
retries: Retry | None = None,
enforce_content_length: bool = True,
request_method: str | None = None,
request_url: str | None = None,
auto_close: bool = True,
police_officer: AsyncTrafficPolice[AsyncHTTPConnection] | None = None,
) -> None:
if isinstance(headers, HTTPHeaderDict):
self.headers = headers
else:
self.headers = HTTPHeaderDict(headers) # type: ignore[arg-type]
try:
self.status = int(status)
except ValueError:
self.status = 0 # merely for tests, was supported due to broken httplib.
self.version = version
self.reason = reason
self.decode_content = decode_content
self._has_decoded_content = False
self._request_url: str | None = request_url
self._retries: Retry | None = None
self._extension: AsyncExtensionFromHTTP | None = None # type: ignore[assignment]
self.retries = retries
self.chunked = False
if "transfer-encoding" in self.headers:
tr_enc = self.headers.get("transfer-encoding", "").lower()
# Don't incur the penalty of creating a list and then discarding it
encodings = (enc.strip() for enc in tr_enc.split(","))
if "chunked" in encodings:
self.chunked = True
self._decoder: ContentDecoder | None = None
self.enforce_content_length = enforce_content_length
self.auto_close = auto_close
self._body = None
self._fp: AsyncLowLevelResponse | typing.IO[typing.Any] | None = None # type: ignore[assignment]
self._original_response = original_response # type: ignore[assignment]
self._fp_bytes_read = 0
if msg is not None:
warnings.warn(
"Passing msg=.. is deprecated and no-op in urllib3.future and is scheduled to be removed in a future major.",
DeprecationWarning,
stacklevel=2,
)
self.msg = msg
if body and isinstance(body, (str, bytes)):
self._body = body
self._pool: AsyncHTTPConnectionPool = pool # type: ignore[assignment]
self._connection: AsyncHTTPConnection = connection # type: ignore[assignment]
if hasattr(body, "read"):
self._fp = body # type: ignore[assignment]
# Are we using the chunked-style of transfer encoding?
self.chunk_left: int | None = None
# Determine length of response
self._request_method: str | None = request_method
self.length_remaining: int | None = self._init_length(self._request_method)
# Used to return the correct amount of bytes for partial read()s
self._decoded_buffer = BytesQueueBuffer()
self._police_officer: AsyncTrafficPolice[AsyncHTTPConnection] | None = (
police_officer # type: ignore[assignment]
)
self._preloaded_content: bool = preload_content
if self._police_officer is not None:
self._police_officer.memorize(self, self._connection)
# we can utilize a ConnectionPool without level-0 PoolManager!
if self._police_officer.parent is not None:
self._police_officer.parent.memorize(self, self._pool)
async def readinto(self, b: bytearray) -> int: # type: ignore[override]
temp = await self.read(len(b))
if len(temp) == 0:
return 0
else:
b[: len(temp)] = temp
return len(temp)
@asynccontextmanager
async def _error_catcher(self) -> typing.AsyncGenerator[None, None]: # type: ignore[override]
"""
Catch low-level python exceptions, instead re-raising urllib3
variants, so that low-level exceptions are not leaked in the
high-level api.
On exit, release the connection back to the pool.
"""
clean_exit = False
try:
try:
yield
except SocketTimeout as e:
# FIXME: Ideally we'd like to include the url in the ReadTimeoutError but
# there is yet no clean way to get at it from this context.
raise ReadTimeoutError(self._pool, None, "Read timed out.") from e # type: ignore[arg-type]
except BaseSSLError as e:
# FIXME: Is there a better way to differentiate between SSLErrors?
if "read operation timed out" not in str(e):
# SSL errors related to framing/MAC get wrapped and reraised here
raise SSLError(e) from e
raise ReadTimeoutError(self._pool, None, "Read timed out.") from e # type: ignore[arg-type]
except (OSError, MustRedialError) as e:
# This includes IncompleteRead.
raise ProtocolError(f"Connection broken: {e!r}", e) from e
# If no exception is thrown, we should avoid cleaning up
# unnecessarily.
clean_exit = True
finally:
# If we didn't terminate cleanly, we need to throw away our
# connection.
if not clean_exit:
# The response may not be closed but we're not going to use it
# anymore so close it now to ensure that the connection is
# released back to the pool.
if self._original_response:
self._original_response.close()
# Closing the response may not actually be sufficient to close
# everything, so if we have a hold of the connection close that
# too.
if self._connection:
await self._connection.close()
# If we hold the original response but it's closed now, we should
# return the connection back to the pool.
if self._original_response and self._original_response.isclosed():
self.release_conn()
async def drain_conn(self) -> None: # type: ignore[override]
"""
Read and discard any remaining HTTP response data in the response connection.
Unread data in the HTTPResponse connection blocks the connection from being released back to the pool.
"""
try:
await self.read(
# Do not spend resources decoding the content unless
# decoding has already been initiated.
decode_content=self._has_decoded_content,
)
except (HTTPError, OSError, BaseSSLError):
pass
@property
def trailers(self) -> HTTPHeaderDict | None:
"""
Retrieve post-response (trailing headers) if any.
This WILL return None if no HTTP Trailer Headers have been received.
"""
if self._fp is None:
return None
if hasattr(self._fp, "trailers"):
return self._fp.trailers
return None
@property
def extension(self) -> AsyncExtensionFromHTTP | None: # type: ignore[override]
return self._extension
async def start_extension(self, item: AsyncExtensionFromHTTP) -> None: # type: ignore[override]
if self._extension is not None:
raise OSError("extension already plugged in")
if not hasattr(self._fp, "_dsa"):
raise ResponseNotReady()
await item.start(self)
self._extension = item
async def json(self) -> typing.Any:
"""
Parses the body of the HTTP response as JSON.
To use a custom JSON decoder pass the result of :attr:`HTTPResponse.data` to the decoder.
This method can raise either `UnicodeDecodeError` or `json.JSONDecodeError`.
Read more :ref:`here <json>`.
"""
data = (await self.data).decode("utf-8")
return _json.loads(data)
@property
async def data(self) -> bytes: # type: ignore[override]
# For backwards-compat with earlier urllib3 0.4 and earlier.
if self._body:
return self._body # type: ignore[return-value]
if self._fp:
return await self.read(cache_content=True)
return None # type: ignore[return-value]
async def _fp_read(self, amt: int | None = None) -> bytes: # type: ignore[override]
"""
Read a response with the thought that reading the number of bytes
larger than can fit in a 32-bit int at a time via SSL in some
known cases leads to an overflow error that has to be prevented
if `amt` or `self.length_remaining` indicate that a problem may
happen.
The known cases:
* 3.8 <= CPython < 3.9.7 because of a bug
https://github.com/urllib3/urllib3/issues/2513#issuecomment-1152559900.
* urllib3 injected with pyOpenSSL-backed SSL-support.
* CPython < 3.10 only when `amt` does not fit 32-bit int.
"""
assert self._fp
c_int_max = 2**31 - 1
if (
(amt and amt > c_int_max)
or (self.length_remaining and self.length_remaining > c_int_max)
) and sys.version_info < (3, 10):
buffer = io.BytesIO()
# Besides `max_chunk_amt` being a maximum chunk size, it
# affects memory overhead of reading a response by this
# method in CPython.
# `c_int_max` equal to 2 GiB - 1 byte is the actual maximum
# chunk size that does not lead to an overflow error, but
# 256 MiB is a compromise.
max_chunk_amt = 2**28
while amt is None or amt != 0:
if amt is not None:
chunk_amt = min(amt, max_chunk_amt)
amt -= chunk_amt
else:
chunk_amt = max_chunk_amt
try:
if isinstance(self._fp, AsyncLowLevelResponse):
data = await self._fp.read(chunk_amt)
else:
data = self._fp.read(chunk_amt) # type: ignore[attr-defined]
except ValueError: # Defensive: overly protective
break # Defensive: can also be an indicator that read ended, should not happen.
if not data:
break
buffer.write(data)
del data # to reduce peak memory usage by `max_chunk_amt`.
return buffer.getvalue()
else:
# StringIO doesn't like amt=None
if isinstance(self._fp, AsyncLowLevelResponse):
return await self._fp.read(amt)
return self._fp.read(amt) if amt is not None else self._fp.read() # type: ignore[no-any-return]
async def _raw_read( # type: ignore[override]
self,
amt: int | None = None,
) -> bytes:
"""
Reads `amt` of bytes from the socket.
"""
if self._fp is None:
return None # type: ignore[return-value]
fp_closed = getattr(self._fp, "closed", False)
async with self._error_catcher():
data = (await self._fp_read(amt)) if not fp_closed else b""
# Mocking library often use io.BytesIO
# which does not auto-close when reading data
# with amt=None.
is_foreign_fp_unclosed = (
amt is None and getattr(self._fp, "closed", False) is False
)
if (amt is not None and amt != 0 and not data) or is_foreign_fp_unclosed:
if is_foreign_fp_unclosed:
self._fp_bytes_read += len(data)
if self.length_remaining is not None:
self.length_remaining -= len(data)
# Platform-specific: Buggy versions of Python.
# Close the connection when no data is returned
#
# This is redundant to what httplib/http.client _should_
# already do. However, versions of python released before
# December 15, 2012 (http://bugs.python.org/issue16298) do
# not properly close the connection in all cases. There is
# no harm in redundantly calling close.
self._fp.close()
if (
self.enforce_content_length
and self.length_remaining is not None
and self.length_remaining != 0
):
# This is an edge case that httplib failed to cover due
# to concerns of backward compatibility. We're
# addressing it here to make sure IncompleteRead is
# raised during streaming, so all calls with incorrect
# Content-Length are caught.
raise IncompleteRead(self._fp_bytes_read, self.length_remaining)
if data and not is_foreign_fp_unclosed:
self._fp_bytes_read += len(data)
if self.length_remaining is not None:
self.length_remaining -= len(data)
return data
async def read1( # type: ignore[override]
self,
amt: int | None = None,
decode_content: bool | None = None,
) -> bytes:
"""
Similar to ``http.client.HTTPResponse.read1`` and documented
in :meth:`io.BufferedReader.read1`, but with an additional parameter:
``decode_content``.
:param amt:
How much of the content to read.
:param decode_content:
If True, will attempt to decode the body based on the
'content-encoding' header.
"""
data = await self.read(
amt=amt or -1,
decode_content=decode_content,
)
if amt is not None and len(data) > amt:
self._decoded_buffer.put(data)
return self._decoded_buffer.get(amt)
return data
async def read( # type: ignore[override]
self,
amt: int | None = None,
decode_content: bool | None = None,
cache_content: bool = False,
) -> bytes:
try:
self._init_decoder()
if decode_content is None:
decode_content = self.decode_content
if amt is not None:
cache_content = False
if amt < 0 and len(self._decoded_buffer):
return self._decoded_buffer.get(len(self._decoded_buffer))
if 0 < amt <= len(self._decoded_buffer):
return self._decoded_buffer.get(amt)
if self._police_officer is not None:
async with self._police_officer.borrow(self):
data = await self._raw_read(amt)
else:
data = await self._raw_read(amt)
if amt and amt < 0:
amt = len(data)
flush_decoder = False
if amt is None:
flush_decoder = True
elif amt != 0 and not data:
flush_decoder = True
if not data and len(self._decoded_buffer) == 0:
return data
if amt is None:
data = self._decode(data, decode_content, flush_decoder)
if cache_content:
self._body = data
else:
# do not waste memory on buffer when not decoding
if not decode_content:
if self._has_decoded_content:
raise RuntimeError(
"Calling read(decode_content=False) is not supported after "
"read(decode_content=True) was called."
)
return data
decoded_data = self._decode(data, decode_content, flush_decoder)
self._decoded_buffer.put(decoded_data)
while len(self._decoded_buffer) < amt and data:
# TODO make sure to initially read enough data to get past the headers
# For example, the GZ file header takes 10 bytes, we don't want to read
# it one byte at a time
if self._police_officer is not None:
async with self._police_officer.borrow(self):
data = await self._raw_read(amt)
else:
data = await self._raw_read(amt)
decoded_data = self._decode(data, decode_content, flush_decoder)
self._decoded_buffer.put(decoded_data)
data = self._decoded_buffer.get(amt)
return data
finally:
if (
self._fp
and hasattr(self._fp, "_eot")
and self._fp._eot
and self._police_officer is not None
):
# an HTTP extension could be live, we don't want to accidentally kill it!
if (
not hasattr(self._fp, "_dsa")
or self._fp._dsa is None
or self._fp._dsa.closed is True
):
self._police_officer.forget(self)
self._police_officer = None
async def stream( # type: ignore[override]
self, amt: int | None = 2**16, decode_content: bool | None = None
) -> typing.AsyncGenerator[bytes, None]:
if self._fp is None:
return
while not is_fp_closed(self._fp) or len(self._decoded_buffer) > 0:
data = await self.read(amt=amt, decode_content=decode_content)
if data:
yield data
async def close(self) -> None: # type: ignore[override]
if self.extension is not None and self.extension.closed is False:
await self.extension.close()
if not self.closed and self._fp:
self._fp.close()
if self._connection:
await self._connection.close()
if not self.auto_close:
io.IOBase.close(self)
async def __aiter__(self) -> typing.AsyncIterator[bytes]:
buffer: list[bytes] = []
async for chunk in self.stream(-1, decode_content=True):
if b"\n" in chunk:
chunks = chunk.split(b"\n")
yield b"".join(buffer) + chunks[0] + b"\n"
for x in chunks[1:-1]:
yield x + b"\n"
if chunks[-1]:
buffer = [chunks[-1]]
else:
buffer = []
else:
buffer.append(chunk)
if buffer:
yield b"".join(buffer)
def __del__(self) -> None:
if not self.closed:
if not self.closed and self._fp:
self._fp.close()
if not self.auto_close:
io.IOBase.close(self)