bizon 0.0.13__tar.gz → 0.0.14__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.
- {bizon-0.0.13 → bizon-0.0.14}/PKG-INFO +1 -1
- {bizon-0.0.13 → bizon-0.0.14}/bizon/sources/kafka/src/source.py +60 -61
- {bizon-0.0.13 → bizon-0.0.14}/pyproject.toml +1 -1
- {bizon-0.0.13 → bizon-0.0.14}/LICENSE +0 -0
- {bizon-0.0.13 → bizon-0.0.14}/README.md +0 -0
- {bizon-0.0.13 → bizon-0.0.14}/bizon/__main__.py +0 -0
- {bizon-0.0.13 → bizon-0.0.14}/bizon/cli/__init__.py +0 -0
- {bizon-0.0.13 → bizon-0.0.14}/bizon/cli/main.py +0 -0
- {bizon-0.0.13 → bizon-0.0.14}/bizon/cli/utils.py +0 -0
- {bizon-0.0.13 → bizon-0.0.14}/bizon/common/errors/backoff.py +0 -0
- {bizon-0.0.13 → bizon-0.0.14}/bizon/common/errors/errors.py +0 -0
- {bizon-0.0.13 → bizon-0.0.14}/bizon/common/models.py +0 -0
- {bizon-0.0.13 → bizon-0.0.14}/bizon/destinations/bigquery/config/bigquery.example.yml +0 -0
- {bizon-0.0.13 → bizon-0.0.14}/bizon/destinations/bigquery/src/config.py +0 -0
- {bizon-0.0.13 → bizon-0.0.14}/bizon/destinations/bigquery/src/destination.py +0 -0
- {bizon-0.0.13 → bizon-0.0.14}/bizon/destinations/bigquery_streaming/src/config.py +0 -0
- {bizon-0.0.13 → bizon-0.0.14}/bizon/destinations/bigquery_streaming/src/destination.py +0 -0
- {bizon-0.0.13 → bizon-0.0.14}/bizon/destinations/bigquery_streaming/src/proto_utils.py +0 -0
- {bizon-0.0.13 → bizon-0.0.14}/bizon/destinations/buffer.py +0 -0
- {bizon-0.0.13 → bizon-0.0.14}/bizon/destinations/config.py +0 -0
- {bizon-0.0.13 → bizon-0.0.14}/bizon/destinations/destination.py +0 -0
- {bizon-0.0.13 → bizon-0.0.14}/bizon/destinations/file/src/config.py +0 -0
- {bizon-0.0.13 → bizon-0.0.14}/bizon/destinations/file/src/destination.py +0 -0
- {bizon-0.0.13 → bizon-0.0.14}/bizon/destinations/logger/src/config.py +0 -0
- {bizon-0.0.13 → bizon-0.0.14}/bizon/destinations/logger/src/destination.py +0 -0
- {bizon-0.0.13 → bizon-0.0.14}/bizon/destinations/models.py +0 -0
- {bizon-0.0.13 → bizon-0.0.14}/bizon/engine/backend/adapters/sqlalchemy/backend.py +0 -0
- {bizon-0.0.13 → bizon-0.0.14}/bizon/engine/backend/adapters/sqlalchemy/config.py +0 -0
- {bizon-0.0.13 → bizon-0.0.14}/bizon/engine/backend/backend.py +0 -0
- {bizon-0.0.13 → bizon-0.0.14}/bizon/engine/backend/config.py +0 -0
- {bizon-0.0.13 → bizon-0.0.14}/bizon/engine/backend/models.py +0 -0
- {bizon-0.0.13 → bizon-0.0.14}/bizon/engine/config.py +0 -0
- {bizon-0.0.13 → bizon-0.0.14}/bizon/engine/engine.py +0 -0
- {bizon-0.0.13 → bizon-0.0.14}/bizon/engine/pipeline/consumer.py +0 -0
- {bizon-0.0.13 → bizon-0.0.14}/bizon/engine/pipeline/models.py +0 -0
- {bizon-0.0.13 → bizon-0.0.14}/bizon/engine/pipeline/producer.py +0 -0
- {bizon-0.0.13 → bizon-0.0.14}/bizon/engine/queue/adapters/kafka/config.py +0 -0
- {bizon-0.0.13 → bizon-0.0.14}/bizon/engine/queue/adapters/kafka/consumer.py +0 -0
- {bizon-0.0.13 → bizon-0.0.14}/bizon/engine/queue/adapters/kafka/queue.py +0 -0
- {bizon-0.0.13 → bizon-0.0.14}/bizon/engine/queue/adapters/python_queue/config.py +0 -0
- {bizon-0.0.13 → bizon-0.0.14}/bizon/engine/queue/adapters/python_queue/consumer.py +0 -0
- {bizon-0.0.13 → bizon-0.0.14}/bizon/engine/queue/adapters/python_queue/queue.py +0 -0
- {bizon-0.0.13 → bizon-0.0.14}/bizon/engine/queue/adapters/rabbitmq/config.py +0 -0
- {bizon-0.0.13 → bizon-0.0.14}/bizon/engine/queue/adapters/rabbitmq/consumer.py +0 -0
- {bizon-0.0.13 → bizon-0.0.14}/bizon/engine/queue/adapters/rabbitmq/queue.py +0 -0
- {bizon-0.0.13 → bizon-0.0.14}/bizon/engine/queue/config.py +0 -0
- {bizon-0.0.13 → bizon-0.0.14}/bizon/engine/queue/queue.py +0 -0
- {bizon-0.0.13 → bizon-0.0.14}/bizon/engine/runner/adapters/process.py +0 -0
- {bizon-0.0.13 → bizon-0.0.14}/bizon/engine/runner/adapters/thread.py +0 -0
- {bizon-0.0.13 → bizon-0.0.14}/bizon/engine/runner/config.py +0 -0
- {bizon-0.0.13 → bizon-0.0.14}/bizon/engine/runner/runner.py +0 -0
- {bizon-0.0.13 → bizon-0.0.14}/bizon/source/auth/authenticators/abstract_oauth.py +0 -0
- {bizon-0.0.13 → bizon-0.0.14}/bizon/source/auth/authenticators/abstract_token.py +0 -0
- {bizon-0.0.13 → bizon-0.0.14}/bizon/source/auth/authenticators/basic.py +0 -0
- {bizon-0.0.13 → bizon-0.0.14}/bizon/source/auth/authenticators/cookies.py +0 -0
- {bizon-0.0.13 → bizon-0.0.14}/bizon/source/auth/authenticators/oauth.py +0 -0
- {bizon-0.0.13 → bizon-0.0.14}/bizon/source/auth/authenticators/token.py +0 -0
- {bizon-0.0.13 → bizon-0.0.14}/bizon/source/auth/builder.py +0 -0
- {bizon-0.0.13 → bizon-0.0.14}/bizon/source/auth/config.py +0 -0
- {bizon-0.0.13 → bizon-0.0.14}/bizon/source/config.py +0 -0
- {bizon-0.0.13 → bizon-0.0.14}/bizon/source/cursor.py +0 -0
- {bizon-0.0.13 → bizon-0.0.14}/bizon/source/discover.py +0 -0
- {bizon-0.0.13 → bizon-0.0.14}/bizon/source/models.py +0 -0
- {bizon-0.0.13 → bizon-0.0.14}/bizon/source/session.py +0 -0
- {bizon-0.0.13 → bizon-0.0.14}/bizon/source/source.py +0 -0
- {bizon-0.0.13 → bizon-0.0.14}/bizon/sources/dummy/config/api_key.example.yml +0 -0
- {bizon-0.0.13 → bizon-0.0.14}/bizon/sources/dummy/config/api_key_kafka.example.yml +0 -0
- {bizon-0.0.13 → bizon-0.0.14}/bizon/sources/dummy/src/fake_api.py +0 -0
- {bizon-0.0.13 → bizon-0.0.14}/bizon/sources/dummy/src/source.py +0 -0
- {bizon-0.0.13 → bizon-0.0.14}/bizon/sources/dummy/tests/dummy_pipeline.py +0 -0
- {bizon-0.0.13 → bizon-0.0.14}/bizon/sources/dummy/tests/dummy_pipeline_bigquery_backend.py +0 -0
- {bizon-0.0.13 → bizon-0.0.14}/bizon/sources/dummy/tests/dummy_pipeline_kafka.py +0 -0
- {bizon-0.0.13 → bizon-0.0.14}/bizon/sources/dummy/tests/dummy_pipeline_rabbitmq.py +0 -0
- {bizon-0.0.13 → bizon-0.0.14}/bizon/sources/dummy/tests/dummy_pipeline_write_data_bigquery.py +0 -0
- {bizon-0.0.13 → bizon-0.0.14}/bizon/sources/dummy/tests/dummy_pipeline_write_data_bigquery_through_kafka.py +0 -0
- {bizon-0.0.13 → bizon-0.0.14}/bizon/sources/gsheets/config/default_auth.example.yml +0 -0
- {bizon-0.0.13 → bizon-0.0.14}/bizon/sources/gsheets/config/service_account.example.yml +0 -0
- {bizon-0.0.13 → bizon-0.0.14}/bizon/sources/gsheets/src/source.py +0 -0
- {bizon-0.0.13 → bizon-0.0.14}/bizon/sources/gsheets/tests/gsheets_pipeline.py +0 -0
- {bizon-0.0.13 → bizon-0.0.14}/bizon/sources/hubspot/config/api_key.example.yml +0 -0
- {bizon-0.0.13 → bizon-0.0.14}/bizon/sources/hubspot/config/oauth.example.yml +0 -0
- {bizon-0.0.13 → bizon-0.0.14}/bizon/sources/hubspot/src/hubspot_base.py +0 -0
- {bizon-0.0.13 → bizon-0.0.14}/bizon/sources/hubspot/src/hubspot_objects.py +0 -0
- {bizon-0.0.13 → bizon-0.0.14}/bizon/sources/hubspot/src/models/hs_object.py +0 -0
- {bizon-0.0.13 → bizon-0.0.14}/bizon/sources/hubspot/tests/hubspot_pipeline.py +0 -0
- {bizon-0.0.13 → bizon-0.0.14}/bizon/sources/kafka/config/kafka.example.yml +0 -0
- {bizon-0.0.13 → bizon-0.0.14}/bizon/sources/kafka/tests/kafka_pipeline.py +0 -0
- {bizon-0.0.13 → bizon-0.0.14}/bizon/sources/periscope/config/periscope_charts.example.yml +0 -0
- {bizon-0.0.13 → bizon-0.0.14}/bizon/sources/periscope/config/periscope_dashboards.example.yml +0 -0
- {bizon-0.0.13 → bizon-0.0.14}/bizon/sources/periscope/src/source.py +0 -0
- {bizon-0.0.13 → bizon-0.0.14}/bizon/sources/periscope/tests/periscope_pipeline_charts.py +0 -0
- {bizon-0.0.13 → bizon-0.0.14}/bizon/sources/periscope/tests/periscope_pipeline_dashboard.py +0 -0
- {bizon-0.0.13 → bizon-0.0.14}/bizon/utils.py +0 -0
|
@@ -3,16 +3,17 @@ import json
|
|
|
3
3
|
import logging
|
|
4
4
|
import struct
|
|
5
5
|
import traceback
|
|
6
|
-
from datetime import datetime
|
|
6
|
+
from datetime import datetime
|
|
7
7
|
from enum import Enum
|
|
8
8
|
from functools import lru_cache
|
|
9
|
-
from typing import Any, List, Literal, Mapping,
|
|
9
|
+
from typing import Any, List, Literal, Mapping, Tuple
|
|
10
10
|
|
|
11
11
|
import fastavro
|
|
12
12
|
from avro.schema import Schema, parse
|
|
13
|
-
from confluent_kafka import Consumer,
|
|
13
|
+
from confluent_kafka import Consumer, KafkaException, TopicPartition
|
|
14
14
|
from loguru import logger
|
|
15
15
|
from pydantic import BaseModel, Field
|
|
16
|
+
from pytz import UTC
|
|
16
17
|
from requests.exceptions import HTTPError
|
|
17
18
|
|
|
18
19
|
from bizon.source.auth.config import AuthConfig, AuthType
|
|
@@ -30,7 +31,6 @@ class ApicurioSchemaNotFound(Exception):
|
|
|
30
31
|
|
|
31
32
|
class SchemaRegistryType(str, Enum):
|
|
32
33
|
APICURIO = "apicurio"
|
|
33
|
-
CONFLUENT = "confluent"
|
|
34
34
|
|
|
35
35
|
|
|
36
36
|
class KafkaAuthConfig(AuthConfig):
|
|
@@ -47,22 +47,40 @@ class KafkaAuthConfig(AuthConfig):
|
|
|
47
47
|
schema_registry_password: str = Field(default="", description="Schema registry password")
|
|
48
48
|
|
|
49
49
|
|
|
50
|
+
def default_kafka_consumer_config():
|
|
51
|
+
return {
|
|
52
|
+
"auto.offset.reset": "earliest",
|
|
53
|
+
"enable.auto.commit": False, # Turn off auto-commit for manual offset handling
|
|
54
|
+
"session.timeout.ms": 45000,
|
|
55
|
+
"security.protocol": "SASL_SSL",
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
|
|
50
59
|
class KafkaSourceConfig(SourceConfig):
|
|
60
|
+
|
|
61
|
+
# Mandatory Kafka configuration
|
|
51
62
|
topic: str = Field(..., description="Kafka topic")
|
|
52
|
-
|
|
63
|
+
bootstrap_servers: str = Field(..., description="Kafka bootstrap servers")
|
|
64
|
+
group_id: str = Field(default="bizon", description="Kafka group id")
|
|
65
|
+
|
|
66
|
+
skip_message_empty_value: bool = Field(
|
|
67
|
+
default=True, description="Skip messages with empty value (tombstone messages)"
|
|
68
|
+
)
|
|
53
69
|
|
|
54
70
|
# Kafka consumer configuration
|
|
55
|
-
batch_size: int = Field(100, description="Kafka batch size")
|
|
56
|
-
consumer_timeout: int = Field(10, description="Kafka consumer timeout in seconds")
|
|
57
|
-
fetch_max_size: int = Field(500000000, description="Kafka fetch max size in bytes")
|
|
58
|
-
receive_message_max_size: int = Field(1000000000, description="Kafka receive message max size in bytes")
|
|
71
|
+
batch_size: int = Field(100, description="Kafka batch size, number of messages to fetch at once.")
|
|
72
|
+
consumer_timeout: int = Field(10, description="Kafka consumer timeout in seconds, before returning batch.")
|
|
59
73
|
|
|
60
|
-
|
|
74
|
+
consumer_config: Mapping[str, Any] = Field(
|
|
75
|
+
default_factory=default_kafka_consumer_config,
|
|
76
|
+
description="Kafka consumer configuration, as described in the confluent-kafka-python documentation",
|
|
77
|
+
)
|
|
61
78
|
|
|
79
|
+
# Schema ID header configuration
|
|
62
80
|
nb_bytes_schema_id: Literal[4, 8] = Field(
|
|
63
|
-
|
|
81
|
+
description="Number of bytes encode SchemaID in Kafka message. Standard is 4.",
|
|
82
|
+
default=4,
|
|
64
83
|
)
|
|
65
|
-
timestamp_ms_name: Optional[str] = Field(default="", description="Name of the timestamp field in the Avro schema")
|
|
66
84
|
|
|
67
85
|
authentication: KafkaAuthConfig = Field(..., description="Authentication configuration")
|
|
68
86
|
|
|
@@ -95,28 +113,18 @@ class KafkaSource(AbstractSource):
|
|
|
95
113
|
|
|
96
114
|
self.config: KafkaSourceConfig = config
|
|
97
115
|
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
"
|
|
102
|
-
"
|
|
103
|
-
"sasl.username": self.config.authentication.params.username,
|
|
104
|
-
"sasl.password": self.config.authentication.params.password,
|
|
105
|
-
"security.protocol": "SASL_SSL",
|
|
106
|
-
"sasl.mechanisms": "PLAIN",
|
|
107
|
-
"session.timeout.ms": 45000,
|
|
108
|
-
"auto.offset.reset": "earliest",
|
|
109
|
-
"enable.auto.commit": False, # Turn off auto-commit for manual offset handling
|
|
110
|
-
# Increase the max fetch / receive message size
|
|
111
|
-
"fetch.max.bytes": self.config.fetch_max_size,
|
|
112
|
-
"receive.message.max.bytes": self.config.receive_message_max_size,
|
|
113
|
-
}
|
|
116
|
+
# Kafka consumer configuration
|
|
117
|
+
if self.config.authentication.type == AuthType.BASIC:
|
|
118
|
+
self.config.consumer_config["sasl.mechanisms"] = "PLAIN"
|
|
119
|
+
self.config.consumer_config["sasl.username"] = self.config.authentication.params.username
|
|
120
|
+
self.config.consumer_config["sasl.password"] = self.config.authentication.params.password
|
|
114
121
|
|
|
115
|
-
#
|
|
116
|
-
self.
|
|
122
|
+
# Set the bootstrap servers and group id
|
|
123
|
+
self.config.consumer_config["group.id"] = self.config.group_id
|
|
124
|
+
self.config.consumer_config["bootstrap.servers"] = self.config.bootstrap_servers
|
|
117
125
|
|
|
118
|
-
#
|
|
119
|
-
self.
|
|
126
|
+
# Consumer instance
|
|
127
|
+
self.consumer = Consumer(self.config.consumer_config, logger=silent_logger)
|
|
120
128
|
|
|
121
129
|
@staticmethod
|
|
122
130
|
def streams() -> List[str]:
|
|
@@ -234,29 +242,21 @@ class KafkaSource(AbstractSource):
|
|
|
234
242
|
|
|
235
243
|
records = []
|
|
236
244
|
|
|
237
|
-
# Set the source timestamp to now, otherwise it will be overwritten by the message timestamp
|
|
238
|
-
source_timestamp = datetime.now(tz=timezone.utc)
|
|
239
|
-
|
|
240
245
|
for message in encoded_messages:
|
|
241
246
|
|
|
242
247
|
if message.error():
|
|
243
248
|
logger.error(
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
# Skip the message if it's too large
|
|
248
|
-
if message.error() == KafkaError.MSG_SIZE_TOO_LARGE:
|
|
249
|
-
logger.error(
|
|
250
|
-
f"Message for partition {message.partition()} and offset {message.offset()} and topic {self.config.topic} is too large."
|
|
249
|
+
(
|
|
250
|
+
f"Error while consuming message for partition {message.partition()} and offset {message.offset()}: "
|
|
251
|
+
f"{message.error()}"
|
|
251
252
|
)
|
|
252
|
-
|
|
253
|
-
# self.topic_offsets.set_partition_offset(message.partition(), message.offset() + 1)
|
|
254
|
-
|
|
253
|
+
)
|
|
255
254
|
raise KafkaException(message.error())
|
|
256
255
|
|
|
257
|
-
|
|
256
|
+
# We skip tombstone messages
|
|
257
|
+
if self.config.skip_message_empty_value and not message.value():
|
|
258
258
|
logger.debug(
|
|
259
|
-
f"Message for partition {message.partition()} and offset {message.offset()}
|
|
259
|
+
f"Message for partition {message.partition()} and offset {message.offset()} is empty, skipping."
|
|
260
260
|
)
|
|
261
261
|
continue
|
|
262
262
|
|
|
@@ -282,27 +282,26 @@ class KafkaSource(AbstractSource):
|
|
|
282
282
|
|
|
283
283
|
# Decode the message
|
|
284
284
|
try:
|
|
285
|
-
data = self.decode(message.value(), schema)
|
|
286
|
-
|
|
287
|
-
# Add the message key to the data
|
|
288
|
-
data["_bizon_message_key"] = message.key().decode("utf-8")
|
|
289
285
|
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
)
|
|
295
|
-
|
|
296
|
-
|
|
286
|
+
data = {
|
|
287
|
+
"offset": message.offset(),
|
|
288
|
+
"partition": message.partition(),
|
|
289
|
+
"timestamp": message.timestamp()[1],
|
|
290
|
+
"value": self.decode(message.value(), schema),
|
|
291
|
+
"key": message.key().decode("utf-8"),
|
|
292
|
+
}
|
|
297
293
|
|
|
298
294
|
records.append(
|
|
299
295
|
SourceRecord(
|
|
300
|
-
id=f"
|
|
301
|
-
timestamp=
|
|
296
|
+
id=f"partition_{message.partition()}_offset_{message.offset()}",
|
|
297
|
+
timestamp=datetime.fromtimestamp(message.timestamp()[1] / 1000, tz=UTC),
|
|
302
298
|
data=data,
|
|
303
299
|
)
|
|
304
300
|
)
|
|
305
301
|
|
|
302
|
+
# Update the offset for the partition
|
|
303
|
+
self.topic_offsets.set_partition_offset(message.partition(), message.offset() + 1)
|
|
304
|
+
|
|
306
305
|
except Exception as e:
|
|
307
306
|
logger.error(
|
|
308
307
|
(
|
|
@@ -337,7 +336,7 @@ class KafkaSource(AbstractSource):
|
|
|
337
336
|
|
|
338
337
|
t1 = datetime.now()
|
|
339
338
|
encoded_messages = self.consumer.consume(self.config.batch_size, timeout=self.config.consumer_timeout)
|
|
340
|
-
logger.info(f"
|
|
339
|
+
logger.info(f"Kafka consumer read : {len(encoded_messages)} messages in {datetime.now() - t1}")
|
|
341
340
|
|
|
342
341
|
records = self.parse_encoded_messages(encoded_messages)
|
|
343
342
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[tool.poetry]
|
|
2
2
|
name = "bizon"
|
|
3
|
-
version = "0.0.
|
|
3
|
+
version = "0.0.14"
|
|
4
4
|
description = "Extract and load your data reliably from API Clients with native fault-tolerant and checkpointing mechanism."
|
|
5
5
|
authors = ["Antoine Balliet <antoine.balliet@gmail.com>", "Anas El Mhamdi <anas.elmhamdi@gmail.com>"]
|
|
6
6
|
readme = "README.md"
|
|
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
|
|
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
|
|
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
|
|
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
|
{bizon-0.0.13 → bizon-0.0.14}/bizon/sources/dummy/tests/dummy_pipeline_write_data_bigquery.py
RENAMED
|
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
|
{bizon-0.0.13 → bizon-0.0.14}/bizon/sources/periscope/config/periscope_dashboards.example.yml
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|