gaard-api 0.2.1__tar.gz → 0.2.3__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 (65) hide show
  1. {gaard_api-0.2.1/src/gaard_api.egg-info → gaard_api-0.2.3}/PKG-INFO +20 -6
  2. {gaard_api-0.2.1 → gaard_api-0.2.3}/pyproject.toml +17 -6
  3. {gaard_api-0.2.1 → gaard_api-0.2.3}/src/gaard_api/admin/database.py +32 -22
  4. {gaard_api-0.2.1 → gaard_api-0.2.3}/src/gaard_api/admin/services.py +78 -14
  5. gaard_api-0.2.3/src/gaard_api/admin-web/assets/getgaard.svg +87 -0
  6. {gaard_api-0.2.1 → gaard_api-0.2.3}/src/gaard_api/admin-web/assets/main.js +713 -61
  7. {gaard_api-0.2.1 → gaard_api-0.2.3}/src/gaard_api/admin-web/assets/styles.css +299 -12
  8. {gaard_api-0.2.1 → gaard_api-0.2.3}/src/gaard_api/api/v1/admin.py +186 -12
  9. {gaard_api-0.2.1 → gaard_api-0.2.3}/src/gaard_api/api/v1/analysis.py +10 -10
  10. {gaard_api-0.2.1 → gaard_api-0.2.3}/src/gaard_api/api/v1/query.py +35 -502
  11. {gaard_api-0.2.1 → gaard_api-0.2.3}/src/gaard_api/cli_commands.py +5 -2
  12. {gaard_api-0.2.1 → gaard_api-0.2.3}/src/gaard_api/core/settings.py +34 -0
  13. gaard_api-0.2.3/src/gaard_api/extensions.py +125 -0
  14. gaard_api-0.2.3/src/gaard_api/license.py +900 -0
  15. {gaard_api-0.2.1 → gaard_api-0.2.3}/src/gaard_api/main.py +27 -5
  16. gaard_api-0.2.3/src/gaard_api/package_updates.py +1011 -0
  17. gaard_api-0.2.3/src/gaard_api/py.typed +0 -0
  18. gaard_api-0.2.3/src/gaard_api/query_hooks.py +322 -0
  19. gaard_api-0.2.3/src/gaard_api/tls_http.py +50 -0
  20. {gaard_api-0.2.1 → gaard_api-0.2.3/src/gaard_api.egg-info}/PKG-INFO +20 -6
  21. {gaard_api-0.2.1 → gaard_api-0.2.3}/src/gaard_api.egg-info/SOURCES.txt +10 -1
  22. gaard_api-0.2.3/src/gaard_api.egg-info/requires.txt +35 -0
  23. {gaard_api-0.2.1 → gaard_api-0.2.3}/tests/test_admin_api.py +240 -450
  24. {gaard_api-0.2.1 → gaard_api-0.2.3}/tests/test_api_registry.py +74 -1
  25. gaard_api-0.2.3/tests/test_license_api.py +438 -0
  26. gaard_api-0.2.3/tests/test_license_service.py +324 -0
  27. {gaard_api-0.2.1 → gaard_api-0.2.3}/tests/test_settings.py +1 -0
  28. gaard_api-0.2.3/tests/test_tls_http.py +70 -0
  29. gaard_api-0.2.1/src/gaard_api/extensions.py +0 -50
  30. gaard_api-0.2.1/src/gaard_api.egg-info/requires.txt +0 -16
  31. {gaard_api-0.2.1 → gaard_api-0.2.3}/MANIFEST.in +0 -0
  32. {gaard_api-0.2.1 → gaard_api-0.2.3}/README.md +0 -0
  33. {gaard_api-0.2.1 → gaard_api-0.2.3}/setup.cfg +0 -0
  34. {gaard_api-0.2.1 → gaard_api-0.2.3}/src/gaard_api/__init__.py +0 -0
  35. {gaard_api-0.2.1 → gaard_api-0.2.3}/src/gaard_api/admin/__init__.py +0 -0
  36. {gaard_api-0.2.1 → gaard_api-0.2.3}/src/gaard_api/admin/defaults.py +0 -0
  37. {gaard_api-0.2.1 → gaard_api-0.2.3}/src/gaard_api/admin/models.py +0 -0
  38. {gaard_api-0.2.1 → gaard_api-0.2.3}/src/gaard_api/admin/prompt_runtime.py +0 -0
  39. {gaard_api-0.2.1 → gaard_api-0.2.3}/src/gaard_api/admin/security.py +0 -0
  40. {gaard_api-0.2.1 → gaard_api-0.2.3}/src/gaard_api/admin-web/index.html +0 -0
  41. {gaard_api-0.2.1 → gaard_api-0.2.3}/src/gaard_api/admin-web/package.json +0 -0
  42. {gaard_api-0.2.1 → gaard_api-0.2.3}/src/gaard_api/api/__init__.py +0 -0
  43. {gaard_api-0.2.1 → gaard_api-0.2.3}/src/gaard_api/api/v1/__init__.py +0 -0
  44. {gaard_api-0.2.1 → gaard_api-0.2.3}/src/gaard_api/api/v1/prompts.py +0 -0
  45. {gaard_api-0.2.1 → gaard_api-0.2.3}/src/gaard_api/api/v1/schema.py +0 -0
  46. {gaard_api-0.2.1 → gaard_api-0.2.3}/src/gaard_api/api_registry.py +0 -0
  47. {gaard_api-0.2.1 → gaard_api-0.2.3}/src/gaard_api/cli.py +0 -0
  48. {gaard_api-0.2.1 → gaard_api-0.2.3}/src/gaard_api/core/__init__.py +0 -0
  49. {gaard_api-0.2.1 → gaard_api-0.2.3}/src/gaard_api/core/error_handlers.py +0 -0
  50. {gaard_api-0.2.1 → gaard_api-0.2.3}/src/gaard_api/core/schema_cache.py +0 -0
  51. {gaard_api-0.2.1 → gaard_api-0.2.3}/src/gaard_api/example_data/__init__.py +0 -0
  52. {gaard_api-0.2.1 → gaard_api-0.2.3}/src/gaard_api/example_data/medical_poc/__init__.py +0 -0
  53. {gaard_api-0.2.1 → gaard_api-0.2.3}/src/gaard_api/example_data/medical_poc/schema.sql +0 -0
  54. {gaard_api-0.2.1 → gaard_api-0.2.3}/src/gaard_api/example_data/medical_poc/seed.sql +0 -0
  55. {gaard_api-0.2.1 → gaard_api-0.2.3}/src/gaard_api/example_database.py +0 -0
  56. {gaard_api-0.2.1 → gaard_api-0.2.3}/src/gaard_api/extension_services.py +0 -0
  57. {gaard_api-0.2.1 → gaard_api-0.2.3}/src/gaard_api/server_cli.py +0 -0
  58. {gaard_api-0.2.1 → gaard_api-0.2.3}/src/gaard_api.egg-info/dependency_links.txt +0 -0
  59. {gaard_api-0.2.1 → gaard_api-0.2.3}/src/gaard_api.egg-info/entry_points.txt +0 -0
  60. {gaard_api-0.2.1 → gaard_api-0.2.3}/src/gaard_api.egg-info/top_level.txt +0 -0
  61. {gaard_api-0.2.1 → gaard_api-0.2.3}/tests/test_analysis_api.py +0 -0
  62. {gaard_api-0.2.1 → gaard_api-0.2.3}/tests/test_error_handlers.py +0 -0
  63. {gaard_api-0.2.1 → gaard_api-0.2.3}/tests/test_prompt_runtime.py +0 -0
  64. {gaard_api-0.2.1 → gaard_api-0.2.3}/tests/test_query_error_messages.py +0 -0
  65. {gaard_api-0.2.1 → gaard_api-0.2.3}/tests/test_server_cli.py +0 -0
@@ -1,18 +1,32 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: gaard-api
3
- Version: 0.2.1
3
+ Version: 0.2.3
4
4
  Summary: GAARD backend web services providing admin interface
5
5
  Requires-Python: >=3.11
6
6
  Description-Content-Type: text/markdown
7
- Requires-Dist: gaard-core==0.2.1
8
- Requires-Dist: gaard-connectors==0.2.1
9
- Requires-Dist: gaard-llm==0.2.1
10
- Requires-Dist: gaard-plugin-api<0.3.0,>=0.2.0
7
+ Requires-Dist: gaard-core==0.2.2
8
+ Requires-Dist: gaard-connectors==0.2.2
9
+ Requires-Dist: gaard-llm==0.2.2
10
+ Requires-Dist: gaard-plugin-api<0.3.0,>=0.2.2
11
11
  Requires-Dist: fastapi>=0.111.0
12
12
  Requires-Dist: uvicorn[standard]>=0.30.0
13
13
  Requires-Dist: pydantic>=2.7.0
14
14
  Requires-Dist: sqlalchemy>=2.0.0
15
- Requires-Dist: httpx>=0.27.0
15
+ Requires-Dist: httpx2>=2.5.0
16
+ Requires-Dist: certifi>=2024.8.30
17
+ Provides-Extra: oracle
18
+ Requires-Dist: oracledb>=2.0.0; extra == "oracle"
19
+ Provides-Extra: mssql
20
+ Requires-Dist: pyodbc>=5.0.0; extra == "mssql"
21
+ Provides-Extra: ibm-db2
22
+ Requires-Dist: ibm-db-sa>=0.4.4; extra == "ibm-db2"
23
+ Provides-Extra: teradata
24
+ Requires-Dist: teradatasqlalchemy>=20.0.0.9; extra == "teradata"
25
+ Provides-Extra: all-datasources
26
+ Requires-Dist: oracledb>=2.0.0; extra == "all-datasources"
27
+ Requires-Dist: pyodbc>=5.0.0; extra == "all-datasources"
28
+ Requires-Dist: ibm-db-sa>=0.4.4; extra == "all-datasources"
29
+ Requires-Dist: teradatasqlalchemy>=20.0.0.9; extra == "all-datasources"
16
30
  Provides-Extra: dev
17
31
  Requires-Dist: pytest>=8.0.0; extra == "dev"
18
32
  Requires-Dist: ruff>=0.5.0; extra == "dev"
@@ -4,24 +4,35 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "gaard-api"
7
- version = "0.2.1"
7
+ version = "0.2.3"
8
8
  description = "GAARD backend web services providing admin interface"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.11"
11
11
 
12
12
  dependencies = [
13
- "gaard-core==0.2.1",
14
- "gaard-connectors==0.2.1",
15
- "gaard-llm==0.2.1",
16
- "gaard-plugin-api>=0.2.0,<0.3.0",
13
+ "gaard-core==0.2.2",
14
+ "gaard-connectors==0.2.2",
15
+ "gaard-llm==0.2.2",
16
+ "gaard-plugin-api>=0.2.2,<0.3.0",
17
17
  "fastapi>=0.111.0",
18
18
  "uvicorn[standard]>=0.30.0",
19
19
  "pydantic>=2.7.0",
20
20
  "sqlalchemy>=2.0.0",
21
- "httpx>=0.27.0"
21
+ "httpx2>=2.5.0",
22
+ "certifi>=2024.8.30"
22
23
  ]
23
24
 
24
25
  [project.optional-dependencies]
26
+ oracle = ["oracledb>=2.0.0"]
27
+ mssql = ["pyodbc>=5.0.0"]
28
+ ibm_db2 = ["ibm-db-sa>=0.4.4"]
29
+ teradata = ["teradatasqlalchemy>=20.0.0.9"]
30
+ all-datasources = [
31
+ "oracledb>=2.0.0",
32
+ "pyodbc>=5.0.0",
33
+ "ibm-db-sa>=0.4.4",
34
+ "teradatasqlalchemy>=20.0.0.9",
35
+ ]
25
36
  dev = [
26
37
  "pytest>=8.0.0",
27
38
  "ruff>=0.5.0",
@@ -1,9 +1,10 @@
1
1
  from collections.abc import Iterator
2
2
  import json
3
+ import threading
3
4
 
4
5
  from sqlalchemy import create_engine, delete, select
5
6
  from sqlalchemy.engine import Engine
6
- from sqlalchemy import inspect, text
7
+ from sqlalchemy import inspect, text, Table
7
8
  from sqlalchemy.orm import Session, sessionmaker
8
9
 
9
10
  from gaard_api.admin.defaults import DEFAULT_GOVERNANCE_POLICY_CONFIG, DEFAULT_PROMPTS
@@ -20,10 +21,13 @@ from gaard_api.admin.models import (
20
21
  from gaard_api.admin.security import hash_password
21
22
  from gaard_api.core.settings import settings
22
23
 
24
+ from typing import cast
25
+
23
26
 
24
27
  _engine: Engine | None = None
25
28
  _session_factory: sessionmaker[Session] | None = None
26
29
  _engine_url: str | None = None
30
+ _init_lock = threading.RLock()
27
31
  LEGACY_PROMPT_KEYS = {"investigation_readiness"}
28
32
 
29
33
 
@@ -66,23 +70,24 @@ def create_session() -> Session:
66
70
 
67
71
 
68
72
  def init_metadata_store() -> None:
69
- engine = get_engine()
70
- Base.metadata.create_all(engine)
71
- ensure_data_query_audit_schema(engine)
72
- ensure_overview_widget_schema(engine)
73
-
74
- if _session_factory is None:
75
- raise RuntimeError("Admin metadata session factory is not initialized.")
76
-
77
- with _session_factory() as session:
78
- seed_admin_user(session)
79
- seed_settings(session)
80
- apply_runtime_settings(session)
81
- seed_prompts(session)
82
- seed_datasource_connectors(session)
83
- seed_overview_widgets(session)
84
- backfill_data_query_audit_types(session)
85
- session.commit()
73
+ with _init_lock:
74
+ engine = get_engine()
75
+ Base.metadata.create_all(engine)
76
+ ensure_data_query_audit_schema(engine)
77
+ ensure_overview_widget_schema(engine)
78
+
79
+ if _session_factory is None:
80
+ raise RuntimeError("Admin metadata session factory is not initialized.")
81
+
82
+ with _session_factory() as session:
83
+ seed_admin_user(session)
84
+ seed_settings(session)
85
+ apply_runtime_settings(session)
86
+ seed_prompts(session)
87
+ seed_datasource_connectors(session)
88
+ seed_overview_widgets(session)
89
+ backfill_data_query_audit_types(session)
90
+ session.commit()
86
91
 
87
92
 
88
93
  def seed_admin_user(session: Session) -> None:
@@ -129,7 +134,6 @@ def seed_settings(session: Session) -> None:
129
134
  ),
130
135
  "data_query_audit_retention_days": str(settings.gaard_audit_retention_days),
131
136
  "schema_cache_ttl_seconds": str(settings.gaard_schema_cache_ttl_seconds),
132
- "license_edition": "community",
133
137
  }
134
138
 
135
139
  for key, value in defaults.items():
@@ -139,6 +143,9 @@ def seed_settings(session: Session) -> None:
139
143
  elif setting.updated_by == "system" and setting.value != value:
140
144
  setting.value = value
141
145
 
146
+ if session.get(AdminSetting, "license_edition") is None:
147
+ session.add(AdminSetting(key="license_edition", value="community"))
148
+
142
149
 
143
150
  def apply_runtime_settings(session: Session) -> None:
144
151
  schema_cache_ttl = session.get(AdminSetting, "schema_cache_ttl_seconds")
@@ -374,8 +381,8 @@ def seed_overview_widgets(session: Session) -> None:
374
381
  existing.active = False
375
382
 
376
383
  if existing is not None and existing.updated_by == "system":
377
- existing.position = int(item["position"])
378
- existing.grid_width = int(item["grid_width"])
384
+ existing.position = int(cast(int,item["position"]))
385
+ existing.grid_width = int(cast(int,item["grid_width"]))
379
386
  existing.result_mode = str(item["result_mode"])
380
387
 
381
388
 
@@ -416,7 +423,10 @@ def ensure_data_query_audit_schema(engine: Engine) -> None:
416
423
  )
417
424
 
418
425
  with engine.begin() as connection:
419
- for index in DataQueryAuditLog.__table__.indexes:
426
+
427
+ audit_log_table = cast(Table, DataQueryAuditLog.__table__)
428
+
429
+ for index in audit_log_table.indexes:
420
430
  index.create(bind=connection, checkfirst=True)
421
431
 
422
432
 
@@ -3,7 +3,7 @@ import re
3
3
  from dataclasses import dataclass
4
4
  from datetime import UTC, datetime, timedelta
5
5
  from pathlib import Path
6
- from typing import Any
6
+ from typing import Any, cast
7
7
 
8
8
  from sqlalchemy.engine import URL
9
9
  from sqlalchemy import delete, desc, select
@@ -139,6 +139,36 @@ def build_sqlalchemy_url_from_connection_config(
139
139
  query_keys=("charset",),
140
140
  )
141
141
 
142
+ if database_type == "oracle":
143
+ return build_server_database_url(
144
+ drivername="oracle+oracledb",
145
+ connection_config=connection_config,
146
+ default_port=1521,
147
+ query_keys=("service_name",),
148
+ database_required=False,
149
+ required_query_keys=("service_name",),
150
+ required_query_key_labels={"service_name": "service name"},
151
+ )
152
+
153
+ if database_type == "mssql":
154
+ return build_server_database_url(
155
+ drivername="mssql+pyodbc",
156
+ connection_config=connection_config,
157
+ default_port=1433,
158
+ query_keys=("driver", "Encrypt", "TrustServerCertificate"),
159
+ )
160
+
161
+ if database_type == "ibm_db2":
162
+ return build_server_database_url(
163
+ drivername="db2+ibm_db",
164
+ connection_config=connection_config,
165
+ default_port=50000,
166
+ query_keys=(),
167
+ )
168
+
169
+ if database_type == "teradata":
170
+ return build_teradata_database_url(connection_config)
171
+
142
172
  raise ValueError(f"Datasource type {database_type!r} does not support generated URLs.")
143
173
 
144
174
 
@@ -147,6 +177,9 @@ def build_server_database_url(
147
177
  connection_config: dict[str, Any],
148
178
  default_port: int,
149
179
  query_keys: tuple[str, ...],
180
+ database_required: bool = True,
181
+ required_query_keys: tuple[str, ...] = (),
182
+ required_query_key_labels: dict[str, str] | None = None,
150
183
  ) -> str:
151
184
  host = str(connection_config.get("host") or "").strip()
152
185
  database = str(connection_config.get("database") or "").strip()
@@ -156,7 +189,7 @@ def build_server_database_url(
156
189
 
157
190
  if not host:
158
191
  raise ValueError("Datasource host is required.")
159
- if not database:
192
+ if database_required and not database:
160
193
  raise ValueError("Datasource database name is required.")
161
194
  if not username:
162
195
  raise ValueError("Datasource username is required.")
@@ -166,6 +199,12 @@ def build_server_database_url(
166
199
  except (TypeError, ValueError) as exc:
167
200
  raise ValueError("Datasource port must be a number.") from exc
168
201
 
202
+ labels = required_query_key_labels or {}
203
+ for key in required_query_keys:
204
+ if not str(connection_config.get(key) or "").strip():
205
+ label = labels.get(key, key.replace("_", " "))
206
+ raise ValueError(f"Datasource {label} is required.")
207
+
169
208
  query = {
170
209
  key: str(connection_config.get(key)).strip()
171
210
  for key in query_keys
@@ -179,7 +218,40 @@ def build_server_database_url(
179
218
  password=password or None,
180
219
  host=host,
181
220
  port=port,
182
- database=database,
221
+ database=database or None,
222
+ query=query,
223
+ )
224
+ .render_as_string(hide_password=False)
225
+ )
226
+
227
+
228
+ def build_teradata_database_url(connection_config: dict[str, Any]) -> str:
229
+ host = str(connection_config.get("host") or "").strip()
230
+ username = str(connection_config.get("username") or "").strip()
231
+ password = str(connection_config.get("password") or "")
232
+ dbs_port_value = connection_config.get("dbs_port") or 1025
233
+
234
+ if not host:
235
+ raise ValueError("Datasource host is required.")
236
+ if not username:
237
+ raise ValueError("Datasource username is required.")
238
+
239
+ try:
240
+ dbs_port = int(dbs_port_value)
241
+ except (TypeError, ValueError) as exc:
242
+ raise ValueError("Datasource port must be a number.") from exc
243
+
244
+ query = {"dbs_port": str(dbs_port)}
245
+ for key in ("database", "tmode"):
246
+ if connection_config.get(key) not in (None, ""):
247
+ query[key] = str(connection_config.get(key)).strip()
248
+
249
+ return (
250
+ URL.create(
251
+ drivername="teradatasql",
252
+ username=username,
253
+ password=password or None,
254
+ host=host,
183
255
  query=query,
184
256
  )
185
257
  .render_as_string(hide_password=False)
@@ -487,7 +559,7 @@ def set_setting(session: Session, key: str, value: str, actor: str) -> AdminSett
487
559
 
488
560
 
489
561
  def default_governance_policy_config() -> dict[str, Any]:
490
- return json.loads(json_dumps(DEFAULT_GOVERNANCE_POLICY_CONFIG))
562
+ return cast(dict[str, Any], json.loads(json_dumps(DEFAULT_GOVERNANCE_POLICY_CONFIG)))
491
563
 
492
564
 
493
565
  def normalize_bool_setting(value: Any, field_name: str) -> bool:
@@ -2014,17 +2086,9 @@ def set_active_datasource_connector(
2014
2086
  connector: DatasourceConnector,
2015
2087
  actor: str,
2016
2088
  ) -> None:
2017
- if is_system_datasource_connector(connector):
2018
- connector.active = False
2019
- return
2020
-
2021
- if connector.connector_key != "default":
2022
- for item in list_datasource_connectors(session):
2023
- if item.connector_key == "default":
2024
- item.active = False
2089
+ from gaard_api.extensions import get_query_hook_registry
2025
2090
 
2026
- connector.active = True
2027
- connector.updated_by = actor
2091
+ get_query_hook_registry().set_active_datasource_connector(session, connector, actor)
2028
2092
 
2029
2093
 
2030
2094
  def test_datasource_connection(connector: DatasourceConnector) -> None:
@@ -0,0 +1,87 @@
1
+ <?xml version="1.0" encoding="UTF-8" standalone="no"?>
2
+ <!-- Created with Inkscape (http://www.inkscape.org/) -->
3
+
4
+ <svg
5
+ version="1.1"
6
+ id="svg1"
7
+ width="1672"
8
+ height="1672"
9
+ viewBox="0 0 1672 1672"
10
+ sodipodi:docname="getgaard.svg"
11
+ inkscape:version="1.4 (e7c3feb1, 2024-10-09)"
12
+ xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
13
+ xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
14
+ xmlns="http://www.w3.org/2000/svg"
15
+ xmlns:svg="http://www.w3.org/2000/svg">
16
+ <defs
17
+ id="defs1" />
18
+ <sodipodi:namedview
19
+ id="namedview1"
20
+ pagecolor="#ffffff"
21
+ bordercolor="#000000"
22
+ borderopacity="0.25"
23
+ inkscape:showpageshadow="2"
24
+ inkscape:pageopacity="0.0"
25
+ inkscape:pagecheckerboard="0"
26
+ inkscape:deskcolor="#d1d1d1"
27
+ inkscape:zoom="0.17088526"
28
+ inkscape:cx="2182.7512"
29
+ inkscape:cy="111.18572"
30
+ inkscape:window-width="1560"
31
+ inkscape:window-height="771"
32
+ inkscape:window-x="0"
33
+ inkscape:window-y="25"
34
+ inkscape:window-maximized="0"
35
+ inkscape:current-layer="g1" />
36
+ <g
37
+ inkscape:groupmode="layer"
38
+ inkscape:label="Image"
39
+ id="g1">
40
+ <g
41
+ id="g6"
42
+ transform="matrix(1.0610508,0,0,1.0610508,-2611.1446,1006.5308)">
43
+ <rect
44
+ style="fill:#111e2f;fill-opacity:1;stroke-width:1.09542"
45
+ id="rect2"
46
+ width="1437.7893"
47
+ height="1437.7893"
48
+ x="2529.908"
49
+ y="-879.6134"
50
+ ry="320.62204" />
51
+ <path
52
+ id="rect1"
53
+ style="fill:#ffffff;stroke-width:1.09542"
54
+ d="m 3234.3408,-626.0861 c -13.6106,0.3309 -25.9368,1.0895 -34.8867,2.3418 -84.1236,11.7709 -151.8696,39.6365 -216.4922,89.0508 -88.4727,67.6513 -151.6819,174.0139 -171.4746,288.5391 -8.8934,51.4587 -8.6698,114.0636 0.5859,163.333996 8.6422,46.004504 21.2085,84.4823042 40.9121,125.273404 23.7832,49.237 50.781,87.8784 88.418,126.5547 91.8576,94.394 218.5332,143.6113 347.0449,134.8359 127.7123,-8.7209 240.4621,-72.1949 316.1836,-177.998 45.9159,-64.1567 74.5299,-138.5546 84.8711,-220.666004 2.9258,-23.231196 4.2525,-75.228896 1.9883,-77.957096 -1.5107,-1.8203 -25.8188,-2.0429 -222.8203,-2.0429 -217.2534,0 -221.1459,0.044 -222.502,2.5781 -0.885,1.6537 -1.3808,21.59 -1.3808,55.5625 0,47.4299 0.2317,53.2391 2.207,55.4218 2.1138,2.3358 8.7633,2.4375 158,2.4376 129.1634,0 155.793,0.305 155.793,1.7832 0,3.2418 -7.4812,26.672 -12.6289,39.5507 -17.8663,44.6984 -41.4198,80.6843 -74.6856,114.1075 -26.441,26.566 -51.6323,44.8176 -82.6855,59.9082 -124.9554,60.7235 -275.5308,28.7986 -373.086,-79.1016 -92.4325,-102.2344 -119.9458,-248.8362 -70.1562,-373.8106 17.3657,-43.5887 43.6592,-83.5871 76.4023,-116.2246 38.482,-38.3578 81.0392,-64.5511 132.1739,-81.3515 48.2572,-15.855 102.913,-19.5453 159.332,-10.7598 6.9666,1.0848 13.6858,2.2858 14.9316,2.6699 1.272,0.392 3.4643,-0.5013 5,-2.0371 2.6697,-2.6697 2.7364,-4.0526 2.7364,-58.0371 v -55.3027 l -3,-1.9356 c -7.5481,-4.8687 -59.9495,-7.7173 -100.7813,-6.7246 z"
55
+ sodipodi:nodetypes="ssssssssssssssssssssssssssscsss" />
56
+ <g
57
+ id="g5"
58
+ transform="translate(2457.7895,-1000.6715)"
59
+ style="fill:#0cc6a6;fill-opacity:1">
60
+ <rect
61
+ style="opacity:1;fill:#0cc6a6;fill-opacity:1;stroke-width:0.938067"
62
+ id="rect3"
63
+ width="72.260895"
64
+ height="134.18539"
65
+ x="936.10278"
66
+ y="636.39392"
67
+ ry="15.974472" />
68
+ <rect
69
+ style="opacity:1;fill:#0cc6a6;fill-opacity:1;stroke-width:1.20121"
70
+ id="rect4"
71
+ width="72.260895"
72
+ height="220.02458"
73
+ x="1046.6084"
74
+ y="550.55475"
75
+ ry="19.286819" />
76
+ <rect
77
+ style="opacity:1;fill:#0cc6a6;fill-opacity:1;stroke-width:1.50056"
78
+ id="rect5"
79
+ width="72.260895"
80
+ height="343.35675"
81
+ x="1159.0873"
82
+ y="427.22256"
83
+ ry="10.289478" />
84
+ </g>
85
+ </g>
86
+ </g>
87
+ </svg>