Skip to content
This repository

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP
Browse code

fms core refactoring to avoid mass import from setup.py

  • Loading branch information...
commit a68ccaad9c4eacc3eb7bf0790952b364d458d5ea 1 parent 7d9abf1
Jean-Charles Bagneris authored
3  docs/en/conf.py
@@ -42,7 +42,8 @@
42 42 # other places throughout the built documents.
43 43 #
44 44 # The full version, including alpha/beta/rc tags.
45   -release = __import__('fms').VERSION
  45 +from fms.version import TAG
  46 +release = TAG
46 47 # The short X.Y version.
47 48 version = release.rsplit('.',1)[0]
48 49
270 fms/__init__.py
... ... @@ -1,273 +1,5 @@
1 1 #!/usr/bin/env python
2 2 # -*- coding: utf8 -*-
3 3 """
4   -FMS core module.
  4 +This module is empty to avoid mass import from e.g. setup.py
5 5 """
6   -
7   -import sys
8   -import os
9   -import optparse
10   -import logging
11   -
12   -from fms.utils import XmlParamsParser, YamlParamsParser
13   -from fms.utils import COMMANDS, OPTS_VAL, OPTS_BOOL
14   -
15   -VERSION = '0.1.6'
16   -
17   -def get_full_version():
18   - """
19   - Return full version number
20   -
21   - The full version includes last commit reference if any,
22   - i.e. if we run from a git repository.
23   - """
24   - from fms.utils import get_git_commit
25   - version = VERSION
26   - git_commit = get_git_commit()
27   - if git_commit:
28   - version = "%s-%s" % (version, git_commit)
29   - return version
30   -
31   -def get_simconffile(args):
32   - """
33   - Get experiment config file name from command line
34   - """
35   - logger = logging.getLogger('fms')
36   - try:
37   - simconffile = args[1]
38   - except IndexError:
39   - logger.critical("Missing simulation config file name.")
40   - sys.exit(2)
41   - return simconffile
42   -
43   -def apply_opts(params, opts):
44   - """
45   - Apply opts to params.
46   -
47   - Command line options override config file parameters
48   - """
49   - for opt in OPTS_VAL:
50   - optvalue = getattr(opts, opt)
51   - if optvalue:
52   - params[opt] = optvalue
53   -
54   - for opt in OPTS_BOOL:
55   - optvalue = getattr(opts, opt)
56   - if isinstance(optvalue, bool):
57   - params[opt] = optvalue
58   - else:
59   - if not opt in params:
60   - params[opt] = OPTS_BOOL[opt]
61   -
62   - if opts.repeat:
63   - for key in ('outputfilename', 'orderslogfilename'):
64   - if key in params:
65   - if params[key] not in ('None', 'sys.stdout'):
66   - parts = params[key].rsplit('.',1)
67   - params[key] = '.'.join((parts[0]+'-%03d', parts[1]))
68   -
69   - if opts.replay:
70   - del params['agents'][1:]
71   - if not params['agents'][0]['classname'] == 'PlayOrderLogFile':
72   - params['agents'][0]['classname'] = 'PlayOrderLogFile'
73   - params['agents'][0]['number'] = 1
74   - if not opts.orderslogfilename and not params['orderslogfilename']:
75   - params['orderslogfilename'] = '.'.join((
76   - params['simconffile'].rsplit('.',1)[0],'log'))
77   - params['agents'][0]['args'] = [os.path.abspath(
78   - params['orderslogfilename'])]
79   - params['orderslogfilename'] = None
80   -
81   - if params['orderslogfilename'] in ('None', 'sys.stdout'):
82   - params['orderslogfilename'] = None
83   -
84   - return params
85   -
86   -def get_params(args, opts):
87   - """
88   - Get params from conffile
89   - """
90   - logger = logging.getLogger('fms')
91   - simconffile = get_simconffile(args)
92   - if os.path.splitext(simconffile)[-1] == '.xml':
93   - logger.debug("Calling XmlParamsParser on %s" % simconffile)
94   - params = XmlParamsParser(simconffile)
95   - else:
96   - logger.debug("Calling YamlParamsParser on %s" % simconffile)
97   - params = YamlParamsParser(simconffile)
98   - params['simconffile'] = simconffile
99   - params = apply_opts(params, opts)
100   - params.printparams()
101   - return params
102   -
103   -def get_command(args, parser):
104   - """
105   - Get command from command line arguments
106   - """
107   - logger = logging.getLogger('fms')
108   - try:
109   - command = args[0]
110   - except IndexError:
111   - parser.print_help()
112   - logger.critical("Missing command name.")
113   - sys.exit(2)
114   - if command not in COMMANDS:
115   - logger.critical("Unknown command: %s" % command)
116   - sys.exit(2)
117   - return command
118   -
119   -def set_parser():
120   - """
121   - Create command line arguments and options parser
122   - """
123   - optp = optparse.OptionParser(
124   - description='run a Financial Market Simulator simulation',
125   - prog='%s' % sys.argv[0],
126   - usage="%prog [options] [command] simulationconffile",
127   - version = "This is FMS v%s" % get_full_version(),)
128   - # general value options
129   - optp.add_option('-L', '--loglevel', metavar='LEVEL', dest='loglevel',
130   - help="set logging level to LEVEL: debug, info, warning, error, critical")
131   - # boolean options overriding config parameters
132   - optp.add_option('-v', '--verbose', action='store_true',
133   - help="set logging level to 'info', overrided by --loglevel")
134   - optp.add_option('--show_books','--show_limits', action='store_true',
135   - dest="show_books", help="show best limits on each step")
136   - optp.add_option('-r', '--replay', action='store_true',
137   - help="Replay an orders logfile.")
138   - optp.add_option('-t', '--timer', action='store_true',
139   - help="Print a timer.")
140   - optp.add_option('--unique_by_agent', action='store_true',
141   - help="Only one order by agent in books.")
142   - optp.add_option('--no_unique_by_agent', action='store_false',
143   - dest='unique_by_agent',
144   - help="More than one order by agent allowed in books.")
145   - # value options overriding config parameters
146   - optp.add_option('--orderslogfilename', dest='orderslogfilename',
147   - help='orders log filename')
148   - optp.add_option('--outputfilename', '-o', dest='outputfilename',
149   - help='output filename')
150   - optp.add_option('--randomseed',
151   - help='random seed')
152   - optp.add_option('--csvdelimiter',
153   - help='csv delimiter')
154   - optp.add_option('--repeat',
155   - help='repeat experiment N times')
156   -
157   - return optp
158   -
159   -def set_logger(options, logname='fms'):
160   - """
161   - Sets main logger instance.
162   - """
163   - if isinstance(options, str):
164   - loglevel = options
165   - else:
166   - loglevel = 'error'
167   - if options.verbose:
168   - loglevel = 'info'
169   - if options.loglevel:
170   - loglevel = options.loglevel
171   - levels = {'debug': logging.DEBUG,
172   - 'info': logging.INFO,
173   - 'warning': logging.WARNING,
174   - 'error': logging.ERROR,
175   - 'critical': logging.CRITICAL,}
176   - logger = logging.getLogger(logname)
177   - lhandler = logging.StreamHandler()
178   - formatter = logging.Formatter("%(levelname)s - %(name)s - %(message)s")
179   - lhandler.setFormatter(formatter)
180   - logger.addHandler(lhandler)
181   - logger.setLevel(levels[loglevel])
182   - return logger
183   -
184   -def import_class(modulename, classname):
185   - """
186   - Try to import classname from modulename.
187   - """
188   - logger = logging.getLogger('fms')
189   - try:
190   - exec "from %s import %s as themodule " % \
191   - (modulename, classname.lower())
192   - except ImportError:
193   - logger.critical("Unknown %s class: %s" % (modulename, classname))
194   - sys.exit(2)
195   - return themodule
196   -
197   -def set_world(params):
198   - """
199   - Import world class and instanciate world
200   - """
201   - logger = logging.getLogger('fms')
202   - worldmodule = import_class('fms.worlds', params['world']['classname'])
203   - world = getattr(worldmodule, params['world']['classname'])(params)
204   - logger.info("Created world %s" % world)
205   - return world
206   -
207   -def set_agents(params):
208   - """
209   - Import agents class and instanciate agents
210   - """
211   - logger = logging.getLogger('fms')
212   - agentslist = []
213   - for (offset, a) in enumerate(params['agents']):
214   - agentmodule = import_class('fms.agents', a['classname'])
215   - for i in range(a['number']):
216   - agentslist.append(getattr(agentmodule, a['classname'])(params, offset))
217   - logger.info("Created %d instances of agent %s" %
218   - (a['number'], agentslist[-1].__class__))
219   - return agentslist
220   -
221   -def set_engines(params):
222   - """
223   - Import engines and markets classes and instanciate them
224   - """
225   - logger = logging.getLogger('fms')
226   - engineslist = []
227   - for (offset, e) in enumerate(params['engines']):
228   - marketmodule = import_class('fms.markets', e['market']['classname'])
229   - e['market']['instance'] = getattr(marketmodule,
230   - e['market']['classname'])(params)
231   - enginemodule = import_class('fms.engines', e['classname'])
232   - e['instance'] = getattr(enginemodule, e['classname'])(params, offset)
233   - engineslist.append(e)
234   - logger.info("Created engine-market %s - %s" %
235   - (e['instance'], e['market']['instance']))
236   - return engineslist
237   -
238   -def set_classes(params):
239   - """
240   - Parse conffile and instanciate classes
241   - """
242   - world = set_world(params)
243   - engineslist = set_engines(params)
244   - agentslist = set_agents(params)
245   - return (world, engineslist, agentslist)
246   -
247   -def do_check(args, opts):
248   - """
249   - Command: check experiment conffile, do not run
250   - """
251   - params = get_params(args, opts)
252   - for turn in xrange(int(params['repeat'])):
253   - (world, engineslist, agentslist) = set_classes(params)
254   -
255   -def do_run(args, opts):
256   - """
257   - Command: run experiment
258   - """
259   - logger = logging.getLogger('fms')
260   - params = get_params(args, opts)
261   - if logger.getEffectiveLevel() < logging.INFO:
262   - params.showbooks = True
263   - for turn in xrange(int(params['repeat'])):
264   - params.create_files(turn)
265   - params.printfileheaders()
266   - (world, engineslist, agentslist) = set_classes(params)
267   - logger.info("All is set, running simulation %03d" % turn)
268   - for e in engineslist:
269   - logger.info("Running %s" % e['instance'])
270   - e['instance'].run(world, agentslist, e['market']['instance'])
271   - logger.info("Done.")
272   - params.close_files(turn)
273   -
259 fms/core.py
... ... @@ -0,0 +1,259 @@
  1 +#!/usr/bin/env python
  2 +# -*- coding: utf8 -*-
  3 +"""
  4 +FMS core module.
  5 +"""
  6 +
  7 +import sys
  8 +import os
  9 +import optparse
  10 +import logging
  11 +
  12 +from fms.utils import COMMANDS, OPTS_VAL, OPTS_BOOL
  13 +from fms.utils.parsers import XmlParamsParser, YamlParamsParser
  14 +
  15 +from fms.version import VERSION
  16 +
  17 +def _get_simconffile(args):
  18 + """
  19 + Get experiment config file name from command line
  20 + """
  21 + logger = logging.getLogger('fms')
  22 + try:
  23 + simconffile = args[1]
  24 + except IndexError:
  25 + logger.critical("Missing simulation config file name.")
  26 + sys.exit(2)
  27 + return simconffile
  28 +
  29 +def _apply_opts(params, opts):
  30 + """
  31 + Apply opts to params.
  32 +
  33 + Command line options override config file parameters
  34 + """
  35 + for opt in OPTS_VAL:
  36 + optvalue = getattr(opts, opt)
  37 + if optvalue:
  38 + params[opt] = optvalue
  39 +
  40 + for opt in OPTS_BOOL:
  41 + optvalue = getattr(opts, opt)
  42 + if isinstance(optvalue, bool):
  43 + params[opt] = optvalue
  44 + else:
  45 + if not opt in params:
  46 + params[opt] = OPTS_BOOL[opt]
  47 +
  48 + if opts.repeat:
  49 + for key in ('outputfilename', 'orderslogfilename'):
  50 + if key in params:
  51 + if params[key] not in ('None', 'sys.stdout'):
  52 + parts = params[key].rsplit('.',1)
  53 + params[key] = '.'.join((parts[0]+'-%03d', parts[1]))
  54 +
  55 + if opts.replay:
  56 + del params['agents'][1:]
  57 + if not params['agents'][0]['classname'] == 'PlayOrderLogFile':
  58 + params['agents'][0]['classname'] = 'PlayOrderLogFile'
  59 + params['agents'][0]['number'] = 1
  60 + if not opts.orderslogfilename and not params['orderslogfilename']:
  61 + params['orderslogfilename'] = '.'.join((
  62 + params['simconffile'].rsplit('.',1)[0],'log'))
  63 + params['agents'][0]['args'] = [os.path.abspath(
  64 + params['orderslogfilename'])]
  65 + params['orderslogfilename'] = None
  66 +
  67 + if params['orderslogfilename'] in ('None', 'sys.stdout'):
  68 + params['orderslogfilename'] = None
  69 +
  70 + return params
  71 +
  72 +def _get_params(args, opts):
  73 + """
  74 + Get params from conffile
  75 + """
  76 + logger = logging.getLogger('fms')
  77 + simconffile = _get_simconffile(args)
  78 + if os.path.splitext(simconffile)[-1] == '.xml':
  79 + logger.debug("Calling XmlParamsParser on %s" % simconffile)
  80 + params = XmlParamsParser(simconffile)
  81 + else:
  82 + logger.debug("Calling YamlParamsParser on %s" % simconffile)
  83 + params = YamlParamsParser(simconffile)
  84 + params['simconffile'] = simconffile
  85 + params = _apply_opts(params, opts)
  86 + params.printparams()
  87 + return params
  88 +
  89 +def get_command(args, parser):
  90 + """
  91 + Get command from command line arguments
  92 + """
  93 + logger = logging.getLogger('fms')
  94 + try:
  95 + command = args[0]
  96 + except IndexError:
  97 + parser.print_help()
  98 + logger.critical("Missing command name.")
  99 + sys.exit(2)
  100 + if command not in COMMANDS:
  101 + logger.critical("Unknown command: %s" % command)
  102 + sys.exit(2)
  103 + return command
  104 +
  105 +def set_parser():
  106 + """
  107 + Create command line arguments and options parser
  108 + """
  109 + optp = optparse.OptionParser(
  110 + description='run a Financial Market Simulator simulation',
  111 + prog='%s' % sys.argv[0],
  112 + usage="%prog [options] [command] simulationconffile",
  113 + version = "This is FMS %s" % VERSION,)
  114 + # general value options
  115 + optp.add_option('-L', '--loglevel', metavar='LEVEL', dest='loglevel',
  116 + help="set logging level to LEVEL: debug, info, warning, error, critical")
  117 + # boolean options overriding config parameters
  118 + optp.add_option('-v', '--verbose', action='store_true',
  119 + help="set logging level to 'info', overrided by --loglevel")
  120 + optp.add_option('--show_books','--show_limits', action='store_true',
  121 + dest="show_books", help="show best limits on each step")
  122 + optp.add_option('-r', '--replay', action='store_true',
  123 + help="Replay an orders logfile.")
  124 + optp.add_option('-t', '--timer', action='store_true',
  125 + help="Print a timer.")
  126 + optp.add_option('--unique_by_agent', action='store_true',
  127 + help="Only one order by agent in books.")
  128 + optp.add_option('--no_unique_by_agent', action='store_false',
  129 + dest='unique_by_agent',
  130 + help="More than one order by agent allowed in books.")
  131 + # value options overriding config parameters
  132 + optp.add_option('--orderslogfilename', dest='orderslogfilename',
  133 + help='orders log filename')
  134 + optp.add_option('--outputfilename', '-o', dest='outputfilename',
  135 + help='output filename')
  136 + optp.add_option('--randomseed',
  137 + help='random seed')
  138 + optp.add_option('--csvdelimiter',
  139 + help='csv delimiter')
  140 + optp.add_option('--repeat',
  141 + help='repeat experiment N times')
  142 +
  143 + return optp
  144 +
  145 +def set_logger(options, logname='fms'):
  146 + """
  147 + Sets main logger instance.
  148 + """
  149 + if isinstance(options, str):
  150 + loglevel = options
  151 + else:
  152 + loglevel = 'error'
  153 + if options.verbose:
  154 + loglevel = 'info'
  155 + if options.loglevel:
  156 + loglevel = options.loglevel
  157 + levels = {'debug': logging.DEBUG,
  158 + 'info': logging.INFO,
  159 + 'warning': logging.WARNING,
  160 + 'error': logging.ERROR,
  161 + 'critical': logging.CRITICAL,}
  162 + logger = logging.getLogger(logname)
  163 + lhandler = logging.StreamHandler()
  164 + formatter = logging.Formatter("%(levelname)s - %(name)s - %(message)s")
  165 + lhandler.setFormatter(formatter)
  166 + logger.addHandler(lhandler)
  167 + logger.setLevel(levels[loglevel])
  168 + return logger
  169 +
  170 +def _import_class(modulename, classname):
  171 + """
  172 + Try to import classname from modulename.
  173 + """
  174 + logger = logging.getLogger('fms')
  175 + try:
  176 + exec "from %s import %s as themodule " % \
  177 + (modulename, classname.lower())
  178 + except ImportError:
  179 + logger.critical("Unknown %s class: %s" % (modulename, classname))
  180 + sys.exit(2)
  181 + return themodule
  182 +
  183 +def _set_world(params):
  184 + """
  185 + Import world class and instanciate world
  186 + """
  187 + logger = logging.getLogger('fms')
  188 + worldmodule = _import_class('fms.worlds', params['world']['classname'])
  189 + world = getattr(worldmodule, params['world']['classname'])(params)
  190 + logger.info("Created world %s" % world)
  191 + return world
  192 +
  193 +def _set_agents(params):
  194 + """
  195 + Import agents class and instanciate agents
  196 + """
  197 + logger = logging.getLogger('fms')
  198 + agentslist = []
  199 + for (offset, a) in enumerate(params['agents']):
  200 + agentmodule = _import_class('fms.agents', a['classname'])
  201 + for i in range(a['number']):
  202 + agentslist.append(getattr(agentmodule, a['classname'])(params, offset))
  203 + logger.info("Created %d instances of agent %s" %
  204 + (a['number'], agentslist[-1].__class__))
  205 + return agentslist
  206 +
  207 +def _set_engines(params):
  208 + """
  209 + Import engines and markets classes and instanciate them
  210 + """
  211 + logger = logging.getLogger('fms')
  212 + engineslist = []
  213 + for (offset, e) in enumerate(params['engines']):
  214 + marketmodule = _import_class('fms.markets', e['market']['classname'])
  215 + e['market']['instance'] = getattr(marketmodule,
  216 + e['market']['classname'])(params)
  217 + enginemodule = _import_class('fms.engines', e['classname'])
  218 + e['instance'] = getattr(enginemodule, e['classname'])(params, offset)
  219 + engineslist.append(e)
  220 + logger.info("Created engine-market %s - %s" %
  221 + (e['instance'], e['market']['instance']))
  222 + return engineslist
  223 +
  224 +def set_classes(params):
  225 + """
  226 + Parse conffile and instanciate classes
  227 + """
  228 + world = _set_world(params)
  229 + engineslist = _set_engines(params)
  230 + agentslist = _set_agents(params)
  231 + return (world, engineslist, agentslist)
  232 +
  233 +def do_check(args, opts):
  234 + """
  235 + Command: check experiment conffile, do not run
  236 + """
  237 + params = _get_params(args, opts)
  238 + for turn in xrange(int(params['repeat'])):
  239 + (world, engineslist, agentslist) = set_classes(params)
  240 +
  241 +def do_run(args, opts):
  242 + """
  243 + Command: run experiment
  244 + """
  245 + logger = logging.getLogger('fms')
  246 + params = _get_params(args, opts)
  247 + if logger.getEffectiveLevel() < logging.INFO:
  248 + params.showbooks = True
  249 + for turn in xrange(int(params['repeat'])):
  250 + params.create_files(turn)
  251 + params.printfileheaders()
  252 + (world, engineslist, agentslist) = set_classes(params)
  253 + logger.info("All is set, running simulation %03d" % turn)
  254 + for e in engineslist:
  255 + logger.info("Running %s" % e['instance'])
  256 + e['instance'].run(world, agentslist, e['market']['instance'])
  257 + logger.info("Done.")
  258 + params.close_files(turn)
  259 +
47 fms/version.py
... ... @@ -0,0 +1,47 @@
  1 +#!/usr/bin/env python
  2 +# -*- coding: utf8 -*-
  3 +"""
  4 +FMS version setting and getting.
  5 +"""
  6 +
  7 +from fms.utils.git import get_git_commit_hash, get_git_status
  8 +
  9 +TAG = 'v0.1.7a1'
  10 +
  11 +def get_full_version():
  12 + """
  13 + Return full version number
  14 +
  15 + The full version includes last commit reference if any,
  16 + i.e. if we run from a git repository.
  17 + """
  18 + from fms.utils.git import get_git_commit_hash, is_repo_clean, is_git_repo
  19 + version = TAG
  20 + if not is_git_repo():
  21 + return version
  22 + git_commit = get_git_commit_hash()[:8]
  23 + if git_commit:
  24 + version = "%s-%s" % (version, git_commit)
  25 + if not is_repo_clean():
  26 + version = "%s-dirty" % version
  27 + return version
  28 +
  29 +def get_version():
  30 + """
  31 + Return current version number.
  32 + Version number is :
  33 + - last tag number if last commit == last tag and repo is clean
  34 + or not a git repo (regular fms install)
  35 + - get_full_version() otherwise
  36 + """
  37 + from fms.utils.git import get_git_commit_hash, is_git_repo, is_repo_clean
  38 + if is_git_repo():
  39 + if is_repo_clean():
  40 + if get_git_commit_hash() == get_git_commit_hash(TAG):
  41 + return TAG
  42 + return get_full_version()
  43 + else:
  44 + return TAG
  45 +
  46 +VERSION = get_version()
  47 +
18 setup.py
@@ -2,22 +2,8 @@
2 2 import os.path
3 3 from distutils.core import setup
4 4
5   -version = __import__('fms').VERSION
6   -
7   -data_files = []
8   -
9   -data_dir = (
10   - 'docs/examples',
11   - 'docs/en/man',
12   - 'docs/en/html',
13   - )
14   -
15   -for directory in data_dir:
16   - for dirpath, dirnames, filenames in os.walk(directory):
17   - newpath = os.path.join('Doc',
18   - 'fms-documentation', dirpath.split(os.sep, 1)[1])
19   - data_files.append([newpath,
20   - [os.path.join(dirpath, f) for f in filenames]])
  5 +import fms.version
  6 +version = fms.version.TAG
21 7
22 8
23 9 setup(name='fms',
10 startfms.py
@@ -10,7 +10,7 @@
10 10 import sys
11 11 import logging
12 12
13   -import fms
  13 +from fms.core import set_parser, set_logger, get_command
14 14
15 15 def main():
16 16 """
@@ -19,11 +19,11 @@ def main():
19 19 - read simulation config file
20 20 - run [command]
21 21 """
22   - parser = fms.set_parser()
  22 + parser = set_parser()
23 23 options, arguments = parser.parse_args()
24   - logger = fms.set_logger(options)
25   - command = fms.get_command(arguments, parser)
26   - getattr(fms, "do_%s" % command)(arguments, options)
  24 + logger = set_logger(options)
  25 + command = get_command(arguments, parser)
  26 + getattr(fms.core, "do_%s" % command)(arguments, options)
27 27 return 0
28 28
29 29 if __name__ == "__main__":
5 tests/runalltests.py
@@ -12,6 +12,7 @@
12 12 from StringIO import StringIO
13 13
14 14 import fms
  15 +import fms.core
15 16 from fms.utils.parsers import YamlParamsParser
16 17
17 18 def sourceList():
@@ -36,7 +37,7 @@ def expList():
36 37 old_dir = os.getcwd()
37 38 os.chdir(os.path.dirname(__file__))
38 39
39   -logger = fms.set_logger('info','fms-tests')
  40 +logger = fms.core.set_logger('info','fms-tests')
40 41
41 42 logger.info("Running unittests")
42 43 suite = unittest.TestSuite()
@@ -70,7 +71,7 @@ def expList():
70 71 params['show_books'] = False
71 72 params['timer'] = False
72 73 params.outputfile = StringIO()
73   - (world, engineslist, agentslist) = fms.set_classes(params)
  74 + (world, engineslist, agentslist) = fms.core.set_classes(params)
74 75 for e in engineslist:
75 76 e['instance'].run(world, agentslist, e['market']['instance'])
76 77 benchfile = "%s.csv" % simconffile.split('.')[0]

0 comments on commit a68ccaa

Please sign in to comment.
Something went wrong with that request. Please try again.