547 lines
22 KiB
Python
Executable File
547 lines
22 KiB
Python
Executable File
#!/usr/bin/env python
|
|
"""
|
|
Script to provide a command line interface to a Refine server.
|
|
"""
|
|
|
|
# Copyright (c) 2011 Paul Makepeace, Real Programmers. All rights reserved.
|
|
|
|
# This program is free software: you can redistribute it and/or modify
|
|
# it under the terms of the GNU General Public License as published by
|
|
# the Free Software Foundation, either version 3 of the License, or
|
|
# (at your option) any later version.
|
|
|
|
# This program is distributed in the hope that it will be useful,
|
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
# GNU General Public License for more details.
|
|
|
|
# You should have received a copy of the GNU General Public License
|
|
# along with this program. If not, see <http://www.gnu.org/licenses/>
|
|
|
|
|
|
import optparse
|
|
import os
|
|
import sys
|
|
import time
|
|
|
|
from google.refine import refine
|
|
|
|
import urllib2_file
|
|
import urllib2
|
|
import urlparse
|
|
|
|
import json
|
|
|
|
class myParser(optparse.OptionParser):
|
|
|
|
def format_epilog(self, formatter):
|
|
return self.epilog
|
|
|
|
PARSER = \
|
|
myParser(description='Script to provide a command line interface to an OpenRefine server.',
|
|
usage='usage: %prog [--help | OPTIONS]',
|
|
epilog="""
|
|
Examples:
|
|
./refine.py --list # show list of Refine projects, ID: name
|
|
./refine.py --list -H 127.0.0.1 -P 80 # specify hostname and port
|
|
./refine.py --create example.csv # create new project from file example.csv
|
|
./refine.py --create example.tsv --format=tsv --encoding=UTF-8
|
|
./refine.py --create example.xml --format=xml --recordPath=collection --recordPath=record
|
|
./refine.py --create example.json --format=json --recordPath=_ --recordPath=_
|
|
./refine.py --create example.xlsx --format=xlsx --sheets=0
|
|
./refine.py --create example.ods --format=ods --sheets=0
|
|
./refine.py --export 1234... > project.tsv # export project 1234... in tsv format
|
|
./refine.py --export --output=project.xls 1234... # export project in xls format
|
|
./refine.py --apply trim.json 1234... # apply rules in trim.json to project 1234...
|
|
|
|
""")
|
|
|
|
group1 = optparse.OptionGroup(PARSER, 'Connection options')
|
|
group1.add_option('-H', '--host', dest='host', metavar='127.0.0.1',
|
|
help='OpenRefine hostname (default: localhost)')
|
|
group1.add_option('-P', '--port', dest='port', metavar='3333',
|
|
help='OpenRefine port (default: 3333)')
|
|
PARSER.add_option_group(group1)
|
|
|
|
group2 = optparse.OptionGroup(PARSER, 'Commands')
|
|
group2.add_option('-l', '--list', dest='list', action='store_true',
|
|
help='List projects: refine.py -l')
|
|
group2.add_option('-c', '--create', dest='create', action='store_true',
|
|
help='Create project from file: refine.py -c [FILE]')
|
|
group2.add_option('-E', '--export', dest='export', action='store_true',
|
|
help='Export project: refine.py -E [PROJECTID]')
|
|
group2.add_option('-f', '--apply', dest='apply', metavar='file.json',
|
|
help='Apply JSON rules: refine.py -f [FILE] [PROJECTID]')
|
|
PARSER.add_option_group(group2)
|
|
|
|
group3 = optparse.OptionGroup(PARSER, 'Export options (optional)')
|
|
group3.add_option('-o', '--output', dest='output', metavar='file.csv',
|
|
help='Specify output filename and filetype. The filename ending (e.g. .csv) defines the output format (csv,tsv,xls,html)')
|
|
PARSER.add_option_group(group3)
|
|
|
|
group4 = optparse.OptionGroup(PARSER, 'Create format (mandatory for xml, json, fixed-width, xlsx, ods)')
|
|
group4.add_option('--format', dest='inputformat',
|
|
help='Specify input format (csv, tsv, xml, json, line-based, fixed-width, xlsx, ods)')
|
|
PARSER.add_option_group(group4)
|
|
|
|
group5 = optparse.OptionGroup(PARSER, 'Create options (mandatory for xml, json, fixed-width, xslx, ods; only together with --format)')
|
|
group5.add_option('--recordPath', dest='recordPath', action='append',
|
|
help='(xml, json), please provide path in multiple arguments without slashes, e.g. /collection/record/ should be entered like this: --recordPath=collection --recordPath=record')
|
|
group5.add_option('--columnWidths', dest='columnWidths',
|
|
help='(fixed-width) please provide widths separated by comma (e.g. 7,5)')
|
|
group5.add_option('--sheets', dest='sheets',
|
|
help='(xlsx, ods), please provide sheets separated by comma (e.g. 0,1), default: 0 (first sheet)')
|
|
PARSER.add_option_group(group5)
|
|
|
|
group6 = optparse.OptionGroup(PARSER, 'More create options (optional, only together with --format)')
|
|
group6.add_option('--limit', dest='limit',
|
|
help='(all formats), default: -1')
|
|
group6.add_option('--includeFileSources', dest='includeFileSources',
|
|
help='(all formats), default: false')
|
|
group6.add_option('--trimStrings', dest='trimStrings',
|
|
help='(xml, json), default: false')
|
|
group6.add_option('--storeEmptyStrings', dest='storeEmptyStrings',
|
|
help='(xml, json), default: true')
|
|
group6.add_option('--guessCellValueTypes', dest='guessCellValueTypes',
|
|
help='(xml, csv, tsv, fixed-width, json), default: false')
|
|
group6.add_option('--encoding', dest='encoding',
|
|
help='(csv, tsv, line-based, fixed-width), please provide short encoding name (e.g. UTF-8)')
|
|
group6.add_option('--ignoreLines', dest='ignoreLines',
|
|
help='(csv, tsv, line-based, fixed-width, xlsx, ods), default: -1')
|
|
group6.add_option('--headerLines', dest='headerLines',
|
|
help='(csv, tsv, fixed-width, xlsx, ods), default: 1')
|
|
group6.add_option('--skipDataLines', dest='skipDataLines',
|
|
help='(csv, tsv, line-based, fixed-width, xlsx, ods), default: 0')
|
|
group6.add_option('--storeBlankRows', dest='storeBlankRows',
|
|
help='(csv, tsv, line-based, fixed-width, xlsx, ods), default: true')
|
|
group6.add_option('--processQuotes', dest='processQuotes',
|
|
help='(csv, tsv), default: true')
|
|
group6.add_option('--storeBlankCellsAsNulls',
|
|
dest='storeBlankCellsAsNulls',
|
|
help='(csv, tsv, line-based, fixed-width, xlsx, ods), default: true')
|
|
group6.add_option('--linesPerRow', dest='linesPerRow',
|
|
help='(line-based), default: 1')
|
|
PARSER.add_option_group(group6)
|
|
|
|
def create_project(options, file_fullpath):
|
|
servernewproject = 'http://' + refine.REFINE_HOST
|
|
if refine.REFINE_PORT != '80':
|
|
servernewproject += ':' + refine.REFINE_PORT
|
|
|
|
input_format = ''
|
|
input_options = ''
|
|
|
|
# xml
|
|
if options.inputformat == 'xml':
|
|
input_format = 'text/xml'
|
|
recordPath = 'record'
|
|
if options.recordPath:
|
|
recordPath = options.recordPath
|
|
limit = '-1'
|
|
if options.limit:
|
|
limit = options.limit
|
|
trimStrings = 'false'
|
|
if options.trimStrings:
|
|
trimStrings = options.trimStrings
|
|
guessCellValueTypes = 'false'
|
|
if options.guessCellValueTypes:
|
|
guessCellValueTypes = options.guessCellValueTypes
|
|
storeEmptyStrings = 'true'
|
|
if options.storeEmptyStrings:
|
|
storeEmptyStrings = options.storeEmptyStrings
|
|
includeFileSources = 'false'
|
|
if options.includeFileSources:
|
|
includeFileSources = options.includeFileSources
|
|
input_options = '{"recordPath":["' + '","'.join(recordPath) + '"]' \
|
|
+ ',"limit":' + limit \
|
|
+ ',"trimStrings":' + trimStrings \
|
|
+ ',"guessCellValueTypes":' + guessCellValueTypes \
|
|
+ ',"storeEmptyStrings":' + storeEmptyStrings \
|
|
+ ',"includeFileSources":' + includeFileSources \
|
|
+ '}'
|
|
|
|
# csv
|
|
if options.inputformat == 'csv':
|
|
input_format = 'text/line-based/*sv'
|
|
encoding = ''
|
|
if options.encoding:
|
|
encoding = options.encoding
|
|
ignoreLines = '-1'
|
|
if options.ignoreLines:
|
|
ignoreLines = options.ignoreLines
|
|
headerLines = '1'
|
|
if options.headerLines:
|
|
headerLines = options.headerLines
|
|
skipDataLines = '0'
|
|
if options.skipDataLines:
|
|
skipDataLines = options.skipDataLines
|
|
limit = '-1'
|
|
if options.limit:
|
|
limit = options.limit
|
|
storeBlankRows = 'true'
|
|
if options.storeBlankRows:
|
|
storeBlankRows = options.storeBlankRows
|
|
guessCellValueTypes = 'false'
|
|
if options.guessCellValueTypes:
|
|
guessCellValueTypes = options.guessCellValueTypes
|
|
processQuotes = 'true'
|
|
if options.processQuotes:
|
|
processQuotes = options.processQuotes
|
|
storeBlankCellsAsNulls = 'true'
|
|
if options.storeBlankCellsAsNulls:
|
|
storeBlankCellsAsNulls = options.storeBlankCellsAsNulls
|
|
includeFileSources = 'false'
|
|
if options.includeFileSources:
|
|
includeFileSources = options.includeFileSources
|
|
input_options = '{"encoding":"' + encoding + '"' \
|
|
+ ',"separator":","' \
|
|
+ ',"ignoreLines":' + ignoreLines \
|
|
+ ',"headerLines":' + headerLines \
|
|
+ ',"skipDataLines":' + skipDataLines \
|
|
+ ',"limit":' + limit \
|
|
+ ',"storeBlankRows":' + storeBlankRows \
|
|
+ ',"guessCellValueTypes":' + guessCellValueTypes \
|
|
+ ',"processQuotes":' + processQuotes \
|
|
+ ',"storeBlankCellsAsNulls":' + storeBlankCellsAsNulls \
|
|
+ ',"includeFileSources":' + includeFileSources \
|
|
+ '}'
|
|
|
|
# tsv
|
|
if options.inputformat == 'tsv':
|
|
input_format = 'text/line-based/*sv'
|
|
encoding = ''
|
|
if options.encoding:
|
|
encoding = options.encoding
|
|
ignoreLines = '-1'
|
|
if options.ignoreLines:
|
|
ignoreLines = options.ignoreLines
|
|
headerLines = '1'
|
|
if options.headerLines:
|
|
headerLines = options.headerLines
|
|
skipDataLines = '0'
|
|
if options.skipDataLines:
|
|
skipDataLines = options.skipDataLines
|
|
limit = '-1'
|
|
if options.limit:
|
|
limit = options.limit
|
|
storeBlankRows = 'true'
|
|
if options.storeBlankRows:
|
|
storeBlankRows = options.storeBlankRows
|
|
guessCellValueTypes = 'false'
|
|
if options.guessCellValueTypes:
|
|
guessCellValueTypes = options.guessCellValueTypes
|
|
processQuotes = 'true'
|
|
if options.processQuotes:
|
|
processQuotes = options.processQuotes
|
|
storeBlankCellsAsNulls = 'true'
|
|
if options.storeBlankCellsAsNulls:
|
|
storeBlankCellsAsNulls = options.storeBlankCellsAsNulls
|
|
includeFileSources = 'false'
|
|
if options.includeFileSources:
|
|
includeFileSources = options.includeFileSources
|
|
input_options = '{"encoding":"' + encoding + '"' \
|
|
+ ',"separator":"\\t"' \
|
|
+ ',"ignoreLines":' + ignoreLines \
|
|
+ ',"headerLines":' + headerLines \
|
|
+ ',"skipDataLines":' + skipDataLines \
|
|
+ ',"limit":' + limit \
|
|
+ ',"storeBlankRows":' + storeBlankRows \
|
|
+ ',"guessCellValueTypes":' + guessCellValueTypes \
|
|
+ ',"processQuotes":' + processQuotes \
|
|
+ ',"storeBlankCellsAsNulls":' + storeBlankCellsAsNulls \
|
|
+ ',"includeFileSources":' + includeFileSources \
|
|
+ '}'
|
|
|
|
# line-based
|
|
if options.inputformat == 'line-based':
|
|
input_format = 'text/line-based'
|
|
encoding = ''
|
|
if options.encoding:
|
|
encoding = options.encoding
|
|
linesPerRow = '1'
|
|
if options.linesPerRow:
|
|
linesPerRow = options.linesPerRow
|
|
ignoreLines = '-1'
|
|
if options.ignoreLines:
|
|
ignoreLines = options.ignoreLines
|
|
limit = '-1'
|
|
if options.limit:
|
|
limit = options.limit
|
|
skipDataLines = '-1'
|
|
if options.skipDataLines:
|
|
skipDataLines = options.skipDataLines
|
|
storeBlankRows = 'true'
|
|
if options.storeBlankRows:
|
|
storeBlankRows = options.storeBlankRows
|
|
storeBlankCellsAsNulls = 'true'
|
|
if options.storeBlankCellsAsNulls:
|
|
storeBlankCellsAsNulls = options.storeBlankCellsAsNulls
|
|
includeFileSources = 'false'
|
|
if options.includeFileSources:
|
|
includeFileSources = options.includeFileSources
|
|
input_options = '{"encoding":"' + encoding + '"' \
|
|
+ ',"linesPerRow":' + linesPerRow \
|
|
+ ',"ignoreLines":' + ignoreLines \
|
|
+ ',"limit":' + limit \
|
|
+ ',"skipDataLines":' + skipDataLines \
|
|
+ ',"storeBlankRows":' + storeBlankRows \
|
|
+ ',"storeBlankCellsAsNulls":' + storeBlankCellsAsNulls \
|
|
+ ',"includeFileSources":' + includeFileSources \
|
|
+ '}'
|
|
|
|
# fixed-width
|
|
if options.inputformat == 'fixed-width':
|
|
input_format = 'text/line-based/fixed-width'
|
|
encoding = ''
|
|
if options.encoding:
|
|
encoding = options.encoding
|
|
columnWidths = ''
|
|
if options.columnWidths:
|
|
columnWidths = options.columnWidths
|
|
ignoreLines = '-1'
|
|
if options.ignoreLines:
|
|
ignoreLines = options.ignoreLines
|
|
headerLines = '0'
|
|
if options.headerLines:
|
|
headerLines = options.headerLines
|
|
skipDataLines = '0'
|
|
if options.skipDataLines:
|
|
skipDataLines = options.skipDataLines
|
|
limit = '-1'
|
|
if options.limit:
|
|
limit = options.limit
|
|
guessCellValueTypes = 'false'
|
|
if options.guessCellValueTypes:
|
|
guessCellValueTypes = options.guessCellValueTypes
|
|
storeBlankRows = 'true'
|
|
if options.storeBlankRows:
|
|
storeBlankRows = options.storeBlankRows
|
|
storeBlankCellsAsNulls = 'true'
|
|
if options.storeBlankCellsAsNulls:
|
|
storeBlankCellsAsNulls = options.storeBlankCellsAsNulls
|
|
includeFileSources = 'false'
|
|
if options.includeFileSources:
|
|
includeFileSources = options.includeFileSources
|
|
input_options = '{"encoding":"' + encoding + '"' \
|
|
+ ',"columnWidths":[' + columnWidths + ']' \
|
|
+ ',"ignoreLines":' + ignoreLines \
|
|
+ ',"headerLines":' + headerLines \
|
|
+ ',"skipDataLines":' + skipDataLines \
|
|
+ ',"limit":' + limit \
|
|
+ ',"guessCellValueTypes":' + guessCellValueTypes \
|
|
+ ',"storeBlankRows":' + storeBlankRows \
|
|
+ ',"storeBlankCellsAsNulls":' + storeBlankCellsAsNulls \
|
|
+ ',"includeFileSources":' + includeFileSources \
|
|
+ '}'
|
|
|
|
# json
|
|
if options.inputformat == 'json':
|
|
input_format = 'text/json'
|
|
recordPath = ['_', '_']
|
|
if options.recordPath:
|
|
recordPath = options.recordPath
|
|
limit = '-1'
|
|
if options.limit:
|
|
limit = options.limit
|
|
trimStrings = 'false'
|
|
if options.trimStrings:
|
|
trimStrings = options.trimStrings
|
|
guessCellValueTypes = 'false'
|
|
if options.guessCellValueTypes:
|
|
guessCellValueTypes = options.guessCellValueTypes
|
|
storeEmptyStrings = 'true'
|
|
if options.storeEmptyStrings:
|
|
storeEmptyStrings = options.storeEmptyStrings
|
|
includeFileSources = 'false'
|
|
if options.includeFileSources:
|
|
includeFileSources = options.includeFileSources
|
|
input_options = '{"recordPath":["' + '","'.join(recordPath) + '"]' \
|
|
+ ',"limit":' + limit \
|
|
+ ',"trimStrings":' + trimStrings \
|
|
+ ',"guessCellValueTypes":' + guessCellValueTypes \
|
|
+ ',"storeEmptyStrings":' + storeEmptyStrings \
|
|
+ ',"includeFileSources":' + includeFileSources \
|
|
+ '}'
|
|
|
|
# xlsx
|
|
if options.inputformat == 'xlsx':
|
|
input_format = 'binary/text/xml/xls/xlsx'
|
|
sheets = '0'
|
|
if options.sheets:
|
|
sheets = options.sheets
|
|
ignoreLines = '-1'
|
|
if options.ignoreLines:
|
|
ignoreLines = options.ignoreLines
|
|
headerLines = '1'
|
|
if options.headerLines:
|
|
headerLines = options.headerLines
|
|
skipDataLines = '0'
|
|
if options.skipDataLines:
|
|
skipDataLines = options.skipDataLines
|
|
limit = '-1'
|
|
if options.limit:
|
|
limit = options.limit
|
|
storeBlankRows = 'true'
|
|
if options.storeBlankRows:
|
|
storeBlankRows = options.storeBlankRows
|
|
storeBlankCellsAsNulls = 'true'
|
|
if options.storeBlankCellsAsNulls:
|
|
storeBlankCellsAsNulls = options.storeBlankCellsAsNulls
|
|
includeFileSources = 'false'
|
|
if options.includeFileSources:
|
|
includeFileSources = options.includeFileSources
|
|
input_options = '{"sheets":[' + sheets + ']' \
|
|
+ ',"ignoreLines":' + ignoreLines \
|
|
+ ',"headerLines":' + headerLines \
|
|
+ ',"skipDataLines":' + skipDataLines \
|
|
+ ',"limit":' + limit \
|
|
+ ',"storeBlankRows":' + storeBlankRows \
|
|
+ ',"storeBlankCellsAsNulls":' + storeBlankCellsAsNulls \
|
|
+ ',"includeFileSources":' + includeFileSources \
|
|
+ '}'
|
|
|
|
# ods
|
|
if options.inputformat == 'ods':
|
|
input_format = 'text/xml/ods'
|
|
sheets = '0'
|
|
if options.sheets:
|
|
sheets = options.sheets
|
|
ignoreLines = '-1'
|
|
if options.ignoreLines:
|
|
ignoreLines = options.ignoreLines
|
|
headerLines = '1'
|
|
if options.headerLines:
|
|
headerLines = options.headerLines
|
|
skipDataLines = '0'
|
|
if options.skipDataLines:
|
|
skipDataLines = options.skipDataLines
|
|
limit = '-1'
|
|
if options.limit:
|
|
limit = options.limit
|
|
storeBlankRows = 'true'
|
|
if options.storeBlankRows:
|
|
storeBlankRows = options.storeBlankRows
|
|
storeBlankCellsAsNulls = 'true'
|
|
if options.storeBlankCellsAsNulls:
|
|
storeBlankCellsAsNulls = options.storeBlankCellsAsNulls
|
|
includeFileSources = 'false'
|
|
if options.includeFileSources:
|
|
includeFileSources = options.includeFileSources
|
|
input_options = '{"sheets":[' + sheets + ']' \
|
|
+ ',"ignoreLines":' + ignoreLines \
|
|
+ ',"headerLines":' + headerLines \
|
|
+ ',"skipDataLines":' + skipDataLines \
|
|
+ ',"limit":' + limit \
|
|
+ ',"storeBlankRows":' + storeBlankRows \
|
|
+ ',"storeBlankCellsAsNulls":' + storeBlankCellsAsNulls \
|
|
+ ',"includeFileSources":' + includeFileSources \
|
|
+ '}'
|
|
|
|
data = {}
|
|
file_name = os.path.split(file_fullpath)[-1]
|
|
data['project-file'] = {'fd': open(file_fullpath),
|
|
'filename': file_name}
|
|
data['project-name'] = file_name
|
|
|
|
response = urllib2.urlopen(servernewproject
|
|
+ '/command/core/create-project-from-upload?format='
|
|
+ input_format + '&options='
|
|
+ input_options, data)
|
|
response_body = response.read()
|
|
url_params = \
|
|
urlparse.parse_qs(urlparse.urlparse(response.geturl()).query)
|
|
|
|
if 'project' in url_params:
|
|
project_id = url_params['project'][0]
|
|
print 'New project: ' + project_id
|
|
else:
|
|
raise Exception('Project not created')
|
|
|
|
# wait until project is created
|
|
def wait_until_idle(self, polling_delay=0.5):
|
|
while True:
|
|
response = urllib2.urlopen(servernewproject + '/command/core/get-processes?project=' + project_id)
|
|
response_body = response.read()
|
|
url_params = \
|
|
urlparse.parse_qs(urlparse.urlparse(response.geturl()).query)
|
|
if 'processes' in url_params and len(url_params['processes']) > 0:
|
|
time.sleep(polling_delay)
|
|
else:
|
|
print 'done'
|
|
return
|
|
|
|
# check number of rows
|
|
response = urllib2.urlopen(servernewproject
|
|
+ '/command/core/get-rows?project='
|
|
+ project_id
|
|
+ '&start=0&limit=0')
|
|
response_body = response.read()
|
|
response_json = json.loads(response_body)
|
|
if 'total' in response_body and response_json['total'] > 0:
|
|
print 'Number of rows:', response_json['total']
|
|
else:
|
|
raise Exception('Project contains 0 rows. Please check --help for mandatory arguments for xml, json, xls and ods')
|
|
|
|
def list_projects():
|
|
"""Query the Refine server and list projects by ID: name."""
|
|
projects = refine.Refine(refine.RefineServer()).list_projects().items()
|
|
|
|
def date_to_epoch(json_dt):
|
|
"""Convert a JSON date time into seconds-since-epoch."""
|
|
return time.mktime(time.strptime(json_dt, '%Y-%m-%dT%H:%M:%SZ'))
|
|
projects.sort(key=lambda v: date_to_epoch(v[1]['modified']), reverse=True)
|
|
for project_id, project_info in projects:
|
|
print('{0:>14}: {1}'.format(project_id, project_info['name']))
|
|
|
|
|
|
def export_project(project, options):
|
|
"""Dump a project to stdout or options.output file."""
|
|
export_format = 'tsv'
|
|
if options.output:
|
|
ext = os.path.splitext(options.output)[1][1:] # 'xls'
|
|
if ext:
|
|
export_format = ext.lower()
|
|
output = open(options.output, 'wb')
|
|
else:
|
|
output = sys.stdout
|
|
output.writelines(project.export(export_format=export_format))
|
|
output.close()
|
|
|
|
|
|
#noinspection PyPep8Naming
|
|
def main():
|
|
"""Main."""
|
|
options, args = PARSER.parse_args()
|
|
|
|
if options.host:
|
|
refine.REFINE_HOST = options.host
|
|
if options.port:
|
|
refine.REFINE_PORT = options.port
|
|
|
|
if not options.list and len(args) != 1:
|
|
PARSER.print_usage()
|
|
|
|
if options.list:
|
|
list_projects()
|
|
|
|
if options.create:
|
|
file_fullpath = args[0]
|
|
create_project(options, file_fullpath)
|
|
|
|
if options.apply:
|
|
project = refine.RefineProject(args[0])
|
|
response = project.apply_operations(options.apply)
|
|
if response != 'ok':
|
|
print >> sys.stderr, 'Failed to apply %s: %s' \
|
|
% (options.apply, response)
|
|
|
|
if options.export:
|
|
project = refine.RefineProject(args[0])
|
|
export_project(project, options)
|
|
|
|
return project
|
|
|
|
if __name__ == '__main__':
|
|
# return project so that it's available interactively, python -i refine.py
|
|
refine_project = main()
|