Spaces:
Running
Running
Update space.py
Browse files
space.py
CHANGED
@@ -1,43 +1,3 @@
|
|
1 |
-
|
2 |
-
import gradio as gr
|
3 |
-
from app import demo as app
|
4 |
-
import os
|
5 |
-
|
6 |
-
_docs = {'LogsView': {'description': 'Creates a component to visualize logs from a subprocess in real-time.', 'members': {'__init__': {'value': {'type': 'str | Callable | tuple[str] | None', 'default': 'None', 'description': 'Default value to show in the code editor. If callable, the function will be called whenever the app loads to set the initial value of the component.'}, 'every': {'type': 'float | None', 'default': 'None', 'description': "If `value` is a callable, run the function 'every' number of seconds while the client connection is open. Has no effect otherwise. The event can be accessed (e.g. to cancel it) via this component's .load_event attribute."}, 'lines': {'type': 'int', 'default': '5', 'description': None}, 'label': {'type': 'str | None', 'default': 'None', 'description': 'The label for this component. Appears above the component and is also used as the header if there are a table of examples for this component. If None and used in a `gr.Interface`, the label will be the name of the parameter this component is assigned to.'}, 'show_label': {'type': 'bool | None', 'default': 'None', 'description': 'if True, will display label.'}, 'container': {'type': 'bool', 'default': 'True', 'description': 'If True, will place the component in a container - providing some extra padding around the border.'}, 'scale': {'type': 'int | None', 'default': 'None', 'description': 'relative size compared to adjacent Components. For example if Components A and B are in a Row, and A has scale=2, and B has scale=1, A will be twice as wide as B. Should be an integer. scale applies in Rows, and to top-level Components in Blocks where fill_height=True.'}, 'min_width': {'type': 'int', 'default': '160', 'description': 'minimum pixel width, will wrap if not sufficient screen space to satisfy this value. If a certain scale value results in this Component being narrower than min_width, the min_width parameter will be respected first.'}, 'visible': {'type': 'bool', 'default': 'True', 'description': 'If False, component will be hidden.'}, 'elem_id': {'type': 'str | None', 'default': 'None', 'description': 'An optional string that is assigned as the id of this component in the HTML DOM. Can be used for targeting CSS styles.'}, 'elem_classes': {'type': 'list[str] | str | None', 'default': 'None', 'description': 'An optional list of strings that are assigned as the classes of this component in the HTML DOM. Can be used for targeting CSS styles.'}, 'render': {'type': 'bool', 'default': 'True', 'description': 'If False, component will not render be rendered in the Blocks context. Should be used if the intention is to assign event listeners now but render the component later.'}}, 'postprocess': {'value': {'type': 'list[Log]', 'description': 'Expects a list of `Log` logs.'}}, 'preprocess': {'return': {'type': 'LogsView', 'description': 'Passes the code entered as a `str`.'}, 'value': None}}, 'events': {'change': {'type': None, 'default': None, 'description': 'Triggered when the value of the LogsView changes either because of user input (e.g. a user types in a textbox) OR because of a function update (e.g. an image receives a value from the output of an event trigger). See `.input()` for a listener that is only triggered by user input.'}, 'input': {'type': None, 'default': None, 'description': 'This listener is triggered when the user changes the value of the LogsView.'}, 'focus': {'type': None, 'default': None, 'description': 'This listener is triggered when the LogsView is focused.'}, 'blur': {'type': None, 'default': None, 'description': 'This listener is triggered when the LogsView is unfocused/blurred.'}}}, '__meta__': {'additional_interfaces': {'Log': {'source': '@dataclass\nclass Log:\n level: Literal[\n "INFO", "DEBUG", "WARNING", "ERROR", "CRITICAL"\n ]\n message: str\n timestamp: str'}, 'LogsView': {'source': 'class LogsView(Component):\n EVENTS = [\n Events.change,\n Events.input,\n Events.focus,\n Events.blur,\n ]\n\n def __init__(\n self,\n value: str | Callable | tuple[str] | None = None,\n *,\n every: float | None = None,\n lines: int = 5,\n label: str | None = None,\n show_label: bool | None = None,\n container: bool = True,\n scale: int | None = None,\n min_width: int = 160,\n visible: bool = True,\n elem_id: str | None = None,\n elem_classes: list[str] | str | None = None,\n render: bool = True,\n ):\n self.language = "shell"\n self.lines = lines\n self.interactive = False\n super().__init__(\n label=label,\n every=every,\n show_label=show_label,\n container=container,\n scale=scale,\n min_width=min_width,\n visible=visible,\n elem_id=elem_id,\n elem_classes=elem_classes,\n render=render,\n value=value,\n )\n\n def preprocess(self, payload: str | None) -> "LogsView":\n raise NotImplementedError(\n "LogsView cannot be used as an input component."\n )\n\n def postprocess(self, value: List[Log]) -> List[Log]:\n return value\n\n def api_info(self) -> dict[str, Any]:\n return {\n "items": {\n "level": "string",\n "message": "string",\n "timestamp": "number",\n },\n "title": "Logs",\n "type": "array",\n }\n\n def example_payload(self) -> Any:\n return [\n Log(\n "INFO",\n "Hello World",\n datetime.now().isoformat(),\n )\n ]\n\n def example_value(self) -> Any:\n return [\n Log(\n "INFO",\n "Hello World",\n datetime.now().isoformat(),\n )\n ]\n\n @classmethod\n def run_process(\n cls,\n command: List[str],\n date_format: str = "%Y-%m-%d %H:%M:%S",\n ) -> Generator[List[Log], None, None]:\n process = subprocess.Popen(\n command,\n stdout=subprocess.PIPE,\n stderr=subprocess.STDOUT,\n text=True,\n )\n\n if process.stdout is None:\n raise ValueError("stdout is None")\n\n logs = []\n\n def _log(level: str, message: str):\n log = Log(\n level=level,\n message=message,\n timestamp=datetime.now().strftime(\n date_format\n ),\n )\n logs.append(log)\n return logs\n\n _log("INFO", f"Running {\' \'.join(command)}")\n for line in process.stdout:\n yield _log("INFO", line.strip())\n\n # TODO: what if task is cancelled but process is still running?\n\n process.stdout.close()\n return_code = process.wait()\n if return_code:\n yield _log(\n "ERROR",\n f"Process exited with code {return_code}",\n )\n else:\n yield _log(\n "INFO", "Process exited successfully"\n )\n\n @classmethod\n def run_thread(\n cls,\n fn: Callable,\n log_level: int = logging.INFO,\n logger_name: str | None = None,\n date_format: str = "%Y-%m-%d %H:%M:%S",\n **kwargs,\n ) -> Generator[List[Log], None, None]:\n logs = [\n Log(\n level="INFO",\n message=f"Running {fn.__name__}({\', \'.join(f\'{k}={v}\' for k, v in kwargs.items())})",\n timestamp=datetime.now().strftime(\n date_format\n ),\n )\n ]\n yield logs\n\n thread = Thread(\n target=non_failing_fn(fn), kwargs=kwargs\n )\n\n def _log(record: logging.LogRecord) -> bool:\n if record.thread != thread.ident:\n return False # Skip if not from the thread\n if logger_name and not record.name.startswith(\n logger_name\n ):\n return False # Skip if not from the logger\n if record.levelno < log_level:\n return False # Skip if too verbose\n log = Log(\n level=record.levelname,\n message=record.getMessage(),\n timestamp=datetime.fromtimestamp(\n record.created\n ).strftime(date_format),\n )\n logs.append(log)\n return True\n\n with capture_logging(log_level) as log_queue:\n thread.start()\n\n # Loop to capture and yield logs from the thread\n while thread.is_alive():\n while True:\n try:\n if _log(log_queue.get_nowait()):\n yield logs\n except queue.Empty:\n break\n thread.join(\n timeout=0.1\n ) # adjust the timeout as needed\n\n # After the thread completes, yield any remaining logs\n while True:\n try:\n if _log(log_queue.get_nowait()):\n yield logs\n except queue.Empty:\n break\n\n logs.append(\n Log(\n level="INFO",\n message="Thread completed successfully",\n timestamp=datetime.now().strftime(\n date_format\n ),\n )\n )'}}, 'user_fn_refs': {'LogsView': ['Log', 'LogsView']}}}
|
7 |
-
|
8 |
-
abs_path = os.path.join(os.path.dirname(__file__), "css.css")
|
9 |
-
|
10 |
-
with gr.Blocks(
|
11 |
-
css=abs_path,
|
12 |
-
theme=gr.themes.Default(
|
13 |
-
font_mono=[
|
14 |
-
gr.themes.GoogleFont("Inconsolata"),
|
15 |
-
"monospace",
|
16 |
-
],
|
17 |
-
),
|
18 |
-
) as demo:
|
19 |
-
gr.Markdown(
|
20 |
-
"""
|
21 |
-
# `gradio_logsview`
|
22 |
-
|
23 |
-
<div style="display: flex; gap: 7px;">
|
24 |
-
<img alt="Static Badge" src="https://img.shields.io/badge/version%20-%200.0.1%20-%20orange">
|
25 |
-
</div>
|
26 |
-
|
27 |
-
Visualize logs in your Gradio app
|
28 |
-
""", elem_classes=["md-custom"], header_links=True)
|
29 |
-
app.render()
|
30 |
-
gr.Markdown(
|
31 |
-
"""
|
32 |
-
## Installation
|
33 |
-
|
34 |
-
```bash
|
35 |
-
pip install gradio_logsview
|
36 |
-
```
|
37 |
-
|
38 |
-
## Usage
|
39 |
-
|
40 |
-
```python
|
41 |
import logging
|
42 |
import random
|
43 |
import time
|
@@ -81,7 +41,7 @@ def fn_thread_failing():
|
|
81 |
yield from LogsView.run_thread(random_values, log_level=logging.INFO, failing=True)
|
82 |
|
83 |
|
84 |
-
markdown_top =
|
85 |
# LogsView Demo
|
86 |
|
87 |
This demo shows how to use the `LogsView` component to display logs from a process or a thread in real-time.
|
@@ -89,10 +49,10 @@ This demo shows how to use the `LogsView` component to display logs from a proce
|
|
89 |
Click on any button to launch a process or a thread and see the logs displayed in real-time.
|
90 |
In the thread example, logs are generated randomly with different log levels.
|
91 |
In the process example, logs are generated by a Python script but any command can be executed.
|
92 |
-
|
93 |
|
94 |
|
95 |
-
markdown_bottom =
|
96 |
## How to run in a thread?
|
97 |
|
98 |
With `LogsView.run_thread`, you can run a function in a separate thread and capture logs in real-time.
|
@@ -130,7 +90,7 @@ with gr.Blocks() as demo:
|
|
130 |
btn = gr.Button("Run process")
|
131 |
btn.click(fn_process, outputs=logs)
|
132 |
```
|
133 |
-
|
134 |
|
135 |
with gr.Blocks() as demo:
|
136 |
gr.Markdown(markdown_top)
|
@@ -153,304 +113,3 @@ with gr.Blocks() as demo:
|
|
153 |
|
154 |
if __name__ == "__main__":
|
155 |
demo.launch()
|
156 |
-
|
157 |
-
```
|
158 |
-
""", elem_classes=["md-custom"], header_links=True)
|
159 |
-
|
160 |
-
|
161 |
-
gr.Markdown("""
|
162 |
-
## `LogsView`
|
163 |
-
|
164 |
-
### Initialization
|
165 |
-
""", elem_classes=["md-custom"], header_links=True)
|
166 |
-
|
167 |
-
gr.ParamViewer(value=_docs["LogsView"]["members"]["__init__"], linkify=['Log', 'LogsView'])
|
168 |
-
|
169 |
-
|
170 |
-
gr.Markdown("### Events")
|
171 |
-
gr.ParamViewer(value=_docs["LogsView"]["events"], linkify=['Event'])
|
172 |
-
|
173 |
-
|
174 |
-
|
175 |
-
|
176 |
-
gr.Markdown("""
|
177 |
-
|
178 |
-
### User function
|
179 |
-
|
180 |
-
The impact on the users predict function varies depending on whether the component is used as an input or output for an event (or both).
|
181 |
-
|
182 |
-
- When used as an Input, the component only impacts the input signature of the user function.
|
183 |
-
- When used as an output, the component only impacts the return signature of the user function.
|
184 |
-
|
185 |
-
The code snippet below is accurate in cases where the component is used as both an input and an output.
|
186 |
-
|
187 |
-
- **As input:** Is passed, passes the code entered as a `str`.
|
188 |
-
- **As output:** Should return, expects a list of `Log` logs.
|
189 |
-
|
190 |
-
```python
|
191 |
-
def predict(
|
192 |
-
value: LogsView
|
193 |
-
) -> list[Log]:
|
194 |
-
return value
|
195 |
-
```
|
196 |
-
""", elem_classes=["md-custom", "LogsView-user-fn"], header_links=True)
|
197 |
-
|
198 |
-
|
199 |
-
|
200 |
-
|
201 |
-
code_Log = gr.Markdown("""
|
202 |
-
## `Log`
|
203 |
-
```python
|
204 |
-
@dataclass
|
205 |
-
class Log:
|
206 |
-
level: Literal[
|
207 |
-
"INFO", "DEBUG", "WARNING", "ERROR", "CRITICAL"
|
208 |
-
]
|
209 |
-
message: str
|
210 |
-
timestamp: str
|
211 |
-
```""", elem_classes=["md-custom", "Log"], header_links=True)
|
212 |
-
|
213 |
-
code_LogsView = gr.Markdown("""
|
214 |
-
## `LogsView`
|
215 |
-
```python
|
216 |
-
class LogsView(Component):
|
217 |
-
EVENTS = [
|
218 |
-
Events.change,
|
219 |
-
Events.input,
|
220 |
-
Events.focus,
|
221 |
-
Events.blur,
|
222 |
-
]
|
223 |
-
|
224 |
-
def __init__(
|
225 |
-
self,
|
226 |
-
value: str | Callable | tuple[str] | None = None,
|
227 |
-
*,
|
228 |
-
every: float | None = None,
|
229 |
-
lines: int = 5,
|
230 |
-
label: str | None = None,
|
231 |
-
show_label: bool | None = None,
|
232 |
-
container: bool = True,
|
233 |
-
scale: int | None = None,
|
234 |
-
min_width: int = 160,
|
235 |
-
visible: bool = True,
|
236 |
-
elem_id: str | None = None,
|
237 |
-
elem_classes: list[str] | str | None = None,
|
238 |
-
render: bool = True,
|
239 |
-
):
|
240 |
-
self.language = "shell"
|
241 |
-
self.lines = lines
|
242 |
-
self.interactive = False
|
243 |
-
super().__init__(
|
244 |
-
label=label,
|
245 |
-
every=every,
|
246 |
-
show_label=show_label,
|
247 |
-
container=container,
|
248 |
-
scale=scale,
|
249 |
-
min_width=min_width,
|
250 |
-
visible=visible,
|
251 |
-
elem_id=elem_id,
|
252 |
-
elem_classes=elem_classes,
|
253 |
-
render=render,
|
254 |
-
value=value,
|
255 |
-
)
|
256 |
-
|
257 |
-
def preprocess(self, payload: str | None) -> "LogsView":
|
258 |
-
raise NotImplementedError(
|
259 |
-
"LogsView cannot be used as an input component."
|
260 |
-
)
|
261 |
-
|
262 |
-
def postprocess(self, value: List[Log]) -> List[Log]:
|
263 |
-
return value
|
264 |
-
|
265 |
-
def api_info(self) -> dict[str, Any]:
|
266 |
-
return {
|
267 |
-
"items": {
|
268 |
-
"level": "string",
|
269 |
-
"message": "string",
|
270 |
-
"timestamp": "number",
|
271 |
-
},
|
272 |
-
"title": "Logs",
|
273 |
-
"type": "array",
|
274 |
-
}
|
275 |
-
|
276 |
-
def example_payload(self) -> Any:
|
277 |
-
return [
|
278 |
-
Log(
|
279 |
-
"INFO",
|
280 |
-
"Hello World",
|
281 |
-
datetime.now().isoformat(),
|
282 |
-
)
|
283 |
-
]
|
284 |
-
|
285 |
-
def example_value(self) -> Any:
|
286 |
-
return [
|
287 |
-
Log(
|
288 |
-
"INFO",
|
289 |
-
"Hello World",
|
290 |
-
datetime.now().isoformat(),
|
291 |
-
)
|
292 |
-
]
|
293 |
-
|
294 |
-
@classmethod
|
295 |
-
def run_process(
|
296 |
-
cls,
|
297 |
-
command: List[str],
|
298 |
-
date_format: str = "%Y-%m-%d %H:%M:%S",
|
299 |
-
) -> Generator[List[Log], None, None]:
|
300 |
-
process = subprocess.Popen(
|
301 |
-
command,
|
302 |
-
stdout=subprocess.PIPE,
|
303 |
-
stderr=subprocess.STDOUT,
|
304 |
-
text=True,
|
305 |
-
)
|
306 |
-
|
307 |
-
if process.stdout is None:
|
308 |
-
raise ValueError("stdout is None")
|
309 |
-
|
310 |
-
logs = []
|
311 |
-
|
312 |
-
def _log(level: str, message: str):
|
313 |
-
log = Log(
|
314 |
-
level=level,
|
315 |
-
message=message,
|
316 |
-
timestamp=datetime.now().strftime(
|
317 |
-
date_format
|
318 |
-
),
|
319 |
-
)
|
320 |
-
logs.append(log)
|
321 |
-
return logs
|
322 |
-
|
323 |
-
_log("INFO", f"Running {' '.join(command)}")
|
324 |
-
for line in process.stdout:
|
325 |
-
yield _log("INFO", line.strip())
|
326 |
-
|
327 |
-
# TODO: what if task is cancelled but process is still running?
|
328 |
-
|
329 |
-
process.stdout.close()
|
330 |
-
return_code = process.wait()
|
331 |
-
if return_code:
|
332 |
-
yield _log(
|
333 |
-
"ERROR",
|
334 |
-
f"Process exited with code {return_code}",
|
335 |
-
)
|
336 |
-
else:
|
337 |
-
yield _log(
|
338 |
-
"INFO", "Process exited successfully"
|
339 |
-
)
|
340 |
-
|
341 |
-
@classmethod
|
342 |
-
def run_thread(
|
343 |
-
cls,
|
344 |
-
fn: Callable,
|
345 |
-
log_level: int = logging.INFO,
|
346 |
-
logger_name: str | None = None,
|
347 |
-
date_format: str = "%Y-%m-%d %H:%M:%S",
|
348 |
-
**kwargs,
|
349 |
-
) -> Generator[List[Log], None, None]:
|
350 |
-
logs = [
|
351 |
-
Log(
|
352 |
-
level="INFO",
|
353 |
-
message=f"Running {fn.__name__}({', '.join(f'{k}={v}' for k, v in kwargs.items())})",
|
354 |
-
timestamp=datetime.now().strftime(
|
355 |
-
date_format
|
356 |
-
),
|
357 |
-
)
|
358 |
-
]
|
359 |
-
yield logs
|
360 |
-
|
361 |
-
thread = Thread(
|
362 |
-
target=non_failing_fn(fn), kwargs=kwargs
|
363 |
-
)
|
364 |
-
|
365 |
-
def _log(record: logging.LogRecord) -> bool:
|
366 |
-
if record.thread != thread.ident:
|
367 |
-
return False # Skip if not from the thread
|
368 |
-
if logger_name and not record.name.startswith(
|
369 |
-
logger_name
|
370 |
-
):
|
371 |
-
return False # Skip if not from the logger
|
372 |
-
if record.levelno < log_level:
|
373 |
-
return False # Skip if too verbose
|
374 |
-
log = Log(
|
375 |
-
level=record.levelname,
|
376 |
-
message=record.getMessage(),
|
377 |
-
timestamp=datetime.fromtimestamp(
|
378 |
-
record.created
|
379 |
-
).strftime(date_format),
|
380 |
-
)
|
381 |
-
logs.append(log)
|
382 |
-
return True
|
383 |
-
|
384 |
-
with capture_logging(log_level) as log_queue:
|
385 |
-
thread.start()
|
386 |
-
|
387 |
-
# Loop to capture and yield logs from the thread
|
388 |
-
while thread.is_alive():
|
389 |
-
while True:
|
390 |
-
try:
|
391 |
-
if _log(log_queue.get_nowait()):
|
392 |
-
yield logs
|
393 |
-
except queue.Empty:
|
394 |
-
break
|
395 |
-
thread.join(
|
396 |
-
timeout=0.1
|
397 |
-
) # adjust the timeout as needed
|
398 |
-
|
399 |
-
# After the thread completes, yield any remaining logs
|
400 |
-
while True:
|
401 |
-
try:
|
402 |
-
if _log(log_queue.get_nowait()):
|
403 |
-
yield logs
|
404 |
-
except queue.Empty:
|
405 |
-
break
|
406 |
-
|
407 |
-
logs.append(
|
408 |
-
Log(
|
409 |
-
level="INFO",
|
410 |
-
message="Thread completed successfully",
|
411 |
-
timestamp=datetime.now().strftime(
|
412 |
-
date_format
|
413 |
-
),
|
414 |
-
)
|
415 |
-
)
|
416 |
-
```""", elem_classes=["md-custom", "LogsView"], header_links=True)
|
417 |
-
|
418 |
-
demo.load(None, js=r"""function() {
|
419 |
-
const refs = {
|
420 |
-
Log: [],
|
421 |
-
LogsView: [], };
|
422 |
-
const user_fn_refs = {
|
423 |
-
LogsView: ['Log', 'LogsView'], };
|
424 |
-
requestAnimationFrame(() => {
|
425 |
-
|
426 |
-
Object.entries(user_fn_refs).forEach(([key, refs]) => {
|
427 |
-
if (refs.length > 0) {
|
428 |
-
const el = document.querySelector(`.${key}-user-fn`);
|
429 |
-
if (!el) return;
|
430 |
-
refs.forEach(ref => {
|
431 |
-
el.innerHTML = el.innerHTML.replace(
|
432 |
-
new RegExp("\\b"+ref+"\\b", "g"),
|
433 |
-
`<a href="#h-${ref.toLowerCase()}">${ref}</a>`
|
434 |
-
);
|
435 |
-
})
|
436 |
-
}
|
437 |
-
})
|
438 |
-
|
439 |
-
Object.entries(refs).forEach(([key, refs]) => {
|
440 |
-
if (refs.length > 0) {
|
441 |
-
const el = document.querySelector(`.${key}`);
|
442 |
-
if (!el) return;
|
443 |
-
refs.forEach(ref => {
|
444 |
-
el.innerHTML = el.innerHTML.replace(
|
445 |
-
new RegExp("\\b"+ref+"\\b", "g"),
|
446 |
-
`<a href="#h-${ref.toLowerCase()}">${ref}</a>`
|
447 |
-
);
|
448 |
-
})
|
449 |
-
}
|
450 |
-
})
|
451 |
-
})
|
452 |
-
}
|
453 |
-
|
454 |
-
""")
|
455 |
-
|
456 |
-
demo.launch()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
import logging
|
2 |
import random
|
3 |
import time
|
|
|
41 |
yield from LogsView.run_thread(random_values, log_level=logging.INFO, failing=True)
|
42 |
|
43 |
|
44 |
+
markdown_top = """
|
45 |
# LogsView Demo
|
46 |
|
47 |
This demo shows how to use the `LogsView` component to display logs from a process or a thread in real-time.
|
|
|
49 |
Click on any button to launch a process or a thread and see the logs displayed in real-time.
|
50 |
In the thread example, logs are generated randomly with different log levels.
|
51 |
In the process example, logs are generated by a Python script but any command can be executed.
|
52 |
+
"""
|
53 |
|
54 |
|
55 |
+
markdown_bottom = """
|
56 |
## How to run in a thread?
|
57 |
|
58 |
With `LogsView.run_thread`, you can run a function in a separate thread and capture logs in real-time.
|
|
|
90 |
btn = gr.Button("Run process")
|
91 |
btn.click(fn_process, outputs=logs)
|
92 |
```
|
93 |
+
"""
|
94 |
|
95 |
with gr.Blocks() as demo:
|
96 |
gr.Markdown(markdown_top)
|
|
|
113 |
|
114 |
if __name__ == "__main__":
|
115 |
demo.launch()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|