pineapple-pine 0.7.0__tar.gz → 0.7.2__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.
- {pineapple_pine-0.7.0 → pineapple_pine-0.7.2}/PKG-INFO +1 -1
- {pineapple_pine-0.7.0 → pineapple_pine-0.7.2}/pine/cli/server.py +112 -18
- {pineapple_pine-0.7.0 → pineapple_pine-0.7.2}/pineapple_pine.egg-info/PKG-INFO +1 -1
- {pineapple_pine-0.7.0 → pineapple_pine-0.7.2}/pineapple_pine.egg-info/SOURCES.txt +2 -1
- {pineapple_pine-0.7.0 → pineapple_pine-0.7.2}/pyproject.toml +1 -1
- pineapple_pine-0.7.2/tests/test_server.py +196 -0
- {pineapple_pine-0.7.0 → pineapple_pine-0.7.2}/README.md +0 -0
- {pineapple_pine-0.7.0 → pineapple_pine-0.7.2}/pine/__init__.py +0 -0
- {pineapple_pine-0.7.0 → pineapple_pine-0.7.2}/pine/cancellation.py +0 -0
- {pineapple_pine-0.7.0 → pineapple_pine-0.7.2}/pine/cli/__init__.py +0 -0
- {pineapple_pine-0.7.0 → pineapple_pine-0.7.2}/pine/cli/codegen.py +0 -0
- {pineapple_pine-0.7.0 → pineapple_pine-0.7.2}/pine/cli/dag.py +0 -0
- {pineapple_pine-0.7.0 → pineapple_pine-0.7.2}/pine/cli/run.py +0 -0
- {pineapple_pine-0.7.0 → pineapple_pine-0.7.2}/pine/config.py +0 -0
- {pineapple_pine-0.7.0 → pineapple_pine-0.7.2}/pine/dag.py +0 -0
- {pineapple_pine-0.7.0 → pineapple_pine-0.7.2}/pine/engine.py +0 -0
- {pineapple_pine-0.7.0 → pineapple_pine-0.7.2}/pine/errors.py +0 -0
- {pineapple_pine-0.7.0 → pineapple_pine-0.7.2}/pine/frame.py +0 -0
- {pineapple_pine-0.7.0 → pineapple_pine-0.7.2}/pine/go_format.py +0 -0
- {pineapple_pine-0.7.0 → pineapple_pine-0.7.2}/pine/operator.py +0 -0
- {pineapple_pine-0.7.0 → pineapple_pine-0.7.2}/pine/operators/__init__.py +0 -0
- {pineapple_pine-0.7.0 → pineapple_pine-0.7.2}/pine/operators/filter_condition.py +0 -0
- {pineapple_pine-0.7.0 → pineapple_pine-0.7.2}/pine/operators/filter_paginate.py +0 -0
- {pineapple_pine-0.7.0 → pineapple_pine-0.7.2}/pine/operators/filter_truncate.py +0 -0
- {pineapple_pine-0.7.0 → pineapple_pine-0.7.2}/pine/operators/merge_dedup.py +0 -0
- {pineapple_pine-0.7.0 → pineapple_pine-0.7.2}/pine/operators/observe_log.py +0 -0
- {pineapple_pine-0.7.0 → pineapple_pine-0.7.2}/pine/operators/recall_resource.py +0 -0
- {pineapple_pine-0.7.0 → pineapple_pine-0.7.2}/pine/operators/recall_static.py +0 -0
- {pineapple_pine-0.7.0 → pineapple_pine-0.7.2}/pine/operators/reorder_shuffle.py +0 -0
- {pineapple_pine-0.7.0 → pineapple_pine-0.7.2}/pine/operators/reorder_sort.py +0 -0
- {pineapple_pine-0.7.0 → pineapple_pine-0.7.2}/pine/operators/transform_by_lua.py +0 -0
- {pineapple_pine-0.7.0 → pineapple_pine-0.7.2}/pine/operators/transform_copy.py +0 -0
- {pineapple_pine-0.7.0 → pineapple_pine-0.7.2}/pine/operators/transform_dispatch.py +0 -0
- {pineapple_pine-0.7.0 → pineapple_pine-0.7.2}/pine/operators/transform_normalize.py +0 -0
- {pineapple_pine-0.7.0 → pineapple_pine-0.7.2}/pine/operators/transform_redis_get.py +0 -0
- {pineapple_pine-0.7.0 → pineapple_pine-0.7.2}/pine/operators/transform_redis_set.py +0 -0
- {pineapple_pine-0.7.0 → pineapple_pine-0.7.2}/pine/operators/transform_remote_pineapple.py +0 -0
- {pineapple_pine-0.7.0 → pineapple_pine-0.7.2}/pine/operators/transform_resource_lookup.py +0 -0
- {pineapple_pine-0.7.0 → pineapple_pine-0.7.2}/pine/operators/transform_size.py +0 -0
- {pineapple_pine-0.7.0 → pineapple_pine-0.7.2}/pine/parallel.py +0 -0
- {pineapple_pine-0.7.0 → pineapple_pine-0.7.2}/pine/py.typed +0 -0
- {pineapple_pine-0.7.0 → pineapple_pine-0.7.2}/pine/registry.py +0 -0
- {pineapple_pine-0.7.0 → pineapple_pine-0.7.2}/pine/result.py +0 -0
- {pineapple_pine-0.7.0 → pineapple_pine-0.7.2}/pine/stats.py +0 -0
- {pineapple_pine-0.7.0 → pineapple_pine-0.7.2}/pine/visualize.py +0 -0
- {pineapple_pine-0.7.0 → pineapple_pine-0.7.2}/pineapple_pine.egg-info/dependency_links.txt +0 -0
- {pineapple_pine-0.7.0 → pineapple_pine-0.7.2}/pineapple_pine.egg-info/requires.txt +0 -0
- {pineapple_pine-0.7.0 → pineapple_pine-0.7.2}/pineapple_pine.egg-info/top_level.txt +0 -0
- {pineapple_pine-0.7.0 → pineapple_pine-0.7.2}/setup.cfg +0 -0
- {pineapple_pine-0.7.0 → pineapple_pine-0.7.2}/tests/test_bench.py +0 -0
- {pineapple_pine-0.7.0 → pineapple_pine-0.7.2}/tests/test_fixtures.py +0 -0
- {pineapple_pine-0.7.0 → pineapple_pine-0.7.2}/tests/test_fuzz.py +0 -0
- {pineapple_pine-0.7.0 → pineapple_pine-0.7.2}/tests/test_lua_pool.py +0 -0
|
@@ -7,7 +7,7 @@ import threading
|
|
|
7
7
|
import time
|
|
8
8
|
from http.server import BaseHTTPRequestHandler, HTTPServer
|
|
9
9
|
from pathlib import Path
|
|
10
|
-
from typing import Any
|
|
10
|
+
from typing import Any, Callable
|
|
11
11
|
|
|
12
12
|
from pine.engine import Engine, StaticResourceProvider
|
|
13
13
|
from pine.errors import ConfigError, RegistryError, ValidationError
|
|
@@ -15,6 +15,8 @@ from pine.go_format import go_json_marshal
|
|
|
15
15
|
|
|
16
16
|
_DEFAULT_MAX_BODY = 10 * 1024 * 1024 # 10MB
|
|
17
17
|
|
|
18
|
+
Middleware = Callable[["_PineHandler", Callable[[], None]], None]
|
|
19
|
+
|
|
18
20
|
|
|
19
21
|
class _ServerState:
|
|
20
22
|
"""Thread-safe mutable server state (engine + reload stats)."""
|
|
@@ -53,31 +55,53 @@ class _ServerState:
|
|
|
53
55
|
class _PineHandler(BaseHTTPRequestHandler):
|
|
54
56
|
state: _ServerState
|
|
55
57
|
max_body: int
|
|
58
|
+
middlewares: list[Middleware]
|
|
56
59
|
|
|
57
60
|
def log_message(self, format, *args):
|
|
58
61
|
pass
|
|
59
62
|
|
|
60
|
-
def
|
|
63
|
+
def _dispatch(self):
|
|
64
|
+
"""Route request to internal handlers or 404."""
|
|
61
65
|
path = self.path.split("?")[0]
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
66
|
+
method = self.command
|
|
67
|
+
|
|
68
|
+
if method == "GET":
|
|
69
|
+
if path == "/health":
|
|
70
|
+
self._json_response(200, {"status": "ok"})
|
|
71
|
+
elif path == "/stats":
|
|
72
|
+
self._handle_stats()
|
|
73
|
+
elif path == "/dag":
|
|
74
|
+
self._handle_dag()
|
|
75
|
+
elif path == "/execute":
|
|
76
|
+
self._method_not_allowed()
|
|
77
|
+
else:
|
|
78
|
+
self._json_response(404, {"error": "not found"})
|
|
79
|
+
elif method == "POST":
|
|
80
|
+
if path == "/execute":
|
|
81
|
+
self._handle_execute()
|
|
82
|
+
elif path in ("/health", "/stats", "/dag"):
|
|
83
|
+
self._method_not_allowed()
|
|
84
|
+
else:
|
|
85
|
+
self._json_response(404, {"error": "not found"})
|
|
70
86
|
else:
|
|
71
|
-
self.
|
|
87
|
+
self._method_not_allowed()
|
|
88
|
+
|
|
89
|
+
def _run_middleware_chain(self):
|
|
90
|
+
"""Execute middleware chain, then dispatch."""
|
|
91
|
+
chain = self.middlewares
|
|
92
|
+
|
|
93
|
+
def build_next(idx: int) -> Callable[[], None]:
|
|
94
|
+
if idx >= len(chain):
|
|
95
|
+
return self._dispatch
|
|
96
|
+
return lambda: chain[idx](self, build_next(idx + 1))
|
|
97
|
+
|
|
98
|
+
build_next(0)()
|
|
99
|
+
|
|
100
|
+
def do_GET(self):
|
|
101
|
+
self._run_middleware_chain()
|
|
72
102
|
|
|
73
103
|
def do_POST(self):
|
|
74
|
-
|
|
75
|
-
if path == "/execute":
|
|
76
|
-
self._handle_execute()
|
|
77
|
-
elif path in ("/health", "/stats", "/dag"):
|
|
78
|
-
self._method_not_allowed()
|
|
79
|
-
else:
|
|
80
|
-
self._json_response(404, {"error": "not found"})
|
|
104
|
+
self._run_middleware_chain()
|
|
81
105
|
|
|
82
106
|
def _method_not_allowed(self):
|
|
83
107
|
self._json_response(405, {"error": "method not allowed"})
|
|
@@ -263,6 +287,75 @@ def _watch_config(state: _ServerState, config_path: str, resource_provider: Any,
|
|
|
263
287
|
print(f"config reload failed: {e}", file=sys.stderr)
|
|
264
288
|
|
|
265
289
|
|
|
290
|
+
class PineServer:
|
|
291
|
+
"""Programmatic Pine server with middleware support.
|
|
292
|
+
|
|
293
|
+
Usage:
|
|
294
|
+
server = PineServer(config_path, port=9000)
|
|
295
|
+
server.add_middleware(my_middleware)
|
|
296
|
+
server.start() # blocks
|
|
297
|
+
"""
|
|
298
|
+
|
|
299
|
+
def __init__(self, config_path: str, *, port: int = 8080, host: str = "",
|
|
300
|
+
max_body: int = _DEFAULT_MAX_BODY,
|
|
301
|
+
resource_provider: Any = None):
|
|
302
|
+
from pine.operators import ensure_registered
|
|
303
|
+
ensure_registered()
|
|
304
|
+
|
|
305
|
+
self._config_path = config_path
|
|
306
|
+
self._host = host
|
|
307
|
+
self._port = port
|
|
308
|
+
self._max_body = max_body
|
|
309
|
+
self._resource_provider = resource_provider
|
|
310
|
+
self._middlewares: list[Middleware] = []
|
|
311
|
+
self._started = False
|
|
312
|
+
self._server: HTTPServer | None = None
|
|
313
|
+
self._stop_event = threading.Event()
|
|
314
|
+
|
|
315
|
+
def add_middleware(self, mw: Middleware):
|
|
316
|
+
if self._started:
|
|
317
|
+
raise RuntimeError("cannot add middleware after server has started")
|
|
318
|
+
self._middlewares.append(mw)
|
|
319
|
+
|
|
320
|
+
def start(self):
|
|
321
|
+
"""Start the server (blocking)."""
|
|
322
|
+
self._started = True
|
|
323
|
+
config_data = Path(self._config_path).read_bytes()
|
|
324
|
+
|
|
325
|
+
try:
|
|
326
|
+
engine = Engine.create(config_data, resource_provider=self._resource_provider)
|
|
327
|
+
except (ConfigError, RegistryError) as e:
|
|
328
|
+
raise RuntimeError(f"error creating engine: {e}") from e
|
|
329
|
+
|
|
330
|
+
state = _ServerState(engine)
|
|
331
|
+
|
|
332
|
+
handler = type("Handler", (_PineHandler,), {
|
|
333
|
+
"state": state,
|
|
334
|
+
"max_body": self._max_body,
|
|
335
|
+
"middlewares": list(self._middlewares),
|
|
336
|
+
})
|
|
337
|
+
|
|
338
|
+
watcher = threading.Thread(
|
|
339
|
+
target=_watch_config,
|
|
340
|
+
args=(state, self._config_path, self._resource_provider, self._stop_event),
|
|
341
|
+
daemon=True,
|
|
342
|
+
)
|
|
343
|
+
watcher.start()
|
|
344
|
+
|
|
345
|
+
self._server = HTTPServer((self._host, self._port), handler)
|
|
346
|
+
print(f"Pine server listening on :{self._port}", file=sys.stderr)
|
|
347
|
+
|
|
348
|
+
try:
|
|
349
|
+
self._server.serve_forever()
|
|
350
|
+
except KeyboardInterrupt:
|
|
351
|
+
self.stop()
|
|
352
|
+
|
|
353
|
+
def stop(self):
|
|
354
|
+
self._stop_event.set()
|
|
355
|
+
if self._server:
|
|
356
|
+
self._server.shutdown()
|
|
357
|
+
|
|
358
|
+
|
|
266
359
|
def main():
|
|
267
360
|
from pine.operators import ensure_registered
|
|
268
361
|
ensure_registered()
|
|
@@ -321,6 +414,7 @@ def main():
|
|
|
321
414
|
handler = type("Handler", (_PineHandler,), {
|
|
322
415
|
"state": state,
|
|
323
416
|
"max_body": max_body,
|
|
417
|
+
"middlewares": [],
|
|
324
418
|
})
|
|
325
419
|
|
|
326
420
|
stop_event = threading.Event()
|
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
"""Server extensibility tests for pine-python.
|
|
2
|
+
|
|
3
|
+
Verifies:
|
|
4
|
+
- Unknown paths return JSON 404 with {"error": "not found"}
|
|
5
|
+
- Middleware can intercept custom paths (e.g. /metrics)
|
|
6
|
+
- Method not allowed returns JSON 405
|
|
7
|
+
"""
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import json
|
|
11
|
+
import threading
|
|
12
|
+
import time
|
|
13
|
+
from http.client import HTTPConnection
|
|
14
|
+
from pathlib import Path
|
|
15
|
+
from typing import Callable
|
|
16
|
+
|
|
17
|
+
import pytest
|
|
18
|
+
from pine.cli.server import PineServer, _PineHandler
|
|
19
|
+
|
|
20
|
+
FIXTURES_ROOT = Path(__file__).parent.parent.parent / "fixtures"
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def _make_config() -> str:
|
|
24
|
+
"""Create a minimal temp config file and return its path."""
|
|
25
|
+
cfg = {
|
|
26
|
+
"pipeline_config": {
|
|
27
|
+
"operators": {
|
|
28
|
+
"noop": {
|
|
29
|
+
"type_name": "transform_copy",
|
|
30
|
+
"direction": "common_to_common",
|
|
31
|
+
"$metadata": {
|
|
32
|
+
"common_input": ["x"],
|
|
33
|
+
"common_output": ["y"],
|
|
34
|
+
"item_input": [],
|
|
35
|
+
"item_output": [],
|
|
36
|
+
},
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
},
|
|
40
|
+
"pipeline_group": {"main": {"pipeline": ["noop"]}},
|
|
41
|
+
"flow_contract": {
|
|
42
|
+
"common_input": ["x"],
|
|
43
|
+
"item_input": [],
|
|
44
|
+
"common_output": ["x", "y"],
|
|
45
|
+
"item_output": [],
|
|
46
|
+
},
|
|
47
|
+
}
|
|
48
|
+
import tempfile
|
|
49
|
+
|
|
50
|
+
f = tempfile.NamedTemporaryFile(mode="w", suffix=".json", delete=False)
|
|
51
|
+
json.dump(cfg, f)
|
|
52
|
+
f.close()
|
|
53
|
+
return f.name
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def _find_free_port() -> int:
|
|
57
|
+
import socket
|
|
58
|
+
|
|
59
|
+
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
|
|
60
|
+
s.bind(("", 0))
|
|
61
|
+
return s.getsockname()[1]
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def _wait_ready(port: int, timeout: float = 5.0):
|
|
65
|
+
deadline = time.time() + timeout
|
|
66
|
+
while time.time() < deadline:
|
|
67
|
+
try:
|
|
68
|
+
conn = HTTPConnection("localhost", port, timeout=1)
|
|
69
|
+
conn.request("GET", "/health")
|
|
70
|
+
resp = conn.getresponse()
|
|
71
|
+
if resp.status == 200:
|
|
72
|
+
conn.close()
|
|
73
|
+
return
|
|
74
|
+
conn.close()
|
|
75
|
+
except (ConnectionRefusedError, OSError):
|
|
76
|
+
pass
|
|
77
|
+
time.sleep(0.1)
|
|
78
|
+
raise RuntimeError(f"Server on port {port} not ready within {timeout}s")
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
class TestServerNotFound:
|
|
82
|
+
"""Unknown paths must return JSON 404."""
|
|
83
|
+
|
|
84
|
+
@pytest.fixture(autouse=True)
|
|
85
|
+
def server(self):
|
|
86
|
+
config_path = _make_config()
|
|
87
|
+
port = _find_free_port()
|
|
88
|
+
srv = PineServer(config_path, port=port)
|
|
89
|
+
t = threading.Thread(target=srv.start, daemon=True)
|
|
90
|
+
t.start()
|
|
91
|
+
_wait_ready(port)
|
|
92
|
+
self.port = port
|
|
93
|
+
self.srv = srv
|
|
94
|
+
yield
|
|
95
|
+
srv.stop()
|
|
96
|
+
Path(config_path).unlink(missing_ok=True)
|
|
97
|
+
|
|
98
|
+
def test_unknown_path_returns_json_404(self):
|
|
99
|
+
conn = HTTPConnection("localhost", self.port)
|
|
100
|
+
conn.request("GET", "/unknown-path")
|
|
101
|
+
resp = conn.getresponse()
|
|
102
|
+
assert resp.status == 404
|
|
103
|
+
body = json.loads(resp.read())
|
|
104
|
+
assert body == {"error": "not found"}
|
|
105
|
+
assert "application/json" in resp.getheader("Content-Type", "")
|
|
106
|
+
conn.close()
|
|
107
|
+
|
|
108
|
+
def test_post_unknown_path_returns_json_404(self):
|
|
109
|
+
conn = HTTPConnection("localhost", self.port)
|
|
110
|
+
conn.request("POST", "/does-not-exist")
|
|
111
|
+
resp = conn.getresponse()
|
|
112
|
+
assert resp.status == 404
|
|
113
|
+
body = json.loads(resp.read())
|
|
114
|
+
assert body == {"error": "not found"}
|
|
115
|
+
conn.close()
|
|
116
|
+
|
|
117
|
+
def test_method_not_allowed_json(self):
|
|
118
|
+
conn = HTTPConnection("localhost", self.port)
|
|
119
|
+
conn.request("POST", "/health")
|
|
120
|
+
resp = conn.getresponse()
|
|
121
|
+
assert resp.status == 405
|
|
122
|
+
body = json.loads(resp.read())
|
|
123
|
+
assert body == {"error": "method not allowed"}
|
|
124
|
+
conn.close()
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
class TestServerMiddleware:
|
|
128
|
+
"""Middleware can intercept custom paths."""
|
|
129
|
+
|
|
130
|
+
@pytest.fixture(autouse=True)
|
|
131
|
+
def server(self):
|
|
132
|
+
config_path = _make_config()
|
|
133
|
+
port = _find_free_port()
|
|
134
|
+
srv = PineServer(config_path, port=port)
|
|
135
|
+
|
|
136
|
+
def metrics_middleware(handler: _PineHandler, next_fn: Callable[[], None]):
|
|
137
|
+
path = handler.path.split("?")[0]
|
|
138
|
+
if path == "/metrics":
|
|
139
|
+
handler._json_response(200, {"custom": True})
|
|
140
|
+
return
|
|
141
|
+
next_fn()
|
|
142
|
+
|
|
143
|
+
srv.add_middleware(metrics_middleware)
|
|
144
|
+
t = threading.Thread(target=srv.start, daemon=True)
|
|
145
|
+
t.start()
|
|
146
|
+
_wait_ready(port)
|
|
147
|
+
self.port = port
|
|
148
|
+
self.srv = srv
|
|
149
|
+
yield
|
|
150
|
+
srv.stop()
|
|
151
|
+
Path(config_path).unlink(missing_ok=True)
|
|
152
|
+
|
|
153
|
+
def test_middleware_intercepts_custom_path(self):
|
|
154
|
+
conn = HTTPConnection("localhost", self.port)
|
|
155
|
+
conn.request("GET", "/metrics")
|
|
156
|
+
resp = conn.getresponse()
|
|
157
|
+
assert resp.status == 200
|
|
158
|
+
body = json.loads(resp.read())
|
|
159
|
+
assert body["custom"] is True
|
|
160
|
+
conn.close()
|
|
161
|
+
|
|
162
|
+
def test_known_path_still_works(self):
|
|
163
|
+
conn = HTTPConnection("localhost", self.port)
|
|
164
|
+
conn.request("GET", "/health")
|
|
165
|
+
resp = conn.getresponse()
|
|
166
|
+
assert resp.status == 200
|
|
167
|
+
body = json.loads(resp.read())
|
|
168
|
+
assert body["status"] == "ok"
|
|
169
|
+
conn.close()
|
|
170
|
+
|
|
171
|
+
def test_unknown_path_not_intercepted(self):
|
|
172
|
+
conn = HTTPConnection("localhost", self.port)
|
|
173
|
+
conn.request("GET", "/other")
|
|
174
|
+
resp = conn.getresponse()
|
|
175
|
+
assert resp.status == 404
|
|
176
|
+
body = json.loads(resp.read())
|
|
177
|
+
assert body == {"error": "not found"}
|
|
178
|
+
conn.close()
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
class TestServerMiddlewareCannotAddAfterStart:
|
|
182
|
+
"""Adding middleware after start raises RuntimeError."""
|
|
183
|
+
|
|
184
|
+
def test_add_middleware_after_start_raises(self):
|
|
185
|
+
config_path = _make_config()
|
|
186
|
+
port = _find_free_port()
|
|
187
|
+
srv = PineServer(config_path, port=port)
|
|
188
|
+
t = threading.Thread(target=srv.start, daemon=True)
|
|
189
|
+
t.start()
|
|
190
|
+
_wait_ready(port)
|
|
191
|
+
try:
|
|
192
|
+
with pytest.raises(RuntimeError, match="cannot add middleware"):
|
|
193
|
+
srv.add_middleware(lambda h, n: n())
|
|
194
|
+
finally:
|
|
195
|
+
srv.stop()
|
|
196
|
+
Path(config_path).unlink(missing_ok=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
|
|
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
|