Try to use ZnServer for GET and POST requests - pharo

Sorry for asking a possibly dumb question, but after reading documentation, looking at examples, etc., I still didn't find a solution for what I'm trying to do.
I used Pythons web server as a part of a larger project. It serves static files via do_GET method and can handle POST requests (sent by the client using XMLHTTPRequest) via do_POST method. Everything is working well.
What I'm trying to do is an implementation of the web server in Pharo. I think that ZnServer (part of Pharo 2.0) is the way to go. It must be able to serve static files and handle POST requests.
Python code for a very simple http server example
# httpd_test.py
# Imports
import os
import sys
import cgi
import json
from BaseHTTPServer import BaseHTTPRequestHandler
from BaseHTTPServer import HTTPServer
# HTTP Request Handler Class
class RequestHandler(BaseHTTPRequestHandler):
# Process GET requests
def do_GET(self):
print(self.path)
# Send response
tex = 'GET: ' + self.path
self.send_response(200)
self.send_header('Content-type','text/plain')
self.send_header('Content-length',str(len(tex)))
self.end_headers()
self.wfile.write(tex)
self.wfile.flush()
# Process POST requests
def do_POST(self):
# Read CGI data
CgiData = cgi.FieldStorage(fp = self.rfile,
headers = self.headers,
environ = {'REQUEST_METHOD':'POST',
'CONTENT_TYPE':self.headers['Content-Type']})
# Invoke action
if self.path == '/handler':
print(repr(CgiData))
else:
print('Unknown POST handler.')
print(repr(CgiData))
# Send response
self.send_response(200)
tex = repr(CgiData)
self.send_header('Content-type','text/plain')
self.send_header('Content-length',str(len(tex)))
self.end_headers()
self.wfile.write(tex)
self.wfile.flush()
# Start Server
if __name__ == '__main__':
server = HTTPServer(('127.0.0.1',8080),RequestHandler)
server.serve_forever()
What I have tried in Pharo (but with no luck)
start
"Start ZnServer default"
| staticFileServerDelegate |
ZnServer startDefaultOn: 8080.
(ZnServer default) logToTranscript.
(ZnServer default) authenticator: (ZnBasicAuthenticator username: 'foo' password: 'bar').
staticFileServerDelegate := ZnStaticFileServerDelegate new.
staticFileServerDelegate prefixFromString: 'zn';
directory: 'C:\Temp'.
" (ZnServer default) delegate prefixMap
at: 'zn'
put: [ :request | staticFileServerDelegate handleRequest: request ];
at: '/'
put: [ :request | ZnResponse redirect: 'C:\Temp' ]."
(ZnServer default) delegate prefixMap
at: '/'
put: [ :request | Transcript cr; show:'1 '; show: request; cr. ];
at: 'handler'
put: [ :request | Transcript cr; show:'3 '; show: request; cr. ].
"start" is a class method of an own class with no base class.
I'm a newbie to Pharo, so it's possible that I'm on the wrong track.
I hope, the description is clear enough, so one can see what I want to do.
Any help on this would be highly appreciated.
Thanks in advance.

As a default server, this should work:
(ZnServer startDefaultOn: 8080)
onRequestRespond: [ :request |
ZnResponse ok: (ZnEntity text: 'Hiya!') ].
Then you can manage the documents answered on ZnResponse.
But why don't you use something more powerful like Seaside? http://seaside.st

Related

How to serve static files in FastAPI

I am trying to serve static files that I have in a package_docs directory. When I open in the browzer:
http://127.0.0.1:8001/packages/docs/index.html , the page is running.
But I want to open the page: http://127.0.0.1:8001/packages/docs/
without the source file. And the output is 404 Not Found
app.mount("/packages/docs",
StaticFiles(directory=pkg_resources.resource_filename(__name__, 'package_docs')
),
name="package_docs")
#app.get("/packages/docs/.*", include_in_schema=False)
def root():
return HTMLResponse(pkg_resources.resource_string(__name__, "package_docs/index.html"))
app.include_router(static.router)
app.include_router(jamcam.router, prefix="/api/v1/cams", tags=["jamcam"])
How can I change my code? Any advice will be helpful. Thank you in advance.
There's a html option in Starlette that can be used within FastAPI. Starlette Documentation
This would let you have something such as:
app.mount("/site", StaticFiles(directory="site", html = True), name="site")
Which would parse /site to /site/index.html, /site/foo/ to /site/foo/index.html, etc.
The other answers can help you redirect if you want to change the folder names in a way not handled by using "directory = /foo", but this is the simplest option if you simply want to load the associated .html files.
You need to use FastAPI's TemplateResponse (actually Starlette's):
from fastapi import Request
from fastapi.staticfiles import StaticFiles
from fastapi.templating import Jinja2Templates
app.mount("/static", StaticFiles(directory="static"), name="static")
templates = Jinja2Templates(directory="package_docs")
#app.get("/items/{id}")
async def example(request: Request):
return templates.TemplateResponse("index.html", {"request": request})
Request as part of the key-value pairs in the context for Jinja2. So, you also have to declare it as query parameter. and you have to specify a html file you want to render it with Jinja ("your.html", {"request": request})
Also to return a HTMLResponse directly you can use HTMLResponse from fastapi.responses
from fastapi.responses import HTMLResponse
#app.get("/items/", response_class=HTMLResponse)
async def read_items():
return """
<html>
<head>
<title></title>
</head>
<body>
</body>
</html>
"""
You can read more about custom response from FastAPI Custom Responses
If app.mount is not an option,
you can always manually read the file and respond with its content...
For example, if your static files are inside /site then:
from os.path import isfile
from fastapi import Response
from mimetypes import guess_type
#app.get("/site/{filename}")
async def get_site(filename):
filename = './site/' + filename
if not isfile(filename):
return Response(status_code=404)
with open(filename) as f:
content = f.read()
content_type, _ = guess_type(filename)
return Response(content, media_type=content_type)
#app.get("/site/")
async def get_site_default_filename():
return await get_site('index.html')
From the docs
The first "/static" refers to the sub-path this "sub-application" will be "mounted" on. So, any path that starts with "/static" will be handled by it.
This means that you mount your directory at http://127.0.0.1:8001/packages/docs/ , but you then need to either specify a file in the URL, or add a handler as you did. The problem though, is that since you mounted the path first, it will not take into consideration the following paths that include part of the path.
A possibility is to first specify the path for http://127.0.0.1:8001/packages/docs/ so that it is handled by fastapi and then to mount the folder, serving static files.
Also, I would redirect users asking for http://127.0.0.1:8001/packages/docs/ to http://127.0.0.1:8001/packages/docs/index.html
▶️ 1. You don't need to create route to serve/render homepage/static-folder explicitly. When you mark a directory as static it will automatically get the first argument as route of the app.mount() in this case it's app.mount("/"). So, when you enter the base url like http://127.0.0.1:8000/ you will get the static files.
▶️ 2. app instance of the FastAPI() class. Yes, you can have as many instance as you want in a single FASTAPI app.
▶️ 3. api_app instance of the FastAPI() for other api related task.
The reason 2 & 3 need because if you want to stick with just app instance, when user req for the url http://127.0.0.1:8000/, user will get the the static file, then when user will req for http://127.0.0.1:8000/hello then the server will try to find hello inside static-folder, but there is no hello inside static-folder!, eventually it will be a not-found type response. That's why it's need to be created another instance of FastAPI api_app, and registered with prefix /api(▶️ -> 5), so every req comes from http://127.0.0.1:8000/api/xx, decorator #api_app (▶️ -> 4) will be fired!
instance declaration and mount() method calling order is important.
from fastapi import FastAPI
from fastapi.staticfiles import StaticFiles
from starlette.templating import Jinja2Templates
templates = Jinja2Templates(directory='homepage-app')
# for ui
# 🔽 -> 2
app = FastAPI(title="homepage-app")
# for api
# 🔽 -> 3
api_app = FastAPI(title="api-app")
# for api and route that starts with "/api" and not static
# 🔽 -> 5
app.mount("/api", api_app)
# for static files and route that starts with "/"
# 🔽 -> 1
app.mount("/", StaticFiles(directory="static-folder", html=True), name="static-folder")
# for your other api routes you can use `api_app` instance of the FastAPI
# 🔽 -> 4
#api_app.get("/hello")
async def say_hello():
print("hello")
return {"message": "Hello World"}

Handling POST requests with http.server module

I'm using Python 3.7.4 and http.server module to receive POST requests that contain a file from an ERP.
Everything works fine (file get received and written correctly) except ERP get connection timeout error.
It's first time I use http.server which looks pretty simple but for sure I'm missing something.
See code below, isnt't "self.send_response(200)" enough?
On ERP vendor website they provide an example in PHP to receive data:
if (move_uploaded_file ($_FILES['file']['tmp_name'], "items.xml")){
echo "OK";
} else {
echo "Error";
}
So ERP expect "OK" after successful connection/transfer
Here it is my Python code:
from http.server import BaseHTTPRequestHandler, HTTPServer
import time
import cgi
class Test_Server(BaseHTTPRequestHandler):
def do_POST(self):
print("POST request received")
self.send_response(200)
form = cgi.FieldStorage(fp=self.rfile, headers=self.headers, environ={'REQUEST_METHOD': 'POST', 'CONTENT_TYPE': self.headers['Content-Type'],})
f = open("data/test-orig.xml","wb+")
f.write(form['file'].value)
f.close()
httpd = HTTPServer((hostName, hostPort), Test_Server)
print(time.asctime(), "Server Starts - %s:%s" % (hostName, hostPort))
try:
httpd.serve_forever()
except KeyboardInterrupt:
pass
httpd.server_close()
print(time.asctime(), "Server Stops - %s:%s" % (hostName, hostPort))
Best regards,
cid
Manage to do it easily with Flask:
from flask import Flask
from flask import request
app = Flask(__name__)
#app.route('/post-data', methods=['POST'])
def test_server():
data = request.files['file']
data.save('data/test.xml')
return "OK"
if __name__ == '__main__':
app.run(host='0.0.0.0')
Solved!

Python HTTPServer responding to curl but not to Postman GET request

Consider a simple server in Python3 with the module BaseHTTPRequestHandler.
import json
import urllib.parse
from http.server import BaseHTTPRequestHandler, HTTPServer
import bson.json_util
class GetHandler(BaseHTTPRequestHandler):
def do_GET(self):
print("/n=================================")
json_string = '{"hello":"world"}'
self.wfile.write(json_string.encode())
self.send_response(200)
self.end_headers()
return
if __name__ == '__main__':
#from BaseHTTPServer import HTTPServer
server = HTTPServer(('localhost', 3030), GetHandler)
print ('Starting server, use <Ctrl-C> to stop')
server.serve_forever()
This is responding correctly with curl from the Terminal:
curl -i http://localhost:3030/
However when trying to send a request from Postman it is not responding. I tried the URL localhost:3030/, http://localhost:3030/ and also with the loopback address.
Why is that?
In all the examples I have seen it was not specifying the content type so I did the same way and seeing that curl worked I did not worry too much.
However content type should be specified: adding these lines before self.wfile.write(...)solves the problem:
self.send_response(200)
self.send_header('Content-type', 'application/json')
self.end_headers()
Please note that actually self.send_response(200) has been moved, not added.

received error: tornado.ioloop.TimeoutError: Operation timed out after 5 seconds.what to do?

Hy. I try to write test for webHander:
import pytest
import tornado
from tornado.testing import AsyncTestCase
from tornado.httpclient import AsyncHTTPClient
from tornado.web import Application, RequestHandler
import urllib.parse
class TestRESTAuthHandler(AsyncTestCase):
#tornado.testing.gen_test
def test_http_fetch_login(self):
data = urllib.parse.urlencode(dict(username='admin', password='123456'))
client = AsyncHTTPClient(self.io_loop)
response = yield client.fetch("http://localhost:8080//#/login", method="POST", body=data)
# Test contents of response
self.assertIn("Automation web console", response.body)
Received error when running test:
raise TimeoutError('Operation timed out after %s seconds' % timeout)
tornado.ioloop.TimeoutError: Operation timed out after 5 seconds
Set ASYNC_TEST_TIMEOUT environment variable.
Runs the IOLoop until stop is called or timeout has passed.
In the event of a timeout, an exception will be thrown. The default timeout is 5 seconds; it may be overridden with a timeout keyword argument or globally with the ASYNC_TEST_TIMEOUT environment variable. -- from http://www.tornadoweb.org/en/stable/testing.html#tornado.testing.AsyncTestCase.wait
You need to use AsyncHTTPTestCase, not just AsyncTestCase. A nice example is in Tornado's self-tests:
https://github.com/tornadoweb/tornado/blob/d7d9c467cda38f4c9352172ba7411edc29a85196/tornado/test/httpclient_test.py#L130-L130
You need to implement get_app to return an application with the RequestHandler you've written. Then, do something like:
class TestRESTAuthHandler(AsyncHTTPTestCase):
def get_app(self):
# implement this
pass
def test_http_fetch_login(self):
data = urllib.parse.urlencode(dict(username='admin', password='123456'))
response = self.fetch("http://localhost:8080//#/login", method="POST", body=data)
# Test contents of response
self.assertIn("Automation web console", response.body)
AsyncHTTPTestCase provides convenient features so you don't need to write coroutines with "gen.coroutine" and "yield".
Also, I notice you're fetching a url with a fragment after "#", please note that in real life web browsers do not include the fragment when they send the URL to the server. So your server would see the URL only as "//", not "//#/login".

CherryPy server name tag

When running a CherryPy app it will send server name tag something like CherryPy/version.
Is it possible to rename/overwrite that from the app without modifying CherryPy so it will show something else?
Maybe something like MyAppName/version (CherryPy/version)
This can now be set on a per application basis in the config file/dict
[/]
response.headers.server = "CherryPy Dev01"
Actually asking on IRC on their official channel fumanchu gived me a more clean way to do this (using latest svn):
import cherrypy
from cherrypy import _cpwsgi_server
class HelloWorld(object):
def index(self):
return "Hello World!"
index.exposed = True
serverTag = "MyApp/%s (CherryPy/%s)" % ("1.2.3", cherrypy.__version__)
_cpwsgi_server.CPWSGIServer.environ['SERVER_SOFTWARE'] = serverTag
cherrypy.config.update({'tools.response_headers.on': True,
'tools.response_headers.headers': [('Server', serverTag)]})
cherrypy.quickstart(HelloWorld())
This string appears to be being set in the CherrPy Response class:
def __init__(self):
self.status = None
self.header_list = None
self._body = []
self.time = time.time()
self.headers = http.HeaderMap()
# Since we know all our keys are titled strings, we can
# bypass HeaderMap.update and get a big speed boost.
dict.update(self.headers, {
"Content-Type": 'text/html',
"Server": "CherryPy/" + cherrypy.__version__,
"Date": http.HTTPDate(self.time),
})
So when you're creating your Response object, you can update the "Server" header to display your desired string. From the CherrPy Response Object documentation:
headers
A dictionary containing the headers of the response. You may set values in
this dict anytime before the finalize phase, after which CherryPy switches
to using header_list ...
EDIT: To avoid needing to make this change with every response object you create, one simple way to get around this is to wrap the Response object. For example, you can create your own Response object that inherits from CherryPy's Response and updates the headers key after initializing:
class MyResponse(Response):
def __init__(self):
Response.__init__(self)
dict.update(self.headers, {
"Server": "MyServer/1.0",
})
RespObject = MyResponse()
print RespObject.headers["Server"]
Then you can can call your object for uses where you need to create a Response object, and it will always have the Server header set to your desired string.

Resources