gamsapi 52.5.0__cp312-cp312-win_amd64.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.
Files changed (257) hide show
  1. gams/__init__.py +27 -0
  2. gams/_version.py +1 -0
  3. gams/connect/__init__.py +28 -0
  4. gams/connect/agents/__init__.py +24 -0
  5. gams/connect/agents/_excel/__init__.py +32 -0
  6. gams/connect/agents/_excel/excelagent.py +312 -0
  7. gams/connect/agents/_excel/workbook.py +155 -0
  8. gams/connect/agents/_sqlconnectors/__init__.py +42 -0
  9. gams/connect/agents/_sqlconnectors/_accesshandler.py +211 -0
  10. gams/connect/agents/_sqlconnectors/_databasehandler.py +250 -0
  11. gams/connect/agents/_sqlconnectors/_mysqlhandler.py +168 -0
  12. gams/connect/agents/_sqlconnectors/_postgreshandler.py +131 -0
  13. gams/connect/agents/_sqlconnectors/_pyodbchandler.py +112 -0
  14. gams/connect/agents/_sqlconnectors/_sqlalchemyhandler.py +74 -0
  15. gams/connect/agents/_sqlconnectors/_sqlitehandler.py +262 -0
  16. gams/connect/agents/_sqlconnectors/_sqlserverhandler.py +179 -0
  17. gams/connect/agents/concatenate.py +440 -0
  18. gams/connect/agents/connectagent.py +743 -0
  19. gams/connect/agents/csvreader.py +675 -0
  20. gams/connect/agents/csvwriter.py +151 -0
  21. gams/connect/agents/domainwriter.py +143 -0
  22. gams/connect/agents/excelreader.py +756 -0
  23. gams/connect/agents/excelwriter.py +467 -0
  24. gams/connect/agents/filter.py +223 -0
  25. gams/connect/agents/gamsreader.py +112 -0
  26. gams/connect/agents/gamswriter.py +239 -0
  27. gams/connect/agents/gdxreader.py +109 -0
  28. gams/connect/agents/gdxwriter.py +146 -0
  29. gams/connect/agents/labelmanipulator.py +303 -0
  30. gams/connect/agents/projection.py +539 -0
  31. gams/connect/agents/pythoncode.py +71 -0
  32. gams/connect/agents/rawcsvreader.py +248 -0
  33. gams/connect/agents/rawexcelreader.py +312 -0
  34. gams/connect/agents/schema/CSVReader.yaml +92 -0
  35. gams/connect/agents/schema/CSVWriter.yaml +44 -0
  36. gams/connect/agents/schema/Concatenate.yaml +52 -0
  37. gams/connect/agents/schema/DomainWriter.yaml +25 -0
  38. gams/connect/agents/schema/ExcelReader.yaml +121 -0
  39. gams/connect/agents/schema/ExcelWriter.yaml +78 -0
  40. gams/connect/agents/schema/Filter.yaml +74 -0
  41. gams/connect/agents/schema/GAMSReader.yaml +20 -0
  42. gams/connect/agents/schema/GAMSWriter.yaml +47 -0
  43. gams/connect/agents/schema/GDXReader.yaml +23 -0
  44. gams/connect/agents/schema/GDXWriter.yaml +32 -0
  45. gams/connect/agents/schema/LabelManipulator.yaml +99 -0
  46. gams/connect/agents/schema/Projection.yaml +24 -0
  47. gams/connect/agents/schema/PythonCode.yaml +6 -0
  48. gams/connect/agents/schema/RawCSVReader.yaml +34 -0
  49. gams/connect/agents/schema/RawExcelReader.yaml +42 -0
  50. gams/connect/agents/schema/SQLReader.yaml +75 -0
  51. gams/connect/agents/schema/SQLWriter.yaml +103 -0
  52. gams/connect/agents/sqlreader.py +301 -0
  53. gams/connect/agents/sqlwriter.py +276 -0
  54. gams/connect/connectdatabase.py +275 -0
  55. gams/connect/connectvalidator.py +93 -0
  56. gams/connect/errors.py +34 -0
  57. gams/control/__init__.py +136 -0
  58. gams/control/database.py +2231 -0
  59. gams/control/execution.py +1900 -0
  60. gams/control/options.py +2792 -0
  61. gams/control/workspace.py +1198 -0
  62. gams/core/__init__.py +24 -0
  63. gams/core/cfg/__init__.py +26 -0
  64. gams/core/cfg/_cfgmcc.cp312-win_amd64.pyd +0 -0
  65. gams/core/cfg/cfgmcc.py +519 -0
  66. gams/core/dct/__init__.py +26 -0
  67. gams/core/dct/_dctmcc.cp312-win_amd64.pyd +0 -0
  68. gams/core/dct/dctmcc.py +574 -0
  69. gams/core/embedded/__init__.py +26 -0
  70. gams/core/embedded/gamsemb.py +1024 -0
  71. gams/core/emp/__init__.py +24 -0
  72. gams/core/emp/emplexer.py +89 -0
  73. gams/core/emp/empyacc.py +281 -0
  74. gams/core/gdx/__init__.py +26 -0
  75. gams/core/gdx/_gdxcc.cp312-win_amd64.pyd +0 -0
  76. gams/core/gdx/gdxcc.py +866 -0
  77. gams/core/gev/__init__.py +26 -0
  78. gams/core/gev/_gevmcc.cp312-win_amd64.pyd +0 -0
  79. gams/core/gev/gevmcc.py +855 -0
  80. gams/core/gmd/__init__.py +26 -0
  81. gams/core/gmd/_gmdcc.cp312-win_amd64.pyd +0 -0
  82. gams/core/gmd/gmdcc.py +917 -0
  83. gams/core/gmo/__init__.py +26 -0
  84. gams/core/gmo/_gmomcc.cp312-win_amd64.pyd +0 -0
  85. gams/core/gmo/gmomcc.py +2046 -0
  86. gams/core/idx/__init__.py +26 -0
  87. gams/core/idx/_idxcc.cp312-win_amd64.pyd +0 -0
  88. gams/core/idx/idxcc.py +510 -0
  89. gams/core/numpy/__init__.py +29 -0
  90. gams/core/numpy/_gams2numpy.cp312-win_amd64.pyd +0 -0
  91. gams/core/numpy/gams2numpy.py +1048 -0
  92. gams/core/opt/__init__.py +26 -0
  93. gams/core/opt/_optcc.cp312-win_amd64.pyd +0 -0
  94. gams/core/opt/optcc.py +840 -0
  95. gams/engine/__init__.py +204 -0
  96. gams/engine/api/__init__.py +13 -0
  97. gams/engine/api/auth_api.py +7653 -0
  98. gams/engine/api/cleanup_api.py +751 -0
  99. gams/engine/api/default_api.py +887 -0
  100. gams/engine/api/hypercube_api.py +2629 -0
  101. gams/engine/api/jobs_api.py +5229 -0
  102. gams/engine/api/licenses_api.py +2220 -0
  103. gams/engine/api/namespaces_api.py +7783 -0
  104. gams/engine/api/usage_api.py +5627 -0
  105. gams/engine/api/users_api.py +5931 -0
  106. gams/engine/api_client.py +804 -0
  107. gams/engine/api_response.py +21 -0
  108. gams/engine/configuration.py +601 -0
  109. gams/engine/exceptions.py +216 -0
  110. gams/engine/models/__init__.py +86 -0
  111. gams/engine/models/bad_input.py +89 -0
  112. gams/engine/models/cleanable_job_result.py +104 -0
  113. gams/engine/models/cleanable_job_result_page.py +113 -0
  114. gams/engine/models/engine_license.py +107 -0
  115. gams/engine/models/files_not_found.py +93 -0
  116. gams/engine/models/forwarded_token_response.py +112 -0
  117. gams/engine/models/generic_key_value_pair.py +89 -0
  118. gams/engine/models/hypercube.py +160 -0
  119. gams/engine/models/hypercube_page.py +111 -0
  120. gams/engine/models/hypercube_summary.py +91 -0
  121. gams/engine/models/hypercube_token.py +97 -0
  122. gams/engine/models/identity_provider.py +107 -0
  123. gams/engine/models/identity_provider_ldap.py +121 -0
  124. gams/engine/models/identity_provider_oauth2.py +146 -0
  125. gams/engine/models/identity_provider_oauth2_scope.py +89 -0
  126. gams/engine/models/identity_provider_oauth2_with_secret.py +152 -0
  127. gams/engine/models/identity_provider_oidc.py +133 -0
  128. gams/engine/models/identity_provider_oidc_with_secret.py +143 -0
  129. gams/engine/models/inex.py +91 -0
  130. gams/engine/models/invitation.py +136 -0
  131. gams/engine/models/invitation_quota.py +106 -0
  132. gams/engine/models/invitation_token.py +87 -0
  133. gams/engine/models/job.py +165 -0
  134. gams/engine/models/job_no_text_entry.py +138 -0
  135. gams/engine/models/job_no_text_entry_page.py +111 -0
  136. gams/engine/models/license.py +91 -0
  137. gams/engine/models/log_piece.py +96 -0
  138. gams/engine/models/message.py +87 -0
  139. gams/engine/models/message_and_token.py +99 -0
  140. gams/engine/models/message_with_webhook_id.py +89 -0
  141. gams/engine/models/model_auth_token.py +87 -0
  142. gams/engine/models/model_configuration.py +125 -0
  143. gams/engine/models/model_default_instance.py +99 -0
  144. gams/engine/models/model_default_user_instance.py +98 -0
  145. gams/engine/models/model_hypercube_job.py +106 -0
  146. gams/engine/models/model_hypercube_usage.py +130 -0
  147. gams/engine/models/model_instance_info.py +116 -0
  148. gams/engine/models/model_instance_info_full.py +123 -0
  149. gams/engine/models/model_instance_pool_info.py +112 -0
  150. gams/engine/models/model_job_labels.py +179 -0
  151. gams/engine/models/model_job_usage.py +133 -0
  152. gams/engine/models/model_pool_usage.py +124 -0
  153. gams/engine/models/model_usage.py +115 -0
  154. gams/engine/models/model_user.py +96 -0
  155. gams/engine/models/model_userinstance_info.py +119 -0
  156. gams/engine/models/model_userinstancepool_info.py +95 -0
  157. gams/engine/models/model_version.py +91 -0
  158. gams/engine/models/models.py +120 -0
  159. gams/engine/models/namespace.py +104 -0
  160. gams/engine/models/namespace_quota.py +96 -0
  161. gams/engine/models/namespace_with_permission.py +96 -0
  162. gams/engine/models/not_found.py +91 -0
  163. gams/engine/models/password_policy.py +97 -0
  164. gams/engine/models/perm_and_username.py +89 -0
  165. gams/engine/models/quota.py +117 -0
  166. gams/engine/models/quota_exceeded.py +97 -0
  167. gams/engine/models/status_code_meaning.py +89 -0
  168. gams/engine/models/stream_entry.py +89 -0
  169. gams/engine/models/system_wide_license.py +92 -0
  170. gams/engine/models/text_entries.py +87 -0
  171. gams/engine/models/text_entry.py +101 -0
  172. gams/engine/models/time_span.py +95 -0
  173. gams/engine/models/time_span_pool_worker.py +99 -0
  174. gams/engine/models/token_forward_error.py +87 -0
  175. gams/engine/models/user.py +127 -0
  176. gams/engine/models/user_group_member.py +96 -0
  177. gams/engine/models/user_groups.py +108 -0
  178. gams/engine/models/vapid_info.py +87 -0
  179. gams/engine/models/webhook.py +138 -0
  180. gams/engine/models/webhook_parameterized_event.py +99 -0
  181. gams/engine/py.typed +0 -0
  182. gams/engine/rest.py +258 -0
  183. gams/magic/__init__.py +32 -0
  184. gams/magic/gams_magic.py +142 -0
  185. gams/magic/interactive.py +402 -0
  186. gams/tools/__init__.py +30 -0
  187. gams/tools/errors.py +34 -0
  188. gams/tools/toolcollection/__init__.py +24 -0
  189. gams/tools/toolcollection/alg/__init__.py +24 -0
  190. gams/tools/toolcollection/alg/rank.py +51 -0
  191. gams/tools/toolcollection/data/__init__.py +24 -0
  192. gams/tools/toolcollection/data/csvread.py +444 -0
  193. gams/tools/toolcollection/data/csvwrite.py +311 -0
  194. gams/tools/toolcollection/data/exceldump.py +47 -0
  195. gams/tools/toolcollection/data/sqlitewrite.py +276 -0
  196. gams/tools/toolcollection/gdxservice/__init__.py +24 -0
  197. gams/tools/toolcollection/gdxservice/gdxencoding.py +104 -0
  198. gams/tools/toolcollection/gdxservice/gdxrename.py +94 -0
  199. gams/tools/toolcollection/linalg/__init__.py +24 -0
  200. gams/tools/toolcollection/linalg/cholesky.py +57 -0
  201. gams/tools/toolcollection/linalg/eigenvalue.py +56 -0
  202. gams/tools/toolcollection/linalg/eigenvector.py +58 -0
  203. gams/tools/toolcollection/linalg/invert.py +55 -0
  204. gams/tools/toolcollection/linalg/ols.py +138 -0
  205. gams/tools/toolcollection/tooltemplate.py +321 -0
  206. gams/tools/toolcollection/win32/__init__.py +24 -0
  207. gams/tools/toolcollection/win32/excelmerge.py +93 -0
  208. gams/tools/toolcollection/win32/exceltalk.py +76 -0
  209. gams/tools/toolcollection/win32/msappavail.py +49 -0
  210. gams/tools/toolcollection/win32/shellexecute.py +54 -0
  211. gams/tools/tools.py +116 -0
  212. gams/transfer/__init__.py +35 -0
  213. gams/transfer/_abcs/__init__.py +37 -0
  214. gams/transfer/_abcs/container_abcs.py +433 -0
  215. gams/transfer/_internals/__init__.py +63 -0
  216. gams/transfer/_internals/algorithms.py +436 -0
  217. gams/transfer/_internals/casepreservingdict.py +124 -0
  218. gams/transfer/_internals/constants.py +270 -0
  219. gams/transfer/_internals/domainviolation.py +103 -0
  220. gams/transfer/_internals/specialvalues.py +172 -0
  221. gams/transfer/containers/__init__.py +26 -0
  222. gams/transfer/containers/_container.py +1794 -0
  223. gams/transfer/containers/_io/__init__.py +28 -0
  224. gams/transfer/containers/_io/containers.py +164 -0
  225. gams/transfer/containers/_io/gdx.py +1029 -0
  226. gams/transfer/containers/_io/gmd.py +872 -0
  227. gams/transfer/containers/_mixins/__init__.py +26 -0
  228. gams/transfer/containers/_mixins/ccc.py +1274 -0
  229. gams/transfer/syms/__init__.py +33 -0
  230. gams/transfer/syms/_methods/__init__.py +24 -0
  231. gams/transfer/syms/_methods/tables.py +120 -0
  232. gams/transfer/syms/_methods/toDict.py +115 -0
  233. gams/transfer/syms/_methods/toList.py +83 -0
  234. gams/transfer/syms/_methods/toValue.py +60 -0
  235. gams/transfer/syms/_mixins/__init__.py +32 -0
  236. gams/transfer/syms/_mixins/equals.py +626 -0
  237. gams/transfer/syms/_mixins/generateRecords.py +499 -0
  238. gams/transfer/syms/_mixins/pivot.py +313 -0
  239. gams/transfer/syms/_mixins/pve.py +627 -0
  240. gams/transfer/syms/_mixins/sa.py +27 -0
  241. gams/transfer/syms/_mixins/sapve.py +27 -0
  242. gams/transfer/syms/_mixins/saua.py +27 -0
  243. gams/transfer/syms/_mixins/sauapve.py +199 -0
  244. gams/transfer/syms/_mixins/spve.py +1528 -0
  245. gams/transfer/syms/_mixins/ve.py +936 -0
  246. gams/transfer/syms/container_syms/__init__.py +31 -0
  247. gams/transfer/syms/container_syms/_alias.py +984 -0
  248. gams/transfer/syms/container_syms/_equation.py +333 -0
  249. gams/transfer/syms/container_syms/_parameter.py +973 -0
  250. gams/transfer/syms/container_syms/_set.py +604 -0
  251. gams/transfer/syms/container_syms/_universe_alias.py +461 -0
  252. gams/transfer/syms/container_syms/_variable.py +321 -0
  253. gamsapi-52.5.0.dist-info/METADATA +150 -0
  254. gamsapi-52.5.0.dist-info/RECORD +257 -0
  255. gamsapi-52.5.0.dist-info/WHEEL +5 -0
  256. gamsapi-52.5.0.dist-info/licenses/LICENSE +22 -0
  257. gamsapi-52.5.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,756 @@
1
+ #
2
+ # GAMS - General Algebraic Modeling System Python API
3
+ #
4
+ # Copyright (c) 2017-2026 GAMS Development Corp. <support@gams.com>
5
+ # Copyright (c) 2017-2026 GAMS Software GmbH <support@gams.com>
6
+ #
7
+ # Permission is hereby granted, free of charge, to any person obtaining a copy
8
+ # of this software and associated documentation files (the "Software"), to deal
9
+ # in the Software without restriction, including without limitation the rights
10
+ # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
11
+ # copies of the Software, and to permit persons to whom the Software is
12
+ # furnished to do so, subject to the following conditions:
13
+ #
14
+ # The above copyright notice and this permission notice shall be included in all
15
+ # copies or substantial portions of the Software.
16
+ #
17
+ # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
18
+ # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
19
+ # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
20
+ # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
21
+ # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
22
+ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
23
+ # SOFTWARE.
24
+ #
25
+
26
+ import warnings
27
+ from copy import copy
28
+ import os
29
+ import sys
30
+ import datetime
31
+ from gams.connect.agents._excel import ExcelAgent
32
+ from gams.connect.agents._excel import Workbook
33
+ from gams.connect.connectvalidator import ConnectValidator
34
+ from gams.core.gdx import GMS_SV_UNDEF
35
+ import gams.transfer as gt
36
+ from gams.transfer.syms._methods.tables import (
37
+ _assert_axes_no_nans,
38
+ _get_implied_dimension_from_axes,
39
+ _flatten_and_convert,
40
+ )
41
+ import numpy as np
42
+ from openpyxl.utils.cell import column_index_from_string
43
+ from pandas.api.types import is_datetime64_any_dtype as is_datetime
44
+ import pandas as pd
45
+
46
+
47
+ class ExcelReader(ExcelAgent):
48
+ _index_parameter_map = {
49
+ "rdim": "rowDimension",
50
+ "rowdimension": "rowDimension",
51
+ "cdim": "columnDimension",
52
+ "columndimension": "columnDimension",
53
+ "skipempty": "skipEmpty",
54
+ "se": "skipEmpty",
55
+ "ignoretext": "ignoreText",
56
+ "automerge": "autoMerge",
57
+ "ignorerows": "ignoreRows",
58
+ "ignorecolumns": "ignoreColumns",
59
+ "mergedcells": "mergedCells",
60
+ }
61
+
62
+ def __init__(self, cdb, inst, agent_index):
63
+ super().__init__(cdb, inst, agent_index)
64
+ self._wb = None
65
+ self._parse_options(self._inst)
66
+ if os.path.splitext(self._file)[1] in [".xls"]:
67
+ self._connect_error("The ExcelReader does not support .xls files.")
68
+
69
+ def _parse_options(self, inst):
70
+ inst["file"] = os.path.abspath(inst["file"])
71
+ self._file = inst["file"]
72
+ self._cdim = inst["columnDimension"]
73
+ self._rdim = inst["rowDimension"]
74
+ self._sym_type = inst["type"]
75
+ self._symbols = self._dict_get(inst, "symbols", [])
76
+ self._merged_cells = inst["mergedCells"]
77
+ self._skip_empty = inst["skipEmpty"]
78
+ self._value_subs = inst["valueSubstitutions"]
79
+ self._index_subs = inst["indexSubstitutions"]
80
+ self._index = inst["index"]
81
+ self._auto_merge = inst["autoMerge"]
82
+ self._ignore_text = inst["ignoreText"]
83
+ self._trace = inst["trace"]
84
+ self._is_xlsb = self._file.endswith(".xlsb")
85
+ if self._is_xlsb:
86
+ if not sys.platform.startswith("win"):
87
+ self._connect_error(
88
+ f"Excel binary files (.xlsb) are supported on Windows only."
89
+ )
90
+ self._engine = "xlwings"
91
+ else:
92
+ self._engine = "openpyxl"
93
+
94
+ def _apply_skip_empty(self, dim, idx, skip_empty):
95
+ stop = None
96
+ count = 0
97
+ if dim > 0 and skip_empty > -1:
98
+ for i in range(idx.shape[1]):
99
+ if (np.array(idx[:, i] == None)).all():
100
+ count += 1
101
+ else:
102
+ count = 0
103
+ if count > skip_empty:
104
+ stop = i - skip_empty
105
+ break
106
+ return stop
107
+
108
+ def _create_index(self, dim, idx):
109
+ if dim > 1:
110
+ return pd.MultiIndex.from_arrays(idx)
111
+ else:
112
+ return idx.flatten()
113
+
114
+ def _remove_missing_index(self, values, rdim, cdim, row_idx, col_idx, method):
115
+ def _keep_list(idx):
116
+ keep = list(range(idx.shape[1]))
117
+ for i in reversed(range(idx.shape[1])):
118
+ if method(
119
+ v is None or v != v for v in idx[:, i]
120
+ ): # drop None and float('nan') records
121
+ del keep[i]
122
+ return keep
123
+
124
+ if rdim > 0:
125
+ keep = _keep_list(row_idx)
126
+ row_idx = row_idx[:, keep]
127
+ values = values[keep]
128
+
129
+ if cdim > 0:
130
+ keep = _keep_list(col_idx)
131
+ col_idx = col_idx[:, keep]
132
+ values = values[:, keep]
133
+
134
+ return values, row_idx, col_idx
135
+
136
+ def _apply_auto_merge(self, idx, dim):
137
+ last_label = [None] * dim
138
+ for i in range(idx.shape[1]):
139
+ if any(idx[:, i] != None):
140
+ for j in range(idx.shape[0]):
141
+ if idx[j, i] is None:
142
+ idx[j, i] = last_label[j]
143
+ last_label = idx[:, i]
144
+ return idx
145
+
146
+ def _create_dataframe(self, col_idx, row_idx, values, rdim, cdim):
147
+ # create column and row index used for DataFrame
148
+ col_idx = self._create_index(cdim, col_idx)
149
+ row_idx = self._create_index(rdim, row_idx)
150
+
151
+ if cdim == rdim == 0:
152
+ df = pd.DataFrame(values.flatten())
153
+ elif cdim == 0:
154
+ if values.size == 0:
155
+ return pd.DataFrame([np.nan] * len(row_idx), index=row_idx)
156
+ values = values[:, 0]
157
+ df = pd.DataFrame(values.flatten(), index=row_idx)
158
+ elif rdim == 0:
159
+ if values.size == 0:
160
+ return pd.DataFrame([np.nan] * len(col_idx), index=col_idx)
161
+ values = values[0, :]
162
+ df = pd.DataFrame(values.flatten(), index=col_idx)
163
+ else:
164
+ df = pd.DataFrame(values, index=row_idx, columns=col_idx)
165
+ return df
166
+
167
+ def _resolve_merged_cells(self, sheet, data):
168
+ # TODO: do this only on the used range for better performance
169
+ if self._engine == "xlwings":
170
+ ## Windows and macOS compatible approach
171
+ #last_cell = sheet.used_range.last_cell
172
+ #used = sheet.range(sheet.range("A1"), last_cell)
173
+ #ranges = set()
174
+ #for row in range(used.rows.count):
175
+ # for col in range(used.columns.count):
176
+ # cell = used[row, col]
177
+ # merge_area = cell.merge_area
178
+ # if merge_area.address != cell.address:
179
+ # ranges.add(merge_area)
180
+
181
+ # Windows only approach using COM
182
+ ranges = {sheet.range(c.MergeArea.Address) for c in sheet.api.UsedRange.Cells if c.MergeCells}
183
+ mr = []
184
+ for rng in ranges:
185
+ mr.append(
186
+ (
187
+ rng.column,
188
+ rng.row,
189
+ rng.column + rng.columns.count - 1,
190
+ rng.row + rng.rows.count - 1,
191
+ )
192
+ )
193
+ else:
194
+ mr = [x.bounds for x in sheet.merged_cells.ranges]
195
+ for b in mr:
196
+ nwc, nwr, sec, ser = b
197
+ value = data[nwr - 1][nwc - 1]
198
+ data[nwr - 1 : ser, nwc - 1 : sec] = value
199
+ return data
200
+
201
+ def _convert_dates(self, df):
202
+ for col in df.columns:
203
+ if is_datetime(df[col]):
204
+ df[col] = (
205
+ pd.DatetimeIndex(df[col]).to_julian_date()
206
+ - pd.Timestamp("1899-12-30").to_julian_date()
207
+ )
208
+
209
+ has_datetime = any(
210
+ isinstance(x, datetime.datetime) for x in df.values.flatten()
211
+ )
212
+ if has_datetime:
213
+ if hasattr(pd.DataFrame, "map"):
214
+ df = df.map(
215
+ lambda x: (
216
+ pd.Timestamp(x).to_julian_date()
217
+ - pd.Timestamp("1899-12-30").to_julian_date()
218
+ if isinstance(x, datetime.datetime)
219
+ else x
220
+ )
221
+ )
222
+ else:
223
+ df = df.applymap(
224
+ lambda x: (
225
+ pd.Timestamp(x).to_julian_date()
226
+ - pd.Timestamp("1899-12-30").to_julian_date()
227
+ if isinstance(x, datetime.datetime)
228
+ else x
229
+ )
230
+ )
231
+
232
+ return df
233
+
234
+ def _index_substitutions(self, row_idx, col_idx, rdim, cdim, index_subs):
235
+ if index_subs and rdim + cdim > 0:
236
+ subs = copy(index_subs)
237
+ for k, v in subs.items():
238
+ if k is None:
239
+ break
240
+ if k != k: # check for float('nan')
241
+ if None not in subs.keys():
242
+ subs[None] = v
243
+ break
244
+ if rdim > 0:
245
+ ri_tmp = copy(row_idx)
246
+ for k, v in subs.items():
247
+ # change value in array if either row_idx==k or (element is .nan (row_idx!=row_idx) and key is .nan (k!=k))
248
+ ri_tmp[
249
+ np.logical_or(
250
+ row_idx == k, np.logical_and(row_idx != row_idx, k != k)
251
+ )
252
+ ] = v
253
+ row_idx = ri_tmp
254
+ if cdim > 0:
255
+ ci_tmp = copy(col_idx)
256
+ for k, v in subs.items():
257
+ # change value in array if either col_idx==k or (element is .nan (col_idx!=col_idx) and key is .nan (k!=k))
258
+ ci_tmp[
259
+ np.logical_or(
260
+ col_idx == k, np.logical_and(col_idx != col_idx, k != k)
261
+ )
262
+ ] = v
263
+ col_idx = ci_tmp
264
+ return row_idx, col_idx
265
+ # alternative approach, but much slower
266
+ # vectorized_replace_rows = np.vectorize(lambda v: dict().get(v, v), otypes=[row_idx.dtype])
267
+ # vectorized_replace_cols = np.vectorize(lambda v: dict().get(v, v), otypes=[col_idx.dtype])
268
+ #
269
+ # if rdim > 0:
270
+ # row_idx = vectorized_replace_rows(row_idx)
271
+ # if cdim > 0:
272
+ # col_idx = vectorized_replace_cols(col_idx)
273
+
274
+ def _value_substitutions(self, df, value_sub):
275
+ if value_sub:
276
+ # pandas-version-check
277
+ if self._pandas_version_before(pd.__version__, "2.2"): # pandas < 2.2.0
278
+ df.replace(value_sub, inplace=True)
279
+ else: # pandas >= 2.2.0
280
+ with pd.option_context("future.no_silent_downcasting", True):
281
+ df = df.replace(value_sub).infer_objects()
282
+ return df
283
+
284
+ def _write(self, df, sym_name, sym_type, rdim, cdim):
285
+ if df is None or df.empty or df.isnull().all().all():
286
+ df = None
287
+ # pass DataFrame as Series for rdim=0 or cdim=0 to not confuse gams.transfer with dimensions
288
+ elif (cdim == 0 and rdim != 0) or (rdim == 0 and cdim != 0):
289
+ df = df[0]
290
+
291
+ if sym_type == "par":
292
+ sym = self._cdb.container.addParameter(
293
+ sym_name,
294
+ ["*"] * (rdim + cdim),
295
+ records=df,
296
+ uels_on_axes=True,
297
+ )
298
+ if df is not None:
299
+ sym.dropUndef() # drop float('nan')
300
+ # TODO: remove this section as soon as gams.transfer supports dropping NaN values
301
+ sym.records = self._value_substitutions(
302
+ sym.records, {GMS_SV_UNDEF: gt.SpecialValues.UNDEF}
303
+ )
304
+
305
+ else: # set
306
+ if df is not None:
307
+ # TODO: remove this section as soon as gams.transfer supports dropping NaN values
308
+ # Nan values become empty set element text and we can not drope those values after they are in the container.
309
+ # This is the workaround to handle this
310
+ _assert_axes_no_nans(df)
311
+ dim = _get_implied_dimension_from_axes(df)
312
+ if dim != rdim + cdim:
313
+ self._connect_error(
314
+ f"Dimensionality of table ({dim}) is inconsistent with set domain specification ({rdim+cdim})"
315
+ )
316
+ df = _flatten_and_convert(df)
317
+ df.dropna(inplace=True)
318
+ sym = self._cdb.container.addSet(
319
+ sym_name,
320
+ ["*"] * (rdim + cdim),
321
+ records=df,
322
+ uels_on_axes=False,
323
+ )
324
+
325
+ # For symbols with None records, empty df is assigned
326
+ self._transform_sym_none_to_empty(sym)
327
+
328
+ def _ignore_rows(self, data, offset, ignore_rows):
329
+ nr_rows = data.shape[0]
330
+ r = list(
331
+ filter(
332
+ lambda x: x + offset + 1 not in ignore_rows,
333
+ range(nr_rows),
334
+ )
335
+ )
336
+ data = data[r]
337
+ return data
338
+
339
+ def _ignore_columns(self, data, offset, ignore_columns):
340
+ nr_cols = data.shape[1]
341
+ r = list(
342
+ filter(
343
+ lambda x: x + offset + 1 not in ignore_columns,
344
+ range(nr_cols),
345
+ )
346
+ )
347
+ data = data[:, r]
348
+ return data
349
+
350
+ def _apply_ignore_rows_columns(
351
+ self, data, ignore_rows, ignore_columns, nw_row, nw_col, sym_name
352
+ ):
353
+ # apply ignoreRows
354
+ if ignore_rows is not None:
355
+ data = self._ignore_rows(data, nw_row, ignore_rows)
356
+ if self._trace > 2:
357
+ self._cdb.print_log(
358
+ f"Raw data after ignoreRows ({sym_name}):\n{data}\n"
359
+ )
360
+
361
+ # apply ignoreColumns
362
+ if ignore_columns is not None:
363
+ data = self._ignore_columns(data, nw_col, ignore_columns)
364
+ if self._trace > 2:
365
+ self._cdb.print_log(
366
+ f"Raw data after ignoreColumns ({sym_name}):\n{data}\n"
367
+ )
368
+ return data
369
+
370
+ def _parse_ignore_rows(self, ignore_rows, nw_row, se_row):
371
+ if ignore_rows is None:
372
+ return []
373
+ if isinstance(ignore_rows, int):
374
+ l = [ignore_rows]
375
+ elif isinstance(ignore_rows, str):
376
+ l = self._parse_rows_range(ignore_rows)
377
+ else: # list
378
+ # [9, "4:7", 11] -> [9, 4, 5, 6, 7, 11]
379
+ l = [
380
+ i
381
+ for r in ignore_rows
382
+ for i in (self._parse_rows_range(r) if isinstance(r, str) else [r])
383
+ ]
384
+ l = set(l)
385
+ if se_row is None:
386
+ return list(l)
387
+ l = list(filter(lambda x: x >= nw_row and x <= se_row, l))
388
+ return l
389
+
390
+ def _parse_ignore_columns(self, ignore_columns, nw_col, se_col):
391
+ if ignore_columns is None:
392
+ return []
393
+ if not isinstance(
394
+ ignore_columns, list
395
+ ): # turn int and str values into a list first
396
+ ignore_columns = [ignore_columns]
397
+ l = []
398
+ for c in ignore_columns:
399
+ if isinstance(c, int):
400
+ l.append(c)
401
+ else: # string
402
+ if ":" in c:
403
+ l.extend(self._parse_columns_range(c))
404
+ else:
405
+ l.append(column_index_from_string(c))
406
+ l = set(l)
407
+ if se_col is None:
408
+ return list(l)
409
+ l = list(filter(lambda x: x >= nw_col and x <= se_col, l))
410
+ return l
411
+
412
+ def _read_symbol(self, sym):
413
+ sym_raw = sym.copy()
414
+ self._update_sym_inst(sym, self._inst)
415
+
416
+ sym["range"] = self._dict_get(sym, "range", sym["name"] + "!A1")
417
+ sym["range"] = self.normalize_range(sym["range"])
418
+
419
+ rdim = sym["rowDimension"]
420
+ cdim = sym["columnDimension"]
421
+ sym_range = sym["range"]
422
+ sym_name = sym["name"]
423
+ sym_type = sym["type"]
424
+ merged_cells = sym["mergedCells"]
425
+ value_subs = sym["valueSubstitutions"]
426
+ index_subs = sym["indexSubstitutions"]
427
+ skip_empty = sym["skipEmpty"]
428
+ auto_merge = sym["autoMerge"]
429
+
430
+ sheet, nw_col, nw_row, se_col, se_row, _ = self.parse_range(sym_range, self._wb)
431
+ nw_only = se_col is None and se_row is None
432
+ ignore_rows = self._parse_ignore_rows(sym["ignoreRows"], nw_row, se_row)
433
+ ignore_columns = self._parse_ignore_columns(
434
+ sym["ignoreColumns"], nw_col, se_col
435
+ )
436
+ required_rows = cdim + 1 + len(ignore_rows)
437
+ required_cols = rdim + 1 + len(ignore_columns)
438
+ if not nw_only:
439
+ nr_cols = se_col - nw_col
440
+ nr_rows = se_row - nw_row
441
+
442
+ # handle ignoreText=infer
443
+ if sym["type"] == "set" and sym["ignoreText"] == "infer":
444
+ sym["ignoreText"] = False
445
+ if rdim == 0:
446
+ if (
447
+ nw_only or nr_rows < required_rows
448
+ ): # nw only or range without set element text
449
+ sym["ignoreText"] = True
450
+ if cdim == 0:
451
+ if (
452
+ nw_only or nr_cols < required_cols
453
+ ): # nw only or range without set element text
454
+ sym["ignoreText"] = True
455
+ ignore_text = sym["ignoreText"]
456
+
457
+ if self._trace > 0:
458
+ self._log_instructions(
459
+ sym, sym_raw, description=f"Read symbol >{sym['name']}<:"
460
+ )
461
+
462
+ self._symbols_exist_cdb(sym_name)
463
+
464
+ # check that sets do not have dim=0
465
+ if sym_type == "set" and rdim == 0 and cdim == 0:
466
+ self._connect_error(
467
+ f"Cannot read set >{sym_name}< with both >rowDimension: 0< and >columnDimension: 0<."
468
+ )
469
+
470
+ # check sufficient ranges
471
+ if sym_type == "set" and ignore_text:
472
+ if cdim == 0:
473
+ required_cols -= 1
474
+ elif rdim == 0:
475
+ required_rows -= 1
476
+ if not nw_only:
477
+ if sym_type == "set" and not ignore_text:
478
+ if cdim == 0 and nr_cols == required_cols - 1:
479
+ self._connect_error(
480
+ "Range and rowDimension specification does not contain set element text but ignoreText has been set to False. Adjust range or rowDimension or set >ignoreText: True<."
481
+ )
482
+ if rdim == 0 and nr_rows == required_rows - 1:
483
+ self._connect_error(
484
+ "Range and columnDimension specification does not contain set element text but ignoreText has been set to False. Adjust range or columnDimension or set >ignoreText: True<."
485
+ )
486
+ if nr_rows < required_rows:
487
+ self._connect_error(
488
+ f"Invalid range >{sym_range}<. With columnDimension: >{cdim}< and {len(ignore_rows)} rows to be ignored, the range must include at least {required_rows} rows."
489
+ )
490
+ if nr_cols < required_cols:
491
+ self._connect_error(
492
+ f"Invalid range >{sym_range}<. With rowDimension: >{rdim}< and {len(ignore_columns)} columns to be ignored, the range must include at least {required_cols} columns."
493
+ )
494
+
495
+ data = self._wb.get_sheet_data(sheet)
496
+
497
+ if len(data) == 0: # no data at all
498
+ self._write(None, sym_name, sym_type, rdim, cdim)
499
+ return
500
+
501
+ if self._trace > 2:
502
+ self._cdb.print_log(f"Raw data ({sym_name}) :\n{data}\n")
503
+
504
+ if merged_cells:
505
+ data = self._resolve_merged_cells(sheet, data)
506
+ if self._trace > 2:
507
+ self._cdb.print_log(
508
+ f"Raw data after resolving merged cells ({sym_name}):\n{data}\n"
509
+ )
510
+
511
+ # shrink data to actual range
512
+ data = data[nw_row:se_row, nw_col:se_col]
513
+ if self._trace > 2:
514
+ self._cdb.print_log(
515
+ f"Raw data after shrinking to range ({sym_name}):\n{data}\n"
516
+ )
517
+
518
+ # apply ignoreRows and ignoreColumns
519
+ data = self._apply_ignore_rows_columns(
520
+ data, ignore_rows, ignore_columns, nw_row, nw_col, sym_name
521
+ )
522
+ if data.size == 0:
523
+ self._write(None, sym_name, sym_type, rdim, cdim)
524
+ return
525
+ # if data.shape[0] < required_rows - len(ignore_rows):
526
+ # self._connect_error(
527
+ # f"Insufficient number of data rows ({sym_name}). Require at least {required_rows}, but got {data.shape[0]}."
528
+ # )
529
+ # if data.shape[1] < required_cols - len(ignore_columns):
530
+ # self._connect_error(
531
+ # f"Insufficient number of data columns ({sym_name}). Require at least {required_cols}, but got {data.shape[1]}."
532
+ # )
533
+
534
+ col_idx = data[:cdim, rdim:]
535
+ row_idx = data[cdim:, :rdim].transpose()
536
+
537
+ if self._trace > 2:
538
+ self._cdb.print_log(f"Initial column index ({sym_name}):\n{col_idx}\n")
539
+ self._cdb.print_log(f"Initial row index ({sym_name}):\n{row_idx}\n")
540
+
541
+ # apply skipEmpty only for nw_only, but not for explicit ranges
542
+ if nw_only:
543
+ stop_col = self._apply_skip_empty(cdim, col_idx, skip_empty)
544
+ col_idx = col_idx[:, :stop_col]
545
+ stop_row = self._apply_skip_empty(rdim, row_idx, skip_empty)
546
+ row_idx = row_idx[:, :stop_row]
547
+ if self._trace > 2:
548
+ self._cdb.print_log(
549
+ f"Column index after skipEmpty ({sym_name}):\n{col_idx}\n"
550
+ )
551
+ self._cdb.print_log(
552
+ f"Row index after skipEmpty ({sym_name}):\n{row_idx}\n"
553
+ )
554
+ else:
555
+ stop_col = None
556
+ stop_row = None
557
+
558
+ if stop_col is not None:
559
+ stop_col += rdim
560
+ if stop_row is not None:
561
+ stop_row += cdim
562
+
563
+ if cdim == 0 and rdim == 0: # handle scalars
564
+ stop_row = 1
565
+ stop_col = 1
566
+ if rdim == 0:
567
+ row_idx = np.empty((0, 0)) # dummy array for header
568
+ if cdim == 0:
569
+ col_idx = np.empty((0, 0)) # dummy array for header
570
+
571
+ values = data[cdim:stop_row, rdim:stop_col]
572
+
573
+ if self._trace > 2:
574
+ self._cdb.print_log(f"Values {(sym_name)}: {values}\n")
575
+
576
+ if self._engine == "xlwings":
577
+ col_idx = self._to_int_if_whole(col_idx)
578
+ row_idx = self._to_int_if_whole(row_idx)
579
+ values = self._to_int_if_whole(values)
580
+
581
+ if auto_merge:
582
+ if cdim > 1:
583
+ col_idx = self._apply_auto_merge(col_idx, cdim)
584
+ if rdim > 1:
585
+ row_idx = self._apply_auto_merge(row_idx, rdim)
586
+ if self._trace > 2:
587
+ self._cdb.print_log(
588
+ f"Row index after autoMerge ({sym_name}):\n{row_idx}\n"
589
+ )
590
+ self._cdb.print_log(
591
+ f"Column index after autoMerge ({sym_name}):\n{col_idx}\n"
592
+ )
593
+
594
+ # replace all set text with empty string for ignoreText=True
595
+ if sym_type == "set" and ignore_text:
596
+ if values.size == 0:
597
+ if cdim == 0:
598
+ values = np.empty((values.shape[0], 1), dtype=str)
599
+ elif rdim == 0:
600
+ values = np.empty((1, values.shape[1]), dtype=str)
601
+ else:
602
+ values = np.empty_like(values, dtype=str)
603
+
604
+ if index_subs:
605
+ # remove all-None entries in column and row header and corresponding values
606
+ values, row_idx, col_idx = self._remove_missing_index(
607
+ values, rdim, cdim, row_idx, col_idx, all
608
+ )
609
+ row_idx, col_idx = self._index_substitutions(
610
+ row_idx, col_idx, rdim, cdim, index_subs
611
+ )
612
+ if self._trace > 2:
613
+ self._cdb.print_log(
614
+ f"Row index after indexSubstitutions ({sym_name}):\n{row_idx}\n"
615
+ )
616
+ self._cdb.print_log(
617
+ f"Column index after indexSubstitutions ({sym_name}):\n{col_idx}\n"
618
+ )
619
+
620
+ # remove any-None entries in column and row header and corresponding values
621
+ values, row_idx, col_idx = self._remove_missing_index(
622
+ values, rdim, cdim, row_idx, col_idx, any
623
+ )
624
+
625
+ if self._trace > 2:
626
+ self._cdb.print_log(
627
+ f"Column index before DataFrame creation ({sym_name}):\n{col_idx}\n"
628
+ )
629
+ self._cdb.print_log(
630
+ f"Row index before DataFrame creation ({sym_name}):\n{row_idx}\n"
631
+ )
632
+ self._cdb.print_log(
633
+ f"Values before DataFrame creation ({sym_name}):\n{values}\n"
634
+ )
635
+
636
+ df = self._create_dataframe(col_idx, row_idx, values, rdim, cdim)
637
+
638
+ if self._trace > 2:
639
+ self._cdb.print_log(f"Initial DataFrame ({sym_name}):\n{df}\n")
640
+
641
+ df = self._convert_dates(df)
642
+
643
+ df = self._value_substitutions(df, value_subs)
644
+ if self._trace > 2:
645
+ self._cdb.print_log(
646
+ f"DataFrame after valueSubstitutions ({sym_name}):\n{df}\n"
647
+ )
648
+
649
+ # TODO: This is a workaround to get UNDEF to survive sym.dropNA/sym.dropUndef - remove as soon as gams.transfer supports dropping NaN values
650
+ if sym_type == "par":
651
+ import re
652
+
653
+ pattern = re.compile(r"undef", re.IGNORECASE)
654
+ # pandas-version-check
655
+ if self._pandas_version_before(pd.__version__, "2.2"): # pandas < 2.2.0
656
+ df.replace(regex=pattern, value=GMS_SV_UNDEF, inplace=True)
657
+ else: # pandas >= 2.2.0
658
+ with pd.option_context("future.no_silent_downcasting", True):
659
+ df = df.replace(regex=pattern, value=GMS_SV_UNDEF).infer_objects()
660
+ self._write(df, sym_name, sym_type, rdim, cdim)
661
+
662
+ def _open(self):
663
+ read_only = not (
664
+ any(sym["mergedCells"] for sym in self._symbols) or self._merged_cells
665
+ )
666
+ try:
667
+ self._wb = Workbook(
668
+ self._file, engine=self._engine, read_only=read_only, data_only=True
669
+ ) # data_only=True is required to read values instead of formulas
670
+ except PermissionError as e:
671
+ self._connect_error(
672
+ str(e)
673
+ + "\nThe file may already be open and might need to be closed first."
674
+ )
675
+
676
+ def _read_symbols(self, symbols, validate=False):
677
+ if validate:
678
+ sym_schema = self._cdb.load_schema(self)["symbols"]["schema"]["schema"]
679
+ v = ConnectValidator(sym_schema)
680
+ for i, sym in enumerate(symbols):
681
+ if validate:
682
+ sym = v.validated(sym)
683
+ if sym is None:
684
+ self._connect_error(
685
+ f"Validation of item {i} in index failed: {v.errors}"
686
+ )
687
+ sym = v.normalize_of_rules(sym)
688
+ self._read_symbol(sym)
689
+
690
+ def _create_symbol_instructions(self, rec):
691
+ is_symbol = not None in (rec[0], rec[1], rec[2])
692
+ inst = {}
693
+ if is_symbol:
694
+ inst["type"] = rec[0].lower().strip()
695
+ inst["name"] = rec[1].strip()
696
+ inst["range"] = rec[2].strip()
697
+ if self._trace > 1:
698
+ self._cdb.print_log(
699
+ f"\nParsing symbol >{inst['name']}< with >type: {inst['type']}< and >range: {inst['range']}<."
700
+ )
701
+ if inst["type"] == "dset":
702
+ inst["type"] = "set"
703
+ self._cdb.print_log(
704
+ f"Warning: Processing unsupported >type: dset< as >type: set< for symbol >{inst['name']}<."
705
+ )
706
+ return inst
707
+
708
+ def _read_from_index(self):
709
+ symbols = self.parse_index(self._index, self._wb, self._index_parameter_map)
710
+
711
+ # reopen the file with read_only=False if required
712
+ if not self._merged_cells:
713
+ read_only = not any(
714
+ self._dict_get(sym, "mergedCells", self._merged_cells)
715
+ for sym in symbols
716
+ )
717
+ if not read_only and not self._is_xlsb:
718
+ self._wb.close()
719
+ try:
720
+ self._wb = Workbook(
721
+ self._file,
722
+ engine=self._engine,
723
+ read_only=read_only,
724
+ data_only=True,
725
+ ) # data_only=True is required to read values instead of formulas
726
+ except PermissionError as e:
727
+ self._connect_error(
728
+ str(e)
729
+ + "\nThe file may already be open and might need to be closed first."
730
+ )
731
+ self._read_symbols(symbols, True)
732
+
733
+ def execute(self):
734
+ if self._trace > 0:
735
+ self._log_instructions(self._inst, self._inst_raw)
736
+ self._describe_container(self._cdb.container, "Connect Container (before):")
737
+
738
+ try:
739
+ self._open()
740
+
741
+ if self._index:
742
+ self._read_from_index()
743
+ else:
744
+ self._read_symbols(self._symbols)
745
+ if self._trace > 2:
746
+ for name, sym in self._cdb.container.data.items():
747
+ self._cdb.print_log(
748
+ f"Connect Container symbol >{name}<:\n {sym.records}\n"
749
+ )
750
+ if self._trace > 0:
751
+ self._describe_container(
752
+ self._cdb.container, "Connect Container (after):"
753
+ )
754
+ finally:
755
+ if self._wb is not None:
756
+ self._wb.close()