Completed
Push — master ( d242d8...16b8b8 )
by Gus
01:27
created

ProcessorsAPI.annotate_from_sentences()   A

Complexity

Conditions 1

Size

Total Lines 5

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
dl 0
loc 5
rs 9.4285
c 0
b 0
f 0
1
#!/usr/bin/env python
2
# -*- coding: utf-8 -*-
3
from __future__ import unicode_literals
4
from pkg_resources import resource_filename
5
from .processors import *
6
from .sentiment import SentimentAnalysisAPI
7
from .odin import OdinAPI
8
import os
9
import shlex
10
import os
11
import subprocess as sp
12
import requests
13
import time
14
import sys
15
import logging
16
17
18
class ProcessorsAPI(object):
19
20
    PROC_VAR = 'PROCESSORS_SERVER'
21
22
    def __init__(self, port, hostname="127.0.0.1", timeout=120, jvm_mem="-Xmx3G", jar_path=None, keep_alive=False, log_file=None):
23
24
        self.hostname = hostname
25
        self.port = port
26
        self.make_address(hostname, port)
27
        self._start_command = "java {} -cp {} NLPServer {}" # mem, jar path, port
28
        self.timeout = timeout
29
        self.jvm_mem = jvm_mem
30
        # whether or not to stop the server when the object is destroyed
31
        self.keep_alive = keep_alive
32
        # how long to wait between requests
33
        self.wait_time = 2
34
        # processors
35
        self.default = Processor(self.address)
36
        self.fastnlp = FastNLPProcessor(self.address)
37
        self.bionlp = BioNLPProcessor(self.address)
38
        # sentiment
39
        self.sentiment = SentimentAnalysisAPI(self.address)
40
        # odin
41
        self.odin = OdinAPI(self.address)
42
        # use the os module's devnull for compatibility with python 2.7
43
        #self.DEVNULL = open(os.devnull, 'wb')
44
        self.logger = logging.getLogger(__name__)
45
        self.log_file = self.prepare_log_file(log_file)
46
47
        # resolve jar path
48
        self.resolve_jar_path(jar_path)
49
        # attempt to establish connection with server
50
        self.establish_connection()
51
52
    def prepare_log_file(self, lf):
53
        """
54
        Configure logger and return file path for logging
55
        """
56
        # log_file
57
        log_file = os.path.expanduser(os.path.join("~", ".py-processors.log")) if not lf else os.path.expanduser(lf)
58
        # configure logger
59
        self.logger.setLevel(logging.DEBUG)
60
        # create console handler and set level to info
61
        handler = logging.StreamHandler()
62
        handler.setLevel(logging.INFO)
63
        formatter = logging.Formatter("%(levelname)s - %(message)s")
64
        handler.setFormatter(formatter)
65
        self.logger.addHandler(handler)
66
        # create debug file handler and set level to debug
67
        handler = logging.FileHandler(log_file, "w")
68
        handler.setLevel(logging.DEBUG)
69
        formatter = logging.Formatter("%(levelname)s - %(message)s")
70
        handler.setFormatter(formatter)
71
        self.logger.addHandler(handler)
72
        return log_file
73
74
    def annotate(self, text):
75
        """
76
        Uses default processor (CoreNLP) to annotate text.  Included for backwards compatibility.
77
        """
78
        return self.default.annotate(text)
79
80
    def annotate_from_sentences(self, sentences):
81
        """
82
        Uses default processor (CoreNLP) to annotate a list of segmented sentences.
83
        """
84
        return self.default.annotate_from_sentences(sentences)
85
86
    def establish_connection(self):
87
        """
88
        Attempt to connect to a server (assumes server is running)
89
        """
90
        if self.annotate("Blah"):
91
            print("Connection with server established!")
92
        else:
93
            try:
94
                # Attempt to start the server
95
                self._start_server()
96
            except Exception as e:
97
                if not os.path.exists(self.jar_path):
98
                    print("\nprocessors-server.jar not found at {}.".format(self.jar_path))
99
                print("Unable to start server. Please start the server manually with .start_server(\"path/to/processors-server.jar\")")
100
                print("\n{}".format(e))
101
102
    def resolve_jar_path(self, jar_path):
103
        """
104
        Attempts to preferentially set value of self.jar_path
105
        """
106
        # Preference 1: if a .jar is given, check to see if the path is valid
107
        if jar_path:
108
            print("Using provided path")
109
            jp = os.path.expanduser(jar_path)
110
            # check if path is valid
111
            if os.path.exists(jp):
112
                self.jar_path = jp
113
        else:
114
            # Preference 2: if a PROCESSORS_SERVER environment variable is defined, check its validity
115
            if ProcessorsAPI.PROC_VAR in os.environ:
116
                print("Using path given via $PROCESSORS_SERVER")
117
                jp = os.path.expanduser(os.environ[ProcessorsAPI.PROC_VAR])
118
                # check if path is valid
119
                if os.path.exists(jp):
120
                    self.jar_path = jp
121
                else:
122
                    print("WARNING: {0} path is invalid.  \nPlease verify this entry in your environment:\n\texport {0}=/path/to/processors-server.jar".format(ProcessorsAPI.PROC_VAR))
123
            # Preference 3: attempt to use the processors-sever.jar downloaded when this package was installed
124
            else:
125
                print("Using default")
126
                self.jar_path = resource_filename(__name__, "processors-server.jar")
127
128
    def start_server(self, jar_path=None, timeout=120):
129
        """
130
        Starts processors-sever.jar
131
        """
132
        self.timeout = int(float(timeout)/2)
133
        if jar_path:
134
            self.jar_path = jar_path
135
        self._start_server()
136
137
    def stop_server(self, port=None):
138
        """
139
        Sends a poison pill to the server and waits for shutdown response
140
        """
141
        port = port or self.port
142
        address = "http://{}:{}".format(self.hostname, port)
143
        shutdown_address = "{}/shutdown".format(address)
144
        # attempt shutdown
145
        try:
146
            response = requests.post(shutdown_address)
147
            if response:
148
                print(response.content.decode("utf-8"))
149
            return True
150
        # will fail if the server is already down
151
        except Exception as e:
152
            pass
153
        return False
154
155
    def _start_server(self, port=None):
156
        """
157
        "Private" method called by start_server()
158
        """
159
        if port:
160
            self.port = port
161
        # build the command
162
        cmd = self._start_command.format(self.jvm_mem, self.jar_path, self.port)
163
        self._process = sp.Popen(shlex.split(cmd),
164
                                 shell=False,
165
                                 stderr=open(self.log_file, 'wb'),
166
                                 stdout=open(self.log_file, 'wb'),
167
                                 universal_newlines=True)
168
169
        self.logger.info("Starting processors-server ({}) ...".format(cmd))
170
        print("\nWaiting for server...")
171
172
        progressbar_length = int(self.timeout/self.wait_time)
173
        for i in range(progressbar_length):
174
            try:
175
                success = self.annotate("blah")
176
                if success:
177
                    print("\n\nConnection with processors-server established ({})".format(self.address))
178
                    return True
179
                sys.stdout.write("\r[{:{}}]".format('='*i, progressbar_length))
180
                time.sleep(self.wait_time)
181
            except Exception as e:
182
                raise(e)
183
184
        # if the server still hasn't started, raise an Exception
185
        raise Exception("Couldn't connect to processors-server. Is the port in use?")
186
187
    def make_address(self, hostname, port):
188
        # update hostname
189
        self.hostname = hostname
190
        # update port
191
        self.port = port
192
        # update address
193
        self.address = "http://{}:{}".format(self.hostname, self.port)
194
195
    def _get_path(self, p):
196
        """
197
        Expand a user-specified path.  Supports "~" shortcut.
198
        """
199
        return os.path.abspath(os.path.normpath(os.path.expanduser(p)))
200
201
    def __del__(self):
202
        """
203
        Stop server unless otherwise specified
204
        """
205
        if not self.keep_alive:
206
            try:
207
                self.stop_server()
208
                # close our file object
209
                #self.DEVNULL.close()
210
                print("Successfully shut down processors-server!")
211
            except Exception as e:
212
                self.logger.debug(e)
213
                print("Couldn't kill processors-server.  Was server started externally?")
214