getstream 3.3.2__tar.gz → 3.3.4__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (118) hide show
  1. {getstream-3.3.2 → getstream-3.3.4}/PKG-INFO +1 -1
  2. {getstream-3.3.2 → getstream-3.3.4}/getstream/video/rtc/encoders_patches.py +54 -8
  3. {getstream-3.3.2 → getstream-3.3.4}/getstream/video/rtc/pc.py +7 -6
  4. {getstream-3.3.2 → getstream-3.3.4}/getstream/video/rtc/reconnection.py +3 -0
  5. {getstream-3.3.2 → getstream-3.3.4}/getstream/video/rtc/signaling.py +17 -9
  6. {getstream-3.3.2 → getstream-3.3.4}/.cursor/worktrees.json +0 -0
  7. {getstream-3.3.2 → getstream-3.3.4}/.env.example +0 -0
  8. {getstream-3.3.2 → getstream-3.3.4}/.github/actions/python-uv-setup/action.yml +0 -0
  9. {getstream-3.3.2 → getstream-3.3.4}/.github/workflows/ci.yml +0 -0
  10. {getstream-3.3.2 → getstream-3.3.4}/.github/workflows/release.yml +0 -0
  11. {getstream-3.3.2 → getstream-3.3.4}/.github/workflows/run_tests.yml +0 -0
  12. {getstream-3.3.2 → getstream-3.3.4}/.github/workflows/stream-py.code-workspace +0 -0
  13. {getstream-3.3.2 → getstream-3.3.4}/.gitignore +0 -0
  14. {getstream-3.3.2 → getstream-3.3.4}/.gitmodules +0 -0
  15. {getstream-3.3.2 → getstream-3.3.4}/.pre-commit-config.yaml +0 -0
  16. {getstream-3.3.2 → getstream-3.3.4}/AGENTS.md +0 -0
  17. {getstream-3.3.2 → getstream-3.3.4}/CHANGELOG.md +0 -0
  18. {getstream-3.3.2 → getstream-3.3.4}/DEVELOPMENT.md +0 -0
  19. {getstream-3.3.2 → getstream-3.3.4}/LICENSE.md +0 -0
  20. {getstream-3.3.2 → getstream-3.3.4}/MIGRATION_v2_to_v3.md +0 -0
  21. {getstream-3.3.2 → getstream-3.3.4}/Makefile +0 -0
  22. {getstream-3.3.2 → getstream-3.3.4}/README.md +0 -0
  23. {getstream-3.3.2 → getstream-3.3.4}/dev.py +0 -0
  24. {getstream-3.3.2 → getstream-3.3.4}/docs/migration-from-stream-chat-python/01-setup-and-auth.md +0 -0
  25. {getstream-3.3.2 → getstream-3.3.4}/docs/migration-from-stream-chat-python/02-users.md +0 -0
  26. {getstream-3.3.2 → getstream-3.3.4}/docs/migration-from-stream-chat-python/03-channels.md +0 -0
  27. {getstream-3.3.2 → getstream-3.3.4}/docs/migration-from-stream-chat-python/04-messages-and-reactions.md +0 -0
  28. {getstream-3.3.2 → getstream-3.3.4}/docs/migration-from-stream-chat-python/05-moderation.md +0 -0
  29. {getstream-3.3.2 → getstream-3.3.4}/docs/migration-from-stream-chat-python/06-devices.md +0 -0
  30. {getstream-3.3.2 → getstream-3.3.4}/docs/migration-from-stream-chat-python/README.md +0 -0
  31. {getstream-3.3.2 → getstream-3.3.4}/generate.sh +0 -0
  32. {getstream-3.3.2 → getstream-3.3.4}/generate_webrtc.sh +0 -0
  33. {getstream-3.3.2 → getstream-3.3.4}/getstream/__init__.py +0 -0
  34. {getstream-3.3.2 → getstream-3.3.4}/getstream/base.py +0 -0
  35. {getstream-3.3.2 → getstream-3.3.4}/getstream/chat/__init__.py +0 -0
  36. {getstream-3.3.2 → getstream-3.3.4}/getstream/chat/async_channel.py +0 -0
  37. {getstream-3.3.2 → getstream-3.3.4}/getstream/chat/async_client.py +0 -0
  38. {getstream-3.3.2 → getstream-3.3.4}/getstream/chat/async_rest_client.py +0 -0
  39. {getstream-3.3.2 → getstream-3.3.4}/getstream/chat/channel.py +0 -0
  40. {getstream-3.3.2 → getstream-3.3.4}/getstream/chat/client.py +0 -0
  41. {getstream-3.3.2 → getstream-3.3.4}/getstream/chat/rest_client.py +0 -0
  42. {getstream-3.3.2 → getstream-3.3.4}/getstream/common/__init__.py +0 -0
  43. {getstream-3.3.2 → getstream-3.3.4}/getstream/common/async_client.py +0 -0
  44. {getstream-3.3.2 → getstream-3.3.4}/getstream/common/async_rest_client.py +0 -0
  45. {getstream-3.3.2 → getstream-3.3.4}/getstream/common/client.py +0 -0
  46. {getstream-3.3.2 → getstream-3.3.4}/getstream/common/rest_client.py +0 -0
  47. {getstream-3.3.2 → getstream-3.3.4}/getstream/common/telemetry.py +0 -0
  48. {getstream-3.3.2 → getstream-3.3.4}/getstream/config.py +0 -0
  49. {getstream-3.3.2 → getstream-3.3.4}/getstream/feeds/__init__.py +0 -0
  50. {getstream-3.3.2 → getstream-3.3.4}/getstream/feeds/client.py +0 -0
  51. {getstream-3.3.2 → getstream-3.3.4}/getstream/feeds/feeds.py +0 -0
  52. {getstream-3.3.2 → getstream-3.3.4}/getstream/feeds/rest_client.py +0 -0
  53. {getstream-3.3.2 → getstream-3.3.4}/getstream/generic.py +0 -0
  54. {getstream-3.3.2 → getstream-3.3.4}/getstream/meta.py +0 -0
  55. {getstream-3.3.2 → getstream-3.3.4}/getstream/models/__init__.py +0 -0
  56. {getstream-3.3.2 → getstream-3.3.4}/getstream/moderation/__init__.py +0 -0
  57. {getstream-3.3.2 → getstream-3.3.4}/getstream/moderation/async_client.py +0 -0
  58. {getstream-3.3.2 → getstream-3.3.4}/getstream/moderation/async_rest_client.py +0 -0
  59. {getstream-3.3.2 → getstream-3.3.4}/getstream/moderation/client.py +0 -0
  60. {getstream-3.3.2 → getstream-3.3.4}/getstream/moderation/rest_client.py +0 -0
  61. {getstream-3.3.2 → getstream-3.3.4}/getstream/rate_limit.py +0 -0
  62. {getstream-3.3.2 → getstream-3.3.4}/getstream/stream.py +0 -0
  63. {getstream-3.3.2 → getstream-3.3.4}/getstream/stream_response.py +0 -0
  64. {getstream-3.3.2 → getstream-3.3.4}/getstream/tests/test_webhook.py +0 -0
  65. {getstream-3.3.2 → getstream-3.3.4}/getstream/utils/__init__.py +0 -0
  66. {getstream-3.3.2 → getstream-3.3.4}/getstream/utils/event_emitter.py +0 -0
  67. {getstream-3.3.2 → getstream-3.3.4}/getstream/utils/retry.py +0 -0
  68. {getstream-3.3.2 → getstream-3.3.4}/getstream/version.py +0 -0
  69. {getstream-3.3.2 → getstream-3.3.4}/getstream/video/__init__.py +0 -0
  70. {getstream-3.3.2 → getstream-3.3.4}/getstream/video/async_call.py +0 -0
  71. {getstream-3.3.2 → getstream-3.3.4}/getstream/video/async_client.py +0 -0
  72. {getstream-3.3.2 → getstream-3.3.4}/getstream/video/async_rest_client.py +0 -0
  73. {getstream-3.3.2 → getstream-3.3.4}/getstream/video/call.py +0 -0
  74. {getstream-3.3.2 → getstream-3.3.4}/getstream/video/client.py +0 -0
  75. {getstream-3.3.2 → getstream-3.3.4}/getstream/video/openai.py +0 -0
  76. {getstream-3.3.2 → getstream-3.3.4}/getstream/video/rest_client.py +0 -0
  77. {getstream-3.3.2 → getstream-3.3.4}/getstream/video/rtc/README.md +0 -0
  78. {getstream-3.3.2 → getstream-3.3.4}/getstream/video/rtc/__init__.py +0 -0
  79. {getstream-3.3.2 → getstream-3.3.4}/getstream/video/rtc/audio_track.py +0 -0
  80. {getstream-3.3.2 → getstream-3.3.4}/getstream/video/rtc/connection_manager.py +0 -0
  81. {getstream-3.3.2 → getstream-3.3.4}/getstream/video/rtc/connection_utils.py +0 -0
  82. {getstream-3.3.2 → getstream-3.3.4}/getstream/video/rtc/coordinator/__init__.py +0 -0
  83. {getstream-3.3.2 → getstream-3.3.4}/getstream/video/rtc/coordinator/backoff.py +0 -0
  84. {getstream-3.3.2 → getstream-3.3.4}/getstream/video/rtc/coordinator/errors.py +0 -0
  85. {getstream-3.3.2 → getstream-3.3.4}/getstream/video/rtc/coordinator/ws.py +0 -0
  86. {getstream-3.3.2 → getstream-3.3.4}/getstream/video/rtc/coordinator_api.py +0 -0
  87. {getstream-3.3.2 → getstream-3.3.4}/getstream/video/rtc/g711.py +0 -0
  88. {getstream-3.3.2 → getstream-3.3.4}/getstream/video/rtc/location_discovery.py +0 -0
  89. {getstream-3.3.2 → getstream-3.3.4}/getstream/video/rtc/models.py +0 -0
  90. {getstream-3.3.2 → getstream-3.3.4}/getstream/video/rtc/network_monitor.py +0 -0
  91. {getstream-3.3.2 → getstream-3.3.4}/getstream/video/rtc/participants.py +0 -0
  92. {getstream-3.3.2 → getstream-3.3.4}/getstream/video/rtc/pb/__init__.py +0 -0
  93. {getstream-3.3.2 → getstream-3.3.4}/getstream/video/rtc/pb/stream/__init__.py +0 -0
  94. {getstream-3.3.2 → getstream-3.3.4}/getstream/video/rtc/pb/stream/video/__init__.py +0 -0
  95. {getstream-3.3.2 → getstream-3.3.4}/getstream/video/rtc/pb/stream/video/sfu/__init__.py +0 -0
  96. {getstream-3.3.2 → getstream-3.3.4}/getstream/video/rtc/pb/stream/video/sfu/event/__init__.py +0 -0
  97. {getstream-3.3.2 → getstream-3.3.4}/getstream/video/rtc/pb/stream/video/sfu/event/events_pb2.py +0 -0
  98. {getstream-3.3.2 → getstream-3.3.4}/getstream/video/rtc/pb/stream/video/sfu/event/events_pb2.pyi +0 -0
  99. {getstream-3.3.2 → getstream-3.3.4}/getstream/video/rtc/pb/stream/video/sfu/models/__init__.py +0 -0
  100. {getstream-3.3.2 → getstream-3.3.4}/getstream/video/rtc/pb/stream/video/sfu/models/models_pb2.py +0 -0
  101. {getstream-3.3.2 → getstream-3.3.4}/getstream/video/rtc/pb/stream/video/sfu/models/models_pb2.pyi +0 -0
  102. {getstream-3.3.2 → getstream-3.3.4}/getstream/video/rtc/pb/stream/video/sfu/signal_rpc/__init__.py +0 -0
  103. {getstream-3.3.2 → getstream-3.3.4}/getstream/video/rtc/pb/stream/video/sfu/signal_rpc/signal_pb2.py +0 -0
  104. {getstream-3.3.2 → getstream-3.3.4}/getstream/video/rtc/pb/stream/video/sfu/signal_rpc/signal_pb2.pyi +0 -0
  105. {getstream-3.3.2 → getstream-3.3.4}/getstream/video/rtc/pb/stream/video/sfu/signal_rpc/signal_twirp.py +0 -0
  106. {getstream-3.3.2 → getstream-3.3.4}/getstream/video/rtc/peer_connection.py +0 -0
  107. {getstream-3.3.2 → getstream-3.3.4}/getstream/video/rtc/recording.py +0 -0
  108. {getstream-3.3.2 → getstream-3.3.4}/getstream/video/rtc/stats_reporter.py +0 -0
  109. {getstream-3.3.2 → getstream-3.3.4}/getstream/video/rtc/stats_tracer.py +0 -0
  110. {getstream-3.3.2 → getstream-3.3.4}/getstream/video/rtc/tracer.py +0 -0
  111. {getstream-3.3.2 → getstream-3.3.4}/getstream/video/rtc/track_util.py +0 -0
  112. {getstream-3.3.2 → getstream-3.3.4}/getstream/video/rtc/tracks.py +0 -0
  113. {getstream-3.3.2 → getstream-3.3.4}/getstream/video/rtc/twirp_client_wrapper.py +0 -0
  114. {getstream-3.3.2 → getstream-3.3.4}/getstream/video/rtc/utils.py +0 -0
  115. {getstream-3.3.2 → getstream-3.3.4}/getstream/webhook.py +0 -0
  116. {getstream-3.3.2 → getstream-3.3.4}/pyproject.toml +0 -0
  117. {getstream-3.3.2 → getstream-3.3.4}/pytest.ini +0 -0
  118. {getstream-3.3.2 → getstream-3.3.4}/uv.lock +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: getstream
3
- Version: 3.3.2
3
+ Version: 3.3.4
4
4
  Summary: GetStream Python SDK - Build scalable activity feeds, chat, and video calling applications
5
5
  Author-email: sachaarbonel <sacha.arbonel@hotmail.fr>, tbarbugli <tbarbugli@gmail.com>
6
6
  License-File: LICENSE.md
@@ -1,8 +1,11 @@
1
+ import fractions
1
2
  import logging
2
3
  import os
3
4
  from typing import Optional
4
5
 
6
+ import av
5
7
  from aiortc import RTCRtpCodecParameters
8
+ from aiortc.codecs.h264 import MAX_FRAME_RATE as H264_MAX_FRAME_RATE
6
9
  from aiortc.codecs.h264 import H264Encoder
7
10
  from aiortc.codecs.vpx import Vp8Encoder
8
11
  from aiortc.rtcrtpsender import RTCEncodedFrame, RTCRtpSender
@@ -26,6 +29,11 @@ BITRATE_PATCH_DISABLED = os.getenv(
26
29
  "off",
27
30
  )
28
31
 
32
+ # libx264 preset for the StreamH264Encoder. Defaults to "ultrafast" for
33
+ # real-time CPU; can be raised (e.g. "veryfast", "medium") for better
34
+ # quality-per-bit at the cost of more CPU per frame.
35
+ STREAM_H264_PRESET = os.getenv("STREAM_PATCH_AIORTC_H264_PRESET", "ultrafast").strip()
36
+
29
37
 
30
38
  try:
31
39
  # Verify the name-mangled attributes we depend on still exist.
@@ -51,11 +59,13 @@ try:
51
59
  self._Vp8Encoder__target_bitrate = bitrate
52
60
 
53
61
  class StreamH264Encoder(H264Encoder):
54
- """H264Encoder subclass with higher bitrate bounds for Stream calls."""
62
+ """H264Encoder subclass with higher bitrate bounds and a real-time
63
+ libx264 preset for Stream calls."""
55
64
 
56
65
  def __init__(self) -> None:
57
66
  super().__init__()
58
67
  self._H264Encoder__target_bitrate = STREAM_VIDEO_DEFAULT_BITRATE
68
+ self.preset = STREAM_H264_PRESET
59
69
 
60
70
  @property
61
71
  def target_bitrate(self) -> int:
@@ -68,10 +78,45 @@ try:
68
78
  )
69
79
  self._H264Encoder__target_bitrate = bitrate
70
80
 
81
+ def _encode_frame(self, frame, force_keyframe):
82
+ # Mirror parent's invalidation policy so we own codec creation in
83
+ # both first-init AND recreation (resolution/bitrate change) paths;
84
+ # otherwise parent's `if self.codec is None` branch silently
85
+ # reverts our preset to libx264's default `medium`.
86
+ codec = self.codec
87
+ if (
88
+ codec is not None
89
+ and codec.bit_rate
90
+ and (
91
+ frame.width != codec.width
92
+ or frame.height != codec.height
93
+ or abs(self.target_bitrate - codec.bit_rate) / codec.bit_rate > 0.1
94
+ )
95
+ ):
96
+ self.buffer_data = b""
97
+ self.buffer_pts = None
98
+ self.codec = None
99
+
100
+ if self.codec is None:
101
+ self.codec = av.CodecContext.create("libx264", "w")
102
+ self.codec.width = frame.width
103
+ self.codec.height = frame.height
104
+ self.codec.bit_rate = self.target_bitrate
105
+ self.codec.pix_fmt = "yuv420p"
106
+ self.codec.framerate = fractions.Fraction(H264_MAX_FRAME_RATE, 1)
107
+ self.codec.time_base = fractions.Fraction(1, H264_MAX_FRAME_RATE)
108
+ self.codec.options = {
109
+ "level": "31",
110
+ "tune": "zerolatency",
111
+ "preset": self.preset,
112
+ }
113
+ self.codec.profile = "Baseline"
114
+ yield from super()._encode_frame(frame, force_keyframe)
115
+
71
116
  except Exception:
72
117
  logger.warning(
73
- "Failed to patch aiortc video encoder subclasses with Stream bitrate values (aiortc internals may have changed), "
74
- "falling back to default aiortc bitrates. \n"
118
+ "Failed to patch aiortc encoder subclasses with Stream values (aiortc internals may have changed), "
119
+ "falling back to default aiortc encoders. \n"
75
120
  "Set STREAM_PATCH_AIORTC_BITRATES=0 to disable patching.",
76
121
  exc_info=True,
77
122
  )
@@ -80,10 +125,11 @@ except Exception:
80
125
 
81
126
 
82
127
  def patch_sender_encoder(sender: RTCRtpSender) -> None:
83
- """Patch a video sender to use Stream's higher-bitrate encoders.
128
+ """Patch a sender to use Stream's tuned encoders for the negotiated codec.
84
129
 
85
- If anything goes wrong (e.g. aiortc internals changed), the sender
86
- is left untouched and will use the stock encoder via get_encoder().
130
+ Works for video (VP8/H264) senders. If anything
131
+ goes wrong (e.g. aiortc internals changed), the sender is left untouched
132
+ and will use the stock encoder via get_encoder().
87
133
  """
88
134
  if StreamVp8Encoder is None or StreamH264Encoder is None:
89
135
  return
@@ -105,8 +151,8 @@ def patch_sender_encoder(sender: RTCRtpSender) -> None:
105
151
  sender._next_encoded_frame = _next_with_stream_encoder # type: ignore[method-assign]
106
152
  except Exception:
107
153
  logger.warning(
108
- "Failed to patch aiortc video encoder subclasses with Stream bitrate values (aiortc internals may have changed), "
109
- "falling back to default aiortc bitrates. \n"
154
+ "Failed to patch aiortc encoder subclasses with Stream values (aiortc internals may have changed), "
155
+ "falling back to default aiortc encoders. \n"
110
156
  "Set STREAM_PATCH_AIORTC_BITRATES=0 to disable patching.",
111
157
  exc_info=True,
112
158
  )
@@ -53,10 +53,6 @@ class PublisherPeerConnection(aiortc.RTCPeerConnection):
53
53
  self._closed = False
54
54
  self._connected_event = asyncio.Event()
55
55
 
56
- for transceiver in self.getTransceivers():
57
- if transceiver.kind == "video":
58
- transceiver.setCodecPreferences(publish_codec_preferences())
59
-
60
56
  @self.on("icegatheringstatechange")
61
57
  def on_icegatheringstatechange():
62
58
  logger.info(
@@ -79,8 +75,13 @@ class PublisherPeerConnection(aiortc.RTCPeerConnection):
79
75
 
80
76
  def addTrack(self, track: MediaStreamTrack) -> RTCRtpSender:
81
77
  sender = super().addTrack(track)
82
- if track.kind == "video" and not BITRATE_PATCH_DISABLED:
83
- patch_sender_encoder(sender)
78
+ if track.kind == "video":
79
+ for transceiver in self.getTransceivers():
80
+ if transceiver.sender is sender:
81
+ transceiver.setCodecPreferences(publish_codec_preferences())
82
+ break
83
+ if not BITRATE_PATCH_DISABLED:
84
+ patch_sender_encoder(sender)
84
85
  return sender
85
86
 
86
87
  async def handle_answer(self, response):
@@ -218,6 +218,9 @@ class ReconnectionManager:
218
218
  token=self.connection_manager.join_response.credentials.token
219
219
  if self.connection_manager.join_response
220
220
  else None,
221
+ ws_url=self.connection_manager.join_response.credentials.server.ws_endpoint
222
+ if self.connection_manager.join_response
223
+ else None,
221
224
  session_id=self.connection_manager.session_id,
222
225
  )
223
226
 
@@ -68,6 +68,10 @@ class WebSocketClient(StreamAsyncIOEventEmitter):
68
68
  self.running = False
69
69
  self.closed = False
70
70
  self._connection_lost_sent = False
71
+ # Set when a `call_ended` SFU event arrives. Read by `_on_close` to
72
+ # suppress `connection_lost`, since the close frame that follows carries
73
+ # no distinguishing status code or reason.
74
+ self._call_ended = False
71
75
 
72
76
  # For ping/health check mechanism
73
77
  self.ping_thread = None
@@ -168,6 +172,9 @@ class WebSocketClient(StreamAsyncIOEventEmitter):
168
172
  event_type = event.WhichOneof("event_payload")
169
173
  logger.debug(f"WebSocket message received {event_type}")
170
174
 
175
+ if event_type and event_type == "call_ended":
176
+ self._call_ended = True
177
+
171
178
  if event.HasField("health_check_response"):
172
179
  logger.debug("received health check response")
173
180
  self.last_health_check_time = time.time()
@@ -199,29 +206,30 @@ class WebSocketClient(StreamAsyncIOEventEmitter):
199
206
 
200
207
  def _on_error(self, ws, error):
201
208
  """Handle WebSocket error."""
202
- if (
209
+ is_close_frame = (
203
210
  isinstance(error, websocket.ABNF)
204
211
  and error.opcode == websocket.ABNF.OPCODE_CLOSE
205
- ):
206
- # For some reason, websockets lib propagates closing frame as an error.
207
- # Simply log a debug here if that happens.
208
- logger.debug(f"WebSocket closed by server: {error}")
212
+ )
213
+ if is_close_frame or not self.running:
214
+ # Close-frame-as-error or post-disconnect teardown noise.
215
+ logger.debug(f"WebSocket error after disconnect: {error}")
209
216
  else:
210
217
  logger.error(f"WebSocket error: {error}")
211
218
 
212
219
  if not self.first_message_event.is_set():
213
- # Create an error event
220
+ # Mid-handshake failure: unblock `connect()` with a synthetic
221
+ # error event instead of leaving it parked on `first_message_event`.
214
222
  error_event = events_pb2.SfuEvent()
215
223
  error_event.error.error.message = str(error)
216
224
  self.first_message = error_event
217
225
  self.first_message_event.set()
218
- elif not self.closed:
226
+ elif not self.closed and not is_close_frame:
219
227
  self._notify_connection_lost(f"error: {error}")
220
228
 
221
229
  def _on_close(self, ws, close_status_code, close_msg):
222
230
  """Handle WebSocket close event."""
223
231
  logger.debug(f"WebSocket connection closed: {close_status_code} {close_msg}")
224
- was_unexpected = not self.closed
232
+ was_unexpected = not self.closed and not self._call_ended
225
233
  self.running = False
226
234
  if was_unexpected:
227
235
  self._notify_connection_lost(
@@ -248,7 +256,7 @@ class WebSocketClient(StreamAsyncIOEventEmitter):
248
256
  logger.exception("Failed to schedule connection_lost emit")
249
257
 
250
258
  def _claim_connection_lost(self) -> bool:
251
- """Return True iff this is the first connection-lost notification."""
259
+ """Return True if this is the first connection-lost notification."""
252
260
  if self._connection_lost_sent:
253
261
  return False
254
262
  self._connection_lost_sent = True
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes