orchestrator-shell 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,202 @@
1
+ Apache License
2
+ Version 2.0, January 2004
3
+ http://www.apache.org/licenses/
4
+
5
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
6
+
7
+ 1. Definitions.
8
+
9
+ "License" shall mean the terms and conditions for use, reproduction,
10
+ and distribution as defined by Sections 1 through 9 of this document.
11
+
12
+ "Licensor" shall mean the copyright owner or entity authorized by
13
+ the copyright owner that is granting the License.
14
+
15
+ "Legal Entity" shall mean the union of the acting entity and all
16
+ other entities that control, are controlled by, or are under common
17
+ control with that entity. For the purposes of this definition,
18
+ "control" means (i) the power, direct or indirect, to cause the
19
+ direction or management of such entity, whether by contract or
20
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
21
+ outstanding shares, or (iii) beneficial ownership of such entity.
22
+
23
+ "You" (or "Your") shall mean an individual or Legal Entity
24
+ exercising permissions granted by this License.
25
+
26
+ "Source" form shall mean the preferred form for making modifications,
27
+ including but not limited to software source code, documentation
28
+ source, and configuration files.
29
+
30
+ "Object" form shall mean any form resulting from mechanical
31
+ transformation or translation of a Source form, including but
32
+ not limited to compiled object code, generated documentation,
33
+ and conversions to other media types.
34
+
35
+ "Work" shall mean the work of authorship, whether in Source or
36
+ Object form, made available under the License, as indicated by a
37
+ copyright notice that is included in or attached to the work
38
+ (an example is provided in the Appendix below).
39
+
40
+ "Derivative Works" shall mean any work, whether in Source or Object
41
+ form, that is based on (or derived from) the Work and for which the
42
+ editorial revisions, annotations, elaborations, or other modifications
43
+ represent, as a whole, an original work of authorship. For the purposes
44
+ of this License, Derivative Works shall not include works that remain
45
+ separable from, or merely link (or bind by name) to the interfaces of,
46
+ the Work and Derivative Works thereof.
47
+
48
+ "Contribution" shall mean any work of authorship, including
49
+ the original version of the Work and any modifications or additions
50
+ to that Work or Derivative Works thereof, that is intentionally
51
+ submitted to Licensor for inclusion in the Work by the copyright owner
52
+ or by an individual or Legal Entity authorized to submit on behalf of
53
+ the copyright owner. For the purposes of this definition, "submitted"
54
+ means any form of electronic, verbal, or written communication sent
55
+ to the Licensor or its representatives, including but not limited to
56
+ communication on electronic mailing lists, source code control systems,
57
+ and issue tracking systems that are managed by, or on behalf of, the
58
+ Licensor for the purpose of discussing and improving the Work, but
59
+ excluding communication that is conspicuously marked or otherwise
60
+ designated in writing by the copyright owner as "Not a Contribution."
61
+
62
+ "Contributor" shall mean Licensor and any individual or Legal Entity
63
+ on behalf of whom a Contribution has been received by Licensor and
64
+ subsequently incorporated within the Work.
65
+
66
+ 2. Grant of Copyright License. Subject to the terms and conditions of
67
+ this License, each Contributor hereby grants to You a perpetual,
68
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
69
+ copyright license to reproduce, prepare Derivative Works of,
70
+ publicly display, publicly perform, sublicense, and distribute the
71
+ Work and such Derivative Works in Source or Object form.
72
+
73
+ 3. Grant of Patent License. Subject to the terms and conditions of
74
+ this License, each Contributor hereby grants to You a perpetual,
75
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
76
+ (except as stated in this section) patent license to make, have made,
77
+ use, offer to sell, sell, import, and otherwise transfer the Work,
78
+ where such license applies only to those patent claims licensable
79
+ by such Contributor that are necessarily infringed by their
80
+ Contribution(s) alone or by combination of their Contribution(s)
81
+ with the Work to which such Contribution(s) was submitted. If You
82
+ institute patent litigation against any entity (including a
83
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
84
+ or a Contribution incorporated within the Work constitutes direct
85
+ or contributory patent infringement, then any patent licenses
86
+ granted to You under this License for that Work shall terminate
87
+ as of the date such litigation is filed.
88
+
89
+ 4. Redistribution. You may reproduce and distribute copies of the
90
+ Work or Derivative Works thereof in any medium, with or without
91
+ modifications, and in Source or Object form, provided that You
92
+ meet the following conditions:
93
+
94
+ (a) You must give any other recipients of the Work or
95
+ Derivative Works a copy of this License; and
96
+
97
+ (b) You must cause any modified files to carry prominent notices
98
+ stating that You changed the files; and
99
+
100
+ (c) You must retain, in the Source form of any Derivative Works
101
+ that You distribute, all copyright, patent, trademark, and
102
+ attribution notices from the Source form of the Work,
103
+ excluding those notices that do not pertain to any part of
104
+ the Derivative Works; and
105
+
106
+ (d) If the Work includes a "NOTICE" text file as part of its
107
+ distribution, then any Derivative Works that You distribute must
108
+ include a readable copy of the attribution notices contained
109
+ within such NOTICE file, excluding those notices that do not
110
+ pertain to any part of the Derivative Works, in at least one
111
+ of the following places: within a NOTICE text file distributed
112
+ as part of the Derivative Works; within the Source form or
113
+ documentation, if provided along with the Derivative Works; or,
114
+ within a display generated by the Derivative Works, if and
115
+ wherever such third-party notices normally appear. The contents
116
+ of the NOTICE file are for informational purposes only and
117
+ do not modify the License. You may add Your own attribution
118
+ notices within Derivative Works that You distribute, alongside
119
+ or as an addendum to the NOTICE text from the Work, provided
120
+ that such additional attribution notices cannot be construed
121
+ as modifying the License.
122
+
123
+ You may add Your own copyright statement to Your modifications and
124
+ may provide additional or different license terms and conditions
125
+ for use, reproduction, or distribution of Your modifications, or
126
+ for any such Derivative Works as a whole, provided Your use,
127
+ reproduction, and distribution of the Work otherwise complies with
128
+ the conditions stated in this License.
129
+
130
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
131
+ any Contribution intentionally submitted for inclusion in the Work
132
+ by You to the Licensor shall be under the terms and conditions of
133
+ this License, without any additional terms or conditions.
134
+ Notwithstanding the above, nothing herein shall supersede or modify
135
+ the terms of any separate license agreement you may have executed
136
+ with Licensor regarding such Contributions.
137
+
138
+ 6. Trademarks. This License does not grant permission to use the trade
139
+ names, trademarks, service marks, or product names of the Licensor,
140
+ except as required for reasonable and customary use in describing the
141
+ origin of the Work and reproducing the content of the NOTICE file.
142
+
143
+ 7. Disclaimer of Warranty. Unless required by applicable law or
144
+ agreed to in writing, Licensor provides the Work (and each
145
+ Contributor provides its Contributions) on an "AS IS" BASIS,
146
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
147
+ implied, including, without limitation, any warranties or conditions
148
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
149
+ PARTICULAR PURPOSE. You are solely responsible for determining the
150
+ appropriateness of using or redistributing the Work and assume any
151
+ risks associated with Your exercise of permissions under this License.
152
+
153
+ 8. Limitation of Liability. In no event and under no legal theory,
154
+ whether in tort (including negligence), contract, or otherwise,
155
+ unless required by applicable law (such as deliberate and grossly
156
+ negligent acts) or agreed to in writing, shall any Contributor be
157
+ liable to You for damages, including any direct, indirect, special,
158
+ incidental, or consequential damages of any character arising as a
159
+ result of this License or out of the use or inability to use the
160
+ Work (including but not limited to damages for loss of goodwill,
161
+ work stoppage, computer failure or malfunction, or any and all
162
+ other commercial damages or losses), even if such Contributor
163
+ has been advised of the possibility of such damages.
164
+
165
+ 9. Accepting Warranty or Additional Liability. While redistributing
166
+ the Work or Derivative Works thereof, You may choose to offer,
167
+ and charge a fee for, acceptance of support, warranty, indemnity,
168
+ or other liability obligations and/or rights consistent with this
169
+ License. However, in accepting such obligations, You may act only
170
+ on Your own behalf and on Your sole responsibility, not on behalf
171
+ of any other Contributor, and only if You agree to indemnify,
172
+ defend, and hold each Contributor harmless for any liability
173
+ incurred by, or claims asserted against, such Contributor by reason
174
+ of your accepting any such warranty or additional liability.
175
+
176
+ END OF TERMS AND CONDITIONS
177
+
178
+ APPENDIX: How to apply the Apache License to your work.
179
+
180
+ To apply the Apache License to your work, attach the following
181
+ boilerplate notice, with the fields enclosed by brackets "[]"
182
+ Copyright [yyyy] [name of copyright owner]
183
+ replaced with your own identifying information. (Don't include
184
+ the brackets!) The text should be enclosed in the appropriate
185
+ comment syntax for the file format. We also recommend that a
186
+ file or class name and description of purpose be included on the
187
+ same "printed page" as the copyright notice for easier
188
+ identification within third-party archives.
189
+
190
+ Copyright [2024] [Workflow Orchestrator Project]
191
+
192
+ Licensed under the Apache License, Version 2.0 (the "License");
193
+ you may not use this file except in compliance with the License.
194
+ You may obtain a copy of the License at
195
+
196
+ http://www.apache.org/licenses/LICENSE-2.0
197
+
198
+ Unless required by applicable law or agreed to in writing, software
199
+ distributed under the License is distributed on an "AS IS" BASIS,
200
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
201
+ See the License for the specific language governing permissions and
202
+ limitations under the License.
@@ -0,0 +1,36 @@
1
+ Metadata-Version: 2.3
2
+ Name: orchestrator-shell
3
+ Version: 0.1.0
4
+ Summary: Shell for interacting with an orchestrator-core database.
5
+ Requires-Python: >=3.11,<3.14
6
+ Classifier: Intended Audience :: Information Technology
7
+ Classifier: Intended Audience :: System Administrators
8
+ Classifier: Operating System :: OS Independent
9
+ Classifier: Programming Language :: Python :: 3
10
+ Classifier: Programming Language :: Python
11
+ Classifier: Topic :: Internet
12
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
13
+ Classifier: Topic :: Software Development :: Libraries
14
+ Classifier: Topic :: Software Development
15
+ Classifier: Typing :: Typed
16
+ Classifier: Development Status :: 5 - Production/Stable
17
+ Classifier: Environment :: Console
18
+ Classifier: Intended Audience :: Developers
19
+ Classifier: Intended Audience :: Telecommunications Industry
20
+ Classifier: License :: OSI Approved :: Apache Software License
21
+ Classifier: Programming Language :: Python :: 3 :: Only
22
+ Classifier: Programming Language :: Python :: 3.13
23
+ Classifier: Programming Language :: Python :: 3.12
24
+ Classifier: Programming Language :: Python :: 3.11
25
+ Classifier: Topic :: Internet :: WWW/HTTP :: HTTP Servers
26
+ Classifier: Topic :: Internet :: WWW/HTTP
27
+ Requires-Dist: cmd2
28
+ Requires-Dist: orchestrator-core
29
+ Requires-Dist: tabulate
30
+ Requires-Dist: mypy ; extra == "dev"
31
+ Requires-Dist: ruff ; extra == "dev"
32
+ Requires-Dist: types-tabulate ; extra == "dev"
33
+ Requires-Dist: bumpversion ; extra == "dev"
34
+ Project-URL: Documentation, https://workfloworchestrator.org/orchestrator-core/
35
+ Project-URL: Source, https://github.com/workfloworchestrator/wfoshell
36
+ Provides-Extra: dev
@@ -0,0 +1,11 @@
1
+ wfoshell/__init__.py,sha256=jELakGRhZNRoPojr_U7yhSxxnl_IHKDeTjuv4WY2opY,666
2
+ wfoshell/__main__.py,sha256=fiFxm7coqdAmwzPXxSpKhT35yTLVtCtkFpvoLznBb_k,15449
3
+ wfoshell/product_block.py,sha256=YsPb9gPrQj2pmU2rZrYBsG-jKPIQb4rcbCiC69X2sOQ,5455
4
+ wfoshell/resource_type.py,sha256=A6JbrFHRZQRscUKCUFyBCd5YnR9-O_GBKXOXw7Qim5w,3221
5
+ wfoshell/settings.py,sha256=iDnQvOBQtb1SCxnTq052CtZqEPc2nThZnNtt5sLcmhI,999
6
+ wfoshell/state.py,sha256=53A7t9I4NiFkmP1tKbkdXYqWuRv-vlKtWdZE_YmsWrw,6312
7
+ wfoshell/subscripition.py,sha256=0lbcNgqwQPpOv7YNEJJkbxEe5ggEoaQtOGCsb4MD554,4583
8
+ orchestrator_shell-0.1.0.dist-info/LICENSE,sha256=mArBeJXIz9xnP96SRvhrKyjGFnJDg9Eu0SbLyUiKRHk,11408
9
+ orchestrator_shell-0.1.0.dist-info/WHEEL,sha256=CpUCUxeHQbRN5UGRQHYRJorO5Af-Qy_fHMctcQ8DSGI,82
10
+ orchestrator_shell-0.1.0.dist-info/METADATA,sha256=bh0DIURYzfgcEGMk642o5POepMEutOtMIMdY4UAdyX0,1616
11
+ orchestrator_shell-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: flit 3.10.1
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
wfoshell/__init__.py ADDED
@@ -0,0 +1,15 @@
1
+ # Copyright 2024 SURF.
2
+ # Licensed under the Apache License, Version 2.0 (the "License");
3
+ # you may not use this file except in compliance with the License.
4
+ # You may obtain a copy of the License at
5
+ #
6
+ # http://www.apache.org/licenses/LICENSE-2.0
7
+ #
8
+ # Unless required by applicable law or agreed to in writing, software
9
+ # distributed under the License is distributed on an "AS IS" BASIS,
10
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
11
+ # See the License for the specific language governing permissions and
12
+ # limitations under the License.
13
+ """Shell for interacting with an orchestrator-core database."""
14
+
15
+ __version__ = "0.1.0"
wfoshell/__main__.py ADDED
@@ -0,0 +1,313 @@
1
+ # Copyright 2024 SURF.
2
+ # Licensed under the Apache License, Version 2.0 (the "License");
3
+ # you may not use this file except in compliance with the License.
4
+ # You may obtain a copy of the License at
5
+ #
6
+ # http://www.apache.org/licenses/LICENSE-2.0
7
+ #
8
+ # Unless required by applicable law or agreed to in writing, software
9
+ # distributed under the License is distributed on an "AS IS" BASIS,
10
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
11
+ # See the License for the specific language governing permissions and
12
+ # limitations under the License.
13
+
14
+ from argparse import Namespace
15
+ from datetime import datetime
16
+
17
+ from cmd2 import Cmd, Cmd2ArgumentParser, Statement, with_argparser
18
+ from orchestrator.db import init_database
19
+
20
+ import wfoshell.product_block
21
+ import wfoshell.resource_type
22
+ import wfoshell.state
23
+ import wfoshell.subscripition
24
+ from wfoshell.settings import settings
25
+ from wfoshell.state import state
26
+
27
+
28
+ class WFOshell(Cmd):
29
+ """WorkFlow Orchestrator shell."""
30
+
31
+ intro = "Welcome to the WFO shell.\n" "Type help or ? to list commands."
32
+
33
+ def __init__(self) -> None:
34
+ """WFO shell initialisation."""
35
+ super().__init__(
36
+ persistent_history_file=str(settings.WFOSHELL_HISTFILE),
37
+ persistent_history_length=settings.WFOSHELL_HISTFILE_SIZE,
38
+ )
39
+ self.prompt = "(wfo) "
40
+ self.hidden_commands.extend(["alias", "edit", "macro", "run_pyscript", "run_script", "shell", "shortcuts"])
41
+ init_database(settings) # type: ignore[arg-type]
42
+
43
+ def do_exit(self, line: Statement) -> bool: # noqa: ARG002
44
+ """Exit the application."""
45
+ return True
46
+
47
+ # subcommand functions for the subscription command
48
+ def subscription_list(self, args: Namespace) -> None: # noqa: ARG002
49
+ """List subcommand of subscription command."""
50
+ self.poutput(wfoshell.subscripition.subscription_list())
51
+
52
+ def subscription_search(self, args: Namespace) -> None:
53
+ """Search subcommand of subscription command."""
54
+ self.poutput(wfoshell.subscripition.subscription_search(args.regular_expression))
55
+
56
+ def subscription_select(self, args: Namespace) -> None:
57
+ """Select subcommand of subscription command."""
58
+ number_of_subscriptions = (
59
+ len(state.filtered_subscriptions) if state.filtered_subscriptions is not None else len(state.subscriptions)
60
+ )
61
+ if not number_of_subscriptions:
62
+ self.pwarning("list or search for subscriptions first")
63
+ elif not 0 <= args.index < number_of_subscriptions:
64
+ self.pwarning(f"selected subscription index not between 0 and {number_of_subscriptions - 1}")
65
+ else:
66
+ self.poutput(wfoshell.subscripition.subscription_select(args.index))
67
+
68
+ def subscription_details(self, args: Namespace) -> None:
69
+ """Details subcommand of subscription command."""
70
+ if state.subscription_index is None:
71
+ self.pwarning("first select a subscription")
72
+ else:
73
+ self.poutput(
74
+ wfoshell.subscripition.subscription_details(
75
+ subscription_only=args.subscription_only, product_blocks_only=args.product_blocks_only
76
+ )
77
+ )
78
+
79
+ def subscription_update(self, args: Namespace) -> None: # noqa: C901
80
+ """Update subcommand of subscription command."""
81
+ if state.subscription_index is None:
82
+ self.pwarning("first select a subscription")
83
+ return
84
+ if args.field in ["insync"]:
85
+ if args.new_value.lower() in ["y", "yes", "true"]:
86
+ args.new_value = True
87
+ elif args.new_value.lower() in ["n", "no", "false"]:
88
+ args.new_value = False
89
+ else:
90
+ self.pwarning("expected y, yes, true, n, no or false")
91
+ return
92
+ if args.field in ["start_date", "end_date"]:
93
+ if args.new_value == "":
94
+ args.new_value = None
95
+ else:
96
+ try:
97
+ args.new_value = datetime.fromisoformat(args.new_value)
98
+ except ValueError as value_error:
99
+ self.pwarning(str(value_error))
100
+ return
101
+ if args.new_value.tzinfo is None:
102
+ args.new_value = args.new_value.astimezone()
103
+ wfoshell.subscripition.subscription_update(args.field, args.new_value)
104
+
105
+ # subscription (sub)commands argument parsers
106
+ s_parser = Cmd2ArgumentParser()
107
+ s_subparser = s_parser.add_subparsers(title="subscription subcommands")
108
+ s_list_parser = s_subparser.add_parser("list", help="list all subscriptions from database")
109
+ s_list_parser.set_defaults(func=subscription_list)
110
+ s_search_parser = s_subparser.add_parser("search", help="case insensitive search subscription descriptions")
111
+ s_search_parser.add_argument("regular_expression", type=str, help="match description on regular expression")
112
+ s_search_parser.set_defaults(func=subscription_search)
113
+ s_select_parser = s_subparser.add_parser("select", help="select subscription to work on")
114
+ s_select_parser.add_argument("index", type=int, help="select by index number")
115
+ s_select_parser.set_defaults(func=subscription_select)
116
+ s_details_parser = s_subparser.add_parser("details", help="show subscription details")
117
+ s_details_parser.add_argument("--subscription_only", action="store_true", help="show subscription details only")
118
+ s_details_parser.add_argument("--product_blocks_only", action="store_true", help="show product block details only")
119
+ s_details_parser.set_defaults(func=subscription_details)
120
+ s_update_parser = s_subparser.add_parser("update", help="update subscription field")
121
+ s_update_parser.add_argument(
122
+ "field",
123
+ choices=[
124
+ "description",
125
+ "status",
126
+ "customer_id",
127
+ "insync",
128
+ "start_date",
129
+ "end_date",
130
+ "note",
131
+ ],
132
+ help="subscription field",
133
+ )
134
+ s_update_parser.add_argument("new_value", type=str, help="new value for selected subscription field")
135
+ s_update_parser.set_defaults(func=subscription_update)
136
+
137
+ # subscription command
138
+ @with_argparser(s_parser)
139
+ def do_subscription(self, args: Namespace) -> None:
140
+ """List, search or select subscriptions, update fields, and show details."""
141
+ if func := getattr(args, "func", None):
142
+ func(self, args)
143
+ else:
144
+ self.do_help("subscription")
145
+
146
+ # subcommand functions for the product_block command
147
+ def product_block_list(self, args: Namespace) -> None: # noqa: ARG002
148
+ """List subcommand of product_block command."""
149
+ if state.subscription_index is None:
150
+ self.pwarning("first select a subscription")
151
+ else:
152
+ self.poutput(wfoshell.product_block.product_block_list())
153
+
154
+ def product_block_select(self, args: Namespace) -> None:
155
+ """Select subcommand of product_block command."""
156
+ if not (number_of_product_blocks := len(state.selected_product_blocks)):
157
+ self.pwarning("list or search for product_blocks first")
158
+ elif not 0 <= args.index < number_of_product_blocks:
159
+ self.pwarning(f"selected product_block index not between 0 and {number_of_product_blocks - 1}")
160
+ else:
161
+ self.poutput(wfoshell.product_block.product_block_select(args.index))
162
+
163
+ def product_block_details(self, args: Namespace) -> None:
164
+ """Details subcommand of product_block command."""
165
+ if state.product_block_index is None:
166
+ self.pwarning("first select a product_block")
167
+ else:
168
+ self.poutput(
169
+ wfoshell.product_block.product_block_details(
170
+ product_block_only=args.product_block_only,
171
+ resource_types_only=args.resource_types_only,
172
+ depends_on_only=args.depends_on_only,
173
+ in_use_by_only=args.in_use_by_only,
174
+ )
175
+ )
176
+
177
+ def product_block_depends_on(self, args: Namespace) -> None:
178
+ """Depends_on subcommand of product_block command."""
179
+ if state.product_block_index is None:
180
+ self.pwarning("first select a product block")
181
+ elif not (number_of_depends_on := len(state.selected_product_block.depends_on)):
182
+ self.pwarning("no depend on product blocks")
183
+ elif not 0 <= args.index < number_of_depends_on:
184
+ self.pwarning(f"selected product_block index not between 0 and {number_of_depends_on - 1}")
185
+ else:
186
+ self.poutput(wfoshell.product_block.product_block_depends_on(args.index))
187
+
188
+ def product_block_in_use_by(self, args: Namespace) -> None:
189
+ """In_use_by subcommand of product_block command."""
190
+ if state.product_block_index is None:
191
+ self.pwarning("first select a product block")
192
+ elif not (number_of_in_use_by := len(state.selected_product_block.in_use_by)):
193
+ self.pwarning("no in use by product blocks")
194
+ elif not 0 <= args.index < number_of_in_use_by:
195
+ self.pwarning(f"selected product_block index not between 0 and {number_of_in_use_by - 1}")
196
+ else:
197
+ self.poutput(wfoshell.product_block.product_block_in_use_by(args.index))
198
+
199
+ # product_block (sub)commands argument parsers
200
+ pb_parser = Cmd2ArgumentParser()
201
+ pb_subparser = pb_parser.add_subparsers(title="product_block subcommands")
202
+ pb_list_parser = pb_subparser.add_parser("list", help="list product blocks of current selected subscription")
203
+ pb_list_parser.set_defaults(func=product_block_list)
204
+ pb_select_parser = pb_subparser.add_parser("select", help="select product block to work on")
205
+ pb_select_parser.add_argument("index", type=int, help="select by index number")
206
+ pb_select_parser.set_defaults(func=product_block_select)
207
+ pb_details_parser = pb_subparser.add_parser("details", help="show product block details")
208
+ pb_details_parser.add_argument("--product_block_only", action="store_true", help="show product block details only")
209
+ pb_details_parser.add_argument("--resource_types_only", action="store_true", help="show resource type details only")
210
+ pb_details_parser.add_argument("--depends_on_only", action="store_true", help="show depends on details only")
211
+ pb_details_parser.add_argument("--in_use_by_only", action="store_true", help="show in use by details only")
212
+ pb_details_parser.set_defaults(func=product_block_details)
213
+ pb_depends_on_parser = pb_subparser.add_parser("depends_on", help="show depends on product blocks")
214
+ pb_depends_on_parser.add_argument("index", type=int, help="select by index number")
215
+ pb_depends_on_parser.set_defaults(func=product_block_depends_on)
216
+ pb_is_use_by_parser = pb_subparser.add_parser("in_use_by", help="show in use by product blocks")
217
+ pb_is_use_by_parser.add_argument("index", type=int, help="select by index number")
218
+ pb_is_use_by_parser.set_defaults(func=product_block_in_use_by)
219
+
220
+ # product_block command
221
+ @with_argparser(pb_parser)
222
+ def do_product_block(self, args: Namespace) -> None:
223
+ """List and select product blocks, show details, or follow depends on and in use by product blocks."""
224
+ if func := getattr(args, "func", None):
225
+ func(self, args)
226
+ else:
227
+ self.do_help("product_block")
228
+
229
+ # subcommand functions for the resource_type command
230
+ def resource_type_list(self, args: Namespace) -> None: # noqa: ARG002
231
+ """List subcommand of resource_type command."""
232
+ if state.product_block_index is None:
233
+ self.pwarning("first select a product block")
234
+ else:
235
+ self.poutput(wfoshell.resource_type.resource_type_list())
236
+
237
+ def resource_type_select(self, args: Namespace) -> None:
238
+ """Select subcommand of resource_type command."""
239
+ if not (number_of_resource_types := len(state.selected_resource_types)):
240
+ self.pwarning("list or search for resource_types first")
241
+ elif not 0 <= args.index < number_of_resource_types:
242
+ self.pwarning(f"selected resource_type index not between 0 and {number_of_resource_types - 1}")
243
+ else:
244
+ self.poutput(wfoshell.resource_type.resource_type_select(args.index))
245
+
246
+ def resource_type_details(self, args: Namespace) -> None: # noqa: ARG002
247
+ """Details subcommand of resource_type command."""
248
+ if state.resource_type_index is None:
249
+ self.pwarning("first select a resource_type")
250
+ else:
251
+ self.poutput(wfoshell.resource_type.resource_type_details())
252
+
253
+ def resource_type_update(self, args: Namespace) -> None:
254
+ """Update subcommand of resource_type command."""
255
+ if state.resource_type_index is None:
256
+ self.pwarning("first select a resource_type")
257
+ else:
258
+ wfoshell.resource_type.resource_type_update(args.new_value)
259
+
260
+ # resource_type (sub)commands argument parsers
261
+ rt_parser = Cmd2ArgumentParser()
262
+ rt_subparser = rt_parser.add_subparsers(title="resource_type subcommands")
263
+ rt_list_parser = rt_subparser.add_parser("list", help="list resource types of current selected product block")
264
+ rt_list_parser.set_defaults(func=resource_type_list)
265
+ rt_select_parser = rt_subparser.add_parser("select", help="select resource type to work on")
266
+ rt_select_parser.add_argument("index", type=int, help="select by index number")
267
+ rt_select_parser.set_defaults(func=resource_type_select)
268
+ rt_details_parser = rt_subparser.add_parser("details", help="show resource type details")
269
+ rt_details_parser.set_defaults(func=resource_type_details)
270
+ rt_update_parser = rt_subparser.add_parser("update", help="update selected resource type")
271
+ rt_update_parser.add_argument("new_value", type=str, help="new value for selected resource type")
272
+ rt_update_parser.set_defaults(func=resource_type_update)
273
+
274
+ # resource_type command
275
+ @with_argparser(rt_parser)
276
+ def do_resource_type(self, args: Namespace) -> None:
277
+ """List, select and update resource types, and show details."""
278
+ if func := getattr(args, "func", None):
279
+ func(self, args)
280
+ else:
281
+ self.do_help("resource_type")
282
+
283
+ # subcommand functions for the state command
284
+ def state_summary(self, args: Namespace) -> None: # noqa: ARG002
285
+ """summary subcommand of state command."""
286
+ if summary := state.summary:
287
+ self.poutput(summary)
288
+
289
+ def state_details(self, args: Namespace) -> None: # noqa: ARG002
290
+ """details subcommand of state command."""
291
+ self.poutput(state.details)
292
+
293
+ # state (sub)commands argument parsers
294
+ state_parser = Cmd2ArgumentParser()
295
+ state_subparser = state_parser.add_subparsers(title="state subcommands")
296
+ state_summary_parser = state_subparser.add_parser("summary", help="show state summary")
297
+ state_summary_parser.set_defaults(func=state_summary)
298
+ state_details_parser = state_subparser.add_parser("details", help="show state details")
299
+ state_details_parser.set_defaults(func=state_details)
300
+
301
+ # resource_type command
302
+ @with_argparser(state_parser)
303
+ def do_state(self, args: Namespace) -> None:
304
+ """Show state summary or details."""
305
+ if func := getattr(args, "func", None):
306
+ func(self, args)
307
+ else:
308
+ self.do_help("state")
309
+
310
+
311
+ if __name__ == "__main__":
312
+ shell = WFOshell()
313
+ shell.cmdloop()
@@ -0,0 +1,131 @@
1
+ # Copyright 2024 SURF.
2
+ # Licensed under the Apache License, Version 2.0 (the "License");
3
+ # you may not use this file except in compliance with the License.
4
+ # You may obtain a copy of the License at
5
+ #
6
+ # http://www.apache.org/licenses/LICENSE-2.0
7
+ #
8
+ # Unless required by applicable law or agreed to in writing, software
9
+ # distributed under the License is distributed on an "AS IS" BASIS,
10
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
11
+ # See the License for the specific language governing permissions and
12
+ # limitations under the License.
13
+
14
+
15
+ from orchestrator.db import SubscriptionInstanceTable
16
+ from tabulate import tabulate
17
+
18
+ from wfoshell.resource_type import resource_type_table
19
+ from wfoshell.state import all_resource_types, state
20
+
21
+
22
+ def product_block_table(product_blocks: list[SubscriptionInstanceTable]) -> str:
23
+ """Return indexed table of product blocks."""
24
+ max_rt_width = max([len(rt.resource_type.resource_type) for pb in product_blocks for rt in all_resource_types(pb)])
25
+ return tabulate(
26
+ [
27
+ [
28
+ tabulate(
29
+ [
30
+ ["name", product_block.product_block.name],
31
+ ["resource types", resource_type_table(all_resource_types(product_block), max_rt_width)],
32
+ ],
33
+ tablefmt="plain",
34
+ )
35
+ ]
36
+ for product_block in product_blocks
37
+ ],
38
+ tablefmt="plain",
39
+ disable_numparse=True,
40
+ showindex=True,
41
+ )
42
+
43
+
44
+ def details_product_block(product_block: SubscriptionInstanceTable) -> list[tuple[str, str]]:
45
+ """Return list of tuples with product block details only."""
46
+ return [
47
+ ("name", product_block.product_block.name),
48
+ ("subscription_instance_id", product_block.subscription_instance_id),
49
+ ("subscription_id", product_block.subscription_id),
50
+ ("product_block_id", product_block.product_block_id),
51
+ ("label", product_block.label),
52
+ ]
53
+
54
+
55
+ def details_resource_types(product_block: SubscriptionInstanceTable) -> list[tuple[str, str]]:
56
+ """Return list of tuples with resource type details only."""
57
+ return [
58
+ ("resource types", resource_type_table(all_resource_types(product_block))),
59
+ ]
60
+
61
+
62
+ def details_depends_on(product_block: SubscriptionInstanceTable) -> list[tuple[str, str]]:
63
+ """Return list of tuples with depends on details only."""
64
+ return [
65
+ ("depends_on", product_block_table(product_block.depends_on) if product_block.depends_on else ""),
66
+ ]
67
+
68
+
69
+ def details_in_use_by(product_block: SubscriptionInstanceTable) -> list[tuple[str, str]]:
70
+ """Return list of tuples with in use by details only."""
71
+ return [
72
+ ("in_use_by", product_block_table(product_block.in_use_by) if product_block.in_use_by else ""),
73
+ ]
74
+
75
+
76
+ def details_all(product_block: SubscriptionInstanceTable) -> list[tuple[str, str]]:
77
+ """Return list of tuples with all product block details."""
78
+ return (
79
+ details_product_block(product_block)
80
+ + details_resource_types(product_block)
81
+ + details_depends_on(product_block)
82
+ + details_in_use_by(product_block)
83
+ )
84
+
85
+
86
+ def product_block_list() -> str:
87
+ """Implementation of the 'product_block list' subcommand."""
88
+ return product_block_table(state.selected_product_blocks)
89
+
90
+
91
+ def product_block_select(index: int) -> str:
92
+ """Implementation of the 'product_block select' subcommand."""
93
+ state.product_block_index = index
94
+ state.resource_type_index = None
95
+ return state.summary
96
+
97
+
98
+ def product_block_details(
99
+ product_block_only: bool, resource_types_only: bool, depends_on_only: bool, in_use_by_only: bool
100
+ ) -> str:
101
+ """Implementation of the 'product_block details' subcommand."""
102
+ if product_block_only:
103
+ return tabulate(details_product_block(state.selected_product_block), tablefmt="plain")
104
+ elif resource_types_only: # noqa: RET505
105
+ return tabulate(details_resource_types(state.selected_product_block), tablefmt="plain")
106
+ elif depends_on_only:
107
+ return tabulate(details_depends_on(state.selected_product_block), tablefmt="plain")
108
+ elif in_use_by_only:
109
+ return tabulate(details_in_use_by(state.selected_product_block), tablefmt="plain")
110
+ else:
111
+ return tabulate(details_all(state.selected_product_block), tablefmt="plain")
112
+
113
+
114
+ def product_block_depends_on(index: int) -> str:
115
+ """Implementation of the 'product_block depends_on' subcommand."""
116
+ depends_on_product_block = state.selected_product_block.depends_on[index]
117
+ state.subscription_index = state.subscriptions.index(depends_on_product_block.subscription)
118
+ # note that the selected_product_blocks list below is of the subscription selected just above
119
+ state.product_block_index = state.selected_product_blocks.index(depends_on_product_block)
120
+ state.resource_type_index = None
121
+ return state.summary
122
+
123
+
124
+ def product_block_in_use_by(index: int) -> str:
125
+ """Implementation of the 'product_block in_use_by' subcommand."""
126
+ in_use_by_product_block = state.selected_product_block.in_use_by[index]
127
+ state.subscription_index = state.subscriptions.index(in_use_by_product_block.subscription)
128
+ # note that the selected_product_blocks list below is of the subscription selected just above
129
+ state.product_block_index = state.selected_product_blocks.index(in_use_by_product_block)
130
+ state.resource_type_index = None
131
+ return state.summary
@@ -0,0 +1,82 @@
1
+ # Copyright 2024 SURF.
2
+ # Licensed under the Apache License, Version 2.0 (the "License");
3
+ # you may not use this file except in compliance with the License.
4
+ # You may obtain a copy of the License at
5
+ #
6
+ # http://www.apache.org/licenses/LICENSE-2.0
7
+ #
8
+ # Unless required by applicable law or agreed to in writing, software
9
+ # distributed under the License is distributed on an "AS IS" BASIS,
10
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
11
+ # See the License for the specific language governing permissions and
12
+ # limitations under the License.
13
+
14
+
15
+ import tabulate
16
+ from orchestrator.db import SubscriptionInstanceValueTable, db, transactional
17
+ from structlog import get_logger
18
+
19
+ from wfoshell.state import sorted_resource_types, state
20
+
21
+ logger = get_logger(__name__)
22
+ tabulate.PRESERVE_WHITESPACE = True
23
+
24
+
25
+ def resource_type_table(resource_types: list[SubscriptionInstanceValueTable], width: int = 0) -> str:
26
+ """Return indexed table of resource types, with name optionally aligned on width."""
27
+ return tabulate.tabulate(
28
+ [
29
+ [
30
+ resource_type.resource_type.resource_type.ljust(width),
31
+ resource_type.value if resource_type.value is not None else "<unset or non-scalar>",
32
+ ]
33
+ for resource_type in sorted_resource_types(resource_types)
34
+ ],
35
+ tablefmt="plain",
36
+ disable_numparse=True,
37
+ showindex=True,
38
+ )
39
+
40
+
41
+ def details(resource_type: SubscriptionInstanceValueTable | None) -> list[tuple[str, str]]:
42
+ """Return list of tuples with resource type detail information."""
43
+ if resource_type is None:
44
+ return []
45
+ return [
46
+ ("resource_type", resource_type.resource_type.resource_type),
47
+ ("value", resource_type.value),
48
+ ("subscription_instance_value_id", resource_type.subscription_instance_value_id),
49
+ ("subscription_instance_id", resource_type.subscription_instance_id),
50
+ ("resource_type_id", resource_type.resource_type_id),
51
+ ]
52
+
53
+
54
+ def resource_type_list() -> str:
55
+ """Implementation of the 'resource_type list' subcommand."""
56
+ return resource_type_table(state.selected_resource_types)
57
+
58
+
59
+ def resource_type_select(index: int) -> str:
60
+ """Implementation of the 'resource_type select' subcommand."""
61
+ state.resource_type_index = index
62
+ return state.summary
63
+
64
+
65
+ def resource_type_details() -> str:
66
+ """Implementation of the 'resource_type details' subcommand."""
67
+ return tabulate.tabulate(details(state.selected_resource_type), tablefmt="plain")
68
+
69
+
70
+ def resource_type_update(new_value: str) -> None:
71
+ """Implementation of the 'resource_type update' subcommand."""
72
+ with transactional(db, logger):
73
+ if state.selected_resource_type.value is None:
74
+ # add previously unset resource type to list of product block values
75
+ state.selected_product_block.values.append(
76
+ SubscriptionInstanceValueTable(
77
+ resource_type_id=state.selected_resource_type.resource_type.resource_type_id, value=new_value
78
+ )
79
+ )
80
+ else:
81
+ # otherwise just update the existing resource type value
82
+ state.selected_resource_type.value = new_value
wfoshell/settings.py ADDED
@@ -0,0 +1,28 @@
1
+ # Copyright 2024 SURF.
2
+ # Licensed under the Apache License, Version 2.0 (the "License");
3
+ # you may not use this file except in compliance with the License.
4
+ # You may obtain a copy of the License at
5
+ #
6
+ # http://www.apache.org/licenses/LICENSE-2.0
7
+ #
8
+ # Unless required by applicable law or agreed to in writing, software
9
+ # distributed under the License is distributed on an "AS IS" BASIS,
10
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
11
+ # See the License for the specific language governing permissions and
12
+ # limitations under the License.
13
+
14
+ from pathlib import Path
15
+
16
+ from pydantic.networks import PostgresDsn
17
+ from pydantic_settings import BaseSettings
18
+
19
+
20
+ class Settings(BaseSettings):
21
+ """WFO Shell settings."""
22
+
23
+ DATABASE_URI: PostgresDsn = "postgresql://nwa:nwa@localhost/orchestrator-core" # type: ignore[assignment]
24
+ WFOSHELL_HISTFILE: Path = Path("~/.wfoshell_history").expanduser()
25
+ WFOSHELL_HISTFILE_SIZE: int = 1000
26
+
27
+
28
+ settings = Settings()
wfoshell/state.py ADDED
@@ -0,0 +1,153 @@
1
+ # Copyright 2024 SURF.
2
+ # Licensed under the Apache License, Version 2.0 (the "License");
3
+ # you may not use this file except in compliance with the License.
4
+ # You may obtain a copy of the License at
5
+ #
6
+ # http://www.apache.org/licenses/LICENSE-2.0
7
+ #
8
+ # Unless required by applicable law or agreed to in writing, software
9
+ # distributed under the License is distributed on an "AS IS" BASIS,
10
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
11
+ # See the License for the specific language governing permissions and
12
+ # limitations under the License.
13
+
14
+ from dataclasses import dataclass, field
15
+
16
+ from orchestrator.db import SubscriptionInstanceTable, SubscriptionInstanceValueTable, SubscriptionTable
17
+ from tabulate import tabulate
18
+
19
+
20
+ @dataclass
21
+ class State:
22
+ """State that is shared between the WFO shell commands."""
23
+
24
+ subscriptions: list[SubscriptionTable] = field(default_factory=list)
25
+ filtered_subscriptions: list[SubscriptionTable] | None = None
26
+ subscription_index: int | None = None
27
+ product_block_index: int | None = None
28
+ resource_type_index: int | None = None
29
+
30
+ @property
31
+ def selected_subscription(self) -> SubscriptionTable:
32
+ """Return the subscription indexed by subscription_index."""
33
+ if self.subscription_index is not None:
34
+ return self.subscriptions[self.subscription_index]
35
+ raise IndexError("subscription_index not set")
36
+
37
+ @property
38
+ def selected_product_blocks(self) -> list[SubscriptionInstanceTable]:
39
+ """Return sorted list of product blocks for the subscription indexed by subscription_index."""
40
+ return (
41
+ (sorted_product_blocks(self.selected_subscription.instances)) if self.subscription_index is not None else []
42
+ )
43
+
44
+ @property
45
+ def selected_product_block(self) -> SubscriptionInstanceTable:
46
+ """Return the product block indexed by product_block_index."""
47
+ if self.product_block_index is not None:
48
+ return self.selected_product_blocks[self.product_block_index]
49
+ raise IndexError("product_block_index not set")
50
+
51
+ @property
52
+ def selected_resource_types(self) -> list[SubscriptionInstanceValueTable]:
53
+ """Return sorted list of resource types for the product block indexed by product_block_index."""
54
+ return (
55
+ sorted_resource_types(all_resource_types(self.selected_product_block))
56
+ if self.product_block_index is not None
57
+ else []
58
+ )
59
+
60
+ @property
61
+ def selected_resource_type(self) -> SubscriptionInstanceValueTable:
62
+ """Return the resource type indexed by resource_type_index."""
63
+ if self.resource_type_index is not None:
64
+ return self.selected_resource_types[self.resource_type_index]
65
+ raise IndexError("resource_type_index not set")
66
+
67
+ @property
68
+ def summary(self) -> str:
69
+ """List summary of the selected subscription, product block and resource type."""
70
+ summary = []
71
+ if self.subscription_index is not None:
72
+ summary.append(
73
+ (
74
+ "subscription",
75
+ self.selected_subscription.description,
76
+ self.selected_subscription.subscription_id,
77
+ )
78
+ )
79
+ if self.product_block_index is not None:
80
+ summary.append(
81
+ (
82
+ "product block",
83
+ self.selected_product_block.product_block.name,
84
+ self.selected_product_block.subscription_instance_id,
85
+ ),
86
+ )
87
+ if self.resource_type_index is not None:
88
+ rt = self.selected_resource_type
89
+ summary.append(
90
+ (
91
+ "resource_type",
92
+ rt.resource_type.resource_type,
93
+ rt.subscription_instance_value_id if rt.value is not None else "<unset or non-scalar>",
94
+ ),
95
+ )
96
+ return tabulate(summary, tablefmt="plain")
97
+
98
+ @property
99
+ def details(self) -> str:
100
+ """Show state details."""
101
+ return tabulate(
102
+ [
103
+ ("number of subscriptions", len(self.subscriptions)),
104
+ (
105
+ "number of filtered subscriptions",
106
+ len(self.filtered_subscriptions) if self.filtered_subscriptions is not None else "0",
107
+ ),
108
+ ("subscription index", self.subscription_index if self.subscription_index is not None else "unset"),
109
+ ("product block index", self.product_block_index if self.subscription_index is not None else "unset"),
110
+ ("resource type index", self.resource_type_index if self.subscription_index is not None else "unset"),
111
+ ("currently selected", self.summary),
112
+ ],
113
+ tablefmt="plain",
114
+ )
115
+
116
+
117
+ state = State()
118
+
119
+
120
+ def all_resource_types(product_block: SubscriptionInstanceTable) -> list[SubscriptionInstanceValueTable]:
121
+ """Add optional unset resource type(s) with value None to list of already set resource types."""
122
+ return list(
123
+ (
124
+ {
125
+ rt.resource_type: SubscriptionInstanceValueTable(
126
+ resource_type_id=rt.resource_type_id, resource_type=rt, value=None
127
+ )
128
+ for rt in product_block.product_block.resource_types
129
+ }
130
+ | {v.resource_type.resource_type: v for v in product_block.values}
131
+ ).values()
132
+ )
133
+
134
+
135
+ def sorted_subscriptions(subscriptions: list[SubscriptionTable]) -> list[SubscriptionTable]:
136
+ """Sort subscriptions on description."""
137
+ return sorted(subscriptions, key=lambda subscription: subscription.description)
138
+
139
+
140
+ def sorted_product_blocks(product_blocks: list[SubscriptionInstanceTable]) -> list[SubscriptionInstanceTable]:
141
+ """Sort product blocks on product block name."""
142
+ return sorted(
143
+ product_blocks,
144
+ key=lambda subscription_instance: subscription_instance.product_block.name,
145
+ )
146
+
147
+
148
+ def sorted_resource_types(resource_types: list[SubscriptionInstanceValueTable]) -> list[SubscriptionInstanceValueTable]:
149
+ """Sort resource types on resource type name."""
150
+ return sorted(
151
+ resource_types,
152
+ key=lambda subscription_instance_value: subscription_instance_value.resource_type.resource_type,
153
+ )
@@ -0,0 +1,113 @@
1
+ # Copyright 2024 SURF.
2
+ # Licensed under the Apache License, Version 2.0 (the "License");
3
+ # you may not use this file except in compliance with the License.
4
+ # You may obtain a copy of the License at
5
+ #
6
+ # http://www.apache.org/licenses/LICENSE-2.0
7
+ #
8
+ # Unless required by applicable law or agreed to in writing, software
9
+ # distributed under the License is distributed on an "AS IS" BASIS,
10
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
11
+ # See the License for the specific language governing permissions and
12
+ # limitations under the License.
13
+
14
+ import re
15
+ from datetime import datetime
16
+
17
+ from orchestrator.db import SubscriptionTable, db, transactional
18
+ from structlog import get_logger
19
+ from tabulate import tabulate
20
+
21
+ from wfoshell.product_block import product_block_table
22
+ from wfoshell.state import sorted_subscriptions, state
23
+
24
+ logger = get_logger(__name__)
25
+
26
+
27
+ def indexed_subscription_list(subscriptions: list[SubscriptionTable]) -> str:
28
+ """Return tabulated indexed list of subscriptions."""
29
+ return tabulate(
30
+ [(subscription.description, subscription.subscription_id) for subscription in subscriptions],
31
+ tablefmt="plain",
32
+ disable_numparse=True,
33
+ showindex=True,
34
+ )
35
+
36
+
37
+ def query_db() -> list[SubscriptionTable]:
38
+ """Return sorted and list of subscriptions from the database."""
39
+ return sorted_subscriptions(SubscriptionTable.query.all())
40
+
41
+
42
+ def filtered_subscriptions(regular_expression: str, subscriptions: list[SubscriptionTable]) -> list[SubscriptionTable]:
43
+ """Return filtered list of subscriptions."""
44
+ pattern = re.compile(regular_expression, flags=re.IGNORECASE)
45
+ return list(filter(lambda subscription: pattern.search(subscription.description), subscriptions))
46
+
47
+
48
+ def details_subscription_only(subscription: SubscriptionTable) -> list[tuple[str, str]]:
49
+ """Return list of tuples with subscription details only."""
50
+ return [
51
+ ("description", subscription.description),
52
+ ("subscription_id", subscription.subscription_id),
53
+ ("status", subscription.status),
54
+ ("product_id", subscription.product_id),
55
+ ("customer_id", subscription.customer_id),
56
+ ("insync", subscription.insync),
57
+ ("start_date", subscription.start_date),
58
+ ("end_date", subscription.end_date),
59
+ ("note", subscription.note),
60
+ ]
61
+
62
+
63
+ def details_product_blocks_only() -> list[tuple[str, str]]:
64
+ """Return list of tuples with product blocks details only."""
65
+ return [
66
+ ("product block(s)", product_block_table(state.selected_product_blocks)),
67
+ ]
68
+
69
+
70
+ def details_all(subscription: SubscriptionTable) -> list[tuple[str, str]]:
71
+ """Return list of tuples with all subscription details."""
72
+ return details_subscription_only(subscription) + details_product_blocks_only()
73
+
74
+
75
+ def subscription_list() -> str:
76
+ """Add list of all subscriptions to the state and return this list tabulated and indexed."""
77
+ state.subscriptions = query_db()
78
+ state.filtered_subscriptions = None
79
+ return indexed_subscription_list(state.subscriptions)
80
+
81
+
82
+ def subscription_search(regular_expression: str) -> str:
83
+ """Add list of filtered subscriptions to the state and return this list tabulated and indexed."""
84
+ state.subscriptions = query_db()
85
+ state.filtered_subscriptions = filtered_subscriptions(regular_expression, state.subscriptions)
86
+ return indexed_subscription_list(state.filtered_subscriptions)
87
+
88
+
89
+ def subscription_select(index: int) -> str:
90
+ """Implementation of the 'subscription select' subcommand."""
91
+ if state.filtered_subscriptions is None:
92
+ state.subscription_index = index
93
+ else:
94
+ state.subscription_index = state.subscriptions.index(state.filtered_subscriptions[index])
95
+ state.product_block_index = None
96
+ state.resource_type_index = None
97
+ return state.summary
98
+
99
+
100
+ def subscription_details(subscription_only: bool, product_blocks_only: bool) -> str:
101
+ """Implementation of the 'subscription details' subcommand."""
102
+ if subscription_only:
103
+ return tabulate(details_subscription_only(state.selected_subscription), tablefmt="plain")
104
+ elif product_blocks_only: # noqa: RET505
105
+ return tabulate(details_product_blocks_only(), tablefmt="plain")
106
+ else:
107
+ return tabulate(details_all(state.selected_subscription), tablefmt="plain")
108
+
109
+
110
+ def subscription_update(field: str, new_value: str | bool | datetime | None) -> None:
111
+ """Implementation of the 'subscription update' subcommand."""
112
+ with transactional(db, logger):
113
+ setattr(state.selected_subscription, field, new_value)