dnaStreaming 2.2.1.dev1__tar.gz → 2.3.1__tar.gz
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.
- {dnastreaming-2.2.1.dev1/dnaStreaming.egg-info → dnastreaming-2.3.1}/PKG-INFO +32 -7
- {dnastreaming-2.2.1.dev1 → dnastreaming-2.3.1}/README.rst +29 -5
- dnastreaming-2.3.1/demo/pubsub_stream.py +81 -0
- dnastreaming-2.3.1/demo/show_ha_stream_async.py +62 -0
- dnastreaming-2.3.1/demo/show_stream.py +57 -0
- dnastreaming-2.3.1/demo/show_stream_async.py +61 -0
- dnastreaming-2.3.1/dnaStreaming/__init__.py +45 -0
- {dnastreaming-2.2.1.dev1 → dnastreaming-2.3.1/dnaStreaming.egg-info}/PKG-INFO +32 -7
- {dnastreaming-2.2.1.dev1 → dnastreaming-2.3.1}/dnaStreaming.egg-info/SOURCES.txt +5 -5
- {dnastreaming-2.2.1.dev1 → dnastreaming-2.3.1}/dnaStreaming.egg-info/requires.txt +1 -0
- {dnastreaming-2.2.1.dev1 → dnastreaming-2.3.1}/dnaStreaming.egg-info/top_level.txt +1 -0
- {dnastreaming-2.2.1.dev1 → dnastreaming-2.3.1}/setup.py +2 -1
- dnastreaming-2.2.1.dev1/dnaStreaming/__init__.py +0 -29
- dnastreaming-2.2.1.dev1/dnaStreaming/demo/pubsub_stream.py +0 -38
- dnastreaming-2.2.1.dev1/dnaStreaming/demo/show_ha_stream_async.py +0 -36
- dnastreaming-2.2.1.dev1/dnaStreaming/demo/show_stream.py +0 -30
- dnastreaming-2.2.1.dev1/dnaStreaming/demo/show_stream_async.py +0 -36
- {dnastreaming-2.2.1.dev1 → dnastreaming-2.3.1}/LICENSE +0 -0
- {dnastreaming-2.2.1.dev1 → dnastreaming-2.3.1}/MANIFEST.in +0 -0
- {dnastreaming-2.2.1.dev1/dnaStreaming → dnastreaming-2.3.1}/demo/__init__.py +0 -0
- {dnastreaming-2.2.1.dev1 → dnastreaming-2.3.1}/dnaStreaming/config.py +0 -0
- {dnastreaming-2.2.1.dev1 → dnastreaming-2.3.1}/dnaStreaming/customer_config.json +0 -0
- {dnastreaming-2.2.1.dev1 → dnastreaming-2.3.1}/dnaStreaming/listener.py +0 -0
- {dnastreaming-2.2.1.dev1 → dnastreaming-2.3.1}/dnaStreaming/services/__init__.py +0 -0
- {dnastreaming-2.2.1.dev1 → dnastreaming-2.3.1}/dnaStreaming/services/authentication_service.py +0 -0
- {dnastreaming-2.2.1.dev1 → dnastreaming-2.3.1}/dnaStreaming/services/availability_service.py +0 -0
- {dnastreaming-2.2.1.dev1 → dnastreaming-2.3.1}/dnaStreaming/services/credentials_service.py +0 -0
- {dnastreaming-2.2.1.dev1 → dnastreaming-2.3.1}/dnaStreaming/services/pubsub_service.py +0 -0
- {dnastreaming-2.2.1.dev1 → dnastreaming-2.3.1}/dnaStreaming/test/__init__.py +0 -0
- {dnastreaming-2.2.1.dev1 → dnastreaming-2.3.1}/dnaStreaming/test/test_config.py +0 -0
- {dnastreaming-2.2.1.dev1 → dnastreaming-2.3.1}/dnaStreaming.egg-info/dependency_links.txt +0 -0
- {dnastreaming-2.2.1.dev1 → dnastreaming-2.3.1}/setup.cfg +0 -0
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: dnaStreaming
|
|
3
|
-
Version: 2.
|
|
3
|
+
Version: 2.3.1
|
|
4
4
|
Summary: Dow Jones DNA Streaming Project
|
|
5
5
|
Home-page: https://github.com/dowjones/dj-dna-streams-python
|
|
6
|
-
Download-URL: https://github.com/dowjones/dj-dna-streams-python/archive/release-2.
|
|
6
|
+
Download-URL: https://github.com/dowjones/dj-dna-streams-python/archive/release-2.3.1.tar.gz
|
|
7
7
|
Author: Zachary Kagan
|
|
8
8
|
Author-email: zachary.kagan@dowjones.com
|
|
9
9
|
License: MIT
|
|
@@ -14,6 +14,7 @@ Classifier: License :: OSI Approved :: MIT License
|
|
|
14
14
|
Classifier: Programming Language :: Python :: 3
|
|
15
15
|
Description-Content-Type: text/x-rst
|
|
16
16
|
License-File: LICENSE
|
|
17
|
+
Requires-Dist: setuptools>=75.1.0
|
|
17
18
|
Requires-Dist: googleapis-common-protos>=1.56.4
|
|
18
19
|
Requires-Dist: google-auth>=2.11.0
|
|
19
20
|
Requires-Dist: google-cloud-pubsub==2.13.6
|
|
@@ -66,7 +67,13 @@ To set your subscription ID, simply set an environment variable named 'SUBSCRIPT
|
|
|
66
67
|
export SUBSCRIPTION_ID="ABC1234567889"
|
|
67
68
|
|
|
68
69
|
|
|
69
|
-
|
|
70
|
+
To set your log folder path, simply set this folder variable named 'LOG_PATH' like so
|
|
71
|
+
|
|
72
|
+
.. code-block::
|
|
73
|
+
|
|
74
|
+
export LOG_PATH="/your/custom/log/path"
|
|
75
|
+
|
|
76
|
+
|
|
70
77
|
|
|
71
78
|
2. Using the configuration file.
|
|
72
79
|
###################################################################
|
|
@@ -146,7 +153,25 @@ You may want to listen messages asynchronously like so:
|
|
|
146
153
|
Log Files
|
|
147
154
|
_________
|
|
148
155
|
|
|
149
|
-
|
|
156
|
+
|
|
157
|
+
Minimal logging is written to a file named `dj-dna-streaming-python.log`.
|
|
158
|
+
|
|
159
|
+
By default, logs are written to the first available directory from the following list:
|
|
160
|
+
|
|
161
|
+
1. A custom path set via the environment variable `LOG_PATH`.
|
|
162
|
+
2. A `logs/` folder located within the package installation directory.
|
|
163
|
+
3. A fallback directory: `~/.dj-dna-streaming-python/logs/`.
|
|
164
|
+
|
|
165
|
+
The first writable location found is selected. A message like `Will log to: /your/custom/log/path` is printed to the console on startup.
|
|
166
|
+
|
|
167
|
+
💡 The log file is overwritten each time the application starts to keep maintenance simple.
|
|
168
|
+
|
|
169
|
+
You can specify:
|
|
170
|
+
|
|
171
|
+
- **Absolute paths**: For example, `/var/log/dna-streaming`.
|
|
172
|
+
- **Relative paths**: For example, `./logs`, relative to the current working directory at runtime.
|
|
173
|
+
|
|
174
|
+
The code verifies that the specified path is writable. If it isn’t, it automatically falls back to the next available option.
|
|
150
175
|
|
|
151
176
|
|
|
152
177
|
Testing
|
|
@@ -195,7 +220,7 @@ If you are enhancing this codebase (and not just using it as a library), follow
|
|
|
195
220
|
virtualenv venv
|
|
196
221
|
|
|
197
222
|
|
|
198
|
-
4. Then activate the
|
|
223
|
+
4. Then activate the virtual environment by executing this command:
|
|
199
224
|
###################################################################
|
|
200
225
|
|
|
201
226
|
.. code-block::
|
|
@@ -233,14 +258,14 @@ Execute the following at the project root:
|
|
|
233
258
|
|
|
234
259
|
.. code-block::
|
|
235
260
|
|
|
236
|
-
python ./
|
|
261
|
+
python ./demo/show_stream.py -s
|
|
237
262
|
|
|
238
263
|
|
|
239
264
|
Or
|
|
240
265
|
|
|
241
266
|
.. code-block::
|
|
242
267
|
|
|
243
|
-
python ./
|
|
268
|
+
python ./demo/show_stream_async.py -s
|
|
244
269
|
|
|
245
270
|
|
|
246
271
|
Running Docker Demo
|
|
@@ -41,7 +41,13 @@ To set your subscription ID, simply set an environment variable named 'SUBSCRIPT
|
|
|
41
41
|
export SUBSCRIPTION_ID="ABC1234567889"
|
|
42
42
|
|
|
43
43
|
|
|
44
|
-
|
|
44
|
+
To set your log folder path, simply set this folder variable named 'LOG_PATH' like so
|
|
45
|
+
|
|
46
|
+
.. code-block::
|
|
47
|
+
|
|
48
|
+
export LOG_PATH="/your/custom/log/path"
|
|
49
|
+
|
|
50
|
+
|
|
45
51
|
|
|
46
52
|
2. Using the configuration file.
|
|
47
53
|
###################################################################
|
|
@@ -121,7 +127,25 @@ You may want to listen messages asynchronously like so:
|
|
|
121
127
|
Log Files
|
|
122
128
|
_________
|
|
123
129
|
|
|
124
|
-
|
|
130
|
+
|
|
131
|
+
Minimal logging is written to a file named `dj-dna-streaming-python.log`.
|
|
132
|
+
|
|
133
|
+
By default, logs are written to the first available directory from the following list:
|
|
134
|
+
|
|
135
|
+
1. A custom path set via the environment variable `LOG_PATH`.
|
|
136
|
+
2. A `logs/` folder located within the package installation directory.
|
|
137
|
+
3. A fallback directory: `~/.dj-dna-streaming-python/logs/`.
|
|
138
|
+
|
|
139
|
+
The first writable location found is selected. A message like `Will log to: /your/custom/log/path` is printed to the console on startup.
|
|
140
|
+
|
|
141
|
+
💡 The log file is overwritten each time the application starts to keep maintenance simple.
|
|
142
|
+
|
|
143
|
+
You can specify:
|
|
144
|
+
|
|
145
|
+
- **Absolute paths**: For example, `/var/log/dna-streaming`.
|
|
146
|
+
- **Relative paths**: For example, `./logs`, relative to the current working directory at runtime.
|
|
147
|
+
|
|
148
|
+
The code verifies that the specified path is writable. If it isn’t, it automatically falls back to the next available option.
|
|
125
149
|
|
|
126
150
|
|
|
127
151
|
Testing
|
|
@@ -170,7 +194,7 @@ If you are enhancing this codebase (and not just using it as a library), follow
|
|
|
170
194
|
virtualenv venv
|
|
171
195
|
|
|
172
196
|
|
|
173
|
-
4. Then activate the
|
|
197
|
+
4. Then activate the virtual environment by executing this command:
|
|
174
198
|
###################################################################
|
|
175
199
|
|
|
176
200
|
.. code-block::
|
|
@@ -208,14 +232,14 @@ Execute the following at the project root:
|
|
|
208
232
|
|
|
209
233
|
.. code-block::
|
|
210
234
|
|
|
211
|
-
python ./
|
|
235
|
+
python ./demo/show_stream.py -s
|
|
212
236
|
|
|
213
237
|
|
|
214
238
|
Or
|
|
215
239
|
|
|
216
240
|
.. code-block::
|
|
217
241
|
|
|
218
|
-
python ./
|
|
242
|
+
python ./demo/show_stream_async.py -s
|
|
219
243
|
|
|
220
244
|
|
|
221
245
|
Running Docker Demo
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import json
|
|
3
|
+
import datetime
|
|
4
|
+
from google.cloud import pubsub_v1
|
|
5
|
+
from dnaStreaming.listener import Listener
|
|
6
|
+
from dnaStreaming import logger
|
|
7
|
+
|
|
8
|
+
gcp_project_id = os.getenv('GCP_PROJECT_ID', None)
|
|
9
|
+
gcp_pubsub_topic = os.getenv('GCP_PUBSUB_TOPIC', None)
|
|
10
|
+
gcp_creds = os.getenv('GOOGLE_APPLICATION_CREDENTIALS', None)
|
|
11
|
+
|
|
12
|
+
def print_message(message):
|
|
13
|
+
print(f"[{datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S')}] {message}")
|
|
14
|
+
|
|
15
|
+
if gcp_project_id is not None and gcp_pubsub_topic is not None and gcp_creds is not None:
|
|
16
|
+
listener = Listener()
|
|
17
|
+
ps_publisher = pubsub_v1.PublisherClient()
|
|
18
|
+
topic_path = ps_publisher.topic_path(gcp_project_id, gcp_pubsub_topic)
|
|
19
|
+
print(f"\n[ACTIVITY] Sending messages to Pub/Sub topic {gcp_pubsub_topic} in GCP...")
|
|
20
|
+
|
|
21
|
+
def callback(factiva_message, subscription_id, file_handle=None):
|
|
22
|
+
try:
|
|
23
|
+
if 'action' in factiva_message.keys():
|
|
24
|
+
# Message is an article event
|
|
25
|
+
# Implement your logic according to the documentation:
|
|
26
|
+
# https://developer.dowjones.com/documents/site-docs-factiva_apis-factiva_analytics_apis-factiva_streams_api#article-specific-events
|
|
27
|
+
if factiva_message['action'] == 'add':
|
|
28
|
+
# Insert the article to the repository as new. Handle repeated messages.
|
|
29
|
+
print_message(f"[ARTICLE] [ADD] AN: {factiva_message['an']} - {factiva_message['title']}")
|
|
30
|
+
elif factiva_message['action'] == 'rep':
|
|
31
|
+
# Upsert/Update/AddNewVersion the article in the repository. Handle repeated messages.
|
|
32
|
+
print_message(f"[ARTICLE] [REP] AN: {factiva_message['an']} - {factiva_message['title']}")
|
|
33
|
+
elif factiva_message['action'] == 'del':
|
|
34
|
+
# Delete or mark as deleted the article in the repository. Handle inexistent article AN.
|
|
35
|
+
print_message(f"[ARTICLE] [DEL] AN: {factiva_message['an']} - *** DELETE ***")
|
|
36
|
+
else:
|
|
37
|
+
print_message(f"Factiva Action Not Handled: {factiva_message['action']}")
|
|
38
|
+
|
|
39
|
+
logger.info(f"Sent to GCP Pub/Sub, message with AN: {factiva_message.get('an', 'N/A')}")
|
|
40
|
+
|
|
41
|
+
elif 'event_type' in factiva_message.keys():
|
|
42
|
+
# Message is a bulk action or service event
|
|
43
|
+
if factiva_message['event_type'] == 'source_delete':
|
|
44
|
+
# Delete all articles from the repository matching the source_code. Handle inexistent source_code and repeated messages.
|
|
45
|
+
print_message(f"[EVENT] [SOURCE_DELETE] Source: {factiva_message['source_code'].upper()} - {factiva_message['description']}")
|
|
46
|
+
else:
|
|
47
|
+
print_message(f"Factiva Event Type Not Handled: {factiva_message['event_type']}")
|
|
48
|
+
|
|
49
|
+
logger.info(f"Sent to GCP Pub/Sub, message with event: {factiva_message.get('event_type', 'N/A')}")
|
|
50
|
+
|
|
51
|
+
else:
|
|
52
|
+
print_message(f"Unexpected Message Format:[{factiva_message}]")
|
|
53
|
+
|
|
54
|
+
# Publish the message to GCP Pub/Sub
|
|
55
|
+
m_data = json.dumps(factiva_message, ensure_ascii=False).encode("utf-8")
|
|
56
|
+
ps_publisher.publish(topic_path, data=m_data)
|
|
57
|
+
|
|
58
|
+
callback.counter += 1
|
|
59
|
+
|
|
60
|
+
if callback.counter % 100 == 0:
|
|
61
|
+
print_message(f"[INFO] *** Processed {callback.counter} messages ***")
|
|
62
|
+
|
|
63
|
+
return True
|
|
64
|
+
|
|
65
|
+
except Exception as e:
|
|
66
|
+
logger.error(f"Error processing Factiva message: {e}")
|
|
67
|
+
# Only return False if you want to stop the listener
|
|
68
|
+
return True
|
|
69
|
+
|
|
70
|
+
callback.counter = 0
|
|
71
|
+
print_message(f"[INFO] *** Processed {callback.counter} messages ***")
|
|
72
|
+
listener.listen(callback)
|
|
73
|
+
|
|
74
|
+
else:
|
|
75
|
+
print("[ERROR]: Required ENV variables not set")
|
|
76
|
+
if gcp_project_id is None:
|
|
77
|
+
print(" - GCP_PROJECT_ID: GCP Project ID")
|
|
78
|
+
if gcp_pubsub_topic is None:
|
|
79
|
+
print(" - GCP_PUBSUB_TOPIC: GCP Pub/Sub Topic Name")
|
|
80
|
+
if gcp_creds is None:
|
|
81
|
+
print(" - GOOGLE_APPLICATION_CREDENTIALS: Path to Service Account JSON Credentials File")
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import datetime
|
|
3
|
+
from time import sleep
|
|
4
|
+
from dnaStreaming.listener import Listener
|
|
5
|
+
|
|
6
|
+
listener = Listener()
|
|
7
|
+
max_secs = 10
|
|
8
|
+
print(f"\n[ACTIVITY] Receiving messages (ASYNC) for {max_secs} seconds...")
|
|
9
|
+
|
|
10
|
+
def print_message(message):
|
|
11
|
+
print(f"[{datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S')}] {message}")
|
|
12
|
+
|
|
13
|
+
def callback(factiva_message, subscription_id):
|
|
14
|
+
try:
|
|
15
|
+
if 'action' in factiva_message.keys():
|
|
16
|
+
# Message is an article event
|
|
17
|
+
# Implement your logic according to the documentation:
|
|
18
|
+
# https://developer.dowjones.com/documents/site-docs-factiva_apis-factiva_analytics_apis-factiva_streams_api#article-specific-events
|
|
19
|
+
if factiva_message['action'] == 'add':
|
|
20
|
+
# Insert the article to the repository as new. Handle repeated messages.
|
|
21
|
+
print_message(f"[ARTICLE] [ADD] AN: {factiva_message['an']} - {factiva_message['title']}")
|
|
22
|
+
elif factiva_message['action'] == 'rep':
|
|
23
|
+
# Upsert/Update/AddNewVersion the article in the repository. Handle repeated messages.
|
|
24
|
+
print_message(f"[ARTICLE] [REP] AN: {factiva_message['an']} - {factiva_message['title']}")
|
|
25
|
+
elif factiva_message['action'] == 'del':
|
|
26
|
+
# Delete or mark as deleted the article in the repository. Handle inexistent article AN.
|
|
27
|
+
print_message(f"[ARTICLE] [DEL] AN: {factiva_message['an']} - *** DELETE ***")
|
|
28
|
+
else:
|
|
29
|
+
print_message(f"[ERROR] Factiva Action Not Handled: {factiva_message['action']}")
|
|
30
|
+
|
|
31
|
+
elif 'event_type' in factiva_message.keys():
|
|
32
|
+
# Message is a bulk action or service event
|
|
33
|
+
if factiva_message['event_type'] == 'source_delete':
|
|
34
|
+
# Delete all articles from the repository matching the source_code. Handle inexistent source_code and repeated messages.
|
|
35
|
+
print_message(f"[EVENT] [SOURCE_DELETE] Source: {factiva_message['source_code'].upper()} - {factiva_message['description']}")
|
|
36
|
+
else:
|
|
37
|
+
print_message(f"[ERROR] Factiva Event Type Not Handled: {factiva_message['event_type']}")
|
|
38
|
+
|
|
39
|
+
else:
|
|
40
|
+
print_message(f"[ERROR] Unexpected Message Format:[{factiva_message}]")
|
|
41
|
+
|
|
42
|
+
callback.counter += 1
|
|
43
|
+
if callback.counter % 100 == 0:
|
|
44
|
+
print_message(f"[INFO] *** Processed {callback.counter} messages ***")
|
|
45
|
+
|
|
46
|
+
except Exception as e:
|
|
47
|
+
print_message(f"[ERROR] Error processing Factiva message: {e}")
|
|
48
|
+
# Only return False if you want to stop the listener
|
|
49
|
+
return True
|
|
50
|
+
return True
|
|
51
|
+
|
|
52
|
+
callback.counter = 0
|
|
53
|
+
listener_controller = listener.listen_async_ha(callback)
|
|
54
|
+
|
|
55
|
+
# Stop receiving messages after max_secs
|
|
56
|
+
for count in range(0, max_secs):
|
|
57
|
+
sleep(1)
|
|
58
|
+
|
|
59
|
+
if listener_controller.listener_is_running():
|
|
60
|
+
listener_controller.stop_listener()
|
|
61
|
+
|
|
62
|
+
print("stop receiving messages")
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import datetime
|
|
3
|
+
from dnaStreaming.listener import Listener
|
|
4
|
+
|
|
5
|
+
listener = Listener()
|
|
6
|
+
|
|
7
|
+
print("\n[ACTIVITY] Receiving messages (SYNC)...")
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def print_message(message):
|
|
11
|
+
print(f"[{datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S')}] {message}")
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def callback(factiva_message: dict, subscription_id: str) -> bool:
|
|
15
|
+
try:
|
|
16
|
+
if 'action' in factiva_message.keys():
|
|
17
|
+
# Message is an article event
|
|
18
|
+
# Implement your logic according to the documentation:
|
|
19
|
+
# https://developer.dowjones.com/documents/site-docs-factiva_apis-factiva_analytics_apis-factiva_streams_api#article-specific-events
|
|
20
|
+
if factiva_message['action'] == 'add':
|
|
21
|
+
# Insert the article to the repository as new. Handle repeated messages.
|
|
22
|
+
print_message(f"[ARTICLE] [ADD] AN: {factiva_message['an']} - {factiva_message['title']}")
|
|
23
|
+
elif factiva_message['action'] == 'rep':
|
|
24
|
+
# Upsert/Update/AddNewVersion the article in the repository. Handle repeated messages.
|
|
25
|
+
print_message(f"[ARTICLE] [REP] AN: {factiva_message['an']} - {factiva_message['title']}")
|
|
26
|
+
elif factiva_message['action'] == 'del':
|
|
27
|
+
# Delete or mark as deleted the article in the repository. Handle inexistent article AN.
|
|
28
|
+
print_message(f"[ARTICLE] [DEL] AN: {factiva_message['an']} - *** DELETE ***")
|
|
29
|
+
else:
|
|
30
|
+
print_message(f"[ERROR] Factiva Action Not Handled: {factiva_message['action']}")
|
|
31
|
+
|
|
32
|
+
elif 'event_type' in factiva_message.keys():
|
|
33
|
+
# Message is a bulk action or service event
|
|
34
|
+
if factiva_message['event_type'] == 'source_delete':
|
|
35
|
+
# Delete all articles from the repository matching the source_code. Handle inexistent source_code and repeated messages.
|
|
36
|
+
print_message(f"[EVENT] [SOURCE_DELETE] Source: {factiva_message['source_code'].upper()} - {factiva_message['description']}")
|
|
37
|
+
else:
|
|
38
|
+
print_message(f"[ERROR] Factiva Event Type Not Handled: {factiva_message['event_type']}")
|
|
39
|
+
|
|
40
|
+
else:
|
|
41
|
+
print_message(f"[ERROR] Unexpected Message Format:[{factiva_message}]")
|
|
42
|
+
|
|
43
|
+
callback.counter += 1
|
|
44
|
+
if callback.counter % 100 == 0:
|
|
45
|
+
print_message(f"[INFO] *** Processed {callback.counter} messages ***")
|
|
46
|
+
|
|
47
|
+
except Exception as e:
|
|
48
|
+
print_message(f"[ERROR] Error processing Factiva message: {e}")
|
|
49
|
+
# Only return False if you want to stop the listener
|
|
50
|
+
return True
|
|
51
|
+
|
|
52
|
+
return True
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
callback.counter = 0
|
|
56
|
+
print_message(f"[INFO] *** Processed {callback.counter} messages ***")
|
|
57
|
+
listener.listen(callback)
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import datetime
|
|
3
|
+
from time import sleep
|
|
4
|
+
from dnaStreaming.listener import Listener
|
|
5
|
+
|
|
6
|
+
listener = Listener()
|
|
7
|
+
max_secs = 10
|
|
8
|
+
print(f"\n[ACTIVITY] Receiving messages (ASYNC) for {max_secs} seconds...")
|
|
9
|
+
|
|
10
|
+
def print_message(message):
|
|
11
|
+
print(f"[{datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S')}] {message}")
|
|
12
|
+
|
|
13
|
+
def callback(factiva_message, subscription_id):
|
|
14
|
+
try:
|
|
15
|
+
if 'action' in factiva_message.keys():
|
|
16
|
+
# Message is an article event
|
|
17
|
+
# Implement your logic according to the documentation:
|
|
18
|
+
# https://developer.dowjones.com/documents/site-docs-factiva_apis-factiva_analytics_apis-factiva_streams_api#article-specific-events
|
|
19
|
+
if factiva_message['action'] == 'add':
|
|
20
|
+
# Insert the article to the repository as new. Handle repeated messages.
|
|
21
|
+
print_message(f"[ARTICLE] [ADD] AN: {factiva_message['an']} - {factiva_message['title']}")
|
|
22
|
+
elif factiva_message['action'] == 'rep':
|
|
23
|
+
# Upsert/Update/AddNewVersion the article in the repository. Handle repeated messages.
|
|
24
|
+
print_message(f"[ARTICLE] [REP] AN: {factiva_message['an']} - {factiva_message['title']}")
|
|
25
|
+
elif factiva_message['action'] == 'del':
|
|
26
|
+
# Delete or mark as deleted the article in the repository. Handle inexistent article AN.
|
|
27
|
+
print_message(f"[ARTICLE] [DEL] AN: {factiva_message['an']} - *** DELETE ***")
|
|
28
|
+
else:
|
|
29
|
+
print_message(f"[ERROR] Factiva Action Not Handled: {factiva_message['action']}")
|
|
30
|
+
elif 'event_type' in factiva_message.keys():
|
|
31
|
+
# Message is a bulk action or service event
|
|
32
|
+
if factiva_message['event_type'] == 'source_delete':
|
|
33
|
+
# Delete all articles from the repository matching the source_code. Handle inexistent source_code and repeated messages.
|
|
34
|
+
print_message(f"[EVENT] [SOURCE_DELETE] Source: {factiva_message['source_code'].upper()} - {factiva_message['description']}")
|
|
35
|
+
else:
|
|
36
|
+
print_message(f"[ERROR] Factiva Event Type Not Handled: {factiva_message['event_type']}")
|
|
37
|
+
else:
|
|
38
|
+
print_message(f"[ERROR] Unexpected Message Format:[{factiva_message}]")
|
|
39
|
+
|
|
40
|
+
callback.counter += 1
|
|
41
|
+
if callback.counter % 100 == 0:
|
|
42
|
+
print_message(f"[INFO] *** Processed {callback.counter} messages ***")
|
|
43
|
+
|
|
44
|
+
except Exception as e:
|
|
45
|
+
print_message(f"[ERROR] Error processing Factiva message: {e}")
|
|
46
|
+
# Only return False if you want to stop the listener
|
|
47
|
+
return True
|
|
48
|
+
return True
|
|
49
|
+
|
|
50
|
+
callback.counter = 0
|
|
51
|
+
print_message(f"[INFO] *** Processed {callback.counter} messages ***")
|
|
52
|
+
future = listener.listen_async(callback)
|
|
53
|
+
|
|
54
|
+
# Stop receiving messages after 5 seconds
|
|
55
|
+
for count in range(0, max_secs):
|
|
56
|
+
sleep(1)
|
|
57
|
+
|
|
58
|
+
if future.running():
|
|
59
|
+
future.cancel()
|
|
60
|
+
|
|
61
|
+
print("stop receiving messages")
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
from __future__ import absolute_import, division, print_function
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
import sys
|
|
5
|
+
import logging
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def get_log_path():
|
|
9
|
+
env_log_path = os.getenv("LOG_PATH")
|
|
10
|
+
fallback_log_dir = os.path.expanduser('~/.dj-dna-streaming-python/logs')
|
|
11
|
+
base_dir = os.path.dirname(__file__)
|
|
12
|
+
default_path = os.path.join(base_dir, 'logs')
|
|
13
|
+
|
|
14
|
+
candidates = [env_log_path, default_path, fallback_log_dir]
|
|
15
|
+
|
|
16
|
+
for path in candidates:
|
|
17
|
+
if path:
|
|
18
|
+
try:
|
|
19
|
+
os.makedirs(path, exist_ok=True)
|
|
20
|
+
testfile = os.path.join(path, '.write_test')
|
|
21
|
+
with open(testfile, 'w') as f:
|
|
22
|
+
f.write('test')
|
|
23
|
+
os.remove(testfile)
|
|
24
|
+
return path
|
|
25
|
+
except Exception as e:
|
|
26
|
+
print(f"WARNING: Cannot write to '{path}': {e}")
|
|
27
|
+
|
|
28
|
+
raise RuntimeError("ERROR: Could not find a writable log directory.")
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
log_path = get_log_path()
|
|
32
|
+
print("Will log to: {}".format(log_path))
|
|
33
|
+
|
|
34
|
+
# Logging setup
|
|
35
|
+
logging.basicConfig(level=logging.WARN)
|
|
36
|
+
logger = logging.getLogger()
|
|
37
|
+
|
|
38
|
+
fileHandler = logging.FileHandler(os.path.join(log_path, 'dj-dna-streaming-python.log'))
|
|
39
|
+
logFormatter = logging.Formatter("%(asctime)s [%(threadName)-12.12s] [%(levelname)-5.5s] %(message)s")
|
|
40
|
+
fileHandler.setFormatter(logFormatter)
|
|
41
|
+
logger.addHandler(fileHandler)
|
|
42
|
+
|
|
43
|
+
consoleHandler = logging.StreamHandler(sys.stdout)
|
|
44
|
+
consoleHandler.setFormatter(logFormatter)
|
|
45
|
+
logger.addHandler(consoleHandler)
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: dnaStreaming
|
|
3
|
-
Version: 2.
|
|
3
|
+
Version: 2.3.1
|
|
4
4
|
Summary: Dow Jones DNA Streaming Project
|
|
5
5
|
Home-page: https://github.com/dowjones/dj-dna-streams-python
|
|
6
|
-
Download-URL: https://github.com/dowjones/dj-dna-streams-python/archive/release-2.
|
|
6
|
+
Download-URL: https://github.com/dowjones/dj-dna-streams-python/archive/release-2.3.1.tar.gz
|
|
7
7
|
Author: Zachary Kagan
|
|
8
8
|
Author-email: zachary.kagan@dowjones.com
|
|
9
9
|
License: MIT
|
|
@@ -14,6 +14,7 @@ Classifier: License :: OSI Approved :: MIT License
|
|
|
14
14
|
Classifier: Programming Language :: Python :: 3
|
|
15
15
|
Description-Content-Type: text/x-rst
|
|
16
16
|
License-File: LICENSE
|
|
17
|
+
Requires-Dist: setuptools>=75.1.0
|
|
17
18
|
Requires-Dist: googleapis-common-protos>=1.56.4
|
|
18
19
|
Requires-Dist: google-auth>=2.11.0
|
|
19
20
|
Requires-Dist: google-cloud-pubsub==2.13.6
|
|
@@ -66,7 +67,13 @@ To set your subscription ID, simply set an environment variable named 'SUBSCRIPT
|
|
|
66
67
|
export SUBSCRIPTION_ID="ABC1234567889"
|
|
67
68
|
|
|
68
69
|
|
|
69
|
-
|
|
70
|
+
To set your log folder path, simply set this folder variable named 'LOG_PATH' like so
|
|
71
|
+
|
|
72
|
+
.. code-block::
|
|
73
|
+
|
|
74
|
+
export LOG_PATH="/your/custom/log/path"
|
|
75
|
+
|
|
76
|
+
|
|
70
77
|
|
|
71
78
|
2. Using the configuration file.
|
|
72
79
|
###################################################################
|
|
@@ -146,7 +153,25 @@ You may want to listen messages asynchronously like so:
|
|
|
146
153
|
Log Files
|
|
147
154
|
_________
|
|
148
155
|
|
|
149
|
-
|
|
156
|
+
|
|
157
|
+
Minimal logging is written to a file named `dj-dna-streaming-python.log`.
|
|
158
|
+
|
|
159
|
+
By default, logs are written to the first available directory from the following list:
|
|
160
|
+
|
|
161
|
+
1. A custom path set via the environment variable `LOG_PATH`.
|
|
162
|
+
2. A `logs/` folder located within the package installation directory.
|
|
163
|
+
3. A fallback directory: `~/.dj-dna-streaming-python/logs/`.
|
|
164
|
+
|
|
165
|
+
The first writable location found is selected. A message like `Will log to: /your/custom/log/path` is printed to the console on startup.
|
|
166
|
+
|
|
167
|
+
💡 The log file is overwritten each time the application starts to keep maintenance simple.
|
|
168
|
+
|
|
169
|
+
You can specify:
|
|
170
|
+
|
|
171
|
+
- **Absolute paths**: For example, `/var/log/dna-streaming`.
|
|
172
|
+
- **Relative paths**: For example, `./logs`, relative to the current working directory at runtime.
|
|
173
|
+
|
|
174
|
+
The code verifies that the specified path is writable. If it isn’t, it automatically falls back to the next available option.
|
|
150
175
|
|
|
151
176
|
|
|
152
177
|
Testing
|
|
@@ -195,7 +220,7 @@ If you are enhancing this codebase (and not just using it as a library), follow
|
|
|
195
220
|
virtualenv venv
|
|
196
221
|
|
|
197
222
|
|
|
198
|
-
4. Then activate the
|
|
223
|
+
4. Then activate the virtual environment by executing this command:
|
|
199
224
|
###################################################################
|
|
200
225
|
|
|
201
226
|
.. code-block::
|
|
@@ -233,14 +258,14 @@ Execute the following at the project root:
|
|
|
233
258
|
|
|
234
259
|
.. code-block::
|
|
235
260
|
|
|
236
|
-
python ./
|
|
261
|
+
python ./demo/show_stream.py -s
|
|
237
262
|
|
|
238
263
|
|
|
239
264
|
Or
|
|
240
265
|
|
|
241
266
|
.. code-block::
|
|
242
267
|
|
|
243
|
-
python ./
|
|
268
|
+
python ./demo/show_stream_async.py -s
|
|
244
269
|
|
|
245
270
|
|
|
246
271
|
Running Docker Demo
|
|
@@ -3,6 +3,11 @@ MANIFEST.in
|
|
|
3
3
|
README.rst
|
|
4
4
|
setup.cfg
|
|
5
5
|
setup.py
|
|
6
|
+
demo/__init__.py
|
|
7
|
+
demo/pubsub_stream.py
|
|
8
|
+
demo/show_ha_stream_async.py
|
|
9
|
+
demo/show_stream.py
|
|
10
|
+
demo/show_stream_async.py
|
|
6
11
|
dnaStreaming/__init__.py
|
|
7
12
|
dnaStreaming/config.py
|
|
8
13
|
dnaStreaming/customer_config.json
|
|
@@ -12,11 +17,6 @@ dnaStreaming.egg-info/SOURCES.txt
|
|
|
12
17
|
dnaStreaming.egg-info/dependency_links.txt
|
|
13
18
|
dnaStreaming.egg-info/requires.txt
|
|
14
19
|
dnaStreaming.egg-info/top_level.txt
|
|
15
|
-
dnaStreaming/demo/__init__.py
|
|
16
|
-
dnaStreaming/demo/pubsub_stream.py
|
|
17
|
-
dnaStreaming/demo/show_ha_stream_async.py
|
|
18
|
-
dnaStreaming/demo/show_stream.py
|
|
19
|
-
dnaStreaming/demo/show_stream_async.py
|
|
20
20
|
dnaStreaming/services/__init__.py
|
|
21
21
|
dnaStreaming/services/authentication_service.py
|
|
22
22
|
dnaStreaming/services/availability_service.py
|
|
@@ -6,7 +6,7 @@ this_directory = path.abspath(path.dirname(__file__))
|
|
|
6
6
|
with open(path.join(this_directory, 'README.rst'), encoding='utf-8') as f:
|
|
7
7
|
long_description = f.read()
|
|
8
8
|
|
|
9
|
-
VERSION = "2.
|
|
9
|
+
VERSION = "2.3.1"
|
|
10
10
|
RELEASE_TAG = f"release-{VERSION}"
|
|
11
11
|
|
|
12
12
|
setup(
|
|
@@ -28,6 +28,7 @@ setup(
|
|
|
28
28
|
include_package_data=True,
|
|
29
29
|
|
|
30
30
|
install_requires=[
|
|
31
|
+
'setuptools>=75.1.0',
|
|
31
32
|
'googleapis-common-protos>=1.56.4',
|
|
32
33
|
'google-auth>=2.11.0',
|
|
33
34
|
'google-cloud-pubsub==2.13.6',
|
|
@@ -1,29 +0,0 @@
|
|
|
1
|
-
from __future__ import absolute_import, division, print_function
|
|
2
|
-
|
|
3
|
-
import os
|
|
4
|
-
import sys
|
|
5
|
-
import logging
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
BASE_DIR = os.path.dirname(__file__)
|
|
9
|
-
|
|
10
|
-
logging.basicConfig(level=logging.WARN)
|
|
11
|
-
|
|
12
|
-
logger = logging.getLogger()
|
|
13
|
-
|
|
14
|
-
log_path = os.path.join(BASE_DIR, 'logs')
|
|
15
|
-
|
|
16
|
-
print("Will log to: {}".format(log_path))
|
|
17
|
-
|
|
18
|
-
if not os.path.exists(log_path):
|
|
19
|
-
os.mkdir(log_path)
|
|
20
|
-
|
|
21
|
-
fileHandler = logging.FileHandler("{0}/{1}.log".format(log_path, 'dj-dna-streaming-python'))
|
|
22
|
-
|
|
23
|
-
logFormatter = logging.Formatter("%(asctime)s [%(threadName)-12.12s] [%(levelname)-5.5s] %(message)s")
|
|
24
|
-
fileHandler.setFormatter(logFormatter)
|
|
25
|
-
logger.addHandler(fileHandler)
|
|
26
|
-
|
|
27
|
-
consoleHandler = logging.StreamHandler(sys.stdout)
|
|
28
|
-
consoleHandler.setFormatter(logFormatter)
|
|
29
|
-
logger.addHandler(consoleHandler)
|
|
@@ -1,38 +0,0 @@
|
|
|
1
|
-
import os
|
|
2
|
-
import json
|
|
3
|
-
from google.cloud import pubsub_v1
|
|
4
|
-
from dnaStreaming.listener import Listener
|
|
5
|
-
from dnaStreaming import logger
|
|
6
|
-
|
|
7
|
-
gcp_project_id = os.getenv('GCP_PROJECT_ID', None)
|
|
8
|
-
gcp_pubsub_topic = os.getenv('GCP_PUBSUB_TOPIC', None)
|
|
9
|
-
gcp_creds = os.getenv('GOOGLE_APPLICATION_CREDENTIALS', None)
|
|
10
|
-
|
|
11
|
-
if gcp_project_id is not None and gcp_pubsub_topic is not None and gcp_creds is not None:
|
|
12
|
-
listener = Listener()
|
|
13
|
-
ps_publisher = pubsub_v1.PublisherClient()
|
|
14
|
-
topic_path = ps_publisher.topic_path(gcp_project_id, gcp_pubsub_topic)
|
|
15
|
-
print("\n[ACTIVITY] Sending messages to Pub/Sub topic {} in GCP\n[0]".format(gcp_pubsub_topic), end='')
|
|
16
|
-
|
|
17
|
-
def callback(message, subscription_id, file_handle=None):
|
|
18
|
-
callback.counter += 1
|
|
19
|
-
logger.info("Processing article with ID: {}".format(message['an']))
|
|
20
|
-
m_data = json.dumps(message, ensure_ascii=False).encode("utf-8")
|
|
21
|
-
ps_publisher.publish(topic_path, data=m_data)
|
|
22
|
-
logger.info("Sent to GCP Pub/Sub, article with ID: {}".format(message['an']))
|
|
23
|
-
if callback.counter % 10 == 0:
|
|
24
|
-
print('[{}]'.format(callback.counter), end='')
|
|
25
|
-
else:
|
|
26
|
-
print('.', end='')
|
|
27
|
-
return True
|
|
28
|
-
|
|
29
|
-
callback.counter = 0
|
|
30
|
-
listener.listen(callback)
|
|
31
|
-
else:
|
|
32
|
-
print("[ERROR]: Required ENV variables not set")
|
|
33
|
-
if gcp_project_id is None:
|
|
34
|
-
print(" - GCP_PROJECT_ID: GCP Project ID")
|
|
35
|
-
if gcp_pubsub_topic is None:
|
|
36
|
-
print(" - GCP_PUBSUB_TOPIC: GCP Pub/Sub Topic Name")
|
|
37
|
-
if gcp_creds is None:
|
|
38
|
-
print(" - GOOGLE_APPLICATION_CREDENTIALS: Path to Service Account JSON Credentials File")
|
|
@@ -1,36 +0,0 @@
|
|
|
1
|
-
import os
|
|
2
|
-
from time import sleep
|
|
3
|
-
from dnaStreaming.listener import Listener
|
|
4
|
-
|
|
5
|
-
listener = Listener()
|
|
6
|
-
quiet_demo = os.getenv('QUIET_DEMO', "false") == "true"
|
|
7
|
-
max_secs = 1000
|
|
8
|
-
print("\n[ACTIVITY] Receiving messages (ASYNC) for {} seconds...\n[0]".format(max_secs), end='')
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
def callback(message, subscription_id):
|
|
12
|
-
callback.counter += 1
|
|
13
|
-
if not quiet_demo:
|
|
14
|
-
if message['action'] != 'del':
|
|
15
|
-
print('[INFO] [SUBSCRIPTION]: {} [MSG] [{}]: AN: {}, TITLE: {}'.format(subscription_id, callback.counter, message['an'], message['title']))
|
|
16
|
-
else:
|
|
17
|
-
print('[INFO] [SUBSCRIPTION]: {} [MSG] [{}]: AN: {}, *** DELETE ***'.format(subscription_id, callback.counter, message['an']))
|
|
18
|
-
else:
|
|
19
|
-
if callback.counter % 10 == 0:
|
|
20
|
-
print('[{}]'.format(callback.counter), end='')
|
|
21
|
-
else:
|
|
22
|
-
print('.', end='')
|
|
23
|
-
return True
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
callback.counter = 0
|
|
27
|
-
listener_controller = listener.listen_async_ha(callback)
|
|
28
|
-
|
|
29
|
-
# Stop receiving messages after 5 seconds
|
|
30
|
-
for count in range(0, max_secs):
|
|
31
|
-
sleep(1)
|
|
32
|
-
|
|
33
|
-
if listener_controller.listener_is_running():
|
|
34
|
-
listener_controller.stop_listener()
|
|
35
|
-
|
|
36
|
-
print("stop receiving messages")
|
|
@@ -1,30 +0,0 @@
|
|
|
1
|
-
import os
|
|
2
|
-
from dnaStreaming.listener import Listener
|
|
3
|
-
|
|
4
|
-
listener = Listener()
|
|
5
|
-
quiet_demo = os.getenv('QUIET_DEMO', "false") == "true"
|
|
6
|
-
print("\n[ACTIVITY] Receiving messages (SYNC)...\n[0]", end='')
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
def callback(message, subscription_id):
|
|
10
|
-
# Replace the code below with the custom operations you'd like to apply to each article (message)
|
|
11
|
-
# like writing to a file, database, or pushing the message to a separate message broker system.
|
|
12
|
-
# This demo only prints some message details.
|
|
13
|
-
# Subscription id parameter is provided and can be used for auditing purposes.
|
|
14
|
-
|
|
15
|
-
callback.counter += 1
|
|
16
|
-
if not quiet_demo:
|
|
17
|
-
if message['action'] != 'del':
|
|
18
|
-
print('[INFO] [MSG] [{}]: AN: {}, TITLE: {}'.format(callback.counter, message['an'], message['title']))
|
|
19
|
-
else:
|
|
20
|
-
print('[INFO] [MSG] [{}]: AN: {}, *** DELETE ***'.format(callback.counter, message['an']))
|
|
21
|
-
else:
|
|
22
|
-
if callback.counter % 10 == 0:
|
|
23
|
-
print('[{}]'.format(callback.counter), end='')
|
|
24
|
-
else:
|
|
25
|
-
print('.', end='')
|
|
26
|
-
return True
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
callback.counter = 0
|
|
30
|
-
listener.listen(callback)
|
|
@@ -1,36 +0,0 @@
|
|
|
1
|
-
import os
|
|
2
|
-
from time import sleep
|
|
3
|
-
from dnaStreaming.listener import Listener
|
|
4
|
-
|
|
5
|
-
listener = Listener()
|
|
6
|
-
quiet_demo = os.getenv('QUIET_DEMO', "false") == "true"
|
|
7
|
-
max_secs = 5
|
|
8
|
-
print("\n[ACTIVITY] Receiving messages (ASYNC) for {} seconds...\n[0]".format(max_secs), end='')
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
def callback(message, subscription_id):
|
|
12
|
-
callback.counter += 1
|
|
13
|
-
if not quiet_demo:
|
|
14
|
-
if message['action'] != 'del':
|
|
15
|
-
print('[INFO] [MSG] [{}]: AN: {}, TITLE: {}'.format(callback.counter, message['an'], message['title']))
|
|
16
|
-
else:
|
|
17
|
-
print('[INFO] [MSG] [{}]: AN: {}, *** DELETE ***'.format(callback.counter, message['an']))
|
|
18
|
-
else:
|
|
19
|
-
if callback.counter % 10 == 0:
|
|
20
|
-
print('[{}]'.format(callback.counter), end='')
|
|
21
|
-
else:
|
|
22
|
-
print('.', end='')
|
|
23
|
-
return True
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
callback.counter = 0
|
|
27
|
-
future = listener.listen_async(callback)
|
|
28
|
-
|
|
29
|
-
# Stop receiving messages after 5 seconds
|
|
30
|
-
for count in range(0, max_secs):
|
|
31
|
-
sleep(1)
|
|
32
|
-
|
|
33
|
-
if future.running():
|
|
34
|
-
future.cancel()
|
|
35
|
-
|
|
36
|
-
print("stop receiving messages")
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{dnastreaming-2.2.1.dev1 → dnastreaming-2.3.1}/dnaStreaming/services/authentication_service.py
RENAMED
|
File without changes
|
{dnastreaming-2.2.1.dev1 → dnastreaming-2.3.1}/dnaStreaming/services/availability_service.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|