channel-app 0.0.156__py3-none-any.whl → 0.0.157a2__py3-none-any.whl

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.
@@ -1,3 +1,4 @@
1
+ import uuid
1
2
  from dataclasses import asdict
2
3
  from typing import List, Generator, Union
3
4
 
@@ -19,6 +20,7 @@ from channel_app.core.data import (BatchRequestResponseDto,
19
20
  CancelOrderDto,
20
21
  ChannelUpdateOrderItemDto)
21
22
  from channel_app.core.settings import OmnitronIntegration, ChannelIntegration
23
+ from channel_app.logs.services import LogService
22
24
  from channel_app.omnitron.batch_request import ClientBatchRequest
23
25
  from channel_app.omnitron.constants import (BatchRequestStatus, ContentType,
24
26
  FailedReasonType)
@@ -33,48 +35,87 @@ class OrderService(object):
33
35
  batch_service = ClientBatchRequest
34
36
 
35
37
  def fetch_and_create_order(self, is_success_log=True):
36
- with OmnitronIntegration(
37
- content_type=ContentType.order.value) as omnitron_integration:
38
- get_orders = ChannelIntegration().do_action(
39
- key='get_orders',
40
- batch_request=omnitron_integration.batch_request
41
- )
42
-
43
- get_orders: Generator
44
- order_batch_objects = []
45
- while True:
46
- try:
47
- channel_create_order, report_list, _ = next(get_orders)
48
- except StopIteration:
49
- break
50
-
51
- # tips
52
- channel_create_order: ChannelCreateOrderDto
53
- report_list: List[ErrorReportDto]
54
- for report in report_list:
55
- if is_success_log or not report.is_ok:
56
- report.error_code = \
57
- f"{omnitron_integration.batch_request.local_batch_id}" \
58
- f"-Channel-GetOrders_{channel_create_order.order.number}"
59
- omnitron_integration.do_action(
60
- key='create_error_report',
61
- objects=report)
38
+ log_service = LogService()
39
+ tx_id = uuid.uuid4()
40
+ log_service.create_flow(
41
+ name="OrderSync",
42
+ transaction_id=tx_id,
43
+ )
62
44
 
63
- order = self.create_order(omnitron_integration=omnitron_integration,
64
- channel_order=channel_create_order)
65
- if order and omnitron_integration.batch_request.objects:
66
- order_batch_objects.extend(omnitron_integration.batch_request.objects)
67
-
68
- omnitron_integration.batch_request.objects = order_batch_objects
69
- try:
70
- self.batch_service(settings.OMNITRON_CHANNEL_ID).to_done(
71
- batch_request=omnitron_integration.batch_request
72
- )
73
- except requests_exceptions.HTTPError as exc:
74
- if exc.response.status_code == 406 and "batch_request_status_100_1" in exc.response.text:
75
- pass
76
- else:
77
- raise exc
45
+ try:
46
+ with log_service.step("fetch_orders"):
47
+ with OmnitronIntegration(
48
+ content_type=ContentType.order.value
49
+ ) as omnitron_integration:
50
+
51
+ with log_service.step("get_orders"):
52
+ get_orders = ChannelIntegration().do_action(
53
+ key='get_orders',
54
+ batch_request=omnitron_integration.batch_request
55
+ )
56
+
57
+ get_orders: Generator
58
+ order_batch_objects = []
59
+ while True:
60
+ try:
61
+ channel_create_order, report_list, _ = next(get_orders)
62
+ except StopIteration:
63
+ break
64
+
65
+ # tips
66
+ channel_create_order: ChannelCreateOrderDto
67
+ metadata = {
68
+ "order_number": channel_create_order.order.number
69
+ }
70
+
71
+ report_list: List[ErrorReportDto]
72
+ for report in report_list:
73
+ if is_success_log or not report.is_ok:
74
+ report.error_code = \
75
+ f"{omnitron_integration.batch_request.local_batch_id}" \
76
+ f"-Channel-GetOrders_{channel_create_order.order.number}"
77
+ try:
78
+
79
+ with log_service.step("create_error_report", metadata=metadata):
80
+ omnitron_integration.do_action(
81
+ key='create_error_report',
82
+ objects=report
83
+ )
84
+ except Exception as err:
85
+ log_service.add_exception(err)
86
+ raise
87
+
88
+ try:
89
+ with log_service.step("create_order", metadata=metadata):
90
+ order = self.create_order(
91
+ omnitron_integration=omnitron_integration,
92
+ channel_order=channel_create_order
93
+ )
94
+ except Exception as err:
95
+ log_service.add_exception(err)
96
+ raise
97
+
98
+ if order and omnitron_integration.batch_request.objects:
99
+ order_batch_objects.extend(omnitron_integration.batch_request.objects)
100
+
101
+ omnitron_integration.batch_request.objects = order_batch_objects
102
+
103
+ with log_service.step("batch_to_done"):
104
+ try:
105
+ self.batch_service(settings.OMNITRON_CHANNEL_ID).to_done(
106
+ batch_request=omnitron_integration.batch_request
107
+ )
108
+ except requests_exceptions.HTTPError as exc:
109
+ log_service.add_exception(exc)
110
+ if exc.response.status_code == 406 and "batch_request_status_100_1" in exc.response.text:
111
+ pass
112
+ else:
113
+ raise exc
114
+ except Exception as fatal:
115
+ log_service.add_exception(fatal)
116
+ raise
117
+ finally:
118
+ log_service.save()
78
119
 
79
120
  def create_order(self, omnitron_integration: OmnitronIntegration,
80
121
  channel_order: ChannelCreateOrderDto
@@ -17,6 +17,7 @@ CACHE_PORT = os.getenv("CACHE_PORT")
17
17
  BROKER_HOST = os.getenv("BROKER_HOST")
18
18
  BROKER_PORT = os.getenv("BROKER_PORT")
19
19
  BROKER_DATABASE_INDEX = os.getenv("BROKER_DATABASE_INDEX")
20
+ DATABASE_URI = os.getenv("DATABASE_URI")
20
21
  SENTRY_DSN = os.getenv("SENTRY_DSN")
21
22
  DEFAULT_CONNECTION_POOL_COUNT = os.getenv("DEFAULT_CONNECTION_POOL_COUNT") or 10
22
23
  DEFAULT_CONNECTION_POOL_MAX_SIZE = os.getenv("DEFAULT_CONNECTION_POOL_COUNT") or 10
File without changes
@@ -0,0 +1,58 @@
1
+ from datetime import datetime, timezone
2
+ import uuid
3
+ from sqlalchemy import JSON, Column, DateTime, ForeignKey, Integer, String, Enum as SqlEnum, Text
4
+ from sqlalchemy.dialects.postgresql import UUID
5
+ from sqlalchemy.orm import DeclarativeBase, relationship
6
+
7
+ from channel_app.logs.enums import LogFlowAuthor, LogStepStatus
8
+
9
+
10
+ class Base(DeclarativeBase):
11
+ pass
12
+
13
+
14
+ class LogFlowModel(Base):
15
+ __tablename__ = "log_flows"
16
+
17
+ id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
18
+ transaction_id = Column(UUID(as_uuid=True), unique=True, nullable=False)
19
+ flow_name = Column(String(255), nullable=False)
20
+ flow_author = Column(SqlEnum(LogFlowAuthor), default=LogFlowAuthor.system, nullable=False)
21
+
22
+ started_at = Column(DateTime(timezone=True), nullable=False, default=lambda: datetime.now(timezone.utc))
23
+ ended_at = Column(DateTime(timezone=True), nullable=True)
24
+
25
+ status = Column(SqlEnum(LogStepStatus), nullable=True)
26
+ s3_key = Column(Text, nullable=True)
27
+
28
+ def __repr__(self):
29
+ return f"<FlowLog(transaction_id={self.transaction_id}, flow_name={self.flow_name})>"
30
+
31
+
32
+ class LogStepModel(Base):
33
+ __tablename__ = "log_steps"
34
+
35
+ id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
36
+ flow_id = Column(UUID(as_uuid=True), ForeignKey("log_flows.id", ondelete="CASCADE"), nullable=False)
37
+ step_name = Column(String(255), nullable=False)
38
+ status = Column(SqlEnum(LogStepStatus, native_enum=False), nullable=False)
39
+ start_time = Column(DateTime(timezone=True), nullable=False)
40
+ end_time = Column(DateTime(timezone=True))
41
+ duration_ms = Column(Integer)
42
+ error_message = Column(String)
43
+ step_metadata = Column(JSON)
44
+
45
+ exceptions = relationship("LogStepExceptionModel", back_populates="step", cascade="all, delete-orphan")
46
+
47
+
48
+ class LogStepExceptionModel(Base):
49
+ __tablename__ = "log_step_exceptions"
50
+
51
+ id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
52
+ step_id = Column(UUID(as_uuid=True), ForeignKey("log_steps.id", ondelete="CASCADE"), nullable=False)
53
+ type = Column(String(128), nullable=False)
54
+ message = Column(String)
55
+ traceback = Column(String)
56
+ created_at = Column(DateTime(timezone=True), nullable=False)
57
+
58
+ step = relationship("LogStepModel", back_populates="exceptions")
@@ -0,0 +1,8 @@
1
+ from sqlalchemy import create_engine
2
+ from channel_app.core import settings
3
+
4
+
5
+ class DatabaseService:
6
+ def create_engine(self):
7
+ engine = create_engine(settings.DATABASE_URI, echo=False)
8
+ return engine
File without changes
@@ -0,0 +1,22 @@
1
+ from datetime import datetime
2
+ import json
3
+ import uuid
4
+
5
+
6
+ class UUIDEncoder(json.JSONEncoder):
7
+ """
8
+ Custom JSON encoder that handles UUID and datetime objects.
9
+
10
+ This encoder extends the standard JSONEncoder to properly serialize:
11
+ - UUID objects (converted to strings)
12
+ - datetime objects (converted to ISO format)
13
+ """
14
+ def default(self, obj):
15
+ if isinstance(obj, uuid.UUID):
16
+ # Convert UUID to string
17
+ return str(obj)
18
+ elif isinstance(obj, datetime):
19
+ # Convert datetime to ISO format string
20
+ return obj.isoformat()
21
+ # Let the base class handle other types or raise TypeError
22
+ return super().default(obj)
@@ -0,0 +1,13 @@
1
+ from enum import Enum
2
+
3
+
4
+ class LogStepStatus(str, Enum):
5
+ in_progress = "IN_PROGRESS"
6
+ success = "SUCCESS"
7
+ failure = "FAILURE"
8
+
9
+
10
+ class LogFlowAuthor(str, Enum):
11
+ user = "User"
12
+ system = "System"
13
+
@@ -0,0 +1,212 @@
1
+ from contextlib import contextmanager
2
+ import json
3
+ import os
4
+ import traceback
5
+ from typing import Optional
6
+ import uuid
7
+ from datetime import datetime, timezone
8
+ import boto3
9
+ from sqlalchemy.orm import scoped_session, sessionmaker
10
+
11
+ from channel_app.database.models import (
12
+ LogFlowModel,
13
+ LogStepExceptionModel,
14
+ LogStepModel,
15
+ )
16
+ from channel_app.database.services import DatabaseService
17
+ from channel_app.logs.encoders import UUIDEncoder
18
+ from channel_app.logs.enums import LogFlowAuthor, LogStepStatus
19
+
20
+
21
+ class LogService:
22
+ database_service = DatabaseService()
23
+
24
+ def __init__(self):
25
+ self.flow = {}
26
+ self.steps = []
27
+ self.exceptions = []
28
+
29
+ self.db_engine = self.database_service.create_engine()
30
+ self.s3_client = S3Client()
31
+
32
+ def create_flow(
33
+ self,
34
+ name: str,
35
+ transaction_id: str,
36
+ flow_author: LogFlowAuthor = LogFlowAuthor.system,
37
+ ):
38
+ self.flow = {
39
+ "id": uuid.uuid4(),
40
+ "transaction_id": transaction_id or str(uuid.uuid4()),
41
+ "flow_name": name,
42
+ "flow_author": flow_author.value,
43
+ "started_at": datetime.now(timezone.utc),
44
+ }
45
+
46
+ @contextmanager
47
+ def step(self, name: str, metadata: Optional[dict] = None):
48
+ now = datetime.now(timezone.utc)
49
+ self._add_step(name, start=True, metadata=metadata)
50
+ try:
51
+ yield
52
+ self._add_step(name, end=True)
53
+ except Exception as exc:
54
+ self.add_exception(exc)
55
+ for step in reversed(self.steps):
56
+ if (
57
+ step["step_name"] == name
58
+ and step.get("status") == LogStepStatus.in_progress.value
59
+ ):
60
+ step["end_time"] = now
61
+ step["status"] = LogStepStatus.failure.value
62
+ step["error"] = str(exc)
63
+ break
64
+ raise
65
+
66
+ def _add_step(self, name, start=False, end=False, metadata=None):
67
+ now = datetime.now(timezone.utc)
68
+ if start:
69
+ self.steps.append(
70
+ {
71
+ "id": uuid.uuid4(),
72
+ "step_name": name,
73
+ "start_time": now,
74
+ "status": LogStepStatus.in_progress.value,
75
+ "metadata": metadata or {},
76
+ }
77
+ )
78
+ elif end:
79
+ for step in reversed(self.steps):
80
+ if (
81
+ step["step_name"] == name
82
+ and step["status"] == LogStepStatus.in_progress.value
83
+ ):
84
+ step["end_time"] = now
85
+ step["status"] = LogStepStatus.success.value
86
+ step["duration_ms"] = int(
87
+ (now - step["start_time"]).total_seconds() * 1000
88
+ )
89
+
90
+ def add_exception(self, exc: Exception):
91
+ tb = traceback.format_exc()
92
+ exc_obj = {
93
+ "id": uuid.uuid4(),
94
+ "type": type(exc).__name__,
95
+ "message": str(exc),
96
+ "traceback": tb,
97
+ }
98
+ self.exceptions.append(exc_obj)
99
+ # If this flow has related step, update the step to FAILURE
100
+ if self.steps:
101
+ self.steps[-1]["status"] = LogStepStatus.failure.value
102
+ self.steps[-1]["error"] = str(exc)
103
+ self.steps[-1].setdefault("exceptions", []).append(exc_obj)
104
+
105
+ def save(self):
106
+ self.flow["ended_at"] = datetime.now(timezone.utc)
107
+ full_log_content = {
108
+ **self.flow,
109
+ "steps": self.steps,
110
+ "exceptions": self.exceptions,
111
+ }
112
+ s3_key = f"logs/{self.flow['flow_name']}/{self.flow['transaction_id']}.json"
113
+
114
+ self.s3_client.upload_object(s3_key, full_log_content)
115
+
116
+ log_flow_object = LogFlowModel(
117
+ id=self.flow["id"],
118
+ transaction_id=str(self.flow["transaction_id"]),
119
+ flow_name=self.flow["flow_name"],
120
+ flow_author=self.flow["flow_author"],
121
+ started_at=self.flow["started_at"],
122
+ ended_at=self.flow["ended_at"],
123
+ status=(
124
+ self.steps[-1]["status"] if self.steps else LogStepStatus.failure.value
125
+ ),
126
+ s3_key=s3_key,
127
+ )
128
+
129
+ step_models = []
130
+ exception_models = []
131
+ for step in self.steps:
132
+ step_model = LogStepModel(
133
+ id=step["id"],
134
+ flow_id=self.flow["id"],
135
+ step_name=step["step_name"],
136
+ status=step["status"],
137
+ start_time=step["start_time"],
138
+ end_time=step.get("end_time"),
139
+ duration_ms=step.get("duration_ms"),
140
+ error_message=step.get("error"),
141
+ step_metadata=step.get("metadata"),
142
+ )
143
+ step_models.append(step_model)
144
+
145
+ for exc in step.get("exceptions", []):
146
+ exception_models.append(
147
+ LogStepExceptionModel(
148
+ id=exc["id"],
149
+ step_id=step["id"],
150
+ type=exc["type"],
151
+ message=exc["message"],
152
+ traceback=exc["traceback"],
153
+ created_at=self.flow["ended_at"],
154
+ )
155
+ )
156
+
157
+ self._save_to_db(log_flow_object, step_models, exception_models)
158
+
159
+ def _save_to_db(self, flow_obj, step_objs, exception_objs):
160
+ session = scoped_session(sessionmaker(bind=self.db_engine))
161
+ try:
162
+ session.add(flow_obj)
163
+ session.add_all(step_objs)
164
+ if exception_objs:
165
+ session.add_all(exception_objs)
166
+ session.commit()
167
+ except Exception:
168
+ session.rollback()
169
+ raise
170
+ finally:
171
+ session.close()
172
+
173
+
174
+ class S3Client:
175
+ def __init__(self):
176
+ self._validate_credentials()
177
+ self.client = boto3.client("s3")
178
+ self.bucket = os.getenv("LOGGING_S3_BUCKET", "default-bucket-name")
179
+
180
+ def _validate_credentials(self):
181
+ required_env_vars = {
182
+ "AWS_ACCESS_KEY_ID": os.getenv("AWS_ACCESS_KEY_ID"),
183
+ "AWS_SECRET_ACCESS_KEY": os.getenv("AWS_SECRET_ACCESS_KEY"),
184
+ "AWS_REGION": os.getenv("AWS_REGION"),
185
+ "LOGGING_S3_BUCKET": os.getenv("LOGGING_S3_BUCKET"),
186
+ }
187
+
188
+ missing_vars = [
189
+ name for name, value in required_env_vars.items() if value is None
190
+ ]
191
+
192
+ if missing_vars:
193
+ raise ValueError(
194
+ f"S3 Client initialization failed: missing AWS credentials: {', '.join(missing_vars)}"
195
+ )
196
+
197
+ def set_bucket(self, bucket_name: str):
198
+ self.bucket = bucket_name
199
+ return self
200
+
201
+ def upload_object(self, key: str, content: dict):
202
+ try:
203
+ body = json.dumps(content, indent=2, cls=UUIDEncoder).encode("utf-8")
204
+ self.client.put_object(
205
+ Bucket=self.bucket,
206
+ Key=key,
207
+ Body=body,
208
+ ContentType="application/json",
209
+ )
210
+ except Exception as e:
211
+ print(f"[S3 Upload Error] {e}")
212
+ raise
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: channel-app
3
- Version: 0.0.156
3
+ Version: 0.0.157a2
4
4
  Summary: Channel app for Sales Channels
5
5
  Home-page: https://github.com/akinon/channel_app
6
6
  Author: akinonteam
@@ -1,7 +1,7 @@
1
1
  channel_app/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
2
2
  channel_app/app/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
3
3
  channel_app/app/order/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
4
- channel_app/app/order/service.py,sha256=DXxMNqC8ltz582m7stlwXX_yRoCp3NzrkVlzpBGGPA4,20454
4
+ channel_app/app/order/service.py,sha256=GZ8StGwvICtI0DWMtYFq1mB9GPdojNpjCh9oVrHlHJE,22366
5
5
  channel_app/app/product/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
6
6
  channel_app/app/product/service.py,sha256=7DZF-Vtoaf5eKT1m_ccEOAqUxWSO7Csop4HEtJmcrvw,10646
7
7
  channel_app/app/product_image/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
@@ -29,9 +29,16 @@ channel_app/core/commands.py,sha256=wM0ZlH_GHaYLKy2SWab_aKuZCsSahUuHeBa_tqi3W4A,
29
29
  channel_app/core/data.py,sha256=SlsXB0MW0epC2nrM0uEnaCBYK3Nz0kXFXZ1n4t8iomg,6931
30
30
  channel_app/core/integration.py,sha256=OqpN8B3KBLsjjrbZXZaNVF6NtObejh7P_7kGFj1xU3o,2817
31
31
  channel_app/core/products.py,sha256=uInjFw-vze1XP8vWEeq4VWDZVQQIiatoe1YsQ6n_H5E,2092
32
- channel_app/core/settings.py,sha256=1KF8PhbjEqyZOWese9EFA_DnaSA1eVcscfShVIroTRo,1247
32
+ channel_app/core/settings.py,sha256=ZkEiumBmRSBXd4W7RdyHrTwj7Un0Ynktab1zr8N_86I,1288
33
33
  channel_app/core/tests.py,sha256=ucgnLyb3D8H2JvjjH6icdRZzZQoMFbnlnFLylhoJ0Js,434
34
34
  channel_app/core/utilities.py,sha256=3iSU4RHFSsdTWBfUYBK23CRGtAIC-nYIBIJLm0Dlx3o,4168
35
+ channel_app/database/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
36
+ channel_app/database/models.py,sha256=cdjLcfJe-OYZzk1fl3JL6ght8jryRYLwMF2uV3srM-o,2314
37
+ channel_app/database/services.py,sha256=0zHLAcJAKRU6hKEaS9DmsX_2gIE29hh__DfHHx3JuSE,216
38
+ channel_app/logs/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
39
+ channel_app/logs/encoders.py,sha256=6CVgtkV7DrjxGpNXCJgT9bn9B2Ep0lHgtm-0ES7A57I,703
40
+ channel_app/logs/enums.py,sha256=If6ZjwRTerbJypYI8WjdsleHR7FjlV-TP2nBppFVEc4,214
41
+ channel_app/logs/services.py,sha256=O488OStOe9C-2AIU34bjHEBB3_XywgGlLEn77JJHUu4,7173
35
42
  channel_app/omnitron/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
36
43
  channel_app/omnitron/batch_request.py,sha256=S8IHtbI1RtVLbnOwtfXSmkrREGp8wUYW2E-eu5omwyY,3550
37
44
  channel_app/omnitron/constants.py,sha256=WZR6k_k2zZfN7lfi1ZLv1PphsHIq_LiZgw6Nd6LduvE,2793
@@ -58,7 +65,7 @@ channel_app/omnitron/commands/tests/test_product_images.py,sha256=y6tmiJ00kd2GTq
58
65
  channel_app/omnitron/commands/tests/test_product_prices.py,sha256=5HPX9PmjGw6gk3oNrwtWLqdrOkfeNx1mYP-pYwOesZU,3496
59
66
  channel_app/omnitron/commands/tests/test_product_stocks.py,sha256=q4RGlrCNUUJyN5CBL1fzrvdd4Q3xt816mbMRQT0XEd0,3496
60
67
  channel_app/omnitron/commands/tests/test_products.py,sha256=uj5KLaubY3XNu0hidOH-u-Djfboe81Hj7-lP--01Le0,103494
61
- channel_app-0.0.156.dist-info/METADATA,sha256=-IM5ZVIX0v5FLJW6rtRKJjppI4poy0ad9_fde-O4emc,309
62
- channel_app-0.0.156.dist-info/WHEEL,sha256=eOLhNAGa2EW3wWl_TU484h7q1UNgy0JXjjoqKoxAAQc,92
63
- channel_app-0.0.156.dist-info/top_level.txt,sha256=JT-gM6L5Cwxr1xEoN7NHrREDs-d6iGFGfRnK-NrJ3tU,12
64
- channel_app-0.0.156.dist-info/RECORD,,
68
+ channel_app-0.0.157a2.dist-info/METADATA,sha256=qyT2b6_Y3MwOreXTMrwfW3D113VS133PS5sAKy711OI,311
69
+ channel_app-0.0.157a2.dist-info/WHEEL,sha256=eOLhNAGa2EW3wWl_TU484h7q1UNgy0JXjjoqKoxAAQc,92
70
+ channel_app-0.0.157a2.dist-info/top_level.txt,sha256=JT-gM6L5Cwxr1xEoN7NHrREDs-d6iGFGfRnK-NrJ3tU,12
71
+ channel_app-0.0.157a2.dist-info/RECORD,,