Skip to content
This repository was archived by the owner on Sep 8, 2023. It is now read-only.

Commit 30d8280

Browse files
authored
Merge pull request #94 from ndw/resync
Resync upstream
2 parents d7f82d6 + 85120dd commit 30d8280

File tree

17 files changed

+489
-193
lines changed

17 files changed

+489
-193
lines changed

.travis.yml

Lines changed: 2 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -7,18 +7,8 @@ before_install:
77
- echo 'America/Los_Angeles' | sudo tee /etc/timezone
88
- sudo dpkg-reconfigure --frontend noninteractive tzdata
99
install:
10-
- if [ "${TRAVIS_SECURE_ENV_VARS}" = "true" ] ; then ./shared/dev-tasks/travis-install-ml.sh
11-
release ; else (exit 0) ; fi
12-
- if [ "${TRAVIS_SECURE_ENV_VARS}" = "true" ] ; then ./shared/dev-tasks/setup-marklogic.sh
13-
; else (exit 0) ; fi
10+
- if [ "${TRAVIS_SECURE_ENV_VARS}" = "true" ] ; then ./shared/dev-tasks/travis-install-ml.sh release ; else (exit 0) ; fi
11+
- if [ "${TRAVIS_SECURE_ENV_VARS}" = "true" ] ; then ./shared/dev-tasks/setup-marklogic.sh ; else (exit 0) ; fi
1412
script:
1513
- python setup.py test
16-
env:
17-
global:
18-
- secure: PnQR4S2RarIhtnZaN1jOd3RyCH3L1PhQ+AAxhmmlO9xqJ8NJ4PCzOIC86e/BK5RiobFscxU0vQIb7wNIIbw0sKKykYg3lWRlusrW7IJ2fEuLj/sm/ixgnh2S8KTz+DzjbLPbN8N5W1spu4cdYNIBFb5hOMHFU+xZ3Vi/SKlQDyvqjUicY/JIKKjYn572Hg5UgEIwUt2QCzWZ/2brRR8jqLC5EiUnT6UbfNIfXIBKQM8z80C/neWGSMDZVFHEnRJHxI+RvPdAqEkSFtvT/zAb/C1n5r//AShBECt9LA09CRC5MZicgAJOKJ1EvlCus92xxeu7JjC96fZ2yBI2ZmkaZoN4o5IBUkNOG8HB5gX6vpAR2PI/GqWj3ubSmrcoWBm7dudju/PRvCAt0wUwt+xAxfIlHuFQCfjaEwr5mOMVLlDxCIrEgVhZxdTqHGPOHAleZqIvycjT5QOflNdFpmWRx04Y121w+gaSfAW6rUHkTt1+UeF+GFFK5gh78hjFy43/V/7M+U6M/DHQExy63bs0r07biIMfEYbKLocEoJcPj1RwZpvQCo28jrLTdwCnbLsdCDsWbK0wojvmKhxXl5j8J8uRSyd5egwu9h3qYZdmJTzk8904cfPYDhPWDc+rauro0gRbFCx0JRtkHiDUYEfHbUWNvW/bXupUp2LHZ2+3OYU=
19-
- secure: QYaSKzNeEexu8YiLA1il9Nn5+DCvFRosIoLiZRtOobMgywHD5sL0xuj04dxP20yvapkdXOLB8MMF5Z+EZLqM4QvMx3bO+EoXNcVd/SQDa1eXIEihdAy9+o8jgIHgF6DTzApvBIbDDy6d4WJDud3lQ/7Mq6ysK4K0YXUS5458TuCNxx7v7gmJp1cZfvaBxMxEua5U/IgzY4/K4Uo14adC15aJ1tUM4bxj3ByYk+LUFVkkk/kNrgTFZoipxAyq1qKTjntQ3GP7K/9cQ6EqVWgkV4DA54CgZLypHdwwIm6ODE3uktTYmuZ425Uvk8vjsKkpQTNGvBu++NK4Y1JehFDqn9HFWrZ9jAIciDaIL0dHvas634jeL65xanB6IzliUFmnWIP3eIS84UedIiAkz6TIpO6f7HOf4ja1IG19d4WtwmD2p+RFPzAlIOstKg3bMauyG8e952tPH5IAqIGA68avtlDeNOMnCP9jN6/DyozcTYaMguq6prhHUIb7Xzo5w6oGS/rq8ctkcL/BkAcGd8KgYYIqXJCckwZdE0HOeUJRUjUguXLvpStFXRycoa2bwg61tJUsLwitO87Gs2iY9Op96tkpfFESZpfqFN+Yc+hxhikRi8mXwnzn5RLp6xCVuYVAF+ioEYP+Ygsd6/3rLStgWFsmfCJ46WkgYRtnOQ+0+Zs=
20-
- secure: sRIY5ub8TaAeMRwLgMOHPIRE+wIk9dbxC4yZ7uGU8kwkS/+I/opMiY3gK8vcMGpdGzvUnOxHadgSmX/dnZQE595g35IKtIqVpyGoC2bqawHToFCozrSmKM0LjsMuAR7ZcmbHMknD4dvrtP9pTMlNz1YntwXJAp90SgTjSIYAmwo2zbTvCBsQQP7HP867v6A6IYm3Y0ZRV/wkzkljecXTc0x9EXRvh4s8Gz9aawhsFW3/Lmvs7GqYdEBbBdSAKUWsmjIE0a+w969bpDyY18EJZdKuCyOMnZnHxhD5ug0xGPacx/pg/8dfhnRAaxJa60Tv7q8ebplg5eeGNiZp5HLNahYzVhjokNNokzSWMSQTYTFHnTcjPv1eKAfVihGpfFG5B0nlUscS/h3t+O9eEMRUbggvAORR94kAKrc7zTnrgbZ5UE6kk3urVJVE9KqkCUOexBlF0xshVfkdqSsHpfHfKqGGYv2KZ16LJ/Zagpag+A9dzBHmeFswtTdFR8pkibpYBy4mQ0+MswZEP7bqoGJwyLKAyQJdMiYU3YvAGdIRqfezIqsI/vwG2DmknuWPJqG3EnhA/KwxH2B9EYoYNexWg/CmL4viX1e978Y/+fhOUyZ02KNL7/D62eEAotMGX6zfJQFbAHAgmnjrl5g0dS3BPGS1oVbeNfb6HJSz0FuYvxI=
21-
- secure: hNF1QKlYgPuRabQ9gQIn7URHgM1K0CT9uPncjcGgDD19J5Sw5GlCJnCPiJz3q/oBCtD+aF0LlNpQbtEPVwd6zqRrxiyN+9lKe6tVkCf7W+EAWV6tEmNK0F/MWy0/hrjKzx5YKLZGJ0ts0RqsPxr6mWsUIn8RLqTEgoRxylSBxIYJtsfpr7jmub8AY2Xa8+pIZB07jssAMWhFLI8PzBw5Bk8qKgcbNLE5HfTF63szZxw65soQc1ALlfkubSqDO78LQygxsF41M2MLmFzPKejmRP6tsZFuWNxNdR04U2d8qb7k4GlE+SuVFi0HhZsWivsarrGh7uZTav7m1VBqrtSG5RXB/i3A8T88wWCXezW7x6V00oxWCjMznbxyyu+8WKC09+DlOdvmxKu1aeFo5eBZZnTstv5dslDrJi8MEOJfK9gVtrRgHhFS9HARAPL8SqrDtl6siga7WRqP7i+qIHYI6zgtwwFNU2WAGR/wizz9TkX9ir+djwn0++jfOmeQzEQstBE+mC0ER1/ALDStTfnFfrml7gU1pBQticfTSHpnZXfP5D3HkCBaW4X6ONpgjXbFARnTn2nk4lOR261Biu/hFPBjld2HK6Elj0IJbyCRZ5o4ZxZ1QUhsuz5Y4r41Y5TA9GbZ/cHzb66fFkWv1KpRJJ+iNASQq10y4fnwuUZMGZU=
22-
- secure: qDaDm7x/OXf3fJrikh2y2mcnB7jGbJj2kbU52kUJPwd5hC6CXB/fVguNybx4rZDdcYjbUDNpU3CfeT4De0/aHpDRvNL9mK0JDQkgAz3BkhEv5QchxNw17Tt4KYany8SUh6mWsMAfMBHdUxumF1PhbTJG+3tw2PcH96nxCgoR2ZA1lN4gutfIKYl/NGKZUhrD1kqGNiFCuZD9jBu1VwT4Sqy0v7PWYtpsuwP0rhDRAdQfEuLFnuzQIXnpkK1Jha4d5XMwwNT8rYme+tfisQDx7SQBw8nIkFp3AMZNEsW0WFReodx2Jwg7XbKLp2JtsUH+jTpXJcXTLsUmmr1hISL7WCWwrgM01Om8EEE7ZlJMzwTbbj4z59GPBo/9pMIxvxitiiurcgMK4PTbQFhZhbANcTxUAHwbZ2/D905mRK1n0ze8+272KqWg+P9vzTE3YKmLnA2g+6ho1PSScMidDqlveKCOMH2X+8VylKdK/cVbCIsOwLlN9VO87cKCSOp047+tDRwbIlbalv2RvpmTvZRu51PItcCRfMgHafKu2/8hkSVKsUmITiXfqWggpc6rbmlB5tFof5xWWsdk5s+0hkm3NJYFLcz7nn+LF39qGmGxxXUu/95317NVunAYpy7ad2BAiLF4B9HWqZcHXwOvQVIdIMUUCx7iooE7/zvmWUQi4jc=
23-
- secure: XmzdhkjTrHPkNX3CZIkrWttsnWrS3+6I79Dkdv+P/maAW4hAQ2ipdHEPtyTMGU4Q2f+TlbFgjFtgUbwxHHEjFKk2NmuUFn01dsSFCOpq71DjLDeH+isHnmwDufpmuD6nACB/EJDkx8GHP1qFMeIkCuabgAxcFO2MtkWiYTZyMZeCfIkQ8jkrFN+kDgnsdsuSxMkkoEHq+Fi+SyWWxgACdraklcAldUeA9GzpFu9mti8Ud4K6YWZtv8iS4EZPuoLnpv+DQaOXbCqjXheOOTAy0UJEC1C+9QnLoJYh+qIcbvMjmyNv6XpGSkteB1gOVCJUQKz6gkF+78iQF18e2qEJGcnjw28PtvX9TKdY3GbxCRPxSLGyAZ5TE30v+WuwW/UPnKhPgt6ay8K949kz4VilW69919iE41/Cyny/ssLe6XFUvme+oWCC9KPhfmnXG42a8USKbZ8HjP5bjlvova92eMCfJpWlF0Q6SXqaf6mYJ4Uk0n6VLsrDTMabTVApady/XcRzHkN345rthGb9Xwp35Zvc1hoS7HsosqYJajRFfLeJohy0LX3RYr3Yb0f1fI22pV4AkcidXQqbPeTFPVzC+q9Iyh3nn1pBKn5SrPu+emwk3IXX8vxzixmyqlIrHLmVLAI63jKMrDG3LcQdk7H6Uy/KEm5qpLRhYJm18dxHRuo=
2414

examples/init-server.py

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
#!/usr/bin/python3
2+
#
3+
# Copyright 2018 MarkLogic Corporation
4+
#
5+
6+
__author__ = 'ndw'
7+
8+
import argparse
9+
import logging
10+
import json
11+
import logging
12+
from marklogic import MarkLogic
13+
14+
class InitServer:
15+
def __init__(self):
16+
pass
17+
18+
#logging.basicConfig(level=logging.INFO)
19+
20+
parser = argparse.ArgumentParser()
21+
parser.add_argument("--host", action='store', default="localhost",
22+
help="Management API host")
23+
parser.add_argument("--username", action='store', default="admin",
24+
help="User name")
25+
parser.add_argument("--password", action='store', default="admin",
26+
help="Password")
27+
parser.add_argument("--wallet", action='store', default="admin",
28+
help="Wallet password")
29+
parser.add_argument('--debug', action='store_true',
30+
help='Enable debug logging')
31+
args = parser.parse_args()
32+
33+
if args.debug:
34+
logging.basicConfig(level=logging.WARNING)
35+
logging.getLogger("requests").setLevel(logging.WARNING)
36+
logging.getLogger("marklogic").setLevel(logging.DEBUG)
37+
38+
print("Initialize host {}".format(args.host))
39+
MarkLogic.instance_init(args.host)
40+
print("Initialize admin {}".format(args.host))
41+
MarkLogic.instance_admin(args.host, "public", args.username, args.password, args.wallet)
42+
43+
print("finished")

examples/mldbmirror.py

Lines changed: 36 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -67,13 +67,39 @@ def connect(self, args):
6767
self.path = os.path.abspath(args['path'])
6868
self.loadconfig(self.path)
6969

70+
if args['hostname'] is None:
71+
if 'host' in self.config:
72+
self.hostname = self.config['host']
73+
if 'port' in self.config:
74+
self.port = self.config['port']
75+
else:
76+
self.port = 8000
77+
if 'management-port' in self.config:
78+
self.management_port = self.config['management-port']
79+
else:
80+
self.management_port = 8002
81+
else:
82+
parts = args['hostname'].split(":")
83+
self.hostname = parts.pop(0)
84+
self.management_port = 8002
85+
self.port = 8000
86+
if parts:
87+
self.management_port = parts.pop(0)
88+
if parts:
89+
self.port = parts.pop(0)
90+
7091
if args['credentials'] is not None:
7192
cred = args['credentials']
7293
else:
7394
if 'user' in self.config and 'pass' in self.config:
7495
cred = self.config['user'] + ":" + self.config['pass']
7596
else:
7697
cred = None
98+
key = self.hostname + ":" + str(self.management_port)
99+
if key in self.config:
100+
obj = self.config[key]
101+
if 'user' in obj and 'pass' in obj:
102+
cred = obj['user'] + ":" + obj['pass']
77103

78104
try:
79105
adminuser, adminpass = re.split(":", cred)
@@ -102,27 +128,6 @@ def connect(self, args):
102128
if self.root.endswith("/"):
103129
self.root = self.root[0:len(self.root)-1]
104130

105-
if args['hostname'] is None:
106-
if 'host' in self.config:
107-
self.hostname = self.config['host']
108-
if 'port' in self.config:
109-
self.port = self.config['port']
110-
else:
111-
self.port = 8000
112-
if 'management-port' in self.config:
113-
self.management_port = self.config['management-port']
114-
else:
115-
self.management_port = 8002
116-
else:
117-
parts = args['hostname'].split(":")
118-
self.hostname = parts.pop(0)
119-
self.management_port = 8002
120-
self.port = 8000
121-
if parts:
122-
self.management_port = parts.pop(0)
123-
if parts:
124-
self.port = parts.pop(0)
125-
126131
self.connection \
127132
= Connection(self.hostname, HTTPDigestAuth(adminuser, adminpass), \
128133
port=self.port, management_port=self.management_port)
@@ -486,15 +491,17 @@ def _download_directory(self, trans):
486491
down_map = {}
487492
skip_list = []
488493
for uri in uris:
489-
if not self.can_store_on_filesystem(uri):
490-
raise RuntimeError("Cannot save URI:", uri)
491-
492494
localfile = self.path + uri
493495
skip = False
494-
if uri in stamps and os.path.exists(localfile):
495-
statinfo = os.stat(localfile)
496-
stamp = self._convert_timestamp(stamps[uri])
497-
skip = statinfo.st_mtime >= stamp.timestamp()
496+
497+
if not self.can_store_on_filesystem(uri):
498+
print("Skipping " + uri + ": cannot store on filesystem")
499+
skip = True
500+
else:
501+
if uri in stamps and os.path.exists(localfile):
502+
statinfo = os.stat(localfile)
503+
stamp = self._convert_timestamp(stamps[uri])
504+
skip = statinfo.st_mtime >= stamp.timestamp()
498505

499506
if skip:
500507
skip_list.append(localfile)
@@ -709,7 +716,7 @@ def can_store_on_filesystem(self, filename):
709716
filesystem because if it's ever uploaded, it'll get a leading /.
710717
"""
711718
if (not filename.startswith("/")) or ("//" in filename) \
712-
or (":" in filename) or ('"' in filename) or ('"' in filename) \
719+
or (":" in filename) or ('"' in filename) or ("'" in filename) \
713720
or ("\\" in filename):
714721
return False
715722
else:

examples/read-everything.py

Lines changed: 127 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,127 @@
1+
#!/usr/bin/python3
2+
#
3+
# Copyright 2015 MarkLogic Corporation
4+
#
5+
# This script attempts to read all of the resource types on the cluster.
6+
# The point of this script is to make sure that we catch any new properties
7+
# that have been added by the server.
8+
9+
__author__ = 'ndw'
10+
11+
import argparse
12+
import logging
13+
import json
14+
import logging
15+
import sys
16+
from requests.auth import HTTPDigestAuth
17+
from marklogic.connection import Connection
18+
from marklogic.models.cluster import LocalCluster
19+
from marklogic.models.group import Group
20+
from marklogic.models.host import Host
21+
from marklogic.models.database import Database
22+
from marklogic.models.permission import Permission
23+
from marklogic.models.privilege import Privilege
24+
from marklogic.models.role import Role
25+
from marklogic.models.forest import Forest
26+
from marklogic.models.server import Server
27+
from marklogic.models.user import User
28+
29+
class ReadEverything:
30+
def __init__(self, connection):
31+
self.databases = {}
32+
self.forests = {}
33+
self.servers = {}
34+
self.users = {}
35+
self.roles = {}
36+
self.privileges = {}
37+
self.connection = connection
38+
pass
39+
40+
def readClass(self, kind, klass, max_read=sys.maxsize):
41+
names = klass.list(self.connection)
42+
for name in names:
43+
if max_read > 0:
44+
if name.find("|") > 0:
45+
parts = name.split("|")
46+
rsrc = klass.lookup(self.connection, parts[0], parts[1])
47+
else:
48+
rsrc = klass.lookup(self.connection, name)
49+
max_read = max_read - 1
50+
print("{}: {}".format(kind, len(names)))
51+
52+
def readPrivileges(self):
53+
names = Privilege.list(self.connection)
54+
max_read = { "execute": 5, "uri": 5 }
55+
counts = { "execute": 0, "uri": 0 }
56+
for name in names:
57+
parts = name.split("|")
58+
kind = parts[0]
59+
pname = parts[1]
60+
61+
counts[kind] = counts[kind] + 1
62+
63+
if max_read[kind] > 0:
64+
rsrc = Privilege.lookup(self.connection, pname, kind)
65+
max_read[kind] = max_read[kind] - 1
66+
67+
print("Execute privileges: {}".format(counts["execute"]))
68+
print("URI privileges: {}".format(counts["uri"]))
69+
70+
def read(self):
71+
conn = self.connection
72+
cluster = LocalCluster(connection=conn).read()
73+
print("Read local cluster: {}".format(cluster.cluster_name()))
74+
75+
x = cluster.security_version()
76+
x = cluster.effective_version()
77+
x = cluster.cluster_id()
78+
x = cluster.cluster_name()
79+
x = cluster.ssl_fips_enabled()
80+
x = cluster.xdqp_ssl_certificate()
81+
x = cluster.xdqp_ssl_private_key()
82+
x = cluster.bootstrap_hosts()
83+
# FIXME:
84+
#x = cluster.foreign_cluster_id()
85+
#x = cluster.foreign_cluster_name()
86+
x = cluster.language_baseline()
87+
x = cluster.opsdirector_log_level()
88+
x = cluster.opsdirector_metering()
89+
x = cluster.opsdirector_session_endpoint()
90+
91+
self.readClass("Groups", Group, max_read=5)
92+
self.readClass("Hosts", Host, max_read=5)
93+
self.readClass("Databases", Database, max_read=5)
94+
self.readClass("Forests", Forest, max_read=5)
95+
self.readClass("Servers", Server)
96+
self.readClass("Roles", Role, max_read=5)
97+
self.readClass("Users", User, max_read=5)
98+
self.readPrivileges()
99+
100+
return
101+
102+
logging.basicConfig(level=logging.INFO)
103+
104+
parser = argparse.ArgumentParser()
105+
parser.add_argument("--host", action='store', default="localhost",
106+
help="Management API host")
107+
parser.add_argument("--username", action='store', default="admin",
108+
help="User name")
109+
parser.add_argument("--password", action='store', default="admin",
110+
help="Password")
111+
parser.add_argument('--debug', action='store_true',
112+
help='Enable debug logging')
113+
args = parser.parse_args()
114+
115+
if args.debug:
116+
logging.basicConfig(level=logging.WARNING)
117+
logging.getLogger("requests").setLevel(logging.WARNING)
118+
logging.getLogger("marklogic").setLevel(logging.DEBUG)
119+
120+
conn = Connection(args.host, HTTPDigestAuth(args.username, args.password))
121+
read_everything = ReadEverything(conn)
122+
123+
print("Reading all resources from {}".format(args.host))
124+
125+
read_everything.read()
126+
127+
print("Finished")

marklogic/__init__.py

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@
3636
from marklogic.models.server import OdbcServer, XdbcServer
3737
from marklogic.exceptions import InvalidAPIRequest, UnexpectedManagementAPIResponse
3838

39-
__version__ = "0.0.14"
39+
__version__ = "0.0.20"
4040

4141
class MarkLogic:
4242
"""
@@ -446,7 +446,7 @@ def instance_init(cls, host):
446446
return Host(host)._set_just_initialized()
447447

448448
@classmethod
449-
def instance_admin(cls,host,realm,admin,password):
449+
def instance_admin(cls,host,realm,admin,password,wallet_password=None):
450450
"""
451451
Initializes the security database of a newly initialized server.
452452
@@ -463,6 +463,9 @@ def instance_admin(cls,host,realm,admin,password):
463463
'realm': realm
464464
}
465465

466+
if wallet_password is not None:
467+
payload["wallet-password"] = wallet_password
468+
466469
uri = "{0}://{1}:8001/admin/v1/instance-admin".format(
467470
conn.protocol, conn.host)
468471

marklogic/cli/manager/marklogic.py

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -152,14 +152,23 @@ def restart(self, args, config, connection):
152152
cluster = LocalCluster(connection=connection).read()
153153
print("Restarting cluster...")
154154
cluster.restart()
155-
155+
# Make sure it's back up
156+
status = self.status(args,config,connection,internal=True)
157+
while status != 'up':
158+
time.sleep(2)
159+
status = self.status(args,config,connection,internal=True)
156160
else:
157161
hostname = connection.host
158162
if hostname == 'localhost':
159163
hostname = socket.gethostname()
160164
host = Host(hostname,connection=connection).read()
161165
print("Restarting host...")
162166
host.restart()
167+
# Make sure it's back up
168+
status = self.status(args,config,connection,internal=True)
169+
while status != 'up':
170+
time.sleep(2)
171+
status = self.status(args,config,connection,internal=True)
163172

164173
def stop(self, args, config, connection):
165174
status = self.status(args, config, connection, internal=True)
@@ -174,6 +183,11 @@ def stop(self, args, config, connection):
174183
cluster = LocalCluster(connection=connection).read()
175184
print("Shutting down cluster...")
176185
cluster.shutdown()
186+
# Make sure it's all the way down
187+
status = self.status(args,config,connection,internal=True)
188+
while status != 'down':
189+
time.sleep(2)
190+
status = self.status(args,config,connection,internal=True)
177191
else:
178192
hostname = connection.host
179193
if hostname == 'localhost':
@@ -190,6 +204,11 @@ def stop(self, args, config, connection):
190204

191205
print("Shutting down host: " + host.host_name())
192206
host.shutdown()
207+
# Make sure it's all the way down
208+
status = self.status(args,config,connection,internal=True)
209+
while status != 'down':
210+
time.sleep(2)
211+
status = self.status(args,config,connection,internal=True)
193212

194213
status = self.status(args,config,connection,internal=True)
195214
while status == 'up':

marklogic/cli/template.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -638,6 +638,8 @@ def _make_parser(self, command, artifact, description=""):
638638
help='Host on which to issue the request')
639639
parser.add_argument('--credentials', default='admin:admin',
640640
help='Login credentials for request')
641+
parser.add_argument('--https', action='store_true',
642+
help='Enable https')
641643
parser.add_argument('--debug', action='store_true',
642644
help='Enable debug logging')
643645
return parser

0 commit comments

Comments
 (0)