Zelyanoth commited on
Commit
c9329ee
·
verified ·
1 Parent(s): aed81dd

Upload 11 files

Browse files
Files changed (11) hide show
  1. Dockerfile +10 -0
  2. Post_management.py +40 -0
  3. Post_robot.py +26 -0
  4. Source_manage.css +6 -0
  5. Source_manage.py +26 -0
  6. app.css +134 -0
  7. app.py +178 -0
  8. functi.py +473 -0
  9. line_db.py +209 -0
  10. requirements.txt +6 -0
  11. timing_lin.py +67 -0
Dockerfile ADDED
@@ -0,0 +1,10 @@
 
 
 
 
 
 
 
 
 
 
 
1
+ FROM python:3.11
2
+
3
+
4
+ WORKDIR /app
5
+
6
+ COPY ./ /app
7
+
8
+ RUN pip install -r requirements.txt
9
+
10
+ CMD python app.py
Post_management.py ADDED
@@ -0,0 +1,40 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from taipy import Gui
2
+ import taipy.gui.builder as tgb
3
+ from functi import post_generation,post_publishing,authen,add_scheduling,delete_account,delete_schedule
4
+
5
+ days_of_week = ["Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday", "Sunday"]
6
+ Social_network_menu = ["Linkedin"]
7
+
8
+ with tgb.Page(class_name="bodyp") as Post_manag :
9
+ with tgb.part(class_name="source_body") :
10
+ tgb.text("Post Managements ",class_name="Title_Page")
11
+ with tgb.part(class_name="layout_top") :
12
+ tgb.text("Linking Account",class_name="header-burgundy")
13
+ with tgb.layout(columns="2fr 1",class_name="table_t") :
14
+ with tgb.part() :
15
+ with tgb.layout(columns="1 1 1") :
16
+ tgb.input("{Linked_account_name}" ,label ="Choose an account name",change_delay = -1,action_on_blur = True)
17
+ tgb.selector("{Linked_social_network}", lov=Social_network_menu,dropdown=True) # type: ignore
18
+ tgb.button(label = "Add account", on_action = authen)
19
+ tgb.table("{data_account}",editable = True, on_add = False,on_edit = False,on_delete = delete_account,columns="social_network;account_name")
20
+ with tgb.part(class_name="table_s") :
21
+ tgb.text("Demo",class_name="header-burgundy table_t")
22
+ tgb.button(label = "Generate a post",on_action = post_generation)
23
+ tgb.text("{generated_post}",mode = "pre",class_name="burgundy-border",width= "45%")
24
+ tgb.button(label = "Publish", on_action = post_publishing)
25
+ with tgb.part(class_name="table_s") :
26
+ tgb.text("Post Scheduling",class_name="header-burgundy")
27
+ with tgb.layout(columns = "3fr 1", class_name="table_t" ) :
28
+ with tgb.layout( columns="1 1 1 1") :
29
+ tgb.input("{time_value_hour}",label = "Hour",change_delay = -1,action_on_blur = True)
30
+ tgb.input("{time_value_minute}",label = "Minute",change_delay = -1,action_on_blur = True)
31
+ tgb.selector("{day_value}", lov=days_of_week,multiple = True,dropdown=True,show_select_all=True) # type: ignore
32
+ tgb.button(label = "Add",on_action = add_scheduling)
33
+ tgb.text("")
34
+ tgb.table("{data_schedule}",editable = True, on_add = False,on_edit = False,on_delete = delete_schedule,class_name="table_t")
35
+
36
+
37
+ # Date complète
38
+ # tgb.date("{day_value}", format="EEEE, MMM dd, yyyy", label="Full Date with Day")
39
+
40
+
Post_robot.py ADDED
@@ -0,0 +1,26 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # import schedule
2
+ # import threading
3
+ # import time
4
+ # import datetime
5
+ # from functi import db_manager,add_scheduling,planning
6
+
7
+
8
+
9
+
10
+ # def background_scheduler():
11
+ # while True:
12
+ # try:
13
+ # schedule.run_pending()
14
+ # except Exception as e:
15
+ # print("Erreur dans run_pending():", e, flush=True)
16
+ # time.sleep(1)
17
+
18
+
19
+ # thread = threading.Thread(target=background_scheduler)
20
+ # thread.start()
21
+ # print("it actually worked")
22
+ # threading.Thread(target=planning, daemon=True).start()
23
+
24
+
25
+
26
+
Source_manage.css ADDED
@@ -0,0 +1,6 @@
 
 
 
 
 
 
 
1
+ .source_body{
2
+ margin: 4em;
3
+ padding: 18em;
4
+ background-color: #ECF4F7;
5
+
6
+ }
Source_manage.py ADDED
@@ -0,0 +1,26 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from taipy import Gui
2
+ import taipy.gui.builder as tgb
3
+ from functi import add_source, delete_source,on_logout,on_my_clicking
4
+
5
+
6
+
7
+ with tgb.Page() as menu_page: # type: ignore
8
+ with tgb.part(render = "{is_logged_in}", id="taipy_menu") : # type: ignore
9
+ tgb.menu(lov=[
10
+ ("Source_Management", "Sources"),("Post", "Post Management"),("Accueil", "Logging Out")
11
+ ],
12
+ on_action = on_my_clicking,width = "20vw")
13
+
14
+ with tgb.Page() as source_page: # type: ignore
15
+ with tgb.part(class_name="source_body") :
16
+ tgb.text("Source Managements",class_name="Title_Page")
17
+ with tgb.layout(columns="4fr 1",class_name="layout_top") :
18
+ with tgb.part() :
19
+ with tgb.layout(columns="1 1") :
20
+ tgb.input("{source_}" ,label ="Source",change_delay = -1,action_on_blur = True,class_name="fullwidth ")
21
+ tgb.button("Add", on_action = add_source)
22
+ with tgb.part(class_name="table_s") :
23
+ tgb.text("Liste des sources")
24
+ tgb.table("{Source_table}",columns="source;categorie;last_update",editable = True, on_add = False,on_edit = False,on_delete = delete_source,class_name="header-plain table_t")
25
+
26
+
app.css ADDED
@@ -0,0 +1,134 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /* 📦 1. Importer la police Pacifico */
2
+ @import url('https://fonts.googleapis.com/css2?family=Pacifico&display=swap');
3
+
4
+ /* 2. Réinitialisation globale */
5
+ div#root{
6
+ margin: 0rem !important;
7
+ }
8
+
9
+ .header-burgundy {
10
+ color: #39404B; /* Burgundy */
11
+ font-weight: bold;
12
+ font-size: 1em;
13
+ margin-bottom: 0.5em;
14
+ }
15
+
16
+ .burgundy-border {
17
+ border: 2px solid #800020; /* Burgundy border */
18
+ color: #800020; /* Burgundy text */
19
+ border-radius: 6px;
20
+ padding: 0.5em 1em;
21
+ }
22
+
23
+ .source_body{
24
+ margin: 2em;
25
+ background-color: #ECF4F7;
26
+
27
+ }
28
+
29
+ .layout_top {
30
+ padding-top: 5em;
31
+ }
32
+
33
+
34
+ .css-w1lhxi{
35
+ /* --color-background-light: #910029; */
36
+ background-color: #910029 ;
37
+ color: aliceblue;
38
+ }
39
+
40
+ .table_s{
41
+ padding-top: 2em;
42
+ }
43
+ .table_t{
44
+ padding-top: 1.3em;
45
+ }
46
+
47
+ .Title_Page {
48
+ font-weight: bold;
49
+ font-size: 2em;
50
+ color: #39404B;
51
+ display: block;
52
+ box-sizing: border-box;
53
+ position: relative; width: 100%; }
54
+ .Title_Page::after {
55
+ content: '';
56
+ position: absolute;
57
+ bottom: -8px; left: 0;
58
+ width: 100%;
59
+ height: 3px;
60
+ background-color: #39404B; }
61
+
62
+ html, body {
63
+ margin: 0;
64
+ padding: 0;
65
+ height: 100%;
66
+ }
67
+
68
+ .bodyp{
69
+ margin: 0;
70
+ padding: 0;
71
+ }
72
+
73
+ /* 3. Partie .presentation sans aucun padding */
74
+ .presentation {
75
+ margin: 0;
76
+ padding: 0;
77
+ height: 100vh;
78
+ width: 100%;
79
+ position: relative;
80
+ background-image:
81
+ linear-gradient(rgba(0, 0, 0, 0.5), rgba(0, 0, 0, 0.5)),
82
+ url("src/image/20250715_1931_Digital Workspace Dashboard_remix_01k07nha1pfn1sas0rkdrt7sj0.webp");
83
+ background-size: cover;
84
+ background-position: center;
85
+ color: white;
86
+ display: flex;
87
+ flex-direction: column;
88
+ justify-content: center;
89
+ align-items: center;
90
+ font-family: sans-serif;
91
+ }
92
+
93
+ /* 4. Nom de l’app en haut à gauche, stylé Pacifico */
94
+ .App_name {
95
+ position: absolute;
96
+ top: 20px;
97
+ left: 30px;
98
+ font-size: 2.5em;
99
+ font-family: 'Pacifico', cursive;
100
+ z-index: 2;
101
+ }
102
+
103
+ /* 5. Texte de présentation avec effet elevated au survol */
104
+ .presentation_text {
105
+ position: relative;
106
+ z-index: 2;
107
+ text-align: center;
108
+ font-size: 2em;
109
+ padding: 20px 30px;
110
+ max-width: 80%;
111
+ background: rgba(255, 255, 255, 0.1);
112
+ border-radius: 8px;
113
+ transition: transform 0.3s ease, background 0.3s ease, box-shadow 0.3s ease;
114
+ }
115
+ .presentation_text:hover {
116
+ background: rgba(255, 255, 255, 0.15);
117
+ transform: translateY(-5px) scale(1.02);
118
+ box-shadow:
119
+ 0 10px 20px rgba(0, 0, 0, 0.25),
120
+ 0 0 15px rgba(255, 255, 255, 0.6);
121
+ }
122
+
123
+ /* 6. Responsive */
124
+ @media (max-width: 768px) {
125
+ .App_name {
126
+ font-size: 1.8em;
127
+ top: 15px;
128
+ left: 20px;
129
+ }
130
+ .presentation_text {
131
+ font-size: 1.3em;
132
+ padding: 15px 20px;
133
+ }
134
+ }
app.py ADDED
@@ -0,0 +1,178 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from taipy import Gui
2
+ from taipy.gui import navigate # type: ignore
3
+ import taipy.gui.builder as tgb
4
+ from functi import *
5
+ from Source_manage import source_page, menu_page
6
+ from Post_management import Post_manag
7
+ from flask import Flask, request
8
+ from urllib.parse import urlparse, parse_qs
9
+ import time
10
+ import logging
11
+ logging.getLogger('requests_oauthlib').setLevel(logging.DEBUG)
12
+
13
+
14
+ import requests
15
+
16
+ def obtenir_access_token(code, client_id, client_secret, redirect_uri):
17
+ url = "https://www.linkedin.com/oauth/v2/accessToken"
18
+ headers = {
19
+ "Content-Type": "application/x-www-form-urlencoded"
20
+ }
21
+ data = {
22
+ "grant_type": "authorization_code",
23
+ "code": code,
24
+ "redirect_uri": redirect_uri,
25
+ "client_id": client_id,
26
+ "client_secret": client_secret
27
+ }
28
+
29
+ response = requests.post(url, headers=headers, data=data)
30
+ response.raise_for_status() # lève une exception si le statut HTTP n’est pas 2xx
31
+ return response.json()
32
+
33
+
34
+ # Presentation text
35
+ presentation_text = """
36
+ Découvrez Lin votre assistant community manager sur Linkedin
37
+ """
38
+
39
+
40
+
41
+
42
+
43
+ def on_navigate(state, page_name: str):
44
+ if page_name == "callback":
45
+ raw = request.environ.get("REQUEST_URI", "")
46
+
47
+
48
+ full = request.scheme + "://" + request.host + raw
49
+ state.authorization_url = full
50
+ state.authorization_url = state.authorization_url.replace("/taipy-jsx", "")
51
+ state.authorization_url = state.authorization_url.rsplit("&v", 1)[0]
52
+ state.authorization_url = state.authorization_url.replace("http://", "https://")
53
+ print(state.authorization_url,flush = True)
54
+ parsed = urlparse(state.authorization_url)
55
+
56
+ # 2. Extrait les paramètres dans un dict (valeur encapsulée dans une liste)
57
+ params = parse_qs(parsed.query)
58
+
59
+ # 3. Récupère la valeur de state
60
+ state_value = params.get("state", [""])[0]
61
+ code = params.get("code", [""])[0]
62
+ print(state_value,flush = True)
63
+ print(state.states,flush = True)
64
+ print(code,flush = True)
65
+ if state_value.endswith('?'):
66
+ state_value = state_value[:-1]
67
+ else:
68
+ state_value = state_value
69
+
70
+ if state_value == state.states :
71
+ if state.Linked_social_network == "Linkedin" :
72
+ response = obtenir_access_token(code, state.client_id, state.client_secret, state.redirect_url)
73
+
74
+ state.token = response["access_token"]
75
+
76
+
77
+ url = "https://api.linkedin.com/v2/userinfo"
78
+ headers = {
79
+ "Authorization": f"Bearer {state.token}"
80
+ }
81
+
82
+ response_open = requests.get(url, headers=headers)
83
+ response_open =response_open.json()
84
+ db_manager.add_token_network(state.token,state.Linked_social_network,state.Linked_account_name,state.user_inf.user.id,response_open)
85
+ navigate(state,"Source_Management" )
86
+
87
+
88
+ print(state.token,flush = True)
89
+ else :
90
+ print("mkjgfzmfvhsmehk")
91
+
92
+
93
+
94
+ return page_name
95
+
96
+
97
+
98
+
99
+
100
+
101
+
102
+
103
+
104
+
105
+ with tgb.Page(class_name="bodyp") as callb:
106
+ tgb.text("attendez un moment")
107
+
108
+ # Create pages
109
+ with tgb.Page(class_name="bodyp") as page_appart:
110
+
111
+ # Show login/register forms if not logged in
112
+ with tgb.part(render="{not is_logged_in}"): # type: ignore
113
+ with tgb.part(class_name="presentation", height="100vh"):
114
+ tgb.text("Lin", class_name="App_name")
115
+
116
+
117
+ with tgb.part(class_name="presentation_text"):
118
+ tgb.text(presentation_text, id="presentation_text_t")
119
+ with tgb.part(class_name="auth-container"):
120
+
121
+
122
+ # Login form (default)
123
+ with tgb.part(render="{not show_register}"): # type: ignore
124
+ tgb.text("Login to Lin")
125
+ tgb.input("{login_email}", label="Email", type="email",change_delay = -1,action_on_blur = True)
126
+ tgb.input("{login_password}", label="Password", password=True,change_delay = -1,action_on_blur = True)
127
+ tgb.button("Login", on_action=on_login )
128
+ tgb.button("Need an account? Register", on_action=toggle_register)
129
+
130
+ # Register form
131
+ with tgb.part(render="{show_register}"): # type: ignore
132
+ tgb.text("Register for Lin", )
133
+ tgb.input("{register_email}", label="Email", type="email",change_delay = -1,action_on_blur = True)
134
+ tgb.input("{register_password}", label="Password", password=True,change_delay = -1,action_on_blur = True)
135
+ tgb.input("{confirm_password}", label="Confirm Password", password=True,change_delay = -1,action_on_blur = True)
136
+ tgb.button("Register", on_action=on_register, class_name="auth-button")
137
+ tgb.button("Already have an account? Login", on_action=toggle_register, )
138
+
139
+ # Message display
140
+ with tgb.part(render="{message}"): # type: ignore
141
+ tgb.text("{message}", class_name="message")
142
+
143
+ # Show main content if logged in
144
+ with tgb.part(render="{is_logged_in}"): # type: ignore
145
+ with tgb.part(class_name="presentation", height="100vh"):
146
+ tgb.text("Lin", class_name="App_name")
147
+ tgb.text(f"Welcome, {'{current_user}'}!", class_name="welcome-message")
148
+ tgb.button("Logout", on_action=on_logout, class_name="logout-button")
149
+
150
+ with tgb.part(class_name="presentation_text"):
151
+ tgb.text(presentation_text, id="presentation_text_t")
152
+
153
+ # Define pages
154
+ pages = {
155
+ "Accueil": page_appart,
156
+ "/": menu_page,
157
+ "Source_Management" : source_page,
158
+ "callback" : callb,
159
+ "Post" : Post_manag
160
+ }
161
+
162
+ stylekit = {
163
+ "color_primary" : "#910029",
164
+
165
+ }
166
+
167
+ if __name__ == "__main__":
168
+ planning()
169
+
170
+ gui = Gui(pages=pages)
171
+ gui.run(
172
+ debug=True,
173
+ port=7860,host = "0.0.0.0",
174
+ stylekit=stylekit,
175
+ title="Lin",
176
+ dark_mode=False,
177
+ use_reloader=True
178
+ )
functi.py ADDED
@@ -0,0 +1,473 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import hashlib
2
+ import secrets
3
+ from http.server import BaseHTTPRequestHandler, HTTPServer
4
+ import threading
5
+ import os
6
+ import time
7
+ import datetime
8
+ from line_db import DatabaseManager
9
+ from urllib.parse import urlencode
10
+ from taipy.gui import navigate # type: ignore
11
+ from gradio_client import Client
12
+ import pandas as pd
13
+ from requests_oauthlib import OAuth2Session
14
+ import requests
15
+
16
+ from timing_lin import *
17
+ from apscheduler.schedulers.background import BackgroundScheduler
18
+ from apscheduler.triggers.cron import CronTrigger
19
+
20
+ # Create the scheduler globally (start it once)
21
+ apsched = BackgroundScheduler()
22
+ apsched.start()
23
+
24
+
25
+
26
+ Linked_account_name = " "
27
+ Linked_social_network = " "
28
+ data_schedule ={}
29
+
30
+ scope = ['openid', 'profile', 'email', 'w_member_social']
31
+
32
+ time_value_hour = 18
33
+ time_value_minute = 00
34
+ day_value = "Monday"
35
+ Linked_social_network = "Linkedin"
36
+
37
+
38
+ api_key_hugging = os.environ.get("hugging_key")
39
+ Source_table = {}
40
+ data_account = {}
41
+ data_schedule = {}
42
+ data_schedule_before = {}
43
+ Source_table_before = {}
44
+ data_account_before = {}
45
+
46
+ url: str = os.environ.get("SUPABASE_URL") # type: ignore
47
+ key: str = os.environ.get("SUPABASE_KEY") # type: ignore
48
+ is_logged_in = False
49
+ current_user = None
50
+ message= ''
51
+ show_register= False
52
+ login_email= ''
53
+ login_password= ''
54
+ register_email= ''
55
+ register_password= ''
56
+ confirm_password= ''
57
+ source_ = " "
58
+ source_add_message = " "
59
+ user_inf = " "
60
+ generated_post = "test"
61
+ token = " "
62
+ authorization_url = " "
63
+ urlss = ""
64
+ states = ""
65
+ social_network = "Linkedin"
66
+
67
+
68
+ db_manager = DatabaseManager(url,key)
69
+ client = Client("Zelyanoth/Linkedin_poster_dev",hf_token = api_key_hugging)
70
+
71
+ client_id = os.environ.get("CLIENT_ID")
72
+ redirect_url = os.environ.get("RED_URL")
73
+ client_secret = os.environ.get("CLIENT_SECRET")
74
+
75
+
76
+
77
+ linkedin = OAuth2Session(client_id, redirect_uri=redirect_url, scope=scope)
78
+
79
+
80
+
81
+
82
+
83
+ def replanifier_toutes_les_tâches(df):
84
+ # Efface toutes les anciennes tâches
85
+ df.apply(
86
+ lambda r: planifier_ligne(r["id"],r["id_social"], r["user_id"], r["schedule_time"],r["social_network"],r["adjusted_time"]),
87
+ axis=1
88
+ )
89
+
90
+ def post_generation_for_robot(id,social,idd) :
91
+ try :
92
+ print("⏳ Tâche planifizzzzzzzzzzzzzzzée pour",flush = True)
93
+ generated_post = client.predict(
94
+ code=id,
95
+ api_name="/poster_linkedin"
96
+
97
+ )
98
+ db_manager.add_post(social,generated_post,idd)
99
+ except Exception as e:
100
+ print("Erreur dans gen():", e, flush=True)
101
+
102
+
103
+
104
+ def post_publishing_for_robot(id_social,id_user,idd,ss) :
105
+ try :
106
+ print("⏳ Tâche planifiée pour post_pubsih",flush = True)
107
+ resp = db_manager.fetching_user_identif(id_user,ss)
108
+ dd = db_manager.fetching_post(id_social,idd)
109
+ data = pd.DataFrame(resp.data)
110
+ print(data)
111
+ first = data[data['id'] == id_social].iloc[0]
112
+ token_value = first["token"]
113
+ sub_value = first["sub"]
114
+ post = dd["Text_content"].iloc[0]
115
+
116
+ print("⏳ Tâche planifiée pour gfjfxd",flush = True)
117
+
118
+ url = "https://api.linkedin.com/v2/ugcPosts"
119
+ headers = {
120
+ "Authorization": f"Bearer {token_value}",
121
+ "X-Restli-Protocol-Version": "2.0.0",
122
+ "Content-Type": "application/json"
123
+ }
124
+ body = {
125
+ "author": f"urn:li:person:{sub_value}",
126
+ "lifecycleState": "PUBLISHED",
127
+ "specificContent": {
128
+ "com.linkedin.ugc.ShareContent": {
129
+ "shareCommentary": {
130
+ "text": post
131
+ },
132
+ "shareMediaCategory": "NONE"
133
+ }
134
+ },
135
+ "visibility": {
136
+ "com.linkedin.ugc.MemberNetworkVisibility": "PUBLIC"
137
+ }
138
+ }
139
+ resp = requests.post(url, headers=headers, json=body)
140
+ db_manager.update_post(id_social,idd)
141
+ print([resp.status_code, resp.text],flush = True)
142
+ except Exception as e:
143
+ print("Erreur dans post():", e, flush=True)
144
+
145
+
146
+
147
+
148
+
149
+
150
+ def planifier_ligne(id_schedule, id_social, user_id, schedule_time_str, ss, adjusted_time):
151
+ # Parse schedule_time_str and adjusted_time
152
+ parts = schedule_time_str.strip().split()
153
+ part_adj = adjusted_time.strip().split()
154
+ if len(parts) != 2 or ':' not in parts[1]:
155
+ print(f"❌ Format invalide : {schedule_time_str}", flush=True)
156
+ return
157
+ if len(part_adj) != 2 or ':' not in part_adj[1]:
158
+ print(f"❌ Format invalide : {adjusted_time}", flush=True)
159
+ return
160
+
161
+ jour, hm = parts
162
+ jour_adj, hm_adj = part_adj
163
+
164
+ try:
165
+ hour, minute = map(int, hm.split(':'))
166
+ hour_adj, minute_adj = map(int, hm_adj.split(':'))
167
+ except ValueError:
168
+ print(f"❌ Heure invalide : {hm}", flush=True)
169
+ return
170
+
171
+ # Map day names to APScheduler format
172
+ day_map = {
173
+ "monday": "mon",
174
+ "tuesday": "tue",
175
+ "wednesday": "wed",
176
+ "thursday": "thu",
177
+ "friday": "fri",
178
+ "saturday": "sat",
179
+ "sunday": "sun",
180
+ }
181
+
182
+ jour_key = jour.lower()
183
+ jour_key_adj = jour_adj.lower()
184
+
185
+ if jour_key not in day_map or jour_key_adj not in day_map:
186
+ print(f"❌ Jour non reconnu : {jour}/{jour_adj}", flush=True)
187
+ return
188
+
189
+ # Remove previous jobs for this schedule (optional, if you want to avoid duplicates)
190
+ try :
191
+ apsched.remove_job(f"pub-{id_schedule}-{schedule_time_str}", jobstore=None)
192
+ apsched.remove_job(f"gen-{id_schedule}-{schedule_time_str}", jobstore=None)
193
+ except Exception as e:
194
+ print(f"❌ Erreur lors de la suppression des tâches : {e}", flush=True)
195
+
196
+ # Schedule publishing
197
+ apsched.add_job(
198
+ lambda: post_publishing_for_robot(id_social, user_id, id_schedule, ss),
199
+ CronTrigger(day_of_week=day_map[jour_key], hour=hour, minute=minute),
200
+ id=f"pub-{id_schedule}-{schedule_time_str}"
201
+ )
202
+
203
+ # Schedule generation
204
+ apsched.add_job(
205
+ lambda: post_generation_for_robot(user_id, id_social, id_schedule),
206
+ CronTrigger(day_of_week=day_map[jour_key_adj], hour=hour_adj, minute=minute_adj),
207
+ id=f"gen-{id_schedule}-{schedule_time_str}"
208
+ )
209
+
210
+ print(f"⏳ APScheduler: Tâche planifiée pour {id_social} ({user_id}) le {jour} à {hour:02d}:{minute:02d} et {jour_adj} à {hour_adj:02d}:{minute_adj:02d}", flush=True)
211
+
212
+
213
+
214
+ def add_scheduling(state):
215
+ """Add new scheduling with thread safety"""
216
+ try:
217
+ if isinstance(state.day_value, list):
218
+ for day in state.day_value:
219
+ timesche = f"{day} {int(state.time_value_hour)}:{int(state.time_value_minute)}"
220
+
221
+ # Get current schedule
222
+ df = db_manager.fetch_schedule_table()
223
+
224
+ if not df.empty:
225
+ df, final_time = add_request(df, timesche)
226
+ else:
227
+ jour, horaire = timesche.split()
228
+ horaire = horaire.replace(';', ':')
229
+ h, m = map(int, horaire.split(':'))
230
+ m -= 7 # 7 minutes before for generation
231
+ final_time = f"{jour} {h}:{m:02d}"
232
+
233
+ # Add to database
234
+ db_manager.create_scheduling_for_user(
235
+ state.user_inf.user.id,
236
+ state.Linked_social_network,
237
+ timesche,
238
+ final_time
239
+ )
240
+ else:
241
+ timesche = f"{state.day_value} {int(state.time_value_hour)}:{int(state.time_value_minute)}"
242
+
243
+ # Get current schedule
244
+ df = db_manager.fetch_schedule_table()
245
+
246
+ if not df.empty:
247
+ df, final_time = add_request(df, timesche)
248
+ else:
249
+ jour, horaire = timesche.split()
250
+ horaire = horaire.replace(';', ':')
251
+ h, m = map(int, horaire.split(':'))
252
+ m -= 7 # 7 minutes before for generation
253
+ final_time = f"{jour} {h}:{m:02d}"
254
+
255
+ # Add to database
256
+ db_manager.create_scheduling_for_user(
257
+ state.user_inf.user.id,
258
+ state.Linked_social_network,
259
+ timesche,
260
+ final_time
261
+ )
262
+
263
+ # Refresh the schedule after adding
264
+ df = db_manager.fetch_schedule_table()
265
+ state.data_schedule = db_manager.fetch_schedule_table_acc(state.user_inf.user.id)
266
+
267
+ # Reschedule all tasks
268
+ replanifier_toutes_les_tâches(df)
269
+
270
+ print(f"✅ Scheduling added successfully", flush=True)
271
+
272
+ except Exception as e:
273
+ print(f"❌ Error in add_scheduling: {e}", flush=True)
274
+
275
+
276
+ def planning():
277
+ df = db_manager.fetch_schedule_table()
278
+ if not df.empty :
279
+ replanifier_toutes_les_tâches(df)
280
+
281
+
282
+ def post_publishing(state) :
283
+
284
+ resp = db_manager.fetching_user_identif(state.user_inf.user.id,state.social_network)
285
+ data = pd.DataFrame(resp.data)
286
+
287
+ first = data[data['social_network'] == state.social_network].iloc[0]
288
+ token_value = first["token"]
289
+ sub_value = first["sub"]
290
+
291
+
292
+
293
+
294
+ url = "https://api.linkedin.com/v2/ugcPosts"
295
+ headers = {
296
+ "Authorization": f"Bearer {token_value}",
297
+ "X-Restli-Protocol-Version": "2.0.0",
298
+ "Content-Type": "application/json"
299
+ }
300
+ body = {
301
+ "author": f"urn:li:person:{sub_value}",
302
+ "lifecycleState": "PUBLISHED",
303
+ "specificContent": {
304
+ "com.linkedin.ugc.ShareContent": {
305
+ "shareCommentary": {
306
+ "text": state.generated_post
307
+ },
308
+ "shareMediaCategory": "NONE"
309
+ }
310
+ },
311
+ "visibility": {
312
+ "com.linkedin.ugc.MemberNetworkVisibility": "PUBLIC"
313
+ }
314
+ }
315
+
316
+ resp = requests.post(url, headers=headers, json=body)
317
+ print([resp.status_code, resp.text],flush = True)
318
+
319
+
320
+
321
+
322
+
323
+ def post_generation(state) :
324
+ state.generated_post = client.predict(
325
+ code=state.user_inf.user.id,
326
+ api_name="/poster_linkedin"
327
+ )
328
+ def authen(state) :
329
+ if state.Linked_social_network == "Linkedin" :
330
+ print("jhdijb",flush = True)
331
+ state.urlss, state.states = linkedin.authorization_url(
332
+ 'https://www.linkedin.com/oauth/v2/authorization'
333
+ )
334
+ navigate(state, state.urlss)
335
+
336
+
337
+
338
+
339
+ def on_my_clicking(state, action, payload) :
340
+ print(action,flush = True)
341
+ print(payload["args"][0],flush = True)
342
+ if payload["args"][0] == "Accueil" :
343
+ on_logout(state)
344
+ navigate(state, payload["args"][0])
345
+
346
+ return " "
347
+
348
+
349
+
350
+
351
+ def add_source(state) :
352
+
353
+ result = client.predict(
354
+ rss_link=state.source_ + "__thi_irrh'èçs_my_id__! "+state.user_inf.user.id,
355
+ api_name="/ajouter_rss"
356
+ )
357
+
358
+ state.source_add_message = result
359
+ data = db_manager.fetch_source_table(state.user_inf.user.id)
360
+ state.Source_table = pd.DataFrame(data)
361
+
362
+
363
+ def delete_source(state, var_name: str, payload: dict) :
364
+ state.Source_table_before = state.Source_table
365
+ state.get_gui().table_on_delete(state, var_name, payload)
366
+
367
+ diff = state.Source_table_before.merge(state.Source_table, how="outer", indicator=True) \
368
+ .query('_merge != "both"') \
369
+ .drop(columns='_merge')
370
+ valeurs = diff['id'].tolist()
371
+ db_manager.delete_from_table("Source",valeurs)
372
+
373
+ def delete_account(state, var_name: str, payload: dict) :
374
+ state.data_account_before = state.data_account
375
+ state.get_gui().table_on_delete(state, var_name, payload)
376
+
377
+ diff = state.data_account_before.merge(state.data_account, how="outer", indicator=True) \
378
+ .query('_merge != "both"') \
379
+ .drop(columns='_merge')
380
+ valeurs = diff['id'].tolist()
381
+ db_manager.delete_from_table("Social_network",valeurs)
382
+
383
+ def delete_schedule(state, var_name: str, payload: dict) :
384
+ state.data_schedule_before = state.data_schedule
385
+ state.get_gui().table_on_delete(state, var_name, payload)
386
+
387
+ diff = state.data_schedule_before.merge(state.data_schedule, how="outer", indicator=True) \
388
+ .query('_merge != "both"') \
389
+ .drop(columns='_merge')
390
+ valeurs = diff['id'].tolist()
391
+ db_manager.delete_from_table("Scheduling",valeurs)
392
+
393
+ def on_login(state, payload):
394
+ """Handle login form submission"""
395
+ time.sleep(0.7)
396
+ email = state.login_email
397
+ password = state.login_password
398
+
399
+ if not email or not password:
400
+ state.message = "Please enter both email and password"
401
+ return
402
+
403
+ success, message,state.user_inf = db_manager.authenticate_user(email, password)
404
+
405
+ if success:
406
+ state.current_user = email
407
+ data = db_manager.fetch_source_table(state.user_inf.user.id)
408
+ dataac = db_manager.fetch_account_table(state.user_inf.user.id)
409
+ state.data_schedule = db_manager.fetch_schedule_table_acc(state.user_inf.user.id)
410
+ state.data_account =pd.DataFrame(dataac)
411
+ state.Source_table = pd.DataFrame(data)
412
+ navigate(state, "Source_Management")
413
+ state.is_logged_in = True
414
+ state.message = f"Welcome back, {email}!"
415
+ # Clear form
416
+ state.login_email = ""
417
+ state.login_password = ""
418
+
419
+
420
+ else:
421
+ state.message = message
422
+
423
+ def on_register(state):
424
+ """Handle registration form submission"""
425
+ time.sleep(0.7)
426
+ email = state.register_email
427
+ password = state.register_password
428
+ confirm_password = state.confirm_password
429
+
430
+ if not email or not password or not confirm_password:
431
+ state.message = "Please fill in all fields"
432
+ return
433
+
434
+ if password != confirm_password:
435
+ state.message = "Passwords do not match"
436
+ return
437
+
438
+ if len(password) < 8:
439
+ state.message = "Password must be at least 8 characters long"
440
+ return
441
+
442
+ success, message,user_inf = db_manager.create_user(email, password) # type: ignore
443
+
444
+ if success:
445
+ state.message = "Registration successful! Please log in."
446
+ state.show_register = False
447
+ # Clear form
448
+ state.register_email = ""
449
+ state.register_password = ""
450
+ state.confirm_password = ""
451
+ else:
452
+ state.message = message
453
+
454
+ def on_logout(state):
455
+
456
+ """Handle logout"""
457
+
458
+ state.current_user = None
459
+ state.is_logged_in = False
460
+ state.message = "Logged out successfully"
461
+ state.login_email = ""
462
+ state.login_password = ""
463
+
464
+ def toggle_register(state):
465
+ """Toggle between login and register forms"""
466
+ state.show_register = not state.show_register
467
+ state.message = ""
468
+ state.login_email = ""
469
+ state.login_password = ""
470
+ state.register_email = ""
471
+ state.register_password = ""
472
+ state.confirm_password = ""
473
+
line_db.py ADDED
@@ -0,0 +1,209 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ from supabase import create_client, Client
3
+ from pandas import json_normalize
4
+ import pandas
5
+
6
+
7
+
8
+
9
+
10
+ class DatabaseManager:
11
+
12
+ def __init__(self,url,key):
13
+ # Supabase connection string format
14
+ # postgresql://postgres:[password]@[host]:[port]/[database]
15
+ self.supabase: Client = create_client(url, key)
16
+
17
+ def create_user(self, email, password):
18
+ response = self.supabase.auth.sign_up(
19
+ {
20
+ "email": email,
21
+ "password": password,
22
+ }
23
+
24
+
25
+ )
26
+ if response.user.aud == "authenticated" : # type: ignore
27
+ return True,"Un mail a été envoyé a votre address mail",response
28
+
29
+ def fetch_source_table(self,filter) :
30
+ response = (
31
+ self.supabase.table("Source")
32
+ .select("*")
33
+ .eq("user_id", filter)
34
+ .execute()
35
+ )
36
+ return response.data
37
+
38
+ def fetch_account_table(self,filter) :
39
+ response = (
40
+ self.supabase.table("Social_network")
41
+ .select("*")
42
+ .eq("id_utilisateur", filter)
43
+ .execute()
44
+ )
45
+ return response.data
46
+
47
+ def fetch_schedule_table_acc(self,filter) :
48
+ response = (
49
+ self.supabase
50
+ .table("Scheduling")
51
+ .select("*, Social_network(id_utilisateur, social_network)")
52
+ .execute()
53
+ )
54
+ print(response.data,flush=True)
55
+
56
+ df = json_normalize(response.data)
57
+ print(df,flush=True)
58
+ # Renomme les colonnes pour simplifier
59
+ if not df.empty :
60
+ df.rename(columns={
61
+ "Social_network.id_utilisateur": "user_id",
62
+ "Social_network.social_network": "social_network"
63
+ }, inplace=True)
64
+
65
+ # Filtre les lignes pour l'utilisateur donné
66
+ df_user = df[df["user_id"] == filter].reset_index(drop=True)
67
+
68
+
69
+ return df_user
70
+ return None
71
+
72
+ def delete_from_table(self,Source,values) :
73
+ response = (
74
+ self.supabase.table(Source)
75
+ .delete()
76
+ .in_("id", values)
77
+ .execute()
78
+ )
79
+
80
+
81
+
82
+ def authenticate_user(self, email, password):
83
+ response = self.supabase.auth.sign_in_with_password(
84
+ {
85
+ "email": email,
86
+ "password": password,
87
+ }
88
+
89
+ )
90
+ if response.user.aud == "authenticated" and response.user.email_confirmed_at is not None : # type: ignore
91
+ return True,"Logged in successfully",response
92
+ elif response.user.aud == "authenticated" : # type: ignore
93
+ return False,"Compte non confirmé",response
94
+ else :
95
+ return False,"Compte non existant",response
96
+
97
+ def add_token_network(self,token,social_network,account_name,uids,data):
98
+ response = (
99
+ self.supabase.table("Social_network")
100
+ .insert({"social_network": social_network,"account_name" :account_name, "id_utilisateur":uids,"token": token,
101
+ "sub" : data["sub"],"given_name" : data["given_name"],"family_name" : data["family_name"],"picture" : data["picture"] })
102
+ .execute()
103
+ )
104
+ def add_post(self,id_social,content,ids,tg : bool =False) :
105
+ response = (
106
+ self.supabase.table("Post_content")
107
+ .insert({"id_social": id_social,"Text_content" :content ,"is_published" : tg,"sched" : ids })
108
+ .execute())
109
+
110
+
111
+
112
+ def update_post(self,ids,idd):
113
+ response = (
114
+ self.supabase.table("Post_content")
115
+ .update({"is_published": True})
116
+ .eq("sched", idd)
117
+ .eq("id_social", ids)
118
+
119
+ .execute()
120
+ )
121
+
122
+ def fetching_post(self,uids,idd,active :bool = False) :
123
+ response = (
124
+ self.supabase.table("Post_content")
125
+ .select("*")
126
+ .eq("id_social", uids)
127
+ .eq("is_published", active)
128
+ .eq("sched", idd)
129
+
130
+
131
+ .execute()
132
+ )
133
+ data = response.data # liste de dicts, chaque dict contient clé 'Social_network'
134
+ df = json_normalize(data)
135
+ return df
136
+
137
+
138
+ def fetching_user_identif(self,uids,rs) :
139
+ response = (
140
+ self.supabase.table("Social_network")
141
+ .select("*")
142
+ .eq("id_utilisateur", uids)
143
+ .eq("account_name", rs)
144
+
145
+ .execute()
146
+ )
147
+ return response
148
+
149
+
150
+
151
+
152
+
153
+ def get_id_social(self,user_id: str, reseau: str):
154
+
155
+ resp = (
156
+ self.supabase
157
+ .table("Social_network")
158
+ .select("id")
159
+ .eq("id_utilisateur", user_id)
160
+ .eq("account_name", reseau)
161
+ .execute()
162
+ )
163
+
164
+ return resp.data[0]["id"]
165
+
166
+
167
+ def create_scheduling_for_user(self,user_id: str, reseau: str, schedule_time: str,adj):
168
+
169
+ id_social = self.get_id_social(user_id, reseau)
170
+ resp = (
171
+ self.supabase
172
+ .table("Scheduling")
173
+ .insert({
174
+ "id_social": id_social,
175
+ "schedule_time": schedule_time,
176
+ "adjusted_time": adj,
177
+
178
+ })
179
+ .execute()
180
+ )
181
+
182
+ print("Scheduling inséré avec succès.")
183
+
184
+
185
+ def fetch_schedule_table(self) :
186
+ response = (
187
+ self.supabase
188
+ .table("Scheduling")
189
+ .select("*, Social_network(id_utilisateur, account_name)")
190
+ .execute()
191
+ )
192
+
193
+ # 2️⃣ On normalise/la platifie la structure JSON en DataFrame
194
+ data = response.data # liste de dicts, chaque dict contient clé 'Social_network'
195
+ df = json_normalize(data)
196
+
197
+
198
+ df = df.rename(columns={"Social_network.id_utilisateur": "user_id"})
199
+ df = df.rename(columns={"Social_network.account_name": "social_network"})
200
+
201
+
202
+ # 4️⃣ On peut réordonner ou filtrer les colonnes si besoin
203
+ # par exemple : id, id_social, user_id, schedule_time, created_at
204
+ cols = ["id", "id_social", "user_id", "schedule_time","social_network","adjusted_time","created_at"]
205
+ df = df[[c for c in cols if c in df.columns]]
206
+
207
+ return df
208
+
209
+
requirements.txt ADDED
@@ -0,0 +1,6 @@
 
 
 
 
 
 
 
1
+ taipy
2
+ psycopg2
3
+ requests-oauthlib
4
+ gradio
5
+ supabase
6
+ apscheduler
timing_lin.py ADDED
@@ -0,0 +1,67 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import pandas as pd
2
+ from datetime import datetime, timedelta
3
+ import bisect
4
+
5
+ JOUR_OFFSET = {
6
+ 'Monday': 0, 'Tuesday': 1, 'Wednesday': 2, 'Thursday': 3,
7
+ 'Friday': 4, 'Saturday': 5, 'Sunday': 6
8
+ }
9
+ BASE_DATE = datetime(2025, 1, 6)
10
+
11
+ def parse_slot(slot_str):
12
+ jour, horaire = slot_str.split()
13
+ horaire = horaire.replace(';', ':')
14
+ h, m = map(int, horaire.split(':'))
15
+ date = BASE_DATE + timedelta(days=JOUR_OFFSET[jour])
16
+ return datetime(date.year, date.month, date.day, h, m)
17
+
18
+ def format_slot(dt):
19
+ return f"{dt.strftime('%A')} {dt.strftime('%H:%M')}"
20
+
21
+
22
+ def add_request(df_fixed, new_slot_str):
23
+ new_dt = parse_slot(new_slot_str)
24
+ desired_dt = new_dt - timedelta(minutes=7)
25
+
26
+ # on récupère les adjusted existants triés
27
+ slots_dt = sorted(parse_slot(s) for s in df_fixed['adjusted_time'])
28
+
29
+ # on commence par placer final_dt = desired_dt, puis on recule au besoin
30
+ final_dt = desired_dt
31
+
32
+ # boucle tant que final_dt viole au moins une contrainte d'espacement
33
+ while True:
34
+ conflict = False
35
+
36
+ # 1) avec le schedule_time
37
+ if new_dt - final_dt < timedelta(minutes=10):
38
+ # on recule pour avoir exactement 7min d'avance
39
+ final_dt = new_dt - timedelta(minutes=10)
40
+ conflict = True
41
+
42
+ # 2) avec chaque adjusted existant
43
+ for slot in slots_dt:
44
+ if abs((slot - final_dt).total_seconds()) < 10*60:
45
+ # on recule de 7min par rapport à cet adjusted
46
+ # si slot > final, on repousse final à slot -7min
47
+ # sinon (slot < final), on recule encore de 7min
48
+ if slot > final_dt:
49
+ final_dt = slot - timedelta(minutes=10)
50
+ else:
51
+ final_dt = slot - timedelta(minutes=10)
52
+ conflict = True
53
+ break
54
+
55
+ if not conflict:
56
+ break # on a trouvé un créneau valide
57
+
58
+ final_str = format_slot(final_dt)
59
+ df_fixed = pd.concat([
60
+ df_fixed,
61
+ pd.DataFrame([{
62
+ 'schedule_time': new_slot_str,
63
+ 'adjusted_time': final_str
64
+ }])
65
+ ], ignore_index=True)
66
+
67
+ return df_fixed.sort_values('adjusted_time').reset_index(drop=True),final_str